1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! Per-account `gh` config directory bootstrap (#7166, owner ruling "do B").
//!
//! Why: `resolve_gh_account_env_with`'s `config_dir` arm is the ONLY selector
//! that actually discriminates between logged-in accounts on a keyring-backed
//! `gh` install (`core::gh_account`'s own #5851 rationale for demoting `-u`)
//! — but nothing built one automatically for the `--account <login>` clone
//! selector; #7166's round-3 fix could only verify a possibly-wrong `-u`
//! token, never select the right one on such a host without a manual
//! `--gh-config-dir` pin. This module builds and reuses ONE isolated config
//! dir per known account, so the selector works out of the box.
//!
//! Layout: `<tm state root>/gh-accounts/<login>/` — `resolve_gh_account_env_with`'s
//! own `config_dir` arm (`core/gh_account.rs`, #5851's PR) accepts ANY
//! operator-chosen directory and defines no per-account convention of its
//! own, so this is a fresh layout, not a pre-existing one being reused.
//! `<tm state root>` is `crate::core::paths::FrameworkPaths::default().root`
//! (`~/.trusty-mpm`) in production; every function here takes it as a
//! parameter so tests point it at a tempdir instead.
//!
//! What: [`ensure_account_config_dir`] is the pure(ish), fully-injectable
//! core (real filesystem I/O, but every path is a parameter — no ambient
//! `$HOME`/`GH_CONFIG_DIR` read); [`ensure_account_config_dir_default`] is
//! the production entry point, resolving both the state root and the
//! operator's own `gh` config dir the same way `gh` itself does
//! ([`crate::core::gh_account::gh_config_dir`]).
//!
//! On first use for a never-before-selected `login`: reads the OPERATOR's
//! `hosts.yml` (never mutated) to confirm `login` is actually logged in —
//! refusing, dir untouched, when it is not — copies `config.yml` if present,
//! and writes a NEW `hosts.yml` naming only `login`. No token is copied or
//! written: the new `hosts.yml` carries no `oauth_token`. Whether
//! `gh auth token` (run with `GH_CONFIG_DIR` pointed at this dir) then finds a
//! credential depends on where `gh` stored it. A keyring-backed login is read
//! from the OS keyring by host and login, outside this dir, so it can resolve.
//! A token kept in the operator's `hosts.yml` (`gh auth login
//! --insecure-storage`) is not carried over, so that account resolves no token
//! here. A token `gh` does return is only a candidate: `gh` can fall back to
//! another account's keyring slot, so callers prove it with `GET /user`
//! ([`crate::core::gh_account_proof`], #8510) before use. `gh auth switch` is
//! never called — the isolation comes from a SEPARATE config dir, not from
//! mutating the shared one's active pointer.
//!
//! Test: `account_config_dir_tests.rs`.
use ;
/// Restrict `dir` to owner-only access (#7166 review follow-up MEDIUM) —
/// `gh`'s own `hosts.yml` is written 0600 because it can hold a token
/// (insecure storage); this directory never holds one, but it does reveal which GitHub accounts
/// are used with `tm` and copies the operator's real `config.yml` verbatim,
/// so it must not be left at the ambient umask on a shared host.
/// Test: `ensure_account_config_dir_sets_restrictive_permissions`.
/// Restrict a written file (`hosts.yml`/`config.yml`) to owner-only access
/// (#7166 review follow-up MEDIUM) — same rationale as
/// [`set_private_dir_permissions`].
/// Test: `ensure_account_config_dir_sets_restrictive_permissions`.
/// Ensure a per-account `gh` config dir exists for `login`, building it from
/// `operator_gh_config_dir` on first use.
///
/// Why: the injectable-paths seam this codebase uses throughout for `gh`/
/// filesystem I/O that cannot run hermetically against the REAL `$HOME` —
/// tests point both paths at tempdirs and a fake `operator_gh_config_dir`.
/// What: `<state_root>/gh-accounts/<login>/hosts.yml` existing already means
/// "reused untouched" — returns `Ok(dir)` immediately, no filesystem writes.
/// Otherwise reads `<operator_gh_config_dir>/hosts.yml`; a missing/unparseable
/// file, or one that does not name `login` (case-insensitively; matches
/// [`crate::core::gh_account::GhAccountStatus::canonical_logged_in_login`]'s
/// convention) is a refusal naming the `gh auth login` remedy — the directory
/// is NEVER created in that case. Otherwise creates the directory, copies
/// `config.yml` when the operator has one (absent is not an error — `gh`
/// tolerates a config dir with no `config.yml`), and writes a fresh
/// `hosts.yml` naming ONLY the canonical spelling of `login` (no token — see
/// the module doc for which credentials `gh` can still resolve here). Both
/// paths leave `config.yml` declaring `version: "1"` (#8510), via
/// [`crate::core::gh_account_dir::ensure_config_version`].
/// Test: `ensure_account_config_dir_builds_from_operator_hosts_yml`,
/// `ensure_account_config_dir_writes_the_config_version`,
/// `ensure_account_config_dir_adds_the_version_to_a_copied_config`,
/// `ensure_account_config_dir_adds_the_version_to_a_reused_dir`,
/// `ensure_account_config_dir_refuses_an_unknown_login`,
/// `ensure_account_config_dir_reuses_an_existing_dir_untouched`,
/// `ensure_account_config_dir_copies_config_yml_when_present`,
/// `ensure_account_config_dir_tolerates_a_missing_config_yml`.
pub
/// Render a `hosts.yml` naming exactly one github.com account.
///
/// Why: split out as a pure function so its exact shape — parseable by
/// [`crate::core::gh_account::parse_gh_account_status_from_hosts_yml`], the
/// SAME parser `gh` itself effectively mirrors — is asserted without any
/// filesystem I/O.
/// What: `github.com:\n users:\n <login>:\n user: <login>\n`.
/// No `git_protocol` line — `gh` defaults it, and omitting it keeps this
/// writer from having an opinion `gh`'s own `hosts.yml` schema does not
/// require.
/// Test: `render_single_account_hosts_yml_round_trips_through_the_parser`.
/// Production entry point: resolve the tm state root and the operator's own
/// `gh` config dir the same way `gh` itself does, then delegate to
/// [`ensure_account_config_dir`].
///
/// Why: the one non-test call site — every other function in this module
/// takes its paths as parameters specifically so this is the ONLY place that
/// reads `$HOME`/`GH_CONFIG_DIR`/`XDG_CONFIG_HOME`.
/// What: state root is
/// [`crate::core::paths::FrameworkPaths::default`]`().root` (`~/.trusty-mpm`);
/// the operator's `gh` config dir is
/// [`crate::core::gh_account::gh_config_dir`] (honours `GH_CONFIG_DIR` /
/// `XDG_CONFIG_HOME`, else `~/.config/gh`), or a synthetic
/// `~/.config/gh` when even `dirs::home_dir()` fails (matches
/// `gh_config_dir`'s own last-resort fallback shape).
/// Test: none directly (thin, ambient-environment wiring); the logic it
/// delegates to is covered by [`ensure_account_config_dir`]'s tests.
pub