Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions modules/den/batteries/nix-on-droid.nix
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,20 @@ in
# modules below, and an mkForce on home.username/homeDirectory in
# core.nix-on-droid-base. All three keep the stock den batteries untouched, so
# nixos/darwin hosts stay byte-identical.)
den.policies.drop-user-to-host-on-droid =
{ host, ... }:
lib.optional (host.class == "droid") (den.lib.policy.exclude den.policies.user-to-host);
# Written as a policy record rather than a bare function so it can carry
# `suppresses`. The exclusion is value-conditional — it fires only where the host
# is droid — so a body fired at a sentinel context takes the false branch and
# produces no suppression at all. The suppression codomain therefore cannot be
# observed by firing the body and has to be stated here; the stratification reads
# suppression edges from the declared graph, and an empty recovery would let the
# first real exclusion be refused.
den.policies.drop-user-to-host-on-droid = {
__isPolicy = true;
suppresses = [ "user-to-host" ];
fn =
{ host, ... }:
lib.optional (host.class == "droid") (den.lib.policy.exclude den.policies.user-to-host);
};

# Registered at default scope (not host scope) to mirror den's os-class.nix:
# user content is emitted at host AND user scope, so the exclude must fire in
Expand Down
82 changes: 47 additions & 35 deletions modules/den/policies/fleet.nix
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,54 @@ in
) environments;

# environment -> hosts: walk den.hosts whose environment matches.
den.policies.env-to-hosts =
{ environment, ... }:
let
inherit (config) fleet;
envGrant = (fleet.user-access.by-environment.${environment.name} or { groups = [ ]; }).groups;
envGate = environment.system-access-groups or [ ];
in
lib.concatMap (
system:
#
# Written as a policy record rather than a bare function so it can carry `binds`.
# The `accessGroups` member binding below is emitted from a value-conditional body,
# and a body fired at a sentinel context takes the false branch and carries no
# binding — so this codomain cannot be observed by firing and has to be declared.
# A containment binding is a positive dependency edge of every policy that
# destructures it (env-users), and the stratification is decided from the declared
# graph, so an edge known only at runtime is one the check never sees.
den.policies.env-to-hosts = {
__isPolicy = true;
binds = [ "accessGroups" ];
fn =
{ environment, ... }:
let
inherit (config) fleet;
envGrant = (fleet.user-access.by-environment.${environment.name} or { groups = [ ]; }).groups;
envGate = environment.system-access-groups or [ ];
in
lib.concatMap (
hostName:
let
hostCfg = den.hosts.${system}.${hostName};
hostGrant = (fleet.user-access.by-host.${hostName} or { groups = [ ]; }).groups;
hostGate = hostCfg.system-access-groups;
# Effective gate: union of env + host gates (matching main's mergedAccessGroups)
effectiveGate = lib.unique (envGate ++ hostGate);
# Effective grant: union of env + host grants + host gates
# (host system-access-groups is both a gate and an implicit grant)
allGrants = lib.unique (envGrant ++ hostGrant ++ hostGate);
# Users must match both a grant AND a gate group
accessGroups =
if effectiveGate == [ ] then
allGrants
else
builtins.filter (g: builtins.elem g effectiveGate) allGrants;
in
lib.optionals ((hostCfg.environment or "prod") == environment.name && hostCfg.intoAttr != [ ]) [
(resolve.to "host" {
host = hostCfg;
inherit accessGroups;
})
(den.lib.policy.instantiate hostCfg)
]
) (builtins.attrNames (den.hosts.${system} or { }))
) (builtins.attrNames (den.hosts or { }));
system:
lib.concatMap (
hostName:
let
hostCfg = den.hosts.${system}.${hostName};
hostGrant = (fleet.user-access.by-host.${hostName} or { groups = [ ]; }).groups;
hostGate = hostCfg.system-access-groups;
# Effective gate: union of env + host gates (matching main's mergedAccessGroups)
effectiveGate = lib.unique (envGate ++ hostGate);
# Effective grant: union of env + host grants + host gates
# (host system-access-groups is both a gate and an implicit grant)
allGrants = lib.unique (envGrant ++ hostGrant ++ hostGate);
# Users must match both a grant AND a gate group
accessGroups =
if effectiveGate == [ ] then
allGrants
else
builtins.filter (g: builtins.elem g effectiveGate) allGrants;
in
lib.optionals ((hostCfg.environment or "prod") == environment.name && hostCfg.intoAttr != [ ]) [
(resolve.to "host" {
host = hostCfg;
inherit accessGroups;
})
(den.lib.policy.instantiate hostCfg)
]
) (builtins.attrNames (den.hosts.${system} or { }))
) (builtins.attrNames (den.hosts or { }));
};

# Schema wiring.
den.schema.flake.includes = [ den.policies.to-fleet ];
Expand Down