1 import Base 2 import ../json.bend as Json 3 import ../act.bend as Act 4 import ../fs.bend as Fs 5 import ../lock.bend as Lock 6 import ../store/base.bend as SBase 7 import ../store/seq.bend as SSeq 8 import ../store/io.bend as SIo 9 10 # autoswitch.py _note_token_identity: the refresh grant that just ran names who 11 # the stored credential really is. A slot whose credential belongs to another 12 # organization or account is not a safe target (identity-conflict); a slot with 13 # no recorded account uuid learns it (never an existing one, and not when the 14 # organization already disagrees). 15 16 def ActM(-A: Type) -> Type: 17 Act.of(A) 18 19 def ActM.pure(-A: Type, x: A) -> Act.of(A): 20 Act.pure(A, x) 21 22 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 23 Act.bind(A, B, m, f) 24 25 def str_of(+j: Json.Json, +k: String) -> String: 26 String.trim(SSeq.field_str(j, k)) 27 28 def rec_of(+m: Maybe<&2, Json.Json>, +slot: String) -> Json.Json: 29 match m: 30 case None{}: 31 Json.JObj{Json.JNil{}} 32 case Some{seq}: 33 Maybe.default(&2, Json.Json, SSeq.slot_record(seq, slot), Json.JObj{Json.JNil{}}) 34 35 def fill2(+empty: Bool, +seq: Json.Json, +slot: String, +uuid: String, +ts: String) -> Json.Json: 36 match empty: 37 case False{}: 38 seq 39 case True{}: 40 SSeq.stamp(Json.set("accounts", Json.set(slot, Json.set("uuid", Json.JStr{uuid}, Maybe.default(&2, Json.Json, SSeq.slot_record(seq, slot), Json.JObj{Json.JNil{}})), SSeq.accounts_of(seq)), seq), ts) 41 42 # Fill an empty uuid, once, under the roster lock (switcher.backfill_account_uuid). 43 def fill(+seq: Json.Json, +slot: String, +uuid: String, +ts: String) -> Json.Json: 44 fill2(String.is_empty(str_of(Maybe.default(&2, Json.Json, SSeq.slot_record(seq, slot), Json.JObj{Json.JNil{}}), "uuid")), seq, slot, uuid, ts) 45 46 def fill_with(+m: Maybe<&2, Json.Json>, +root: String, +slot: String, +uuid: String, +ts: String) -> Act.of(Unit): 47 match m: 48 case None{}: 49 Act.pure(Unit, Unit{}) 50 case Some{seq}: 51 SIo.write_seq(root, fill(seq, slot, uuid, ts)) 52 53 def backfill_locked(+root: String, +slot: String, +uuid: String) -> Act.of(Unit): 54 do ActM: 55 ts : String <- SIo.now_iso() 56 m : Maybe<&2, Json.Json> <- SIo.read_seq(root) 57 fill_with(m, root, slot, uuid, ts) 58 59 def backfill(+root: String, +slot: String, +uuid: String) -> Act.of(Unit): 60 Lock.with_lock(Unit, SBase.lock_path(root), 10000, backfill_locked(root, slot, uuid)) 61 62 def org_differs(+ta_org: String, +slot_org: String) -> Bool: 63 Bool.not(String.is_empty(ta_org)) && Bool.not(String.is_empty(slot_org)) && Bool.not(String.eq(ta_org, slot_org)) 64 65 def verdict(+conflict: Bool) -> String: 66 match conflict: 67 case True{}: 68 "identity-conflict" 69 case False{}: 70 "ok" 71 72 def swallow(r: Fs.Res(Unit)) -> Act.of(Unit): 73 Act.pure(Unit, Unit{}) 74 75 def best_effort(m: Act.of(Unit)) -> Act.of(Unit): 76 IO.bind(Fs.Res(Unit), Fs.Res(Unit), m, r => swallow(r)) 77 78 def learned(+root: String, +slot: String, +ta_uuid: String) -> Act.of(String): 79 do ActM: 80 best_effort(backfill(root, slot, ta_uuid)) 81 return "ok" 82 83 def learn2(+empty: Bool, +slot_uuid: String, +root: String, +slot: String, +ta_uuid: String) -> Act.of(String): 84 match empty: 85 case True{}: 86 learned(root, slot, ta_uuid) 87 case False{}: 88 Act.pure(String, verdict(Bool.not(String.eq(slot_uuid, ta_uuid)))) 89 90 def learn(+slot_uuid: String, +root: String, +slot: String, +ta_uuid: String) -> Act.of(String): 91 learn2(String.is_empty(slot_uuid), slot_uuid, root, slot, ta_uuid) 92 93 def judged2(+conflict: Bool, +root: String, +slot: String, +ta_uuid: String, +rec: Json.Json) -> Act.of(String): 94 match conflict: 95 case True{}: 96 Act.pure(String, "identity-conflict") 97 case False{}: 98 learn(str_of(rec, "uuid"), root, slot, ta_uuid) 99 100 def judged(+root: String, +slot: String, +ta_uuid: String, +ta_org: String, +rec: Json.Json) -> Act.of(String): 101 judged2(org_differs(ta_org, str_of(rec, "organizationUuid")), root, slot, ta_uuid, rec) 102 103 def judged_in(+root: String, +slot: String, +ta_uuid: String, +ta_org: String, +m: Maybe<&2, Json.Json>) -> Act.of(String): 104 judged(root, slot, ta_uuid, ta_org, rec_of(m, slot)) 105 106 def known(+root: String, +slot: String, +ta_uuid: String, +ta_org: String) -> Act.of(String): 107 do ActM: 108 m : Maybe<&2, Json.Json> <- SIo.read_seq(root) 109 judged_in(root, slot, ta_uuid, ta_org, m) 110 111 def with_uuid(+empty: Bool, +root: String, +slot: String, +ta_uuid: String, +ta_org: String) -> Act.of(String): 112 match empty: 113 case True{}: 114 Act.pure(String, "ok") 115 case False{}: 116 known(root, slot, ta_uuid, ta_org) 117 118 # "ok" or "identity-conflict" for the account the token endpoint named 119 # (JNull when it named none). 120 def check(+root: String, +slot: String, +acct: Json.Json) -> Act.of(String): 121 with_uuid(String.is_empty(str_of(acct, "uuid")), root, slot, str_of(acct, "uuid"), str_of(acct, "organizationUuid"))