Follow-up to M3 (#1918) and A2's security review P2-1 (#1873): the accounts store checks every ancestor of its config directory.
assertAccountsOwner refused a symlink or non-directory on the config path but checked owner and mode only on the leaf. An ancestor owned by another user, or writable by others without the sticky bit, let that user rename the directory out from under the check and swap in their own accounts.json. Since M3, accounts.json decides who holds root authority on a deployed Console.
New packages/organization/src/config-dir-trust.mjs, assertTrustedConfigDir(configDir, { uid, code }), holding A2's agent-levels rule unchanged: every ancestor belongs to root or this uid and is group or other writable only with the sticky bit; configDir is this uid's and not group or other writable; no symlink anywhere.
assertAccountsOwner and the agent-levels writer both call it, each with its own code (accounts-unsafe, agents-unsafe). Agent-levels messages and check order are unchanged (its 15 tests pass untouched). Every account read and write already went through assertAccountsOwner: load, lock, locked writer, mosaic accounts verbs including init, webui stamp check and SSO reloads.
Two leaf refusals no longer quote a path (redaction rule, #1916). A missing config directory now refuses accounts-unsafe: config directory is missing instead of a raw ENOENT.
Old-path re-export at packages/business/src/config-dir-trust.mjs, as rename-b-identity requires.
Shipped layouts pass: ~/.config/mosaic-NAME under a user-owned home, and Case 2's /srv/mosaic/.config/mosaic-dev (root 0755 ancestors, mosaic 0700 home, .config 0755 from case2-install.sh). A group-writable .config without the sticky bit now refuses; that is the rule, on purpose.
Tests: 0777 parent and grandparent refuse load, init and lock with nothing created; 1777 passes; another uid refuses; symlinked ancestor refuses; no refusal contains the sk-test-NOT-A-SECRET-1234 path component or a /. CLI init, list and user remove exit 3 under a 0777 ancestor. All new tests fail on base.
Lane: Opus. Gate on d0cfebe28: 275 files (organization, plus every cli and webui importer of accounts, sso and agent-levels-write) and test-config, 2490/0. Security review daybreak-blue: approve, no findings.
Deferred: util.mjsreadJsonFile still quotes the file path for a missing, symlinked or invalid accounts.json. Follow-up after the refusal-names change lands.
Follow-up to M3 (#1918) and A2's security review P2-1 (#1873): the accounts store checks every ancestor of its config directory.
`assertAccountsOwner` refused a symlink or non-directory on the config path but checked owner and mode only on the leaf. An ancestor owned by another user, or writable by others without the sticky bit, let that user rename the directory out from under the check and swap in their own `accounts.json`. Since M3, `accounts.json` decides who holds root authority on a deployed Console.
- New `packages/organization/src/config-dir-trust.mjs`, `assertTrustedConfigDir(configDir, { uid, code })`, holding A2's agent-levels rule unchanged: every ancestor belongs to root or this uid and is group or other writable only with the sticky bit; configDir is this uid's and not group or other writable; no symlink anywhere.
- `assertAccountsOwner` and the agent-levels writer both call it, each with its own code (`accounts-unsafe`, `agents-unsafe`). Agent-levels messages and check order are unchanged (its 15 tests pass untouched). Every account read and write already went through `assertAccountsOwner`: load, lock, locked writer, `mosaic accounts` verbs including `init`, webui stamp check and SSO reloads.
- Two leaf refusals no longer quote a path (redaction rule, #1916). A missing config directory now refuses `accounts-unsafe: config directory is missing` instead of a raw ENOENT.
- Old-path re-export at `packages/business/src/config-dir-trust.mjs`, as `rename-b-identity` requires.
Shipped layouts pass: `~/.config/mosaic-NAME` under a user-owned home, and Case 2's `/srv/mosaic/.config/mosaic-dev` (root 0755 ancestors, mosaic 0700 home, `.config` 0755 from `case2-install.sh`). A group-writable `.config` without the sticky bit now refuses; that is the rule, on purpose.
Tests: 0777 parent and grandparent refuse load, init and lock with nothing created; 1777 passes; another uid refuses; symlinked ancestor refuses; no refusal contains the `sk-test-NOT-A-SECRET-1234` path component or a `/`. CLI `init`, `list` and `user remove` exit 3 under a 0777 ancestor. All new tests fail on base.
Lane: Opus. Gate on d0cfebe28: 275 files (organization, plus every cli and webui importer of accounts, sso and agent-levels-write) and test-config, 2490/0. Security review daybreak-blue: approve, no findings.
Deferred: `util.mjs` `readJsonFile` still quotes the file path for a missing, symlinked or invalid `accounts.json`. Follow-up after the refusal-names change lands.
assertAccountsOwner refused a symlink or non-directory on the config path,
but checked owner and mode only on the leaf. An ancestor owned by another
user, or writable by others without the sticky bit, let that user rename the
directory away and put their own accounts.json in its place. Since M3 that
file decides root authority on a deployed Console.
One shared helper, config-dir-trust.mjs, now holds A2's rule: every ancestor
belongs to root or this uid and is group or other writable only with the
sticky bit; configDir itself is this uid's and not group or other writable.
The accounts store (load, init, lock, locked write, mosaic accounts) and the
root agent file's writer both call it, each with its own refusal code. A2's
messages are unchanged.
Accounts refusals no longer quote a path: the config directory and accounts
file uid messages name the rule and the uid only (redaction rule, #1916).
A missing config directory now refuses accounts-unsafe instead of a raw
ENOENT.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follow-up to M3 (#1918) and A2's security review P2-1 (#1873): the accounts store checks every ancestor of its config directory.
assertAccountsOwnerrefused a symlink or non-directory on the config path but checked owner and mode only on the leaf. An ancestor owned by another user, or writable by others without the sticky bit, let that user rename the directory out from under the check and swap in their ownaccounts.json. Since M3,accounts.jsondecides who holds root authority on a deployed Console.packages/organization/src/config-dir-trust.mjs,assertTrustedConfigDir(configDir, { uid, code }), holding A2's agent-levels rule unchanged: every ancestor belongs to root or this uid and is group or other writable only with the sticky bit; configDir is this uid's and not group or other writable; no symlink anywhere.assertAccountsOwnerand the agent-levels writer both call it, each with its own code (accounts-unsafe,agents-unsafe). Agent-levels messages and check order are unchanged (its 15 tests pass untouched). Every account read and write already went throughassertAccountsOwner: load, lock, locked writer,mosaic accountsverbs includinginit, webui stamp check and SSO reloads.accounts-unsafe: config directory is missinginstead of a raw ENOENT.packages/business/src/config-dir-trust.mjs, asrename-b-identityrequires.Shipped layouts pass:
~/.config/mosaic-NAMEunder a user-owned home, and Case 2's/srv/mosaic/.config/mosaic-dev(root 0755 ancestors, mosaic 0700 home,.config0755 fromcase2-install.sh). A group-writable.configwithout the sticky bit now refuses; that is the rule, on purpose.Tests: 0777 parent and grandparent refuse load, init and lock with nothing created; 1777 passes; another uid refuses; symlinked ancestor refuses; no refusal contains the
sk-test-NOT-A-SECRET-1234path component or a/. CLIinit,listanduser removeexit 3 under a 0777 ancestor. All new tests fail on base.Lane: Opus. Gate on
d0cfebe28: 275 files (organization, plus every cli and webui importer of accounts, sso and agent-levels-write) and test-config, 2490/0. Security review daybreak-blue: approve, no findings.Deferred:
util.mjsreadJsonFilestill quotes the file path for a missing, symlinked or invalidaccounts.json. Follow-up after the refusal-names change lands.