RPF-WP-0057: capabilities-self grant for the hub-writer caller and UI workload role choice (NK-WP-0051-T06)
credential-change.py generates sys/capabilities-self (update) when openbao.token_capabilities_self is true. Proposed CCR-2026-0045 sets it for workload-kv-read-reuse-surface-runtime so the ops-mason proof stops calling sys/capabilities with a root token. The OpenBao login page keeps its platform-admin button and adds an allow-listed workload role choice. Repo only; nothing applied to OpenBao or the cluster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1041694@bnt-lap001 Assistant-Session: 7af0bb68-ac4a-4ce3-8639-24ddd84c0b52
This commit is contained in:
parent
bb6838f588
commit
0f0282a436
12 changed files with 462 additions and 19 deletions
|
|
@ -0,0 +1,121 @@
|
|||
id: CCR-2026-0045
|
||||
kind: credential-change-request
|
||||
schema_version: 1
|
||||
request_type: workload-kv-read
|
||||
title: Let the reuse-surface hub-writer caller query its own capabilities
|
||||
status: proposed
|
||||
created: '2026-10-14'
|
||||
updated: '2026-10-14'
|
||||
supersedes: CCR-2026-0044
|
||||
requester:
|
||||
agent: ops-mason
|
||||
reason: NK-WP-0051-T06 / MASON-WP-0012. The CCR-2026-0044 member proof called sys/capabilities
|
||||
with a root or platform-admin token because the lane policy has no capabilities-self grant.
|
||||
This request adds a generated sys/capabilities-self grant (update; the endpoint is a POST
|
||||
and returns capability names only) so the lane token proves its own reach. Policy, role,
|
||||
bound claims and token bounds are otherwise unchanged from CCR-2026-0044. Do not attach
|
||||
workload-kv-read-reuse-surface-runtime to entity 4a1f663c-0621-0483-edd0-f852274f4b6b. No
|
||||
secret value is read, written or rotated.
|
||||
review:
|
||||
required: true
|
||||
required_approvers:
|
||||
- platform-operator
|
||||
- reuse-surface-owner
|
||||
comments:
|
||||
- at: '2026-10-14T21:00:00Z'
|
||||
reviewer: claude
|
||||
decision: filed_proposed
|
||||
comment: Filed for NK-WP-0051-T06 (RPF-WP-0057). Repo policy file now carries the generated
|
||||
sys/capabilities-self grant; the live policy is unchanged until approval and a delegated
|
||||
apply. Bound claims are those confirmed under CCR-2026-0044 (groups=reuse-surface-hub-writers,
|
||||
/assurance/level=aal2); this request does not change them.
|
||||
target:
|
||||
domain: financials
|
||||
tenant: reuse
|
||||
workload: reuse-surface
|
||||
environment: production
|
||||
purpose: Attended read of the existing hub writer token by one dedicated caller.
|
||||
openbao:
|
||||
mount: platform
|
||||
kv_path: platform/workloads/reuse/reuse-surface/runtime-secrets
|
||||
fields:
|
||||
- REUSE_SURFACE_TOKEN
|
||||
- REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET
|
||||
policy_name: workload-kv-read-reuse-surface-runtime
|
||||
policy_file: openbao/policies/workload-kv-read-reuse-surface-runtime.hcl
|
||||
metadata_read: true
|
||||
token_self_lifecycle: true
|
||||
auth:
|
||||
method: oidc
|
||||
mount: netkingdom
|
||||
role: reuse-surface-hub-writer-workload-kv-read
|
||||
allowed_redirect_uris:
|
||||
- http://localhost:8250/oidc/callback
|
||||
- http://127.0.0.1:8250/oidc/callback
|
||||
- http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback
|
||||
oidc_scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- groups
|
||||
user_claim: sub
|
||||
groups_claim: groups
|
||||
bound_claims_type: string
|
||||
bound_claims:
|
||||
groups:
|
||||
- reuse-surface-hub-writers
|
||||
/assurance/level: aal2
|
||||
bound_claims_confirmed: true
|
||||
policies:
|
||||
- workload-kv-read-reuse-surface-runtime
|
||||
ttl: 15m
|
||||
token_max_ttl: 30m
|
||||
token_explicit_max_ttl: 30m
|
||||
token_no_default_policy: true
|
||||
token_type: service
|
||||
token_capabilities_self: true
|
||||
access_frontdoor:
|
||||
type: ops-warden
|
||||
catalog_id: reuse-surface-hub-write-token
|
||||
selector: reuse-surface federation hub write bearer token
|
||||
command: After a passing member receipt, run warden access reuse-surface-hub-write-token --exec
|
||||
--field REUSE_SURFACE_TOKEN -- <silent helper> with the unprinted lane token. Do not use
|
||||
--fetch.
|
||||
resolvable: false
|
||||
readiness: pending-review
|
||||
risk:
|
||||
classification: high
|
||||
notes:
|
||||
- sys/capabilities-self reports capability names for paths the caller names. It returns no
|
||||
secret value and grants no access.
|
||||
- The platform-admin entity must not receive this policy. OpenBao merges entity policies into
|
||||
every token for that entity.
|
||||
- The ESO policy workload-kv-read-reuse-surface-runtime-eso stays on its Kubernetes role and
|
||||
is unchanged.
|
||||
- Until this apply, the live lane policy differs from openbao/policies/workload-kv-read-reuse-surface-runtime.hcl
|
||||
by this one stanza; CCR-2026-0044 member proofs stay on the earlier operator-token path.
|
||||
verification:
|
||||
positive:
|
||||
- A member token's policy set is exactly workload-kv-read-reuse-surface-runtime.
|
||||
- That token's capabilities are read on the exact data path and the exact metadata path.
|
||||
- The member token can call sys/capabilities-self on the five proof paths without any operator
|
||||
or root token.
|
||||
negative:
|
||||
- The same token is deny on platform/data/workloads/reuse/reuse-surface, its metadata parent,
|
||||
and platform/data/workloads/issue-core/issue-core/issue-core-runtime.
|
||||
- A principal outside reuse-surface-hub-writers is refused by the role.
|
||||
- Entity 4a1f663c-0621-0483-edd0-f852274f4b6b is not the member token's entity.
|
||||
activation_conditions:
|
||||
- platform-operator and reuse-surface-owner approve this request.
|
||||
- Founder applies the policy through the delegated applier. No KV write, no role change beyond
|
||||
re-asserting the CCR-2026-0044 role.
|
||||
- ops-mason scripts/verify-reuse-surface-hub-writer.py member passes using capabilities-self
|
||||
only, and non-member records the OpenBao refusal verbatim. Receipts stay mode 0600 with
|
||||
no token or field value.
|
||||
evidence: []
|
||||
lifecycle:
|
||||
deactivate: Re-apply the CCR-2026-0044 policy body (drop the sys/capabilities-self stanza).
|
||||
Leave the role, KV path and ESO lane in place.
|
||||
rotate: unchanged from CCR-2026-0005. This request does not rotate field values.
|
||||
compromised: detach the role, revoke outstanding tokens for the role, and follow the CCR-2026-0005
|
||||
compromise procedure if a field value was exposed.
|
||||
|
|
@ -5,6 +5,13 @@ Streamlines the browser login mask reached through the named
|
|||
single **Sign in with KeyCape** action. Namespace, auth method, mount path, and
|
||||
role are preset in `presets.json` and hidden by `overlay.css` / `overlay.js`.
|
||||
|
||||
The main button always uses role `platform-admin`. Below it, "Sign in with a
|
||||
workload role" offers only the roles listed in `presets.json` `workloadRoles`
|
||||
(no free-text role), for example `reuse-surface-hub-writer-workload-kv-read`
|
||||
(NK-WP-0051-T06). Each listed role must allow the loopback UI callback. Use a
|
||||
private window for a workload role so the platform-admin KeyCape session is
|
||||
not reused.
|
||||
|
||||
## Mechanism (T01 decision)
|
||||
|
||||
OpenBao ships UI assets inside the container image. There is no supported API
|
||||
|
|
@ -30,7 +37,7 @@ behaviour.
|
|||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `VERSION` | OpenBao image tag this overlay targets (`openbao-values.yaml`) |
|
||||
| `presets.json` | Hidden login defaults (`netkingdom`, `platform-admin`, …) |
|
||||
| `presets.json` | Hidden login defaults (`netkingdom`, `platform-admin`, …) and the `workloadRoles` allow-list |
|
||||
| `overlay.css` | Hide raw OpenBao login fields |
|
||||
| `overlay.js` | Apply presets, branding on post-login Ember pages |
|
||||
| `login.html` / `login.js` / `login.css` | Standalone KeyCape login at `/ui/vault/auth` |
|
||||
|
|
|
|||
|
|
@ -82,4 +82,41 @@ body {
|
|||
|
||||
.login-error.is-visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.login-workload {
|
||||
margin-top: 1.25rem;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-workload summary {
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-workload p {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.login-workload label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.login-workload select {
|
||||
width: 100%;
|
||||
padding: 0.55rem 0.6rem;
|
||||
margin-bottom: 0.9rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.login-button-secondary {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@
|
|||
<button id="login-submit" class="login-button" type="button">
|
||||
Sign in with KeyCape
|
||||
</button>
|
||||
<details id="login-workload" class="login-workload" hidden>
|
||||
<summary id="login-workload-summary">Sign in with a workload role</summary>
|
||||
<p id="login-workload-hint"></p>
|
||||
<label for="login-workload-role">Role</label>
|
||||
<select id="login-workload-role"></select>
|
||||
<button id="login-workload-submit" class="login-button login-button-secondary" type="button">
|
||||
Sign in with this role
|
||||
</button>
|
||||
</details>
|
||||
<div id="login-error" class="login-error" role="alert"></div>
|
||||
</main>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function redirectToKeyCape(presets) {
|
||||
// Only roles listed in presets.workloadRoles can be chosen; there is no
|
||||
// free-text role. The main button always uses presets.role (platform-admin).
|
||||
function workloadRoles(presets) {
|
||||
const roles = Array.isArray(presets.workloadRoles) ? presets.workloadRoles : [];
|
||||
return roles.filter(
|
||||
(entry) =>
|
||||
entry &&
|
||||
typeof entry.role === "string" &&
|
||||
/^[a-z0-9][a-z0-9-]*$/.test(entry.role) &&
|
||||
entry.role !== (presets.role || "platform-admin")
|
||||
);
|
||||
}
|
||||
|
||||
async function redirectToKeyCape(presets, role) {
|
||||
const mount = presets.mount || "netkingdom";
|
||||
const role = presets.role || "platform-admin";
|
||||
const redirectUri = `${window.location.origin}/ui/vault/auth/${mount}/oidc/callback`;
|
||||
|
||||
const response = await fetch(`/v1/auth/${mount}/oidc/auth_url`, {
|
||||
|
|
@ -67,18 +79,45 @@
|
|||
|
||||
if (!button) return;
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
await redirectToKeyCape(presets);
|
||||
} catch (error) {
|
||||
button.disabled = false;
|
||||
showError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Sign-in failed. Contact your administrator."
|
||||
);
|
||||
}
|
||||
function bind(control, chooseRole) {
|
||||
control.addEventListener("click", async () => {
|
||||
control.disabled = true;
|
||||
try {
|
||||
await redirectToKeyCape(presets, chooseRole());
|
||||
} catch (error) {
|
||||
control.disabled = false;
|
||||
showError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Sign-in failed. Contact your administrator."
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind(button, () => presets.role || "platform-admin");
|
||||
|
||||
const roles = workloadRoles(presets);
|
||||
const workload = document.getElementById("login-workload");
|
||||
const select = document.getElementById("login-workload-role");
|
||||
const workloadButton = document.getElementById("login-workload-submit");
|
||||
if (!roles.length || !workload || !select || !workloadButton) return;
|
||||
|
||||
const summary = document.getElementById("login-workload-summary");
|
||||
const hint = document.getElementById("login-workload-hint");
|
||||
if (summary && presets.workloadLabel) summary.textContent = presets.workloadLabel;
|
||||
if (hint && presets.workloadHint) hint.textContent = presets.workloadHint;
|
||||
for (const entry of roles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = entry.role;
|
||||
option.textContent = entry.label || entry.role;
|
||||
select.appendChild(option);
|
||||
}
|
||||
workload.hidden = false;
|
||||
bind(workloadButton, () => {
|
||||
const chosen = roles.find((entry) => entry.role === select.value);
|
||||
if (!chosen) throw new Error("Choose a listed workload role.");
|
||||
return chosen.role;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,5 +5,13 @@
|
|||
"role": "platform-admin",
|
||||
"title": "Sign in with KeyCape",
|
||||
"signInLabel": "Sign in with KeyCape",
|
||||
"banner": "Platform operators authenticate through KeyCape at kc.coulomb.social."
|
||||
}
|
||||
"banner": "Platform operators authenticate through KeyCape at kc.coulomb.social.",
|
||||
"workloadRoles": [
|
||||
{
|
||||
"role": "reuse-surface-hub-writer-workload-kv-read",
|
||||
"label": "reuse-surface hub writer (workload read)"
|
||||
}
|
||||
],
|
||||
"workloadLabel": "Sign in with a workload role",
|
||||
"workloadHint": "A workload role needs its own person. Open this page in a private window, or sign out of KeyCape first, so the platform-admin session is not reused."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
# CCR-2026-0044 adds lookup-self and revoke-self for the dedicated caller.
|
||||
# CCR-2026-0045 (NK-WP-0051-T06) adds capabilities-self so the caller proves
|
||||
# its own reach without an operator or root token.
|
||||
# Do not attach this policy to entity 4a1f663c-0621-0483-edd0-f852274f4b6b.
|
||||
|
||||
path "platform/data/workloads/reuse/reuse-surface/runtime-secrets" {
|
||||
|
|
@ -15,4 +17,8 @@ path "auth/token/lookup-self" {
|
|||
|
||||
path "auth/token/revoke-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
}
|
||||
|
||||
path "sys/capabilities-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,6 +253,8 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
|||
errors.append("openbao.token_self_lifecycle must be boolean")
|
||||
if "token_revoke_self" in openbao and not isinstance(openbao["token_revoke_self"], bool):
|
||||
errors.append("openbao.token_revoke_self must be boolean")
|
||||
if "token_capabilities_self" in openbao and not isinstance(openbao["token_capabilities_self"], bool):
|
||||
errors.append("openbao.token_capabilities_self must be boolean")
|
||||
if "kv_metadata" in openbao:
|
||||
validate_kv_metadata(openbao["kv_metadata"], errors)
|
||||
fields = [str(field) for field in require_list(openbao.get("fields"), "openbao.fields", errors)]
|
||||
|
|
@ -621,6 +623,11 @@ def generated_policy_hcl(ccr: dict[str, Any]) -> str:
|
|||
'\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n')
|
||||
elif openbao.get("token_revoke_self", False):
|
||||
body += '\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n'
|
||||
if openbao.get("token_capabilities_self", False):
|
||||
# NK-WP-0051-T06: the caller proves its own reach. capabilities-self is a
|
||||
# POST, so the grant is "update"; it reveals capabilities, never values.
|
||||
body += ('\npath "sys/capabilities-self" {\n'
|
||||
' capabilities = ["update"]\n}\n')
|
||||
return body
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ require_pattern \
|
|||
"$presets_json" \
|
||||
'"role"[[:space:]]*:[[:space:]]*"platform-admin"'
|
||||
|
||||
require_pattern \
|
||||
"presets.json offers the workload role selection (NK-WP-0051-T06)" \
|
||||
"$presets_json" \
|
||||
'"workloadRoles"'
|
||||
|
||||
require_pattern \
|
||||
"overlay.css hides namespace picker" \
|
||||
"$overlay_css" \
|
||||
|
|
|
|||
|
|
@ -683,6 +683,31 @@ class CredentialChangeTests(unittest.TestCase):
|
|||
self.assertTrue(ccr["openbao"]["auth"]["bound_claims_confirmed"])
|
||||
self.assertEqual(ccr["review"]["comments"][-1]["decision"], "binding_confirmed")
|
||||
|
||||
def test_hub_writer_caller_gains_generated_capabilities_self(self) -> None:
|
||||
# NK-WP-0051-T06: the proof no longer needs sys/capabilities with a root token.
|
||||
path = REPO_DIR / "credential-change-requests/CCR-2026-0045-reuse-surface-hub-writer-capabilities-self.yaml"
|
||||
ccr, errors, _warnings = credential_change.validate_ccr(path)
|
||||
self.assertEqual(errors, [])
|
||||
body = credential_change.generated_policy_hcl(ccr)
|
||||
self.assertIn('path "sys/capabilities-self" {\n capabilities = ["update"]\n}', body)
|
||||
self.assertNotIn('path "sys/capabilities" {', body)
|
||||
self.assertNotIn("*", body)
|
||||
self.assertEqual(credential_change.policy_artifact_diff(ccr)["status"], "matches")
|
||||
self.assertEqual(credential_change.applier_policy_violations(ccr), [])
|
||||
self.assertEqual(ccr["openbao"]["auth"]["policies"], ["workload-kv-read-reuse-surface-runtime"])
|
||||
without = json.loads(json.dumps(ccr))
|
||||
without["openbao"]["token_capabilities_self"] = False
|
||||
self.assertNotIn("capabilities-self", credential_change.generated_policy_hcl(without))
|
||||
|
||||
def test_token_capabilities_self_must_be_boolean(self) -> None:
|
||||
path = REPO_DIR / "credential-change-requests/CCR-2026-0045-reuse-surface-hub-writer-capabilities-self.yaml"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bad = Path(tmp) / "bad.yaml"
|
||||
bad.write_text(path.read_text(encoding="utf-8").replace(
|
||||
"token_capabilities_self: true", "token_capabilities_self: 'yes'"), encoding="utf-8")
|
||||
_ccr, errors, _warnings = credential_change.validate_ccr(bad)
|
||||
self.assertIn("openbao.token_capabilities_self must be boolean", errors)
|
||||
|
||||
def test_generated_policy_is_narrow(self) -> None:
|
||||
ccr, _errors, _warnings = credential_change.validate_ccr(self.sample)
|
||||
policy = credential_change.generated_policy_hcl(ccr)
|
||||
|
|
|
|||
95
tests/test_openbao_ui_role_select.py
Normal file
95
tests/test_openbao_ui_role_select.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""NK-WP-0051-T06: the OpenBao login page can choose the workload role.
|
||||
|
||||
The main button stays role platform-admin. Workload roles come only from the
|
||||
presets allow-list. The script runs in node against a minimal DOM stub.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
OVERLAY = REPO_DIR / "helm/openbao-ui-overlay"
|
||||
WORKLOAD_ROLE = "reuse-surface-hub-writer-workload-kv-read"
|
||||
|
||||
HARNESS = r"""
|
||||
const fs = require("fs");
|
||||
const [script, presetsPath, clickId, selectValue] = process.argv.slice(1);
|
||||
const presets = JSON.parse(fs.readFileSync(presetsPath, "utf8"));
|
||||
presets.workloadRoles = (presets.workloadRoles || []).concat([{ role: "Bad Role!" }]);
|
||||
const els = {};
|
||||
function el(id) {
|
||||
const node = { id, hidden: false, disabled: false, textContent: "", value: "",
|
||||
children: [], listeners: {}, classList: { add() {} },
|
||||
appendChild(c) { this.children.push(c); if (!this.value) this.value = c.value; },
|
||||
addEventListener(t, f) { this.listeners[t] = f; } };
|
||||
els[id] = node; return node;
|
||||
}
|
||||
["login-title","login-banner","login-submit","login-error","login-workload",
|
||||
"login-workload-summary","login-workload-hint","login-workload-role","login-workload-submit"].forEach(el);
|
||||
els["login-workload"].hidden = true;
|
||||
const posted = [];
|
||||
global.document = { getElementById: (id) => els[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }) };
|
||||
global.window = { location: { origin: "http://127.0.0.1:18200", assign() {} } };
|
||||
global.fetch = async (url, opts) => {
|
||||
if (url.endsWith("presets.json")) return { ok: true, json: async () => presets };
|
||||
posted.push({ url, body: JSON.parse(opts.body) });
|
||||
return { ok: true, json: async () => ({ data: { auth_url: "https://kc.example/authorize" } }) };
|
||||
};
|
||||
eval(fs.readFileSync(script, "utf8"));
|
||||
setTimeout(async () => {
|
||||
if (selectValue) els["login-workload-role"].value = selectValue;
|
||||
await els[clickId].listeners.click();
|
||||
console.log(JSON.stringify({ posted,
|
||||
options: els["login-workload-role"].children.map((c) => c.value),
|
||||
workloadHidden: els["login-workload"].hidden,
|
||||
error: els["login-error"].textContent }));
|
||||
}, 20);
|
||||
"""
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
class OpenBaoUiRoleSelectTests(unittest.TestCase):
|
||||
def run_page(self, click_id: str, select_value: str = "") -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", "-e", HARNESS, str(OVERLAY / "login.js"), str(OVERLAY / "presets.json"),
|
||||
click_id, select_value],
|
||||
capture_output=True, text=True, check=True, timeout=30,
|
||||
)
|
||||
return json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
|
||||
def test_presets_keep_platform_admin_and_list_workload_role(self) -> None:
|
||||
presets = json.loads((OVERLAY / "presets.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(presets["role"], "platform-admin")
|
||||
self.assertEqual(presets["mount"], "netkingdom")
|
||||
self.assertIn(WORKLOAD_ROLE, [entry["role"] for entry in presets["workloadRoles"]])
|
||||
|
||||
def test_main_button_stays_platform_admin(self) -> None:
|
||||
result = self.run_page("login-submit")
|
||||
self.assertEqual(len(result["posted"]), 1)
|
||||
self.assertEqual(result["posted"][0]["url"], "/v1/auth/netkingdom/oidc/auth_url")
|
||||
self.assertEqual(result["posted"][0]["body"]["role"], "platform-admin")
|
||||
self.assertEqual(
|
||||
result["posted"][0]["body"]["redirect_uri"],
|
||||
"http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback",
|
||||
)
|
||||
|
||||
def test_workload_button_uses_selected_listed_role(self) -> None:
|
||||
result = self.run_page("login-workload-submit", WORKLOAD_ROLE)
|
||||
self.assertFalse(result["workloadHidden"])
|
||||
self.assertEqual(result["options"], [WORKLOAD_ROLE])
|
||||
self.assertEqual(result["posted"][0]["body"]["role"], WORKLOAD_ROLE)
|
||||
|
||||
def test_unlisted_role_is_refused(self) -> None:
|
||||
result = self.run_page("login-workload-submit", "platform-admin")
|
||||
self.assertEqual(result["posted"], [])
|
||||
self.assertIn("listed workload role", result["error"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
84
workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md
Normal file
84
workplans/RPF-WP-0057-sign-in-diagnosis-caller-proof.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
id: RPF-WP-0057
|
||||
type: workplan
|
||||
title: "Let the hub-writer caller prove its own reach and pick the workload role in the OpenBao UI"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: active
|
||||
owner: claude
|
||||
topic_slug: railiance
|
||||
created: "2026-10-14"
|
||||
updated: "2026-10-14"
|
||||
---
|
||||
|
||||
Demand: NetKingdom NK-WP-0051-T06 (`net-kingdom/workplans/NK-WP-0051-sign-in-diagnosis.md`).
|
||||
Contract: `net-kingdom/docs/sign-in-diagnosis.md`. Paired ops-mason work:
|
||||
MASON-WP-0012. Background: MASON-WP-0011 and CCR-2026-0044.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Repo changes and tests only. Nothing is applied to OpenBao or the cluster.
|
||||
- `ops-mason-survey` is not touched.
|
||||
- `workload-kv-read-reuse-surface-runtime` is not attached to entity
|
||||
`4a1f663c-0621-0483-edd0-f852274f4b6b`.
|
||||
- No `verbose_oidc_logging`. No token, code, email or OTP in any file or log.
|
||||
|
||||
## Generate a capabilities-self grant for the lane policy
|
||||
|
||||
```task
|
||||
id: RPF-WP-0057-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
`scripts/credential-change.py` gains the boolean `openbao.token_capabilities_self`.
|
||||
When true, the generated policy adds `sys/capabilities-self` with `update`
|
||||
(the endpoint is a POST; it returns capability names only). Proposed
|
||||
CCR-2026-0045 sets it for the reuse-surface hub-writer caller and supersedes
|
||||
CCR-2026-0044 once applied. `openbao/policies/workload-kv-read-reuse-surface-runtime.hcl`
|
||||
matches the generated body. Tests in `tests/test_credential_change.py`.
|
||||
|
||||
Done 2026-10-14 in the repo. The live policy is unchanged.
|
||||
|
||||
## Apply CCR-2026-0045
|
||||
|
||||
```task
|
||||
id: RPF-WP-0057-T02
|
||||
status: wait
|
||||
depends_on: [RPF-WP-0057-T01]
|
||||
priority: high
|
||||
```
|
||||
|
||||
Needs platform-operator and reuse-surface-owner approval, then the founder's
|
||||
delegated metadata apply (`credential-change.py applier-apply CCR-2026-0045`).
|
||||
No KV write. Afterwards ops-mason re-runs its member and non-member proofs
|
||||
with capabilities-self only, and this CCR records that evidence.
|
||||
|
||||
## Let the OpenBao UI choose the workload role
|
||||
|
||||
```task
|
||||
id: RPF-WP-0057-T03
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
`helm/openbao-ui-overlay/login.{html,js,css}` and `presets.json`: the main
|
||||
button stays role `platform-admin`; a "Sign in with a workload role" section
|
||||
offers only the roles in `presets.json` `workloadRoles`
|
||||
(`reuse-surface-hub-writer-workload-kv-read`). Test:
|
||||
`tests/test_openbao_ui_role_select.py` (node harness).
|
||||
|
||||
Done 2026-10-14 in the repo. The live overlay is unchanged.
|
||||
|
||||
## Deploy the overlay
|
||||
|
||||
```task
|
||||
id: RPF-WP-0057-T04
|
||||
status: wait
|
||||
depends_on: [RPF-WP-0057-T03]
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Operator runs `make openbao-overlay-apply` then
|
||||
`make openbao-verify-login-overlay`, and one attended browser login per role
|
||||
(the workload role in a private window).
|
||||
Loading…
Add table
Add a link
Reference in a new issue