domain/architecture/system/deployment/azure/ops.bend
near-v-framework@0.6.0.0 · 197 lines · 0 definitions
1import Base
2import near-v-framework@0.6.0.0/domain/architecture/system/deployment/azure/type.bend as Az
3import near-v-framework@0.6.0.0/domain/architecture/system/plan/type.bend as P
4import near-v-framework@0.6.0.0/domain/architecture/environment/agreement/type.bend as G
5import near-v-framework@0.6.0.0/domain/architecture/environment/agreement/ops.bend as GO
6
7def keep_if(c: Bool, x: String, rest: List<&2, String>) -> List<&2, String>:
8 match c:
9 case True{}:
10 x <> rest
11 case False{}:
12 rest
13
14# ---- resources: what Azure has against what the system declares ----
15def same(a: Az.Resource, b: Az.Resource) -> Bool:
16 match a b:
17 case Az.Resource{an, ak, ag} Az.Resource{bn, bk, bg}:
18 String.eq(an, bn) && String.eq(ak, bk) && String.eq(ag, bg)
19
20def listed(+r: Az.Resource, rs: List<&2, Az.Resource>) -> Bool:
21 match rs:
22 case []:
23 False{}
24 case x <> rest:
25 same(r, x) || listed(r, rest)
26
27def keep_resource(c: Bool, r: Az.Resource, rest: List<&2, Az.Resource>) -> List<&2, Az.Resource>:
28 match c:
29 case True{}:
30 r <> rest
31 case False{}:
32 rest
33
34# The resources in `xs` that are not in `ys` (same name, type and resource group).
35def absent(xs: List<&2, Az.Resource>, +ys: List<&2, Az.Resource>) -> List<&2, Az.Resource>:
36 match xs:
37 case []:
38 []
39 case +r <> rest:
40 keep_resource(Bool.not(listed(r, ys)), r, absent(rest, ys))
41
42# What Azure has that the system does not declare.
43def undeclared(observed: List<&2, Az.Resource>, +declared: List<&2, Az.Resource>) -> List<&2, Az.Resource>:
44 absent(observed, declared)
45
46# What the system declares that Azure does not have.
47def missing(declared: List<&2, Az.Resource>, +observed: List<&2, Az.Resource>) -> List<&2, Az.Resource>:
48 absent(declared, observed)
49
50def show(r: Az.Resource) -> String:
51 match r:
52 case Az.Resource{n, k, g}:
53 n ++ " (" ++ k ++ " in " ++ g ++ ")"
54
55def shown(rs: List<&2, Az.Resource>) -> List<&2, String>:
56 match rs:
57 case []:
58 []
59 case r <> rest:
60 show(r) <> shown(rest)
61
62# ---- permissions ----
63# ARM patterns use '*' for zero or more characters; matching is case insensitive.
64def glob_chars(+ps: List<&2, Char>, +vs: List<&2, Char>) -> Bool:
65 match ps:
66 case []:
67 match vs:
68 case []:
69 True{}
70 case _ <> _:
71 False{}
72 case p <> rest:
73 match p:
74 case '*':
75 match vs:
76 case []:
77 glob_chars(rest, [])
78 case v <> tail:
79 glob_chars(rest, v <> tail) || glob_chars('*' <> rest, tail)
80 case _:
81 match vs:
82 case []:
83 False{}
84 case v <> tail:
85 Char.is_eq(p, v) && glob_chars(rest, tail)
86
87def glob(pattern: String, value: String) -> Bool:
88 glob_chars(String.to_list(String.to_lower(pattern)), String.to_list(String.to_lower(value)))
89
90# A wildcard pattern is covered only by the same pattern or '*'. This conservative rule avoids claiming
91# that overlapping wildcards grant exactly equal permissions.
92def covers(+want: String, +actual: String) -> Bool:
93 String.eq(String.to_lower(want), String.to_lower(actual)) ||
94 String.eq(want, "*") ||
95 (Bool.not(String.contains(actual, "*")) && glob(want, actual))
96
97def covered(+action: String, patterns: List<&2, String>) -> Bool:
98 match patterns:
99 case []:
100 False{}
101 case p <> rest:
102 covers(p, action) || covered(action, rest)
103
104def every_covered(xs: List<&2, String>, +by: List<&2, String>) -> Bool:
105 match xs:
106 case []:
107 True{}
108 case x <> rest:
109 covered(x, by) && every_covered(rest, by)
110
111def actions_if(hit: Bool, acts: List<&2, String>, rest: List<&2, String>) -> List<&2, String>:
112 match hit:
113 case True{}:
114 List.append(&2, String, acts, rest)
115 case False{}:
116 rest
117
118def scope_actions(+scope: String, xs: List<&2, Az.Permission>) -> List<&2, String>:
119 match xs:
120 case []:
121 []
122 case Az.Permission{+s, acts, _} <> rest:
123 actions_if(String.eq(scope, s), acts, scope_actions(scope, rest))
124
125def no_exclusions(+scope: String, xs: List<&2, Az.Permission>) -> Bool:
126 match xs:
127 case []:
128 True{}
129 case Az.Permission{+s, _, exclusions} <> rest:
130 (Bool.not(String.eq(scope, s)) || List.is_empty(&2, String, exclusions)) && no_exclusions(scope, rest)
131
132# Whether the permissions on `scope` are exactly `want`, with nothing excluded.
133def exact(+scope: String, +want: List<&2, String>, +actual: List<&2, Az.Permission>) -> Bool:
134 +got = scope_actions(scope, actual)
135 no_exclusions(scope, actual) && every_covered(got, want) && every_covered(want, got)
136
137# The resource groups where `person` has other than exactly what the agreement grants there (nothing
138# granted means no access at all).
139def not_minimal(groups: List<&2, String>, +a: G.Agreement, +person: String, +actual: List<&2, Az.Permission>) -> List<&2, String>:
140 match groups:
141 case []:
142 []
143 case +g <> rest:
144 keep_if(Bool.not(exact(g, GO.granted(a, person, g), actual)), g, not_minimal(rest, a, person, actual))
145
146# Permission minimality: on every resource group, `person` has exactly what the agreement grants there.
147def access_minimal(groups: List<&2, String>, +a: G.Agreement, +person: String, +actual: List<&2, Az.Permission>) -> Bool:
148 List.is_empty(&2, String, not_minimal(groups, a, person, actual))
149
150# ---- identities ----
151def identity_names(xs: List<&2, Az.Identity>) -> List<&2, String>:
152 match xs:
153 case []:
154 []
155 case Az.Identity{_, name} <> rest:
156 name <> identity_names(rest)
157
158# Who is signed in with this profile, or "" when no one is.
159def signed_in(+profile: String, xs: List<&2, Az.Identity>) -> String:
160 match xs:
161 case []:
162 ""
163 case Az.Identity{+p, name} <> rest:
164 Bool.pick(String, String.eq(profile, p), name, signed_in(profile, rest))
165
166# ---- the steps ----
167def blocked_if(+what: String, +xs: List<&2, String>, why: String) -> List<&2, P.Step>:
168 Bool.pick(List<&2, P.Step>, List.is_empty(&2, String, xs), [], [P.Blocked{what, why ++ String.join(xs, ", ")}])
169
170def verdict(+what: String, +bs: List<&2, P.Step>) -> List<&2, P.Step>:
171 Bool.pick(List<&2, P.Step>, List.is_empty(&2, P.Step, bs), [P.Current{what}], bs)
172
173def domain(a: G.Agreement) -> String:
174 match a:
175 case G.Agreement{d, _, _, _}:
176 d
177
178def judged(+what: String, +a: G.Agreement, +declared: List<&2, Az.Resource>, o: Az.Observed) -> List<&2, P.Step>:
179 match o:
180 case Az.Observed{+ids, +rs, gs, ps, +unread}:
181 +user = signed_in("azure", ids)
182 Bool.pick(List<&2, P.Step>, List.is_empty(&2, String, unread),
183 verdict(what, List.concat(&2, P.Step, [
184 blocked_if(what, shown(undeclared(rs, declared)), "undeclared resources: "),
185 blocked_if(what, shown(missing(declared, rs)), "declared resources missing: "),
186 blocked_if(what, GO.unknown_identities(identity_names(ids), a), "identities not in the agreement: "),
187 blocked_if(what, GO.outside_domain(a), "people outside the directory domain " ++ domain(a) ++ ": "),
188 blocked_if(what, not_minimal(gs, a, GO.person_of(a, user), ps), user ++ " has other access than the agreement grants on: ")])),
189 [P.Blocked{what, "cannot read " ++ String.join(unread, ", ") ++ " (is az installed and signed in?)"}])
190
191# What Azure as observed means for the system: Current when every resource is declared and every
192# declared resource exists, every identity in use is one of the agreement's people, every person is in
193# its directory domain, and the signed-in user has exactly what the agreement grants; else Blocked.
194def steps(az: Az.Azure, +a: G.Agreement, o: Az.Observed) -> List<&2, P.Step>:
195 match az:
196 case Az.Azure{org, project, declared}:
197 judged("azure " ++ org ++ " " ++ project, a, declared, o)