Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
df18527
Revise `let` to accept objects of bindings
amrc-benmorrow Dec 10, 2024
f70e992
Implement new let syntax
amrc-benmorrow Dec 10, 2024
ece95f8
Start building dumps for the new auth setup
amrc-benmorrow Dec 10, 2024
43f7f95
Remove _Principal group_ from the main dump
amrc-benmorrow Dec 10, 2024
91ad7e0
Start working on ConfigDB dump format version 2
amrc-benmorrow Dec 10, 2024
1f30d0b
Start work on a new Auth service
amrc-benmorrow Dec 11, 2024
e40a8ae
Don't leave an empty YAML object
amrc-benmorrow Dec 11, 2024
f3c3590
I think Helm is not respecting YAML comments
amrc-benmorrow Dec 11, 2024
6dfbf6f
Helm chomping fix
amrc-benmorrow Dec 11, 2024
4356f7f
The new auth has a correct Dockerfile CMD
amrc-benmorrow Dec 11, 2024
3ff6c34
Apparently the default CMD doesn't work correctly?
amrc-benmorrow Dec 11, 2024
e825839
Correct JS import
amrc-benmorrow Dec 11, 2024
1433e3a
Restart deployments using `kubectl scale`
amrc-benmorrow Dec 11, 2024
8536661
Import Debug from the right place
amrc-benmorrow Dec 11, 2024
cfa03c2
Create database
amrc-benmorrow Dec 11, 2024
5b5b88b
Watch templates from the ConfigDB
amrc-benmorrow Dec 12, 2024
4bba1fd
Handle compat with the old `version` table
amrc-benmorrow Dec 12, 2024
3c783c6
Create an Auth group for the Auth service
amrc-benmorrow Dec 12, 2024
0f40f32
We need the new rx-client which returns Sets
amrc-benmorrow Dec 12, 2024
46ed0c5
Rework analysis using immutable objects
amrc-benmorrow Dec 13, 2024
3cec0b3
Don't always sort JSON responses
amrc-benmorrow Dec 13, 2024
5eaed3a
Convert dump validation to a JSON schema
amrc-benmorrow Dec 13, 2024
4e832d6
Start reworking the v2 dump format
amrc-benmorrow Dec 13, 2024
d9ac6ec
Load dumps via the new endpoint
amrc-benmorrow Dec 13, 2024
ee58a7a
Promote _Permission group_ to a rank 2 class
amrc-benmorrow Dec 13, 2024
b6615b6
Don't create _Permission group_ in the main dump
amrc-benmorrow Dec 13, 2024
2c33834
Move over to `service-client`
amrc-benmorrow Dec 13, 2024
88969ab
Check ConfigDB dumps against the schema
amrc-benmorrow Dec 13, 2024
8829e17
Fix dump schema problems
amrc-benmorrow Dec 13, 2024
6e345e7
Update eslint
amrc-benmorrow Dec 13, 2024
ca8728d
Move the Git Auth entries back to dumps.yaml
amrc-benmorrow Dec 16, 2024
969e349
Remove Helm annotation on service-setup Job
amrc-benmorrow Dec 16, 2024
e2234a2
Set service-setup backoff limit to 0
amrc-benmorrow Dec 16, 2024
2f7ee38
Functions to write s-exprs
amrc-benmorrow Dec 16, 2024
bd03239
Support s-expr input in the ConfigDB
amrc-benmorrow Dec 16, 2024
d3c8807
Record template lookups in more detail
amrc-benmorrow Dec 16, 2024
e97b508
Start a model class
amrc-benmorrow Dec 16, 2024
8f67ba9
We must not use local node_modules for build
amrc-benmorrow Dec 16, 2024
f99f12d
fixup! Start a model
amrc-benmorrow Dec 16, 2024
a02452d
Support s-exprs in service-setup
amrc-benmorrow Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,204 changes: 760 additions & 444 deletions acs-auth/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions acs-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"license": "MIT",
"dependencies": {
"@amrc-factoryplus/utilities": "^2.0.0",
"@thi.ng/sexpr": "^0.5.52",
"express": "^4.18.1",
"express-basic-auth": "^1.2.1",
"http-errors": "^2.0.0",
"immutable": "^5.0.3",
"timers-promises": "^1.0.1"
}
}
34 changes: 19 additions & 15 deletions acs-configdb/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { signal } from "https://esm.sh/@preact/[email protected]";
import htm from "https://esm.sh/[email protected]";
import yaml from "https://esm.sh/[email protected]";

