New token form: only templates within what the token's user may do
A token never does more than its user, so offering wider templates only misled. On the Users page the templates follow the token's user, not the administrator making it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5cd22f52d4
commit
0e663ab36c
3 files changed
+21
-5
No files matched your search
@@ -16,6 +16,9 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
- A linked zone's Compare views tab starts with a view it is linked to,
|
- A linked zone's Compare views tab starts with a view it is linked to,
|
||||||
not the first other view.
|
not the first other view.
|
||||||
|
- The New token form only offers templates within what the token's user
|
||||||
|
may do. A token never does more than its user, so offering more only
|
||||||
|
misled.
|
||||||
|
|
||||||
## [0.6.0] - 2026-10-13
|
## [0.6.0] - 2026-10-13
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function render(el) {
|
|||||||
const tokensEl = h("div");
|
const tokensEl = h("div");
|
||||||
const drawTokens = async () => {
|
const drawTokens = async () => {
|
||||||
const tokens = await get("/me/tokens");
|
const tokens = await get("/me/tokens");
|
||||||
mount(tokensEl, tokenTable(tokens, drawTokens), newTokenForm("/me/tokens", grants, drawTokens));
|
mount(tokensEl, tokenTable(tokens, drawTokens), newTokenForm("/me/tokens", grants, drawTokens, { admin: me.admin, grants: me.user.grants }));
|
||||||
};
|
};
|
||||||
const cur = input({ type: "password", autocomplete: "current-password" });
|
const cur = input({ type: "password", autocomplete: "current-password" });
|
||||||
const pw1 = input({ type: "password", autocomplete: "new-password" });
|
const pw1 = input({ type: "password", autocomplete: "new-password" });
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export async function tokensDialog(u, grants, own) {
|
|||||||
const base = own ? "/me/tokens" : "/users/" + u.id + "/tokens";
|
const base = own ? "/me/tokens" : "/users/" + u.id + "/tokens";
|
||||||
const draw = async () => {
|
const draw = async () => {
|
||||||
const tokens = await get(base);
|
const tokens = await get(base);
|
||||||
mount(box, tokenTable(tokens, draw), newTokenForm(base, grants, draw));
|
mount(box, tokenTable(tokens, draw), newTokenForm(base, grants, draw, u));
|
||||||
};
|
};
|
||||||
await draw();
|
await draw();
|
||||||
await modal("API tokens of " + u.username, box, [{ label: "Close", value: null }], { wide: true });
|
await modal("API tokens of " + u.username, box, [{ label: "Close", value: null }], { wide: true });
|
||||||
@@ -185,13 +185,26 @@ async function rotateDialog(t, redraw) {
|
|||||||
await modal("Rotate token " + t.name, box, [{ label: "Close", value: null }], { sticky: true });
|
await modal("Rotate token " + t.name, box, [{ label: "Close", value: null }], { sticky: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newTokenForm(base, grants, redraw) {
|
// coveredBy reports whether a user (admin, or grants) may do what a grant
|
||||||
|
// string ("records:write names=..." ) allows, by resource and access. A
|
||||||
|
// filter on either side only narrows the token further, so it is ignored.
|
||||||
|
function coveredBy(user, grant) {
|
||||||
|
if (!user || user.admin) return true;
|
||||||
|
const [res, acc] = grant.split(/\s+/)[0].split(":");
|
||||||
|
return (user.grants || []).some((g) => (g.resource === "*" || g.resource === res) && (g.access === "write" || acc === "read"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTokenForm makes a token for owner (the user it belongs to). Its
|
||||||
|
// templates are only the roles within what owner may do: a token never
|
||||||
|
// does more than its user, so offering more would only mislead.
|
||||||
|
export function newTokenForm(base, grants, redraw, owner) {
|
||||||
const err = h("div");
|
const err = h("div");
|
||||||
const out = h("div");
|
const out = h("div");
|
||||||
const name = input({ placeholder: "e.g. certbot on web01" });
|
const name = input({ placeholder: "e.g. certbot on web01" });
|
||||||
const exp = select([["", "never expires"], ["30d", "30 days"], ["90d", "90 days"], ["365d", "1 year"]], "90d");
|
const exp = select([["", "never expires"], ["30d", "30 days"], ["90d", "90 days"], ["365d", "1 year"]], "90d");
|
||||||
const gr = textarea("", { class: "mono", rows: 3, placeholder: "optional restriction, e.g.\nrecords:write zones=example.com names=_acme-challenge.* types=TXT" });
|
const gr = textarea("", { class: "mono", rows: 3, placeholder: "optional restriction, e.g.\nrecords:write zones=example.com names=_acme-challenge.* types=TXT" });
|
||||||
const tmpl = select([["", "Insert a template…"], ...Object.keys(grants.roles).map((r) => [r, r])], "");
|
const roles = Object.keys(grants.roles).filter((r) => grants.roles[r].every((g) => coveredBy(owner, g)));
|
||||||
|
const tmpl = select([["", "Insert a template…"], ...roles.map((r) => [r, r])], "");
|
||||||
tmpl.addEventListener("change", () => {
|
tmpl.addEventListener("change", () => {
|
||||||
if (!tmpl.value) return;
|
if (!tmpl.value) return;
|
||||||
gr.value = (gr.value.trim() ? gr.value.trim() + "\n" : "") + grants.roles[tmpl.value].join("\n");
|
gr.value = (gr.value.trim() ? gr.value.trim() + "\n" : "") + grants.roles[tmpl.value].join("\n");
|
||||||
@@ -203,7 +216,7 @@ export function newTokenForm(base, grants, redraw) {
|
|||||||
field("Name", name),
|
field("Name", name),
|
||||||
field("Expires", exp),
|
field("Expires", exp),
|
||||||
field("Restrict to", gr, "Leave empty for all of the user's permissions. A token can never do more than its user."),
|
field("Restrict to", gr, "Leave empty for all of the user's permissions. A token can never do more than its user."),
|
||||||
h("span"), tmpl),
|
roles.length ? [h("span"), tmpl] : null),
|
||||||
err,
|
err,
|
||||||
h("div", { class: "actions", style: { marginTop: "8px" } }, h("button", { class: "primary", onclick: async () => {
|
h("div", { class: "actions", style: { marginTop: "8px" } }, h("button", { class: "primary", onclick: async () => {
|
||||||
mount(err); mount(out);
|
mount(err); mount(out);
|
||||||
|
|||||||
Reference in new issue
Block a user