domain/architecture/system/deployment/azure/effects.bend
near-v-framework@0.5.0.0 · 131 lines · 0 definitions
1import Base
2import bend-net-json@0.3.0.0/json.bend as Json
3import bend-kit-files@0.1.0.0/files.bend as Fs
4import near-v-framework@0.5.0.0/domain/architecture/system/effs/io.bend as X
5import near-v-framework@0.5.0.0/domain/architecture/system/plan/type.bend as P
6import near-v-framework@0.5.0.0/domain/architecture/system/deployment/azure/type.bend as Az
7import near-v-framework@0.5.0.0/domain/architecture/system/deployment/azure/ops.bend as AzO
8import near-v-framework@0.5.0.0/domain/architecture/environment/agreement/type.bend as G
9
10# Azure, observed read-only: az's read commands with narrow projections, and the Azure DevOps identity
11# from the az-devops profile. az runs through env, never a shell, with its command log file disabled so
12# even metadata stays out of files.
13
14# ---- JSON ----
15def value(m: Maybe<&2, Json.Val>) -> Json.Val:
16 match m:
17 case None{}:
18 Json.Null{}
19 case Some{v}:
20 v
21
22def at(v: Json.Val, k: String) -> Json.Val:
23 value(Json.get(v, k))
24
25def string(v: Json.Val) -> String:
26 match v:
27 case Json.Str{s}:
28 s
29 case _:
30 ""
31
32def array(v: Json.Val) -> List<&2, Json.Val>:
33 match v:
34 case Json.Arr{xs}:
35 xs
36 case _:
37 []
38
39def strings(xs: List<&2, Json.Val>) -> List<&2, String>:
40 match xs:
41 case []:
42 []
43 case x <> rest:
44 string(x) <> strings(rest)
45
46# [what] when its JSON could not be read.
47def unread_if(what: String, m: Maybe<&2, Json.Val>) -> List<&2, String>:
48 match m:
49 case None{}:
50 [what]
51 case Some{_}:
52 []
53
54# ---- reading Azure ----
55# az's standard output for these arguments, or "" when it fails.
56def az_run(args: List<&2, String>) -> IO(String):
57 X.run("env", "AZURE_LOGGING_ENABLE_LOG_FILE=false" <> "az" <> args)
58
59def resource(+v: Json.Val) -> Az.Resource:
60 Az.Resource{string(at(v, "name")), string(at(v, "type")), string(at(v, "resourceGroup"))}
61
62def resources(xs: List<&2, Json.Val>) -> List<&2, Az.Resource>:
63 match xs:
64 case []:
65 []
66 case x <> rest:
67 resource(x) <> resources(rest)
68
69def permission(scope: String, +v: Json.Val) -> Az.Permission:
70 Az.Permission{scope, strings(array(at(v, "actions"))), strings(array(at(v, "notActions")))}
71
72def permissions_of(+scope: String, xs: List<&2, Json.Val>) -> List<&2, Az.Permission>:
73 match xs:
74 case []:
75 []
76 case x <> rest:
77 permission(scope, x) <> permissions_of(scope, rest)
78
79def add(o: Az.Observed, ps: List<&2, Az.Permission>, unread: List<&2, String>) -> Az.Observed:
80 match o:
81 case Az.Observed{ids, rs, gs, qs, us}:
82 Az.Observed{ids, rs, gs, List.append(&2, Az.Permission, qs, ps), List.append(&2, String, us, unread)}
83
84# The signed-in user's effective permissions on each resource group.
85def permissions(+subscription: String, gs: List<&2, String>, o: Az.Observed) -> IO(Az.Observed):
86 match gs:
87 case []:
88 IO.pure(Az.Observed, o)
89 case +g <> rest:
90 do IO<Az.Observed>:
91 out : String <- az_run(["rest", "--method", "get", "--url", "https://management.azure.com/subscriptions/" ++ subscription ++ "/resourceGroups/" ++ g ++ "/providers/Microsoft.Authorization/permissions?api-version=2015-07-01", "--query", "value[].{actions:actions,notActions:notActions}", "-o", "json"])
92 +m : Maybe<&2, Json.Val> = Json.parse(out)
93 permissions(subscription, rest, add(o, permissions_of(g, array(value(m))), unread_if("permissions on " ++ g, m)))
94
95# Who az-devops is signed in as, or "" when it has no profile.
96def devops() -> IO(String):
97 do IO<String>:
98 h : String <- X.home()
99 text : String <- X.read(Fs.path.join(h, ".config/azure-plugin/profiles.json"))
100 return string(at(at(value(Json.parse(text)), "devops"), "username"))
101
102def identities(user: String, +devops: String) -> List<&2, Az.Identity>:
103 Az.Identity{"azure", user} <> Bool.pick(List<&2, Az.Identity>, String.is_empty(devops), [], [Az.Identity{"devops", devops}])
104
105def signed(m: Maybe<&2, Json.Val>) -> IO(Az.Observed):
106 match m:
107 case None{}:
108 IO.pure(Az.Observed, Az.Observed{[], [], [], [], ["the signed-in Azure account"]})
109 case Some{+account}:
110 do IO<Az.Observed>:
111 rs : String <- az_run(["resource", "list", "--query", "[].{name:name,type:type,resourceGroup:resourceGroup}", "-o", "json"])
112 gs : String <- az_run(["group", "list", "--query", "[].name", "-o", "json"])
113 d : String <- devops()
114 +mr : Maybe<&2, Json.Val> = Json.parse(rs)
115 +mg : Maybe<&2, Json.Val> = Json.parse(gs)
116 +groups : List<&2, String> = strings(array(value(mg)))
117 permissions(string(at(account, "id")), groups,
118 Az.Observed{identities(string(at(account, "name")), d), resources(array(value(mr))), groups, [],
119 List.append(&2, String, unread_if("Azure resources", mr), unread_if("resource groups", mg))})
120
121# Azure as it is: who is signed in, its resources and groups, and the signed-in user's permissions.
122def observe() -> IO(Az.Observed):
123 do IO<Az.Observed>:
124 account : String <- az_run(["account", "show", "--query", "{name:user.name,id:id}", "-o", "json"])
125 signed(Json.parse(account))
126
127# The Azure checks against the declared resources and the agreement (see AzO.steps).
128def checks(az: Az.Azure, a: G.Agreement) -> IO(List<&2, P.Step>):
129 do IO<List<&2, P.Step>>:
130 o : Az.Observed <- observe()
131 return AzO.steps(az, a, o)