import { from_sx, to_sx } from "./s-expr.js";

const html = htm.bind(h);

const AppUuid = {
Expand Down Expand Up @@ -57,12 +59,12 @@ async function service_fetch(path, opts) {
return res;
}

async function fetch_json(path) {
async function fetch_json(path, sort) {
const rsp = await service_fetch(`/v2/${path}`);
if (rsp.status != 200) return;

const json = await rsp.json();
if (Array.isArray(json))
if (sort)
json.sort();

return json;
Expand Down Expand Up @@ -105,7 +107,7 @@ function patch_json (path, patch) {
}

async function post_json(path, json) {
const rsp = await service_fetch(`/v1/${path}`, {
const rsp = await service_fetch(`/${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -271,7 +273,7 @@ function Editor(props) {
<h1>ACS | Config Store</h1>
<p>The display does not always update when you change things.
You might need to reopen sections or refresh the page.</p>
<${SetUseYAML}/>
<${SetFormatter}/>
<dl>
<${Opener} title="Applications">
<${Apps}/>
Expand Down Expand Up @@ -299,10 +301,14 @@ const Formats = {
indent: 2,
}),
},
sx: {
read: from_sx,
write: to_sx,
},
};
const Formatter = createContext(signal(Formats.yaml));

function SetUseYAML (props) {
function SetFormatter (props) {
const format = useContext(Formatter);

const option = (frm, label) => html`
Expand All @@ -318,6 +324,7 @@ function SetUseYAML (props) {
<span>
${option(Formats.yaml, "YAML")}
${option(Formats.json, "JSON")}
${option(Formats.sx, "S-expr")}
</span>
`;
}
Expand All @@ -326,7 +333,7 @@ function Apps(props) {
const [apps, set_apps] = useState([]);
const [msg, set_msg] = useState("");

const update_apps = async () => set_apps(await fetch_json("app"));
const update_apps = async () => set_apps(await fetch_json("app", true));

useEffect(update_apps, []);

Expand All @@ -338,7 +345,7 @@ function Apps(props) {
uuid: new_uuid.current.value,
name: new_name.current.value,
};
if (await post_json(`app`, body)) {
if (await post_json(`v1/app`, body)) {
set_msg("App created");
} else {
set_msg("Error");
Expand Down Expand Up @@ -397,8 +404,8 @@ function Klass(props) {
const [status, set_status] = useState(null);

const update = () => {
fetch_json(`class/${klass}/direct/member`).then(set_objs);
fetch_json(`class/${klass}/direct/subclass`).then(set_subs);
fetch_json(`class/${klass}/direct/member`, true).then(set_objs);
fetch_json(`class/${klass}/direct/subclass`, true).then(set_subs);
};
useEffect(update, [klass]);

Expand Down Expand Up @@ -494,7 +501,7 @@ function NewObj(props) {
uuid: new_obj.current.value || undefined,
"class": new_class.current.value,
};
const rsp = await post_json("object", spec);
const rsp = await post_json("v2/object", spec);

if (rsp) {
set_msg(html`Created
Expand Down Expand Up @@ -529,7 +536,7 @@ function App(props) {
const {app} = props;
const [objs, set_objs] = useState([]);

const update = async () => set_objs(await fetch_json(`app/${app}/object`));
const update = async () => set_objs(await fetch_json(`app/${app}/object`, true));
useEffect(update, []);

return html`
Expand Down Expand Up @@ -639,17 +646,15 @@ function Dumps(props) {

const dump_r = useRef(null);
const file_r = useRef(null);
const ovrw_r = useRef(null);

const load = async () => {
const dump = JSON.parse(dump_r.current?.value);
if (dump == null) {
set_msg("Error reading dump from textbox");
return;
}
const ovrw = !!ovrw_r.current?.checked;

const ok = await post_json(`load?overwrite=${ovrw}`, dump);
const ok = await post_json(`load`, dump);
set_msg(ok ? "Loaded dump" : "Failed");
if (ok) dump_r.current.value = "";
};
Expand All @@ -670,7 +675,6 @@ function Dumps(props) {
</p>
<p><textarea cols=80 rows=24 ref=${dump_r}></textarea></p>
<p><input type=file ref=${file_r} onChange=${read_file}/></p>
<p><label><input type=checkbox ref=${ovrw_r}/> Overwrite existing entries</label></p>
<p>
<button onClick=${load}>Load JSON dump</button>
${msg}
Expand Down
103 changes: 103 additions & 0 deletions acs-configdb/editor/s-expr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { parse } from "https://esm.run/@thi.ng/sexpr";

class BadSexpr extends Error {};

function fail (msg, val) {
throw new BadSexpr(msg.replace("%o", val.toString()));
}

function sx_to_json (sx) {
/* fall through is important */
switch (sx.type) {
case "root":
return sx.children.length > 1
? fail("multiple sx")
: sx_to_json(sx.children[0]);
case "sym":
switch (sx.value) {
case "null": return null;
case "true": return true;
case "false": return false;
}
case "str":
case "num":
return sx.value;
case "expr":
switch (sx.value) {
case "[":
return sx.children.map(sx_to_json);
case "{":
return sx.children.reduce(
([key, obj], val) => key
? [null, {...obj, [key]: sx_to_json(val) }]
: [val.type == "sym" && val.value.endsWith(":")
? val.value.slice(0, -1)
: fail("bad object key: %o", val), obj],
[null, {}])[1];
}
default:
fail("unknown sx: %o", sx)
}
}

export function sx (strs) {
if (strs.length != 1)
fail("sx with interpolation");
return from_sx(strs[0]);
}

export function from_sx (str) {
return sx_to_json(parse(str, { scopes: [["[", "]"], ["{", "}"]] }));
}

export function to_sx (v) {
const s = _to_sx(v);
return Array.isArray(s) ? s.join("\n") : s;
}

function _to_sx (v) {
if (v == null)
return "null";
if (typeof v == "string")
return _to_sx_str(v);
if (typeof v != "object")
return JSON.stringify(v);
if (Array.isArray(v))
return _to_sx_array(v);
return _to_sx_obj(v);
}

function _to_sx_str (s) {
const sym = /^[a-zA-Z]+$/.test(s);
return sym ? s : JSON.stringify(s);
}

function _to_sx_array (a) {
const items = a.map(v => _to_sx(v));
return _to_sx_items(items, "[", " ", "]");
}

function _to_sx_obj (o) {
const items = Object.entries(o).map(([k, v]) => {
const sk = _to_sx_str(k);
const sv = _to_sx(v);
if (Array.isArray(sv))
return [`${sk}: ${sv[0]}`, ...sv.slice(1)];
return `${sk}: ${sv}`;
});
return _to_sx_items(items, "{", ", ", "}");
}

function _to_sx_items (items, start, sep, end) {
if (!items.some(Array.isArray)) {
const line = `${start}${items.join(sep)}${end}`;
const opens = line.match(/\[/g);
if (line.length < 80 && !(opens?.length > 4)) {
return line;
}
}
const ind = is => is.flatMap(i => Array.isArray(i) ? ind(i) : [` ${i}`]);
const lines = ind(items.slice(1));
return [start + items[0], ...lines.slice(0, -1), lines.at(-1) + end];
}

2 changes: 1 addition & 1 deletion acs-configdb/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default [
args: "none",
caughtErrors: "none",
}],
"no-fallthrough": "off",
},
},
];
47 changes: 45 additions & 2 deletions acs-configdb/lib/api-v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ export class APIv1 {

["/object", "/v2/object"],
["/object/:object", "/v2/object/:object"],

["/load", "/load"],
];

for (const [source, dest] of forwards) {
Expand All @@ -86,6 +84,7 @@ export class APIv1 {
api.get("/app/:app/search", this.config_search.bind(this));
api.get("/app/:app/class/:class/search", this.config_search.bind(this));

api.post("/load", this.dump_load.bind(this));
api.get("/save", this.dump_save.bind(this));
}

Expand Down Expand Up @@ -171,6 +170,50 @@ export class APIv1 {
return res.status(200).json(json);
}

/* This is different from /load in that it accepts an overwrite
* query parameter. */
async dump_load(req, res) {
const dump = req.body;
const booleans = {
"true": true, "false": false,
"1": true, "0": false,
on: true, off: false,
yes: true, no: false,
};

const overwrite = booleans[req.query.overwrite];
if (overwrite == undefined)
return res.status(400).end();

if (!this.model.dump_validate(dump)) {
this.log("Dump failed to validate: %o", this.model.dump_validate.errors);
return res.status(400).end();
}
if (dump.version != 1) {
this.log("/v1/load can only load version 1 dumps");
return res.status(400).end();
}

const perms = {
classes: Perm.Manage_Obj,
objects: Perm.Manage_Obj,
configs: Perm.Write_App,
};
for (const [key, perm] of Object.entries(perms)) {
if (key in dump) {
const ok = await this.auth.check_acl(
req.auth, perm, UUIDs.Null, false);
if (!ok) {
this.log("Refusing dump (%s)", key);
return res.status(403).end();
}
}
}

const st = await this.model.dump_load(dump, !!overwrite);
res.status(st).end();
}

async dump_save(req, res) {
const ok = await this.auth.check_acl(req.auth, Perm.Manage_Obj, UUIDs.Null)
&& await this.auth.check_acl(req.auth, Perm.Read_App, UUIDs.Null);
Expand Down
37 changes: 0 additions & 37 deletions acs-configdb/lib/api-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export class APIv2 {
.delete(this.config_delete.bind(this))
.patch(this.config_patch.bind(this));

api.post("/load", this.dump_load.bind(this));
api.get("/save", this.dump_save.bind(this));
}

Expand Down Expand Up @@ -350,42 +349,6 @@ export class APIv2 {
return res.status(200).json(json);
}

async dump_load(req, res) {
const dump = req.body;
const booleans = {
"true": true, "false": false,
"1": true, "0": false,
on: true, off: false,
yes: true, no: false,
};

const overwrite = booleans[req.query.overwrite];
if (overwrite == undefined)
return res.status(400).end();

if (!await this.model.dump_validate(dump))
return res.status(400).end();

const perms = {
classes: Perm.Manage_Obj,
objects: Perm.Manage_Obj,
configs: Perm.Write_App,
};
for (const [key, perm] of Object.entries(perms)) {
if (key in dump) {
const ok = await this.auth.check_acl(
req.auth, perm, UUIDs.Null, false);
if (!ok) {
this.log("Refusing dump (%s)", key);
return res.status(403).end();
}
}
}

const st = await this.model.dump_load(dump, !!overwrite);
res.status(st).end();
}

async dump_save(req, res) {
const ok = await this.auth.check_acl(req.auth, Perm.Manage_Obj, UUIDs.Null)
&& await this.auth.check_acl(req.auth, Perm.Read_App, UUIDs.Null);
Expand Down
Loading