authz.bend

bend-kit-authz@0.1.0.0 · 193 lines · 20 definitions

1# Pure authorization policies: deny-overrides, default deny, intersection, and fail-closed missing context.
2import Base
3
4# What a policy may ask about one request. A `None` field is missing information:
5# the caller did not authenticate a subject, load roles, or find the owner.
6type Request is Data:
7  Request{subject: Maybe<&2, String>, roles: Maybe<&2, List<&2, String>>, owner: Maybe<&2, String>, action: String, resource: String}
8
9# A target answers Yes, No, or Unknown when it needs a missing field.
10type Tri is Data:
11  Yes{}
12  No{}
13  Unknown{}
14
15type Target is Data:
16  Everyone{}
17  Nobody{}
18  Action{name: String}
19  Resource{name: String}
20  Subject{name: String}
21  Role{name: String}
22  Owner{}
23  AllOf{a: Target, b: Target}
24
25# Abstain means that no rule applies. `granted` treats it as a denial.
26type Decision is Data:
27  Allow{}
28  Deny{}
29  Abstain{}
30
31type Policy is Data:
32  Permit{target: Target}
33  Forbid{target: Target}
34  DenyOverrides{a: Policy, b: Policy}
35  Intersect{a: Policy, b: Policy}
36
37def tri(b: Bool) -> Tri:
38  match b:
39    case True{}:
40      Yes{}
41    case False{}:
42      No{}
43
44def has(+name: String, xs: List<&2, String>) -> Bool:
45  match xs:
46    case Nil{}:
47      False{}
48    case x <> rest:
49      String.eq(x, name) || has(name, rest)
50
51def subject.is(name: String, s: Maybe<&2, String>) -> Tri:
52  match s:
53    case None{}:
54      Unknown{}
55    case Some{x}:
56      tri(String.eq(x, name))
57
58def role.is(name: String, r: Maybe<&2, List<&2, String>>) -> Tri:
59  match r:
60    case None{}:
61      Unknown{}
62    case Some{xs}:
63      tri(has(name, xs))
64
65def owner.is(x: String, o: Maybe<&2, String>) -> Tri:
66  match o:
67    case None{}:
68      Unknown{}
69    case Some{y}:
70      tri(String.eq(x, y))
71
72def owner.of(s: Maybe<&2, String>, o: Maybe<&2, String>) -> Tri:
73  match s:
74    case None{}:
75      Unknown{}
76    case Some{x}:
77      owner.is(x, o)
78
79# Kleene conjunction: No wins, then Unknown.
80def all(a: Tri, b: Tri) -> Tri:
81  match a:
82    case No{}:
83      No{}
84    case Yes{}:
85      b
86    case Unknown{}:
87      match b:
88        case No{}:
89          No{}
90        case Yes{}:
91          Unknown{}
92        case Unknown{}:
93          Unknown{}
94
95def matches(t: Target, +req: Request) -> Tri:
96  match t:
97    case Everyone{}:
98      Yes{}
99    case Nobody{}:
100      No{}
101    case Action{name}:
102      Request{s, r, o, action, resource} = req
103      tri(String.eq(action, name))
104    case Resource{name}:
105      Request{s, r, o, action, resource} = req
106      tri(String.eq(resource, name))
107    case Subject{name}:
108      Request{s, r, o, action, resource} = req
109      subject.is(name, s)
110    case Role{name}:
111      Request{s, r, o, action, resource} = req
112      role.is(name, r)
113    case Owner{}:
114      Request{s, r, o, action, resource} = req
115      owner.of(s, o)
116    case AllOf{a, b}:
117      all(matches(a, req), matches(b, req))
118
119# A permit grants only on Yes. Unknown cannot grant.
120def permit(m: Tri) -> Decision:
121  match m:
122    case Yes{}:
123      Allow{}
124    case No{}:
125      Abstain{}
126    case Unknown{}:
127      Abstain{}
128
129# A forbid denies on Yes and on Unknown: it fails closed.
130def forbid(m: Tri) -> Decision:
131  match m:
132    case Yes{}:
133      Deny{}
134    case No{}:
135      Abstain{}
136    case Unknown{}:
137      Deny{}
138
139# Deny wins, then Allow, then Abstain.
140def deny_overrides(a: Decision, b: Decision) -> Decision:
141  match a:
142    case Deny{}:
143      Deny{}
144    case Abstain{}:
145      b
146    case Allow{}:
147      match b:
148        case Deny{}:
149          Deny{}
150        case Allow{}:
151          Allow{}
152        case Abstain{}:
153          Allow{}
154
155# Allow only when both allow. Deny wins over Abstain.
156def intersect(a: Decision, b: Decision) -> Decision:
157  match a:
158    case Deny{}:
159      Deny{}
160    case Allow{}:
161      b
162    case Abstain{}:
163      match b:
164        case Deny{}:
165          Deny{}
166        case Allow{}:
167          Abstain{}
168        case Abstain{}:
169          Abstain{}
170
171def decide(p: Policy, +req: Request) -> Decision:
172  match p:
173    case Permit{t}:
174      permit(matches(t, req))
175    case Forbid{t}:
176      forbid(matches(t, req))
177    case DenyOverrides{a, b}:
178      deny_overrides(decide(a, req), decide(b, req))
179    case Intersect{a, b}:
180      intersect(decide(a, req), decide(b, req))
181
182def allowed(d: Decision) -> Bool:
183  match d:
184    case Allow{}:
185      True{}
186    case Deny{}:
187      False{}
188    case Abstain{}:
189      False{}
190
191# Default deny: only Allow grants.
192def granted(p: Policy, +req: Request) -> Bool:
193  allowed(decide(p, req))