diff --git a/dag/gunbc/fabric/fabric_storage_placement.dag b/dag/gunbc/fabric/fabric_storage_placement.dag index 2b6dfdc2899..dc23ff67072 100644 --- a/dag/gunbc/fabric/fabric_storage_placement.dag +++ b/dag/gunbc/fabric/fabric_storage_placement.dag @@ -1,9 +1,14 @@ module gunbc.fabric_storage_placement -import std.types { String, NonEmptyStr, FilePath, Int, Bool, Port } +import std.types { String, NonEmptyStr, FilePath, Int, Bool, Port, List } import product.placement_supply { HostIdentity } import gunbc.fleet_intent_network { operator_host_srv1, fleet_intent_network } import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_fabric_storage_root, srv1_live_dashboard_instance } +import std.effect_grant { Read, Write, Execute } +import gunbc.ownership { Ensured } +import gunbc.managed_directory { ManagedDirectory, DirectoryDependent, ManagedDirectoryAdmission, managed_directory_admit, srv1_dashboard_service_principal } +import gunbc.fleet_posix_accounts { fleet_posix_ci_runner_user } +import gunbc.fabric_storage_file_store { fabric_storage_file_root } // WHERE THE FABRIC DB IS: one host holds the store's files and answers the endpoint. This is the // POLICY fact of the three (DESIGN 3) -- std.fabric_storage is the interface, the file store and the @@ -67,3 +72,53 @@ fn fabric_storage_placement() -> FabricStoragePlacement { Present { value: e } => FabricStoragePlaced { host: operator_host_srv1, store_root: fabric_storage_store_root(), endpoint: e } } } + +// ── WHO WRITES THE STORE, AND SO WHAT ITS DIRECTORIES ARE ──────────────────────────────────────── +// +// TWO PRINCIPALS WRITE THIS STORE, and until this row neither was stated. The served endpoint +// (gunbc.live_deploy.emit live_deploy_fabric_storage_unit_file) runs as the dashboard service +// principal and publishes objects and advances heads for every remote writer. The CI job principal +// writes it DIRECTLY: gunbc.spark.host_commitment claim_host_effect_live resolves the executor's own +// store through host_effect_landing_live, and on the placed host that is these local files, not the +// endpoint. The deploy created the areas owned by the service principal alone at 0755, so the job +// principal's first create-new in objects/ refused permission_denied (fleet-converge run +// 36139624393) -- a writer the store's ownership never admitted. +// +// Both writes are create-only in flat directories: an object is an exclusive create, and a head +// generation is a staged sibling hard-linked into its name (gunbc.durable_cas_file_store +// DefaultAccessCreateOnly). So each writer needs Write and Execute on objects/ and heads/ and Read to +// list and fetch, and only Execute on the root to reach them; no file is ever rewritten, so no file +// needs group write, and the umask's 0644 already lets each writer read the other's entries. +// +// THE GROUP IS THE CI JOB PRINCIPAL'S OWN PRIMARY GROUP, not a new shared group, and that is derived +// rather than preferred: with one owner and exactly one other writer, the smallest group that admits +// the writer is the one it already has. That needs no group creation, no membership edit and no new +// sudo grant, and grants nothing to anyone but that principal. setgid follows from the group +// differing from the owner's (gunbc.managed_directory managed_directory_permissions). A third writer +// with its own gid would land in the other class and REFUSE at managed_directory_admit, which is the +// point at which a genuinely shared group is modeled -- not before, and never as o+w. +fn fabric_storage_directory(member: NonEmptyStr, path: NonEmptyStr, writes: Bool) -> ManagedDirectory { + let service = srv1_dashboard_service_principal() + let needs = if writes { [Read, Write, Execute] } else { [Execute] } + ManagedDirectory { + member: member, + path: path as String as FilePath, + owner: service, + group_principal: fleet_posix_ci_runner_user, + dependents: [ + DirectoryDependent { who: service, needs: [Read, Write, Execute] }, + DirectoryDependent { who: fleet_posix_ci_runner_user, needs: needs }, + ], + ownership: Ensured, + } +} + +fn fabric_storage_store_directories() -> List { + let root = fabric_storage_store_root() + let areas = fabric_storage_file_root(root: root) + [ + managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-root", path: root, writes: false)), + managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-objects", path: areas.objects, writes: true)), + managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-heads", path: areas.heads, writes: true)), + ] +} diff --git a/dag/gunbc/live_deploy/emit.dag b/dag/gunbc/live_deploy/emit.dag index dc8898cb35f..c3568e883c3 100644 --- a/dag/gunbc/live_deploy/emit.dag +++ b/dag/gunbc/live_deploy/emit.dag @@ -10,6 +10,7 @@ import std.evaluation_budget { EvaluationLimit, LimitSet, LimitUnset } import std.types { String, List, Bool, NonEmptyStr, Int, FilePath, CommitSha, Port } import gunbc.managed_directory { ManagedDirectory, + ManagedDirectoryAdmission, ManagedDirectoryAdmitted, ManagedDirectoryRefused, managed_directory_mode_octal, attempt_state_directory_at, } @@ -110,7 +111,7 @@ import gunbc.live_deploy.spec { DeploymentSpec, DeploymentStep, ArtifactStep, Dependency, DeploymentDependencyStep, - EnsuredSubject, EnsuredPackage, EnsuredHostDirectory, ensured_subject_identity, + EnsuredSubject, EnsuredPackage, EnsuredHostDirectory, EnsuredManagedHostDirectory, ensured_subject_identity, deployment_provider_state_step, DeploymentArtifactStep, ServeBinary, GunbcSourceTree, DispatchWorktreeRoot, AttemptStateRoot, ComputeRoot, SystemdUnit, BeltTimerUnit, FleetConvergeTimerUnit, TailscaleServeMapping, DeployedTreeRemoteUnit, PublicationHelperTimerUnit, FabricStorageServeUnit, FabricStorageServeMapping, @@ -1374,10 +1375,25 @@ fn ensure_dependency_steps(dep: DeploymentDependencyStep, privileged: Bool) -> L EnsuredHostDirectory { path, owner } => [ deploy_raw(command: install_d_owned_for_user(user: owner, path: path as String)), ] + EnsuredManagedHostDirectory { admission } => [deploy_raw(command: ensure_managed_directory_command(admission: admission))] } } -// `-g` is spelled with the OWNER'S USER NAME, not a modeled group name, and that is inherited from +// A REFUSED ADMISSION LOWERS TO A MARKER THAT IS NOT SHELL, so the apply fails at this step naming +// the writer the directory's owner and group do not cover -- it never emits an install with a wider +// mode, and never a directory the writer then cannot write. +data managed_directory_writer_uncovered_poison: String = "__GUNBC_DEPLOY_REFUSED__ managed host directory NOT ENSURED: a declared writer is neither its owner nor in its group, and gunbc.managed_directory managed_directory_admit refuses rather than deriving a world-writable mode. member=" + +fn ensure_managed_directory_command(admission: ManagedDirectoryAdmission) -> String { + match admission { + ManagedDirectoryAdmitted { dir } => install_d_managed_command(dir: dir) + ManagedDirectoryRefused { member, path, writer } => + join([managed_directory_writer_uncovered_poison, member as String, " path=", path as String, " writer=", writer as String], "") + } +} + +// `-g` is spelled with the GROUP PRINCIPAL'S USER NAME (the owner's, unless the directory declares +// another writer's group), not a modeled group name, and that is inherited from // install_d_owned_command rather than introduced here: PosixUser carries a gid but no group name, // and srv1's per-user groups happen to share their user's name so the emitted argv is // byte-identical either way. It is written down because the coincidence is load-bearing and silent @@ -1389,7 +1405,7 @@ fn install_d_managed_command(dir: ManagedDirectory) -> String { install_directory_owned_command( mode: managed_directory_mode_octal(d: dir) as NonEmptyStr, owner: dir.owner.name, - group: dir.owner.name, + group: dir.group_principal.name, path: dir.path as String, ) } diff --git a/dag/gunbc/live_deploy/spec.dag b/dag/gunbc/live_deploy/spec.dag index 49bcb780de4..d3c7884dfdd 100644 --- a/dag/gunbc/live_deploy/spec.dag +++ b/dag/gunbc/live_deploy/spec.dag @@ -40,8 +40,8 @@ import gunbc.host_layout { srv1_dispatch_attempt_state_root, srv1_devboot_artifact_store_root, } -import gunbc.fabric_storage_file_store { fabric_storage_file_root } -import gunbc.fabric_storage_placement { fabric_storage_store_root, fabric_storage_placed_on, fabric_storage_https_port, fabric_storage_listen_port } +import gunbc.fabric_storage_placement { fabric_storage_store_directories, fabric_storage_placed_on, fabric_storage_https_port, fabric_storage_listen_port } +import gunbc.managed_directory { ManagedDirectoryAdmission, managed_directory_admission_path } import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_dispatch_worktree_root, @@ -127,14 +127,20 @@ type EnsuredDependencyKind = // reading a deployment spec, so an ensured host resource stays a fact about the HOST and needs no // instance to be stated. +// A MANAGED host directory is the arm whose owner, group and mode are DERIVED from its declared +// dependents (gunbc.managed_directory) rather than asserted as one owner at the shared 0755. It +// carries the ADMISSION, not the directory, so a directory with a writer the owner and group do not +// cover reaches the emitter as a refusal and can never be lowered to an install at all. type EnsuredSubject = EnsuredPackage { package: NonEmptyStr } | EnsuredHostDirectory { path: NonEmptyStr, owner: NonEmptyStr } + | EnsuredManagedHostDirectory { admission: ManagedDirectoryAdmission } fn ensured_subject_identity(subject: EnsuredSubject) -> NonEmptyStr { match subject { EnsuredPackage { package } => package EnsuredHostDirectory { path, owner } => path + EnsuredManagedHostDirectory { admission } => managed_directory_admission_path(a: admission) as String as NonEmptyStr } } @@ -1067,8 +1073,15 @@ fn deployment_ensured_packages() -> List { // absent area refuses not_found -- fleet-converge run 35818578751's host-effect claim did exactly // that. The areas are derived from the store's own root function, so the ensured paths and the // written paths cannot disagree, and this deployment is the store's one production creator. +// +// AND AN AREA THE WRITER CANNOT WRITE IS NOT A STORE EITHER. The three directories are managed: +// their owner, group and mode come from the writers gunbc.fabric_storage_placement +// fabric_storage_store_directories declares (the service principal and the CI job principal), not +// from gunbc_service_user at the shared 0755 -- which is what refused fleet-converge run +// 36139624393's claim permission_denied. install -d re-applies owner, group and mode to a directory +// that already exists, so the next apply repairs the areas #12126 created. fn deployment_ensured_srv1_host_directories() -> List { - [ + concat([ DeploymentDependencyStep { kind: DevbootArtifactStoreDirectory, subject: EnsuredHostDirectory { @@ -1076,27 +1089,10 @@ fn deployment_ensured_srv1_host_directories() -> List owner: gunbc_service_user, }, }, - DeploymentDependencyStep { + ], concat(map(fabric_storage_store_directories(), a => DeploymentDependencyStep { kind: FabricStorageStoreDirectory, - subject: EnsuredHostDirectory { - path: fabric_storage_store_root(), - owner: gunbc_service_user, - }, - }, - DeploymentDependencyStep { - kind: FabricStorageStoreDirectory, - subject: EnsuredHostDirectory { - path: fabric_storage_file_root(root: fabric_storage_store_root()).objects, - owner: gunbc_service_user, - }, - }, - DeploymentDependencyStep { - kind: FabricStorageStoreDirectory, - subject: EnsuredHostDirectory { - path: fabric_storage_file_root(root: fabric_storage_store_root()).heads, - owner: gunbc_service_user, - }, - }, + subject: EnsuredManagedHostDirectory { admission: a }, + }), [ DeploymentDependencyStep { kind: CodexRuntimeReleaseDirectory, subject: EnsuredHostDirectory { @@ -1104,7 +1100,7 @@ fn deployment_ensured_srv1_host_directories() -> List owner: gunbc_service_user, }, }, - ] + ])) } fn deployment_ensured_steps(target: DeploymentHostTarget) -> List { diff --git a/dag/gunbc/managed_directory.dag b/dag/gunbc/managed_directory.dag index 7f3bdb6a648..86b1d829b5b 100644 --- a/dag/gunbc/managed_directory.dag +++ b/dag/gunbc/managed_directory.dag @@ -114,10 +114,17 @@ type DirectoryDependent { needs: List } +// THE DIRECTORY'S GROUP IS A PRINCIPAL'S PRIMARY GROUP, NAMED BY THAT PRINCIPAL. It is the capability +// the frontier note above names as missing -- a group distinct from the owner's -- and it is carried +// as a PosixUser rather than a group name because PosixUser is where the roster already states a gid, +// and srv1's primary groups share their user's name (the coincidence install_d_managed_command +// records). For every directory whose group is its owner's, group_principal IS the owner and the +// derivation is unchanged byte for byte. type ManagedDirectory { member: NonEmptyStr path: FilePath owner: PosixUser + group_principal: PosixUser dependents: List ownership: Ownership } @@ -132,10 +139,10 @@ fn dependent_class_eq(a: DependentClass, b: DependentClass) -> Bool { } } -fn dependent_class_of(owner: PosixUser, who: PosixUser) -> DependentClass { +fn dependent_class_of(owner: PosixUser, group_gid: Int, who: PosixUser) -> DependentClass { if who.uid == owner.uid { OwnerClass - } else if who.gid == owner.gid { + } else if who.gid == group_gid { GroupClass } else { OtherClass @@ -148,6 +155,7 @@ fn needs_verb(needs: List, v: Verb) -> Bool { fn class_needs( owner: PosixUser, + group_gid: Int, dependents: List, cls: DependentClass, ) -> List { @@ -155,7 +163,7 @@ fn class_needs( filter( xs: dependents, predicate: d => dependent_class_eq( - a: dependent_class_of(owner: owner, who: d.who), + a: dependent_class_of(owner: owner, group_gid: group_gid, who: d.who), b: cls, ), ), @@ -165,10 +173,11 @@ fn class_needs( fn class_bits( owner: PosixUser, + group_gid: Int, dependents: List, cls: DependentClass, ) -> PermissionBits { - let needs = class_needs(owner: owner, dependents: dependents, cls: cls) + let needs = class_needs(owner: owner, group_gid: group_gid, dependents: dependents, cls: cls) PermissionBits { read: needs_verb(needs: needs, v: Read), write: needs_verb(needs: needs, v: Write), @@ -176,20 +185,62 @@ fn class_bits( } } +// SETGID IS DERIVED FROM THE GROUP, NOT CHOSEN: it is set exactly when the directory's group is not +// its owner's. Such a directory exists because a second principal writes into it, and without setgid +// every entry the OWNER creates would land in the owner's group while the other writer's entries land +// in theirs -- two groups inside one directory, so any later tightening of the other-class bits would +// silently cut one writer off from the other's entries. With setgid every entry carries the +// directory's group, and the group is what both writers share. fn managed_directory_permissions(d: ManagedDirectory) -> FilePermissions { + let g = d.group_principal.gid FilePermissions { - ownership: FileOwnership { uid: d.owner.uid, gid: d.owner.gid }, + ownership: FileOwnership { uid: d.owner.uid, gid: g }, mode: FileMode { - owner: class_bits(owner: d.owner, dependents: d.dependents, cls: OwnerClass), - group: class_bits(owner: d.owner, dependents: d.dependents, cls: GroupClass), - other: class_bits(owner: d.owner, dependents: d.dependents, cls: OtherClass), + owner: class_bits(owner: d.owner, group_gid: g, dependents: d.dependents, cls: OwnerClass), + group: class_bits(owner: d.owner, group_gid: g, dependents: d.dependents, cls: GroupClass), + other: class_bits(owner: d.owner, group_gid: g, dependents: d.dependents, cls: OtherClass), setuid: false, - setgid: false, + setgid: g != d.owner.gid, sticky: false, }, } } +// ── ADMISSION: A WRITER THE OWNER AND GROUP DO NOT COVER REFUSES ───────────────────────────────── +// +// The derivation above is total: a dependent in the other class that needs Write gets o+w, because +// that is what it asked for. For a directory the fleet ensures that is the wrong answer -- a +// world-writable store is not a way to admit one more writer -- so a consumer that emits an ensure +// takes the directory through this admission, and a writer outside the owner and the group REFUSES +// with its name rather than widening the mode. The remedy the refusal points at is the model: +// make that writer the owner, or the directory's group principal, or (for a third writer with its +// own gid) a shared group, which this module does not model yet and so refuses rather than fakes. +type ManagedDirectoryAdmission + = ManagedDirectoryAdmitted { dir: ManagedDirectory } + | ManagedDirectoryRefused { member: NonEmptyStr, path: FilePath, writer: NonEmptyStr } + +fn managed_directory_admit(d: ManagedDirectory) -> ManagedDirectoryAdmission { + let g = d.group_principal.gid + let stray = filter( + xs: d.dependents, + predicate: dep => needs_verb(needs: dep.needs, v: Write) && dependent_class_eq( + a: dependent_class_of(owner: d.owner, group_gid: g, who: dep.who), + b: OtherClass, + ), + ) + match first(stray) { + Absent => ManagedDirectoryAdmitted { dir: d } + Present { value: dep } => ManagedDirectoryRefused { member: d.member, path: d.path, writer: dep.who.name } + } +} + +fn managed_directory_admission_path(a: ManagedDirectoryAdmission) -> FilePath { + match a { + ManagedDirectoryAdmitted { dir } => dir.path + ManagedDirectoryRefused { member: _, path, writer: _ } => path + } +} + fn managed_directory_mode_octal(d: ManagedDirectory) -> String { file_mode_octal(mode: managed_directory_permissions(d: d).mode) } @@ -232,6 +283,7 @@ fn attempt_state_directory_at(path: FilePath) -> ManagedDirectory { member: "attempt-state-root" as NonEmptyStr, path: path, owner: service, + group_principal: service, dependents: [ DirectoryDependent { who: service, needs: [Read, Write, Execute] }, ], diff --git a/dag/test/claim/devboot_subject_identity_witness_test.dag b/dag/test/claim/devboot_subject_identity_witness_test.dag index a5a63cd9e1d..57b9f9a2948 100644 --- a/dag/test/claim/devboot_subject_identity_witness_test.dag +++ b/dag/test/claim/devboot_subject_identity_witness_test.dag @@ -20,6 +20,7 @@ import gunbc.live_deploy.spec { ArtifactStep, DeploymentDependencyStep, EnsuredHostDirectory, + EnsuredManagedHostDirectory, EnsuredPackage, ensured_subject_identity, deployment_ensured_steps, @@ -727,6 +728,7 @@ fn devboot_ensured_step_projects_layout(acc: Int, step: DeploymentDependencyStep EnsuredHostDirectory { path, owner } => if (path as String) == (srv1_devboot_artifact_store_root as String) { acc + 1 } else { acc } EnsuredPackage { package } => acc + EnsuredManagedHostDirectory { admission: _ } => acc } } diff --git a/dag/test/claim/live_deploy/emit_test.dag b/dag/test/claim/live_deploy/emit_test.dag index 5615cc82650..536175a2394 100644 --- a/dag/test/claim/live_deploy/emit_test.dag +++ b/dag/test/claim/live_deploy/emit_test.dag @@ -22,6 +22,7 @@ import gunbc.live_deploy.spec { deployment_fabric_storage_steps, deployment_fabric_storage_serve_endpoint, EnsuredPackage, + EnsuredManagedHostDirectory, TailscalePackage, TmuxPackage, deployment_plan_host_identity, @@ -108,6 +109,7 @@ import gunbc.auth.approval_broker_endpoint { approval_broker_listen_host } import gunbc.host_layout { srv1_gunbc_approval_broker_root, srv1_gunbc_repo_root } import gunbc.live_deploy.spec { approval_broker_serve_binary_is_per_release_poison } import gunbc.live_deploy.emit { + ensure_managed_directory_command, approval_broker_tree_receipt_path_for, approval_broker_release_dir, approval_broker_release_binary_path, @@ -1272,6 +1274,30 @@ test fn the_fabric_storage_store_areas_reach_the_srv1_apply_path() -> Bool { srv1_ensures_directory(path: files.objects as String) && srv1_ensures_directory(path: files.heads as String) } +// THE AREAS ARE WRITABLE BY THE CLAIM PRINCIPAL, READ OFF THE ENSURE THE SRV1 SPEC ACTUALLY LOWERS. +// Owner the service principal, group the CI job principal's, 2770 with setgid: fleet-converge run +// 36139624393's host-effect claim refused `object publication refused: permission_denied` against +// the briansrls:briansrls 0755 areas the previous ensure created. Red if either area goes back to a +// single-owner ensure, or its admission refuses. +fn srv1_fabric_area_ensure(path: String) -> String { + fold(deployment_spec_srv1().steps, init: "", f: (acc, st) => match st { + Dependency { step: dep } => match dep.subject { + EnsuredManagedHostDirectory { admission } => + if (ensured_subject_identity(subject: dep.subject) as String) == path { ensure_managed_directory_command(admission: admission) } else { acc } + _ => acc + } + ArtifactStep { step: _ } => acc + }) +} + +test fn the_fabric_storage_areas_are_ensured_writable_by_the_claim_principal() -> Bool { + let files = fabric_storage_file_root(root: dashboard_instance_fabric_storage_root(instance: srv1_live_dashboard_instance()) as String as NonEmptyStr) + let objects = srv1_fabric_area_ensure(path: files.objects as String) + let heads = srv1_fabric_area_ensure(path: files.heads as String) + string_contains(s: objects, pattern: "2770") && string_contains(s: objects, pattern: "briansrls") && string_contains(s: objects, pattern: "ghrunner") && !string_contains(s: objects, pattern: "REFUSED") + && string_contains(s: heads, pattern: "2770") && string_contains(s: heads, pattern: "ghrunner") && !string_contains(s: heads, pattern: "REFUSED") +} + // THE LOG IS NOT DEPLOYMENT STATE: no retract may remove the store. Moving this root back into the // owned roster (where the event-log mirror root sat, torn down with rm -rf) turns this red. test fn no_retract_can_remove_the_fabric_storage_store() -> Bool { diff --git a/dag/test/claim/managed_directory_witness_test.dag b/dag/test/claim/managed_directory_witness_test.dag index 6238bfb921c..ecec9e12077 100644 --- a/dag/test/claim/managed_directory_witness_test.dag +++ b/dag/test/claim/managed_directory_witness_test.dag @@ -18,6 +18,8 @@ import gunbc.managed_directory { dependent_class_of, managed_directory_permissions, managed_directory_mode_octal, + ManagedDirectoryAdmission, ManagedDirectoryAdmitted, ManagedDirectoryRefused, + managed_directory_admit, } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -57,6 +59,7 @@ fn dir_with(dependents: List) -> ManagedDirectory { member: "synthetic", path: "/synthetic/root", owner: owner_user(), + group_principal: owner_user(), dependents: dependents, ownership: Owned, } @@ -71,15 +74,15 @@ fn class_label(c: DependentClass) -> String { } test fn witness_same_uid_is_owner_class() -> Bool { - class_label(c: dependent_class_of(owner: owner_user(), who: owner_user())) == "owner" + class_label(c: dependent_class_of(owner: owner_user(), group_gid: owner_user().gid, who: owner_user())) == "owner" } test fn witness_shared_gid_is_group_class() -> Bool { - class_label(c: dependent_class_of(owner: owner_user(), who: group_mate_user())) == "group" + class_label(c: dependent_class_of(owner: owner_user(), group_gid: owner_user().gid, who: group_mate_user())) == "group" } test fn witness_unrelated_principal_is_other_class() -> Bool { - class_label(c: dependent_class_of(owner: owner_user(), who: stranger_user())) == "other" + class_label(c: dependent_class_of(owner: owner_user(), group_gid: owner_user().gid, who: stranger_user())) == "other" } test fn witness_sole_owner_dependent_derives_0700() -> Bool { @@ -188,3 +191,47 @@ test fn an_unreadable_octal_mode_refuses_rather_than_reading_as_all_denied() -> Absent => false }) } + +// ── A SECOND WRITER'S GROUP, AND THE WRITER NOBODY COVERS ──────────────────────────────────────── +// +// The fabric store's shape on synthetic principals: the owner writes, and one writer with its own +// gid writes too. Naming that writer as the group principal admits it as group and sets setgid +// (2770); leaving the group at the owner's puts it in the other class, and admission REFUSES naming +// it -- the model-level RED for an ensure the claim principal could not write. The derivation alone +// would have granted the other class write (0707) there, which is the world-writable arm the admission exists to close. + +fn writers_dir(group: PosixUser) -> ManagedDirectory { + ManagedDirectory { + member: "synthetic-store", + path: "/synthetic/store", + owner: owner_user(), + group_principal: group, + dependents: [ + DirectoryDependent { who: owner_user(), needs: [Read, Write, Execute] }, + DirectoryDependent { who: stranger_user(), needs: [Read, Write, Execute] }, + ], + ownership: Owned, + } +} + +test fn witness_second_writer_group_derives_setgid_2770() -> Bool { + managed_directory_mode_octal(d: writers_dir(group: stranger_user())) == "2770" +} + +test fn witness_second_writer_group_is_admitted() -> Bool { + match managed_directory_admit(d: writers_dir(group: stranger_user())) { + ManagedDirectoryAdmitted { dir: _ } => true + ManagedDirectoryRefused { member: _, path: _, writer: _ } => false + } +} + +test fn witness_uncovered_writer_refuses_naming_it() -> Bool { + match managed_directory_admit(d: writers_dir(group: owner_user())) { + ManagedDirectoryAdmitted { dir: _ } => false + ManagedDirectoryRefused { member: _, path: _, writer } => (writer as String) == "outsider" + } +} + +test fn witness_uncovered_writer_would_have_derived_world_write() -> Bool { + managed_directory_permissions(d: writers_dir(group: owner_user())).mode.other.write +}