domain/architecture/system/deployment/azure/effects.bend

near-v-framework@0.4.0.0 · 131 lines · 18 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.4.0.0/domain/architecture/system/effs/io.bend as X
5import near-v-framework@0.4.0.0/domain/architecture/system/plan/type.bend as P
6import near-v-framework@0.4.0.0/domain/architecture/system/deployment/azure/type.bend as Az
7import near-v-framework@0.4.0.0/domain/architecture/system/deployment/azure/ops.bend as AzO
8import near-v-framework@0.4.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)