authelia: a user outside a rule's group is denied instead of falling through to one_factor
authelia takes the first matching rule, and a rule with a `subject` does not match a subject outside it. The swarm domain's and the bao UI's `group:admins` rules had nothing after them, so an account in no group skipped them and landed on `default_policy = one_factor`: any authenticated account reached the swarm UI, its `/api/` (by cookie or by bearer) and the bao UI. Each of the two group rules is now an `operatorsOnly` pair: the group allow, then a subject-less `deny` for the same domain, the shape `collectorOnly` already gives the collector's metrics rules. `default_policy` stays `one_factor`, which the forge and chat rely on. module-eval-swarm-otel-identity gains two cases on `allLocal`: an account in no group is refused the swarm domain (four paths) and the bao UI while `group:admins` passes and the forge stays open; and a predicate, `autheliaSubjectRulesClosed` in lib.nix, holds over the rendered list: every subject-scoped allow is closed by a later subject-less `deny` on its domain with no subject-less allow in between, and nothing follows a domain-wide `deny` on its domain. Fixes #5120
This commit is contained in:
parent
b99ce2b572
commit
4a9acd1d0c
4 changed files with 194 additions and 15 deletions
|
|
@ -21,6 +21,8 @@ let
|
|||
hive
|
||||
runGroup
|
||||
otelSettings
|
||||
autheliaRules
|
||||
autheliaSubjectRulesClosed
|
||||
;
|
||||
|
||||
# A collector holding no store identity at all. Unlike Grafana's mirror
|
||||
|
|
@ -640,6 +642,123 @@ let
|
|||
in
|
||||
!(lib.any (n: lib.elem n (u.after ++ u.wants ++ u.requires)) storeUnits);
|
||||
}
|
||||
{
|
||||
# An account in no group, on the swarm UI and the store's UI. A group
|
||||
# rule alone does not refuse it: it skips the rule and lands on
|
||||
# `default_policy`. `swarmctl --swarm`'s bearer is an operator's own
|
||||
# token, so it is the `group:admins` arm here. The forge has no rule and
|
||||
# stays open to any session.
|
||||
name = "authelia refuses an account outside the operator group on the swarm UI and the store's UI";
|
||||
ok =
|
||||
let
|
||||
h = allLocal.services.hyperhive;
|
||||
ui = h.swarm.bao.ui.domain;
|
||||
policy = autheliaPolicy allLocal;
|
||||
paths = [
|
||||
"/"
|
||||
"/api/agents"
|
||||
"/api/whoami"
|
||||
"/api/docs/"
|
||||
];
|
||||
in
|
||||
h.deploy.swarm-controller.enable
|
||||
&& h.swarm.domain == "t.local"
|
||||
&& ui == "bao-ui.t.local"
|
||||
&& lib.all (p: policy "user:nobody" "t.local" p == "deny") paths
|
||||
&& policy "user:nobody" ui "/" == "deny"
|
||||
&& lib.all (p: policy "group:admins" "t.local" p == "one_factor") paths
|
||||
&& policy "group:admins" ui "/" == "one_factor"
|
||||
&& policy "user:nobody" "forge.t.local" "/" == "one_factor";
|
||||
}
|
||||
{
|
||||
# Every subject-scoped rule the host renders, read in order. The `elem`
|
||||
# clauses are the control for the predicate's vacuous case; the
|
||||
# synthetic lists pin what it accepts and refuses, the first of them
|
||||
# being the real rules with the operator denies cut out.
|
||||
name = "every subject-scoped authelia rule is closed by a deny on its domain";
|
||||
ok =
|
||||
let
|
||||
rules = autheliaRules allLocal;
|
||||
scoped = lib.filter (r: r ? subject) rules;
|
||||
closed = autheliaSubjectRulesClosed;
|
||||
rejects = rs: !(closed rs);
|
||||
allow = {
|
||||
domain = "t.local";
|
||||
subject = [ "group:admins" ];
|
||||
policy = "one_factor";
|
||||
};
|
||||
deny = {
|
||||
domain = "t.local";
|
||||
policy = "deny";
|
||||
};
|
||||
narrow = deny // {
|
||||
resources = [ "^/m$" ];
|
||||
};
|
||||
operatorDeny =
|
||||
r:
|
||||
r.policy == "deny"
|
||||
&& !(r ? subject)
|
||||
&& !(r ? resources)
|
||||
&& lib.elem r.domain [
|
||||
"t.local"
|
||||
"bao-ui.t.local"
|
||||
];
|
||||
in
|
||||
closed rules
|
||||
&& lib.elem allow scoped
|
||||
&& lib.elem (allow // { domain = "bao-ui.t.local"; }) scoped
|
||||
&& lib.any (
|
||||
r: r.subject == [ "oauth2:client:${allLocal.services.hyperhive.swarm.otel.clientId}" ]
|
||||
) scoped
|
||||
&& rejects (lib.filter (r: !(operatorDeny r)) rules)
|
||||
&& closed [
|
||||
allow
|
||||
deny
|
||||
]
|
||||
# a rule for anyone needs no deny behind it
|
||||
&& closed [ (removeAttrs allow [ "subject" ]) ]
|
||||
&& closed [
|
||||
(allow // { resources = narrow.resources; })
|
||||
narrow
|
||||
]
|
||||
# a narrower deny between two allows refuses everyone it matches
|
||||
&& closed [
|
||||
(allow // { subject = [ "oauth2:client:c" ]; })
|
||||
narrow
|
||||
allow
|
||||
deny
|
||||
]
|
||||
&& rejects [ allow ]
|
||||
&& rejects [
|
||||
allow
|
||||
narrow
|
||||
]
|
||||
&& rejects [
|
||||
allow
|
||||
(deny // { domain = "other.local"; })
|
||||
]
|
||||
&& rejects [
|
||||
allow
|
||||
(deny // { subject = [ "user:x" ]; })
|
||||
]
|
||||
# a subject-less allow in between admits everyone first
|
||||
&& rejects [
|
||||
allow
|
||||
(deny // { policy = "one_factor"; })
|
||||
deny
|
||||
]
|
||||
# nothing reaches a rule after a domain-wide deny
|
||||
&& rejects [
|
||||
deny
|
||||
allow
|
||||
]
|
||||
&& rejects [
|
||||
allow
|
||||
deny
|
||||
(allow // { subject = [ "group:other" ]; })
|
||||
deny
|
||||
];
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "swarm-otel-identity" cases
|
||||
|
|
|
|||
Loading…
Reference in a new issue