├── .clippy.toml
├── .dockerignore
├── .github
├── renovate.json
└── workflows
│ ├── audit.yml
│ └── tests.yml
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── ego.iml
├── encodings.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
├── runConfigurations
│ ├── cargo_build.xml
│ ├── cargo_clippy.xml
│ ├── cargo_fix.xml
│ ├── cargo_run.xml
│ └── cargo_test.xml
├── vcs.xml
└── watcherTasks.xml
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── src
├── cli.rs
├── errors.rs
├── logging.rs
├── main.rs
├── snapshots
│ ├── check_user_homedir.txt
│ └── ego.help
├── tests.rs
├── util.rs
└── x11.rs
└── varia
├── Dockerfile.tests
├── README.md
├── ego-completion.bash
├── ego-completion.fish
├── ego-completion.zsh
├── ego.rules
├── ego.sudoers
├── ego.sysusers.conf
└── ego.tmpfiles.conf
/.clippy.toml:
--------------------------------------------------------------------------------
1 | allow-mixed-uninlined-format-args = false
2 | doc-valid-idents = ["PulseAudio"]
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /target
2 | _local
3 | .idea
4 | .git
5 | .github
6 | varia/Dockerfile.tests
7 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "mergeConfidence:all-badges",
4 | "config:recommended"
5 | ],
6 | "rangeStrategy": "bump",
7 | "lockFileMaintenance": {
8 | "enabled": true,
9 | "schedule": ["before 5am on saturday"]
10 | },
11 | "packageRules": [
12 | {
13 | "matchManagers": ["cargo"],
14 | "matchPackageNames": ["clap", "clap_complete", "clap_builder", "clap_lex"],
15 | "groupName": "Clap updates"
16 | },
17 | {
18 | "matchManagers": ["cargo"],
19 | "matchUpdateTypes": ["patch"],
20 | "groupName": "Cargo patch",
21 | "schedule": ["before 5am on saturday"]
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/audit.yml:
--------------------------------------------------------------------------------
1 | # Doc: https://github.com/actions-rs/audit-check#scheduled-audit
2 | name: Cargo packages audit
3 | on:
4 | schedule:
5 | # 16:21 UTC on Wednesdays
6 | - cron: "21 16 * * WED"
7 | workflow_dispatch:
8 |
9 | jobs:
10 | audit:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions-rs/audit-check@v1
15 | with:
16 | token: ${{ secrets.GITHUB_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | schedule:
7 | # 16:21 UTC on Tuesdays
8 | - cron: "21 16 * * TUE"
9 | repository_dispatch:
10 | types: [tests]
11 |
12 | env:
13 | DOCKER_BUILDKIT: 1
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Build
20 | run: docker build . --pull -f varia/Dockerfile.tests --tag ego-build
21 | - name: Test suite
22 | run: docker run --rm ego-build cargo test --color=always
23 | - name: Clippy lints
24 | run: docker run --rm ego-build cargo clippy --color=always --all-targets --all-features -- -D warnings
25 | - name: rustfmt
26 | run: docker run --rm ego-build cargo fmt -- --color=always --check
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | _local/
4 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/ego.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/cargo_build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/cargo_clippy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/cargo_fix.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/cargo_run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/cargo_test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "acl-sys"
7 | version = "1.2.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "bbc079f9bdd3124fd18df23c67f7e0f79d24751ae151dcffd095fcade07a3eb2"
10 | dependencies = [
11 | "libc",
12 | ]
13 |
14 | [[package]]
15 | name = "anstream"
16 | version = "0.6.18"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
19 | dependencies = [
20 | "anstyle",
21 | "anstyle-parse",
22 | "anstyle-query",
23 | "anstyle-wincon",
24 | "colorchoice",
25 | "is_terminal_polyfill",
26 | "utf8parse",
27 | ]
28 |
29 | [[package]]
30 | name = "anstyle"
31 | version = "1.0.10"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
34 |
35 | [[package]]
36 | name = "anstyle-parse"
37 | version = "0.2.6"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
40 | dependencies = [
41 | "utf8parse",
42 | ]
43 |
44 | [[package]]
45 | name = "anstyle-query"
46 | version = "1.1.2"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
49 | dependencies = [
50 | "windows-sys",
51 | ]
52 |
53 | [[package]]
54 | name = "anstyle-wincon"
55 | version = "3.0.6"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
58 | dependencies = [
59 | "anstyle",
60 | "windows-sys",
61 | ]
62 |
63 | [[package]]
64 | name = "bitflags"
65 | version = "1.3.2"
66 | source = "registry+https://github.com/rust-lang/crates.io-index"
67 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
68 |
69 | [[package]]
70 | name = "bitflags"
71 | version = "2.6.0"
72 | source = "registry+https://github.com/rust-lang/crates.io-index"
73 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
74 |
75 | [[package]]
76 | name = "cfg-if"
77 | version = "1.0.0"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
80 |
81 | [[package]]
82 | name = "cfg_aliases"
83 | version = "0.2.1"
84 | source = "registry+https://github.com/rust-lang/crates.io-index"
85 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
86 |
87 | [[package]]
88 | name = "clap"
89 | version = "4.5.36"
90 | source = "registry+https://github.com/rust-lang/crates.io-index"
91 | checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
92 | dependencies = [
93 | "clap_builder",
94 | ]
95 |
96 | [[package]]
97 | name = "clap_builder"
98 | version = "4.5.36"
99 | source = "registry+https://github.com/rust-lang/crates.io-index"
100 | checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
101 | dependencies = [
102 | "anstream",
103 | "anstyle",
104 | "clap_lex",
105 | "strsim",
106 | ]
107 |
108 | [[package]]
109 | name = "clap_complete"
110 | version = "4.5.47"
111 | source = "registry+https://github.com/rust-lang/crates.io-index"
112 | checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6"
113 | dependencies = [
114 | "clap",
115 | ]
116 |
117 | [[package]]
118 | name = "clap_lex"
119 | version = "0.7.4"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
122 |
123 | [[package]]
124 | name = "colorchoice"
125 | version = "1.0.3"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
128 |
129 | [[package]]
130 | name = "ego"
131 | version = "1.1.7"
132 | dependencies = [
133 | "anstyle",
134 | "clap",
135 | "clap_complete",
136 | "log",
137 | "nix",
138 | "posix-acl",
139 | "shell-words",
140 | "simple-error",
141 | "snapbox",
142 | "testing_logger",
143 | "xcb",
144 | ]
145 |
146 | [[package]]
147 | name = "is_terminal_polyfill"
148 | version = "1.70.1"
149 | source = "registry+https://github.com/rust-lang/crates.io-index"
150 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
151 |
152 | [[package]]
153 | name = "libc"
154 | version = "0.2.172"
155 | source = "registry+https://github.com/rust-lang/crates.io-index"
156 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
157 |
158 | [[package]]
159 | name = "log"
160 | version = "0.4.27"
161 | source = "registry+https://github.com/rust-lang/crates.io-index"
162 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
163 |
164 | [[package]]
165 | name = "memchr"
166 | version = "2.7.4"
167 | source = "registry+https://github.com/rust-lang/crates.io-index"
168 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
169 |
170 | [[package]]
171 | name = "nix"
172 | version = "0.30.0"
173 | source = "registry+https://github.com/rust-lang/crates.io-index"
174 | checksum = "537bc3c4a347b87fd52ac6c03a02ab1302962cfd93373c5d7a112cdc337854cc"
175 | dependencies = [
176 | "bitflags 2.6.0",
177 | "cfg-if",
178 | "cfg_aliases",
179 | "libc",
180 | ]
181 |
182 | [[package]]
183 | name = "normalize-line-endings"
184 | version = "0.3.0"
185 | source = "registry+https://github.com/rust-lang/crates.io-index"
186 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
187 |
188 | [[package]]
189 | name = "posix-acl"
190 | version = "1.2.0"
191 | source = "registry+https://github.com/rust-lang/crates.io-index"
192 | checksum = "9928b761309e4a4ca4f2d90eb03029142e3f7164107e18db4d46516515b87441"
193 | dependencies = [
194 | "acl-sys",
195 | "libc",
196 | ]
197 |
198 | [[package]]
199 | name = "quick-xml"
200 | version = "0.30.0"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
203 | dependencies = [
204 | "memchr",
205 | ]
206 |
207 | [[package]]
208 | name = "shell-words"
209 | version = "1.1.0"
210 | source = "registry+https://github.com/rust-lang/crates.io-index"
211 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
212 |
213 | [[package]]
214 | name = "similar"
215 | version = "2.6.0"
216 | source = "registry+https://github.com/rust-lang/crates.io-index"
217 | checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
218 |
219 | [[package]]
220 | name = "simple-error"
221 | version = "0.3.1"
222 | source = "registry+https://github.com/rust-lang/crates.io-index"
223 | checksum = "7e2accd2c41a0e920d2abd91b2badcfa1da784662f54fbc47e0e3a51f1e2e1cf"
224 |
225 | [[package]]
226 | name = "snapbox"
227 | version = "0.6.21"
228 | source = "registry+https://github.com/rust-lang/crates.io-index"
229 | checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b"
230 | dependencies = [
231 | "anstream",
232 | "anstyle",
233 | "normalize-line-endings",
234 | "similar",
235 | "snapbox-macros",
236 | ]
237 |
238 | [[package]]
239 | name = "snapbox-macros"
240 | version = "0.3.10"
241 | source = "registry+https://github.com/rust-lang/crates.io-index"
242 | checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af"
243 | dependencies = [
244 | "anstream",
245 | ]
246 |
247 | [[package]]
248 | name = "strsim"
249 | version = "0.11.1"
250 | source = "registry+https://github.com/rust-lang/crates.io-index"
251 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
252 |
253 | [[package]]
254 | name = "testing_logger"
255 | version = "0.1.1"
256 | source = "registry+https://github.com/rust-lang/crates.io-index"
257 | checksum = "6d92b727cb45d33ae956f7f46b966b25f1bc712092aeef9dba5ac798fc89f720"
258 | dependencies = [
259 | "log",
260 | ]
261 |
262 | [[package]]
263 | name = "utf8parse"
264 | version = "0.2.2"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
267 |
268 | [[package]]
269 | name = "windows-sys"
270 | version = "0.59.0"
271 | source = "registry+https://github.com/rust-lang/crates.io-index"
272 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
273 | dependencies = [
274 | "windows-targets",
275 | ]
276 |
277 | [[package]]
278 | name = "windows-targets"
279 | version = "0.52.6"
280 | source = "registry+https://github.com/rust-lang/crates.io-index"
281 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
282 | dependencies = [
283 | "windows_aarch64_gnullvm",
284 | "windows_aarch64_msvc",
285 | "windows_i686_gnu",
286 | "windows_i686_gnullvm",
287 | "windows_i686_msvc",
288 | "windows_x86_64_gnu",
289 | "windows_x86_64_gnullvm",
290 | "windows_x86_64_msvc",
291 | ]
292 |
293 | [[package]]
294 | name = "windows_aarch64_gnullvm"
295 | version = "0.52.6"
296 | source = "registry+https://github.com/rust-lang/crates.io-index"
297 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
298 |
299 | [[package]]
300 | name = "windows_aarch64_msvc"
301 | version = "0.52.6"
302 | source = "registry+https://github.com/rust-lang/crates.io-index"
303 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
304 |
305 | [[package]]
306 | name = "windows_i686_gnu"
307 | version = "0.52.6"
308 | source = "registry+https://github.com/rust-lang/crates.io-index"
309 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
310 |
311 | [[package]]
312 | name = "windows_i686_gnullvm"
313 | version = "0.52.6"
314 | source = "registry+https://github.com/rust-lang/crates.io-index"
315 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
316 |
317 | [[package]]
318 | name = "windows_i686_msvc"
319 | version = "0.52.6"
320 | source = "registry+https://github.com/rust-lang/crates.io-index"
321 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
322 |
323 | [[package]]
324 | name = "windows_x86_64_gnu"
325 | version = "0.52.6"
326 | source = "registry+https://github.com/rust-lang/crates.io-index"
327 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
328 |
329 | [[package]]
330 | name = "windows_x86_64_gnullvm"
331 | version = "0.52.6"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
334 |
335 | [[package]]
336 | name = "windows_x86_64_msvc"
337 | version = "0.52.6"
338 | source = "registry+https://github.com/rust-lang/crates.io-index"
339 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
340 |
341 | [[package]]
342 | name = "xcb"
343 | version = "1.5.0"
344 | source = "registry+https://github.com/rust-lang/crates.io-index"
345 | checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
346 | dependencies = [
347 | "bitflags 1.3.2",
348 | "libc",
349 | "quick-xml",
350 | ]
351 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ego"
3 | version = "1.1.7"
4 | edition = "2021"
5 | rust-version = "1.74.0"
6 |
7 | # Metadata
8 | authors = ["Marti Raudsepp "]
9 | description = "Alter Ego: run Linux desktop applications under a different local user"
10 | readme = "README.md"
11 | license = "MIT"
12 | homepage = "https://github.com/intgr/ego"
13 | repository = "https://github.com/intgr/ego"
14 | keywords = ["sudo", "security", "wayland", "pulseaudio"]
15 | categories = ["command-line-utilities"]
16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
17 |
18 | [dependencies]
19 | simple-error = "0.3.1"
20 | posix-acl = "1.2.0"
21 | clap = { version = "~4.5.36", features = ["cargo"] }
22 | log = { version = "0.4.27", features = ["std"] }
23 | shell-words = "1.1.0"
24 | nix = { version = "0.30.0", default-features = false, features = ["user"] }
25 | anstyle = "1.0.10"
26 | xcb = "1.5.0"
27 |
28 | [features]
29 | default = []
30 |
31 | [dev-dependencies]
32 | clap_complete = "~4.5.47"
33 | snapbox = "0.6.21"
34 | testing_logger = "0.1.1"
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2022 Marti Raudsepp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ego (a.k.a Alter Ego)
2 | =====================
3 |
4 | [](https://crates.io/crates/ego)
5 | [](https://github.com/intgr/ego/actions?query=workflow:Tests)
6 |
7 | > Do all your games need access to your documents, browser history, SSH private keys?
8 | >
9 | > ... No? Just run `ego steam`!
10 |
11 | **Ego** is a tool to run Linux desktop applications under a different local user. Currently
12 | integrates with Wayland, Xorg, PulseAudio and xdg-desktop-portal. You may think of it as `xhost`
13 | for Wayland and PulseAudio. This is done using filesystem ACLs and X11 host access control.
14 |
15 | Disclaimer: **DO NOT RUN UNTRUSTED PROGRAMS VIA EGO.** However, using ego is more secure than
16 | running applications directly under your primary user.
17 |
18 | Distro packages
19 | ---------------
20 | Distribution packages are available for:
21 | * Arch Linux (user-contributed package) https://aur.archlinux.org/packages/ego/
22 |
23 | After installing the package, add yourself to the `ego-users` group. After logout and login,
24 | the `ego` command should just work.
25 |
26 | ([varia/README.md](varia/README.md) documents recommendations for distro packagers)
27 |
28 | Manual setup
29 | ------------
30 | Ego aims to come with sane defaults and be easy to set up.
31 |
32 | **Requirements:**
33 | * [Rust & cargo](https://www.rust-lang.org/tools/install)
34 | * `libacl.so` library (Debian/Ubuntu: libacl1-dev; Fedora: libacl-devel; Arch: acl)
35 | * `libxcb.so` library (Debian/Ubuntu: libxcb1-dev; Fedora: libxcb-devel; Arch: libxcb)
36 |
37 | **Recommended:** (Not needed when using `--sudo` mode, but some desktop functionality may not work).
38 | * `machinectl` command (Debian/Ubuntu/Fedora: systemd-container; Arch: systemd)
39 | * `xdg-desktop-portal-gtk` (Debian/Ubuntu/Fedora/Arch: xdg-desktop-portal-gtk)
40 |
41 | **Installation:**
42 |
43 | 1. Run:
44 |
45 | cargo install ego
46 | sudo cp ~/.cargo/bin/ego /usr/local/bin/
47 |
48 | 2. Create local user named "ego": [1]
49 |
50 | sudo useradd ego --uid 155 --create-home
51 |
52 | 3. That's all, try it:
53 |
54 | ego xdg-open .
55 |
56 | [1] No extra groups are needed by the ego user.
57 | UID below 1000 hides this user on the login screen.
58 |
59 | ### Avoid password prompt
60 | If using "machinectl" mode (default if available), you need the rather new systemd version >=247
61 | and polkit >=0.106 to do this securely.
62 |
63 | Create file `/etc/polkit-1/rules.d/50-ego-machinectl.rules`, polkit will automatically load it
64 | (replace `` with your own username):
65 |
66 | ```js
67 | polkit.addRule(function(action, subject) {
68 | if (action.id == "org.freedesktop.machine1.host-shell" &&
69 | action.lookup("user") == "ego" &&
70 | subject.user == "") {
71 | return polkit.Result.YES;
72 | }
73 | });
74 | ```
75 |
76 | ##### sudo mode
77 | For sudo, add the following to `/etc/sudoers` (replace `` with your own username):
78 |
79 | ALL=(ego) NOPASSWD:ALL
80 |
81 | Changelog
82 | ---------
83 |
84 | ##### Unreleased
85 | * Use X11 protocol directly via `libxcb`. The `xhost` dependency is no longer needed. (#163)
86 |
87 | ##### 1.1.7 (2023-06-26)
88 | * Distro packaging: added tmpfiles.d conf to create missing ego user home directory (#134, fixed issue #131)
89 | * Ego now detects and warns when target user's home directory does not exist or has wrong ownership (#139)
90 | * Minimum Supported Rust Version (MSRV) is now 1.64.0 (#116)
91 | * Various minor cleanups, replaced unmaintained dependencies, dependency updates.
92 |
93 | ##### 1.1.6 (2023-01-21)
94 | * Updated to clap 4.0.x (#101) and many other dependency updates
95 | * Fixes for new clippy lints (#95, #93, #111)
96 | * Use `snapbox` instead of hand-coded snapshot testing (#102)
97 | * Minimum Supported Rust Version (MSRV) was determined to be 1.60.0 (#113)
98 |
99 | ##### 1.1.5 (2022-01-02)
100 | * Document xhost requirement, improve xhost error reporting (#76)
101 | * Upgrade to clap 3.0.0 stable (#71)
102 |
103 | (Version 1.1.4 was yanked, it was accidentally released with a regression)
104 |
105 | ##### 1.1.3 (2021-11-12)
106 | * Pin clap version (fixes #65) (#68)
107 |
108 | ##### 1.1.2 (2021-05-08)
109 | * Enable sudo askpass helper if SUDO_ASKPASS is set (#58)
110 | * Example how to set up a GUI password prompt with sudo: https://askubuntu.com/a/314401
111 | * Note: For a GUI password prompt with the machinectl mode, you need to run a
112 | Polkit authentication agent instead
113 |
114 | ##### 1.1.1 (2021-03-23)
115 | * Include drop-in files for polkit, sudoers.d, sysusers.d -- for distro packages (#53)
116 | * Documentation tweaks (#51, #53)
117 |
118 | ##### 1.1.0 (2021-03-07)
119 | * Default to `machinectl` if available, fall back to `sudo` otherwise (#47)
120 | * Documentation & minor improvements (#46, #48)
121 |
122 | ##### 0.4.1 (2021-01-29)
123 | * Fixed `--machinectl` on Ubuntu, Debian with dash shell (#42)
124 | * Fixed error reporting when command execution fails (#43)
125 | * Documented how to avoid password prompt with machinectl & other doc tweaks (#41)
126 |
127 | ##### 0.4.0 (2021-01-29)
128 | * Improved integration with desktop environments:
129 | * Launch xdg-desktop-portal-gtk in machinectl session (#6, #31)
130 | * Old behavior is still available via `--machinectl-bare` switch.
131 | * Shell completion files are now auto-generated with clap-generate 3.0.0-beta.2 (#36, #28)
132 | * bash, zsh and fish shells are supported out of the box.
133 | * Code reorganization and CI improvements (#21, #23)
134 | * Dependency updates (#20, #24, #27, #22, #26, #33, #35, #38, #37, #39)
135 |
136 | ##### 0.3.1 (2020-03-17)
137 | * Improved error message for missing target user (#16)
138 |
139 | ##### 0.3.0 (2020-03-02)
140 | * Initial machinectl support (using `--machinectl`) (#8)
141 | * Updated: posix-acl (#9)
142 |
143 | ##### 0.2.0 (2020-02-17)
144 | * Added zsh completion support (#5)
145 | * Added `--verbose` flag (#4)
146 | * Added `--user` argument and command-line parsing (#3)
147 |
148 | ##### 0.1.0 (2020-02-13)
149 | Initial version
150 |
151 | Appendix
152 | --------
153 | Ego is licensed under the MIT License (see the `LICENSE` file). Ego was created by Marti Raudsepp.
154 | Ego's primary website is at https://github.com/intgr/ego
155 |
156 | Thanks to Alexander Payne (myrrlyn) for relinquishing the unused "ego" crate name.
157 |
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | use clap::{command, Arg, ArgAction, ArgGroup, Command, ValueHint};
2 | use log::Level;
3 | use std::ffi::OsString;
4 |
5 | #[derive(Debug, PartialEq, Eq)]
6 | pub enum Method {
7 | Sudo,
8 | Machinectl,
9 | MachinectlBare,
10 | }
11 |
12 | /// Data type for parsed settings
13 | pub struct Args {
14 | pub user: String,
15 | pub command: Vec,
16 | pub log_level: Level,
17 | pub method: Option,
18 | pub old_xhost: bool,
19 | }
20 |
21 | pub fn build_cli() -> Command {
22 | command!()
23 | .arg(
24 | Arg::new("user")
25 | .short('u')
26 | .long("user")
27 | .value_name("USER")
28 | .default_value("ego")
29 | .help("Specify a username (default: ego)")
30 | .value_hint(ValueHint::Username),
31 | )
32 | .arg(
33 | Arg::new("sudo")
34 | .long("sudo")
35 | .action(ArgAction::SetTrue)
36 | .help("Use 'sudo' to change user"),
37 | )
38 | .arg(
39 | Arg::new("machinectl")
40 | .long("machinectl")
41 | .action(ArgAction::SetTrue)
42 | .help("Use 'machinectl' to change user (default, if available)"),
43 | )
44 | .arg(
45 | Arg::new("machinectl-bare")
46 | .long("machinectl-bare")
47 | .action(ArgAction::SetTrue)
48 | .help("Use 'machinectl' but skip xdg-desktop-portal setup"),
49 | )
50 | .group(ArgGroup::new("method").args(["sudo", "machinectl", "machinectl-bare"]))
51 | .arg(
52 | Arg::new("old-xhost")
53 | .long("old-xhost")
54 | .action(ArgAction::SetTrue)
55 | .help("Execute 'xhost' command instead of connecting to X11 directly"),
56 | )
57 | .arg(
58 | Arg::new("command")
59 | .help("Command name and arguments to run (default: user shell)")
60 | .num_args(1..)
61 | .trailing_var_arg(true)
62 | .value_hint(ValueHint::CommandWithArguments),
63 | )
64 | .arg(
65 | Arg::new("verbose")
66 | .short('v')
67 | .long("verbose")
68 | .action(ArgAction::Count)
69 | .help("Verbose output. Use multiple times for more output."),
70 | )
71 | }
72 |
73 | pub fn parse_args + Clone>(args: impl IntoIterator- ) -> Args {
74 | let matches = build_cli().get_matches_from(args);
75 |
76 | Args {
77 | user: matches.get_one::("user").unwrap().clone(),
78 | command: matches
79 | .get_many("command")
80 | .unwrap_or_default()
81 | .cloned()
82 | .collect(),
83 | log_level: match matches.get_count("verbose") {
84 | 0 => Level::Warn,
85 | 1 => Level::Info,
86 | 2 => Level::Debug,
87 | _ => Level::Trace,
88 | },
89 | old_xhost: matches.get_flag("old-xhost"),
90 | method: if matches.get_flag("machinectl") {
91 | Some(Method::Machinectl)
92 | } else if matches.get_flag("machinectl-bare") {
93 | Some(Method::MachinectlBare)
94 | } else if matches.get_flag("sudo") {
95 | Some(Method::Sudo)
96 | } else {
97 | None
98 | },
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/errors.rs:
--------------------------------------------------------------------------------
1 | //! Error handling helpers and the `ErrorWithHint` type for more verbose error messages.
2 |
3 | use crate::util::paint;
4 | use anstyle::{AnsiColor, Color, Style};
5 | use log::error;
6 | use std::error::Error;
7 | use std::fmt;
8 |
9 | /// Shorter alias for `Box`
10 | pub type AnyErr = Box;
11 |
12 | const COLOR_HINT: Style = Style::new()
13 | .fg_color(Some(Color::Ansi(AnsiColor::Green)))
14 | .bold();
15 |
16 | /// Advanced error type that can supply hints to the user
17 | #[derive(Debug)]
18 | pub struct ErrorWithHint {
19 | err: String,
20 | hint: String,
21 | }
22 |
23 | impl ErrorWithHint {
24 | pub fn new(err: String, hint: String) -> ErrorWithHint {
25 | ErrorWithHint { err, hint }
26 | }
27 | }
28 |
29 | impl Error for ErrorWithHint {}
30 |
31 | impl fmt::Display for ErrorWithHint {
32 | #[inline]
33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34 | self.err.fmt(f)?;
35 |
36 | if !self.hint.is_empty() {
37 | write!(f, "\n{}: {}", paint(COLOR_HINT, "hint"), self.hint)?;
38 | }
39 | Ok(())
40 | }
41 | }
42 |
43 | pub fn print_error(err: &AnyErr) {
44 | error!("{err}");
45 | }
46 |
--------------------------------------------------------------------------------
/src/logging.rs:
--------------------------------------------------------------------------------
1 | //! Logging for command line output.
2 | //! Adapted from `simple_logger` by Sam Clements:
3 |
4 | use crate::util::paint;
5 | use anstyle::{AnsiColor, Color, Style};
6 | use log::{trace, Level, Log, Metadata, Record};
7 |
8 | struct SimpleLogger {
9 | level: Level,
10 | }
11 |
12 | const COLOR_TRACE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Magenta)));
13 | const COLOR_WARN: Style = Style::new()
14 | .fg_color(Some(Color::Ansi(AnsiColor::Yellow)))
15 | .bold();
16 | const COLOR_ERROR: Style = Style::new()
17 | .fg_color(Some(Color::Ansi(AnsiColor::Red)))
18 | .bold();
19 |
20 | impl Log for SimpleLogger {
21 | fn enabled(&self, metadata: &Metadata) -> bool {
22 | metadata.level() <= self.level
23 | }
24 |
25 | fn log(&self, record: &Record) {
26 | if !self.enabled(record.metadata()) {
27 | return;
28 | }
29 | match record.level() {
30 | Level::Trace => {
31 | let target = if record.target().is_empty() {
32 | record.module_path().unwrap_or_default()
33 | } else {
34 | record.target()
35 | };
36 | println!("[{}] {}", paint(COLOR_TRACE, target), record.args());
37 | }
38 | Level::Warn => {
39 | println!("{}: {}", paint(COLOR_WARN, "warning"), record.args());
40 | }
41 | Level::Error => {
42 | println!("{}: {}", paint(COLOR_ERROR, "error"), record.args());
43 | }
44 | _ => {
45 | println!("{}", record.args());
46 | }
47 | }
48 | }
49 |
50 | fn flush(&self) {}
51 | }
52 |
53 | /// Initializes the global logger with a `SimpleLogger` instance with
54 | /// `max_log_level` set to a specific log level.
55 | pub fn init_with_level(level: Level) {
56 | let logger = SimpleLogger { level };
57 | log::set_boxed_logger(Box::new(logger)).expect("Set logger failed");
58 | log::set_max_level(level.to_level_filter());
59 |
60 | trace!("Log level {level}");
61 | }
62 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #![warn(clippy::pedantic)]
2 | #![warn(clippy::cargo)]
3 | #![allow(clippy::module_name_repetitions)]
4 | #![allow(clippy::multiple_crate_versions)]
5 |
6 | #[macro_use]
7 | extern crate simple_error;
8 |
9 | use crate::cli::{parse_args, Method};
10 | use crate::errors::{print_error, AnyErr, ErrorWithHint};
11 | use crate::util::{exec_command, have_command, run_command, sd_booted};
12 | use crate::x11::x11_add_acl;
13 | use log::{debug, info, log, warn, Level};
14 | use nix::libc::uid_t;
15 | use nix::unistd::{Uid, User};
16 | use posix_acl::{PosixACL, Qualifier, ACL_EXECUTE, ACL_READ, ACL_RWX};
17 | use simple_error::SimpleError;
18 | use std::env::VarError;
19 | use std::fs::DirBuilder;
20 | use std::io::ErrorKind::PermissionDenied;
21 | use std::os::unix::fs::DirBuilderExt;
22 | use std::os::unix::fs::MetadataExt;
23 | use std::os::unix::fs::PermissionsExt;
24 | use std::path::{Path, PathBuf};
25 | use std::process::exit;
26 | use std::{env, fs};
27 |
28 | mod cli;
29 | mod errors;
30 | mod logging;
31 | #[cfg(test)]
32 | mod tests;
33 | mod util;
34 | mod x11;
35 |
36 | #[derive(Clone)]
37 | struct EgoContext {
38 | runtime_dir: PathBuf,
39 | target_user: String,
40 | target_uid: uid_t,
41 | target_user_shell: PathBuf,
42 | target_user_homedir: PathBuf,
43 | }
44 |
45 | fn main_inner() -> Result<(), AnyErr> {
46 | let args = parse_args(env::args());
47 | logging::init_with_level(args.log_level);
48 |
49 | let mut vars: Vec = Vec::new();
50 | let ctx = create_context(&args.user)?;
51 |
52 | info!(
53 | "Setting up Alter Ego for target user {} ({})",
54 | ctx.target_user, ctx.target_uid
55 | );
56 |
57 | check_user_homedir(&ctx);
58 |
59 | let ret = prepare_runtime_dir(&ctx);
60 | if let Err(msg) = ret {
61 | bail!("Error preparing runtime dir: {msg}");
62 | }
63 | match prepare_wayland(&ctx) {
64 | Err(msg) => bail!("Error preparing Wayland: {msg}"),
65 | Ok(ret) => vars.extend(ret),
66 | }
67 | match prepare_x11(&ctx, args.old_xhost) {
68 | Err(msg) => bail!("Error preparing X11: {msg}"),
69 | Ok(ret) => vars.extend(ret),
70 | }
71 | match prepare_pulseaudio(&ctx) {
72 | Err(msg) => bail!("Error preparing PulseAudio: {msg}"),
73 | Ok(ret) => vars.extend(ret),
74 | }
75 |
76 | let method = args.method.unwrap_or_else(detect_method);
77 | let ret = match method {
78 | Method::Sudo => run_sudo_command(&ctx, vars, args.command),
79 | Method::Machinectl => run_machinectl_command(&ctx, &vars, args.command, false),
80 | Method::MachinectlBare => run_machinectl_command(&ctx, &vars, args.command, true),
81 | };
82 | if let Err(msg) = ret {
83 | bail!("{msg}");
84 | }
85 |
86 | Ok(())
87 | }
88 |
89 | fn main() {
90 | let ret = main_inner();
91 | if let Err(err) = ret {
92 | print_error(&err);
93 | exit(1);
94 | }
95 | }
96 |
97 | /// Optionally get an environment variable.
98 | /// Returns `Ok(None)` for missing env variable.
99 | fn getenv_optional(key: &str) -> Result