1 import Base 2 import bend-net-json@0.3.0.0/json.bend as Json 3 import bend-kit-files@0.1.0.0/files.bend as Fs 4 import ../../effs/io.bend as X 5 import ../../plan/type.bend as P 6 import ./type.bend as Az 7 import ./ops.bend as AzO 8 import ../../../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 ---- 15 def value(m: Maybe<&2, Json.Val>) -> Json.Val: 16 match m: 17 case None{}: 18 Json.Null{} 19 case Some{v}: 20 v 21 22 def at(v: Json.Val, k: String) -> Json.Val: 23 value(Json.get(v, k)) 24 25 def string(v: Json.Val) -> String: 26 match v: 27 case Json.Str{s}: 28 s 29 case _: 30 "" 31 32 def array(v: Json.Val) -> List<&2, Json.Val>: 33 match v: 34 case Json.Arr{xs}: 35 xs 36 case _: 37 [] 38 39 def 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. 47 def 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. 56 def az_run(args: List<&2, String>) -> IO(String): 57 X.run("env", "AZURE_LOGGING_ENABLE_LOG_FILE=false" <> "az" <> args) 58 59 def resource(+v: Json.Val) -> Az.Resource: 60 Az.Resource{string(at(v, "name")), string(at(v, "type")), string(at(v, "resourceGroup"))} 61 62 def 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 69 def permission(scope: String, +v: Json.Val) -> Az.Permission: 70 Az.Permission{scope, strings(array(at(v, "actions"))), strings(array(at(v, "notActions")))} 71 72 def 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 79 def 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. 85 def 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: 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. 96 def devops() -> IO(String): 97 do IO: 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 102 def 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 105 def 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: 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. 122 def observe() -> IO(Az.Observed): 123 do IO: 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). 128 def checks(az: Az.Azure, a: G.Agreement) -> IO(List<&2, P.Step>): 129 do IO>: 130 o : Az.Observed <- observe() 131 return AzO.steps(az, a, o)