swlive.bend
cswap-bend-cli@0.1.0.0 · 274 lines · 47 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fs.bend as Fs
4import cswap-bend-cli@0.1.0.0/act.bend as Act
5import cswap-bend-cli@0.1.0.0/store.bend as Store
6import cswap-bend-cli@0.1.0.0/lock.bend as Lock
7import cswap-bend-cli@0.1.0.0/dirlock.bend as Dirlock
8import cswap-bend-cli@0.1.0.0/b64.bend as B64
9import cswap-bend-cli@0.1.0.0/swbase.bend as B
10import cswap-bend-cli@0.1.0.0/swcreds.bend as C
11import cswap-bend-cli@0.1.0.0/fs/write.bend as FsW
12import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
13import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
14import cswap-bend-cli@0.1.0.0/store/plan.bend as SPlan
15import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
16
17
18
19
20
21type Oracle is Data:
22 OHttp{}
23 OFile{path: String}
24 ONone{}
25
26type Opts is Data:
27 Opts{delay_ms: U32, oracle: Oracle}
28
29
30type Live is Data:
31 Live{cfg_text: Maybe<&2, String>, cfg: Maybe<&2, Json.Json>, creds_text: Maybe<&2, String>}
32
33
34type Pre is Data:
35 Pre{live: String, prof: SPlan.Prof}
36
37def ActM(-A: Type) -> Type:
38 Act.of(A)
39
40def ActM.pure(-A: Type, x: A) -> Act.of(A):
41 Act.pure(A, x)
42
43def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
44 Act.bind(A, B, m, f)
45
46def opts_delay(+o: Opts) -> U32:
47 match o:
48 case Opts{d, r}:
49 d
50
51def opts_oracle(+o: Opts) -> Oracle:
52 match o:
53 case Opts{d, r}:
54 r
55
56def file_prof(r: Fs.Res(String)) -> SPlan.Prof:
57 match r:
58 case Fail{e}:
59 SPlan.ProfNone{}
60 case Done{t}:
61 SPlan.profile_of_text(t)
62
63
64
65def run_oracle(+o: Oracle, +token: String) -> IO(SPlan.Prof):
66 match o:
67 case OHttp{}:
68 Store.profile_http(token)
69 case OFile{path}:
70 IO.bind(Fs.Res(String), SPlan.Prof, Fs.read_file(path), r => IO.pure(SPlan.Prof, file_prof(r)))
71 case ONone{}:
72 IO.pure(SPlan.Prof, SPlan.ProfNone{})
73
74def read_text_opt2(+ex: Bool, +path: String) -> Act.of(Maybe<&2, String>):
75 match ex:
76 case False{}:
77 Act.pure(Maybe<&2, String>, None{})
78 case True{}:
79 do ActM<Maybe<&2, String>>:
80 t : String <- Fs.read_file(path)
81 return Some{t}
82
83
84def read_text_opt(+path: String) -> Act.of(Maybe<&2, String>):
85 do ActM<Maybe<&2, String>>:
86 ex : Bool <- Act.lift(Bool, Fs.exists(path))
87 read_text_opt2(ex, path)
88
89def parse_text2(r: Result<&2, &2, String, Json.Json>) -> Maybe<&2, Json.Json>:
90 match r:
91 case Done{j}:
92 Some{j}
93 case Fail{e}:
94 None{}
95
96def parse_text(+m: Maybe<&2, String>) -> Maybe<&2, Json.Json>:
97 match m:
98 case None{}:
99 None{}
100 case Some{t}:
101 parse_text2(Json.parse(t))
102
103def read_live(+ctx: SBase.Ctx) -> Act.of(Live):
104 do ActM<Live>:
105 +cfg_text : Maybe<&2, String> <- read_text_opt(SBase.global_config_path(ctx))
106 creds_text : Maybe<&2, String> <- read_text_opt(SBase.live_creds_path(ctx))
107 return Live{cfg_text, parse_text(cfg_text), creds_text}
108
109def lv_cfg(+l: Live) -> Json.Json:
110 match l:
111 case Live{t, +c, k}:
112 Store.sel.j(B.is_some_json(c), B.unwrap_json(c), Json.JNull{})
113
114def lv_cfg_text(+l: Live) -> Maybe<&2, String>:
115 match l:
116 case Live{t, c, k}:
117 t
118
119def lv_cfg_ok(+l: Live) -> Bool:
120 match l:
121 case Live{t, c, k}:
122 B.is_some_json(c)
123
124def lv_creds_text(+l: Live) -> Maybe<&2, String>:
125 match l:
126 case Live{t, c, k}:
127 k
128
129
130def lv_creds(+l: Live) -> String:
131 SBase.sel.s(B.is_some_str(lv_creds_text(l)), B.text_or(lv_creds_text(l)), SSeq.field_str(lv_cfg(l), "primaryApiKey"))
132
133
134def lv_email(+l: Live) -> String:
135 Store.id_email(Store.ident_of(lv_cfg(l)))
136
137def lv_org(+l: Live) -> String:
138 Store.id_org(Store.ident_of(lv_cfg(l)))
139
140def lv_slot2(+has: Bool, +l: Live, +seq: Json.Json) -> Maybe<&2, String>:
141 match has:
142 case True{}:
143 SSeq.find_slot(seq, lv_email(l), lv_org(l))
144 case False{}:
145 None{}
146
147
148def lv_slot(+l: Live, +seq: Json.Json) -> Maybe<&2, String>:
149 lv_slot2(SBase.nonempty(lv_email(l)), l, seq)
150
151def sel_o(+c: Bool, +x: List<&2, C.Other>, +y: List<&2, C.Other>) -> List<&2, C.Other>:
152 match c:
153 case True{}:
154 x
155 case False{}:
156 y
157
158def other_creds(+root: String, +seq: Json.Json, +slots: List<&2, String>, +current: String) -> Act.of(List<&2, C.Other>):
159 match slots:
160 case Nil{}:
161 Act.pure(List<&2, C.Other>, [])
162 case Con{s, t}:
163 do ActM<List<&2, C.Other>>:
164 c : String <- SIo.read_cred(root, s, SIo.slot_email(seq, s))
165 +rest : List<&2, C.Other> <- other_creds(root, seq, t, current)
166 return sel_o(String.eq(s, current), rest, C.Other{s, c} <> rest)
167
168def archive_live(+root: String, +email: String, +creds: String, +ts: String) -> Act.of(Unit):
169 do ActM<Unit>:
170 SIo.secure_dir(SBase.removed_dir(root))
171 SIo.write_secret(SBase.removed_dir(root) ++ "/" ++ ts ++ "-live-" ++ SBase.sel.s(String.is_empty(email), "unknown", email) ++ ".enc", B64.encode(creds))
172 SIo.log(root, "WARNING", "Archived the live login (" ++ SBase.sel.s(String.is_empty(email), "unknown", email) ++ ") to credentials/.removed/" ++ ts ++ "-live-...enc before replacing it; credentials are moved aside, never deleted")
173
174def archive_if(+needed: Bool, +root: String, +email: String, +creds: String, +ts: String) -> Act.of(Unit):
175 Act.cond(Unit, needed, archive_live(root, email, creds, ts), Act.pure(Unit, Unit{}))
176
177def write_live_creds2(+api_key: Bool, +ctx: SBase.Ctx, +creds: String) -> Act.of(Unit):
178 match api_key:
179 case True{}:
180 FsW.remove(SBase.live_creds_path(ctx))
181 case False{}:
182 do ActM<Unit>:
183 FsW.mkdir_p(Fs.dirname(SBase.live_creds_path(ctx)))
184 SIo.write_secret(SBase.live_creds_path(ctx), creds)
185
186
187def write_live_creds(+ctx: SBase.Ctx, +creds: String) -> Act.of(Unit):
188 write_live_creds2(SBase.looks_like_api_key(creds), ctx, creds)
189
190def write_cfg(+ctx: SBase.Ctx, +j: Json.Json) -> Act.of(Unit):
191 SIo.write_secret(SBase.global_config_path(ctx), Json.emit_pretty(j))
192
193def restore_text(+path: String, +m: Maybe<&2, String>) -> Act.of(Unit):
194 match m:
195 case Some{t}:
196 SIo.write_secret(path, t)
197 case None{}:
198 FsW.remove(path)
199
200
201def restore(+ctx: SBase.Ctx, +root: String, +creds: Maybe<&2, String>, +cfg: Maybe<&2, String>, +seq_text: String) -> Act.of(Unit):
202 do ActM<Unit>:
203 restore_text(SBase.live_creds_path(ctx), creds)
204 restore_text(SBase.global_config_path(ctx), cfg)
205 SIo.write_secret(SBase.seq_path(root), seq_text)
206
207def wrap_failure(r: Fs.Res(Unit)) -> Fs.Res(Unit):
208 match r:
209 case Done{u}:
210 Done{u}
211 case Fail{(code, msg)}:
212 Fail{(code, "Switch failed and was rolled back: " ++ msg)}
213
214def config_home_of(+ctx: SBase.Ctx) -> String:
215 match ctx:
216 case SBase.Ctx{h, c, g}:
217 c
218
219
220
221def with_locks(-A: Type, +ctx: SBase.Ctx, act: Act.of(A)) -> Act.of(A):
222 Lock.with_lock(A, SBase.lock_path(SBase.root_of(ctx)), 10000,
223 Dirlock.with_dirlock(A, config_home_of(ctx) ++ "/.oauth_refresh.lock", 9000, 60,
224 Dirlock.with_dirlock(A, config_home_of(ctx) ++ ".lock", 9000, 60,
225 Dirlock.with_dirlock(A, SBase.global_config_path(ctx) ++ ".lock", 9000, 10, act))))
226
227def sel_prof(+c: Bool, +x: SPlan.Prof, +y: SPlan.Prof) -> SPlan.Prof:
228 match c:
229 case True{}:
230 x
231 case False{}:
232 y
233
234def pre_prof(+pre: Pre, +live: String) -> SPlan.Prof:
235 match pre:
236 case Pre{l, p}:
237 sel_prof(String.eq(l, live), p, SPlan.ProfNone{})
238
239def no_pre() -> Pre:
240 Pre{"", SPlan.ProfNone{}}
241
242def need_lookup(+slot: Maybe<&2, String>, +live: String, +backup: String) -> Bool:
243 B.is_some_str(slot) && SBase.nonempty(live) && Bool.not(String.eq(backup, live)) && Bool.not(C.same_lineage(backup, live))
244
245
246
247def prefetch2(+want: Bool, +o: Oracle, +live: String) -> Act.of(SPlan.Prof):
248 Act.cond(SPlan.Prof, want && SBase.nonempty(C.access_of(live)), Act.lift(SPlan.Prof, run_oracle(o, C.access_of(live))), Act.pure(SPlan.Prof, SPlan.ProfNone{}))
249
250def lookup_need(+l: Live, +seq: Json.Json, +backup: String) -> Bool:
251 need_lookup(lv_slot(l, seq), lv_creds(l), backup)
252
253def backup_of(+ctx: SBase.Ctx, +l: Live, +seq: Json.Json) -> Act.of(String):
254 SIo.read_cred(SBase.root_of(ctx), SSeq.str_or(lv_slot(l, seq), ""), lv_email(l))
255
256def lookup_for(+ctx: SBase.Ctx, +o: Oracle, +l: Live, +seq: Json.Json) -> Act.of(SPlan.Prof):
257 do ActM<SPlan.Prof>:
258 backup : String <- backup_of(ctx, l, seq)
259 prefetch2(lookup_need(l, seq, backup), o, lv_creds(l))
260
261def prefetch3(+ctx: SBase.Ctx, +o: Oracle, +l: Live, +m: Maybe<&2, Json.Json>) -> Act.of(Pre):
262 match m:
263 case None{}:
264 Act.pure(Pre, no_pre())
265 case Some{seq}:
266 do ActM<Pre>:
267 p : SPlan.Prof <- lookup_for(ctx, o, l, seq)
268 return Pre{lv_creds(l), p}
269
270def prefetch(+ctx: SBase.Ctx, +o: Oracle) -> Act.of(Pre):
271 do ActM<Pre>:
272 l : Live <- read_live(ctx)
273 m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
274 prefetch3(ctx, o, l, m)