feat(k8s+tests): keycloak bootstrap Job + AgentRun actor trace tests
- new deploy/k8s/keycloak/keycloak-bootstrap-job.yaml: one-shot Job that waits for Keycloak, authenticates as admin, creates hwlab realm + hwlab-cloud-web OIDC client via REST API - secret hwlab-cloud-web-client holds the OIDC client_secret used by cloud-api - kustomization.yaml picks up the new Job and Secret - access-control.test.ts: cloud api AgentRun trace records real actor.userId from OIDC-upserted user (issue 788 spec) - access-control.test.ts: cloud api /v1/users/me returns actor with authMethod=api-key for HWLAB_API_KEY Bearer token - 31 pass / 4 pre-existing fail
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: keycloak-bootstrap-realm
|
||||
namespace: keycloak
|
||||
labels:
|
||||
app.kubernetes.io/name: keycloak-bootstrap-realm
|
||||
hwlab.pikastech.local/component: identity
|
||||
spec:
|
||||
backoffLimit: 5
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: keycloak-bootstrap-realm
|
||||
hwlab.pikastech.local/component: identity
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: bootstrap
|
||||
image: curlimages/curl:8.10.1
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ceu
|
||||
- |
|
||||
set -eu
|
||||
KC=http://keycloak.keycloak.svc.cluster.local:8080
|
||||
until curl -sf $KC/health/ready > /dev/null; do
|
||||
echo waiting-for-keycloak; sleep 5
|
||||
done
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASSWORD=$(cat /tmp/admin-password)
|
||||
CLIENT_ID=hwlab-cloud-web
|
||||
CLIENT_SECRET=$(cat /tmp/client-secret)
|
||||
REALM=hwlab
|
||||
until TOKEN=$(curl -sf -X POST "$KC/realms/master/protocol/openid-connect/token" -H "content-type: application/x-www-form-urlencoded" -d "username=$ADMIN_USER&password=$ADMIN_PASSWORD&grant_type=password&client_id=admin-cli" | sed -n '"'"'s/.*"access_token":"\([^"]*\)".*/\1/p'"'"'); do
|
||||
echo waiting-for-admin-cli; sleep 5
|
||||
done
|
||||
if curl -sf -H "Authorization: Bearer $TOKEN" $KC/admin/realms/$REALM > /dev/null; then
|
||||
echo realm-already-exists; exit 0
|
||||
fi
|
||||
curl -sf -X POST $KC/admin/realms -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d "{\"realm\":\"$REALM\",\"enabled\":true,\"registrationAllowed\":true,\"verifyEmail\":false,\"loginWithEmailAllowed\":true,\"duplicateEmailsAllowed\":false,\"resetPasswordAllowed\":false,\"editUsernameAllowed\":false,\"bruteForceProtected\":false,\"accessTokenLifespan\":1440,\"ssoSessionIdleTimeout\":86400,\"ssoSessionMaxLifespan\":86400}"
|
||||
echo realm-created
|
||||
curl -sf -X POST $KC/admin/realms/$REALM/clients -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d "{\"clientId\":\"$CLIENT_ID\",\"secret\":\"$CLIENT_SECRET\",\"redirectUris\":[\"http://74.48.78.17:19667/auth/oidc/callback\",\"https://auth.74-48-78-17.nip.io/auth/oidc/callback\"],\"webOrigins\":[\"+\"],\"standardFlowEnabled\":true,\"directAccessGrantsEnabled\":true}"
|
||||
echo client-created
|
||||
env:
|
||||
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
||||
value: admin
|
||||
volumeMounts:
|
||||
- name: admin-password
|
||||
mountPath: /tmp/admin-password
|
||||
subPath: password
|
||||
readOnly: true
|
||||
- name: client-secret
|
||||
mountPath: /tmp/client-secret
|
||||
subPath: client-secret
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: admin-password
|
||||
secret:
|
||||
secretName: keycloak-admin
|
||||
- name: client-secret
|
||||
secret:
|
||||
secretName: hwlab-cloud-web-client
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: hwlab-cloud-web-client
|
||||
namespace: keycloak
|
||||
labels:
|
||||
app.kubernetes.io/name: hwlab-cloud-web-client
|
||||
hwlab.pikastech.local/component: identity
|
||||
type: Opaque
|
||||
stringData:
|
||||
client-secret: kc-client-secret-placeholder
|
||||
@@ -5,6 +5,7 @@ resources:
|
||||
- namespace.yaml
|
||||
- postgres.yaml
|
||||
- keycloak.yaml
|
||||
- keycloak-bootstrap-job.yaml
|
||||
commonLabels:
|
||||
app.kubernetes.io/part-of: hwlab
|
||||
hwlab.pikastech.local/profile: dev
|
||||
|
||||
@@ -2721,3 +2721,57 @@ test("cloud api issues a 24h Web session with Secure/SameSite cookie and exposes
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api /v1/users/me returns actor with authMethod=api-key for HWLAB_API_KEY Bearer token", async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
|
||||
now: () => "2026-06-03T00:00:00.000Z"
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const setup = await postJson(port, "/v1/setup/first-admin", { username: "actor-admin", password: "actor-pass" });
|
||||
const cookie = setup.cookie;
|
||||
const apiKey = (await getJson(port, "/v1/api-keys/default", cookie)).body.key.displaySecret;
|
||||
const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
|
||||
assert.equal(me.status, 200);
|
||||
assert.equal(me.body.authMethod, "api-key");
|
||||
assert.equal(me.body.actor.username, "actor-admin");
|
||||
assert.equal(me.body.actor.role, "admin");
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api AgentRun trace records real actor.userId from OIDC-upserted user (issue 788 spec)", async () => {
|
||||
const accessController = createAccessController({
|
||||
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: "https://auth.74-48-78-17.nip.io/realms/hwlab", HWLAB_KEYCLOAK_CLIENT_ID: "hwlab-cloud-web", HWLAB_KEYCLOAK_CLIENT_SECRET: "secret" },
|
||||
fetchImpl: makeOidcFetchMock(),
|
||||
now: () => "2026-06-03T00:00:00.000Z"
|
||||
});
|
||||
const state = "actor-state";
|
||||
await accessController.store.upsertOidcState({ state, nonce: "actor-nonce", returnTo: "/", createdAt: "2026-06-03T00:00:00.000Z", expiresAt: "2026-06-03T00:10:00.000Z" });
|
||||
const server = createCloudApiServer({
|
||||
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
|
||||
accessController,
|
||||
now: () => "2026-06-03T00:00:00.000Z"
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/callback?code=oidc-code&state=${state}`, { redirect: "manual" });
|
||||
const cookie = (response.headers.get("set-cookie") ?? "").split(";")[0];
|
||||
const session = await getJson(port, "/v1/auth/session", cookie);
|
||||
const actor = session.body.actor;
|
||||
assert.equal(actor.username, "oidc-user");
|
||||
assert.equal(actor.role, "user");
|
||||
const apiKeys = await getJson(port, "/v1/api-keys", cookie);
|
||||
assert.equal(apiKeys.body.keys.length, 1);
|
||||
const apiKey = apiKeys.body.keys[0].displaySecret;
|
||||
const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
|
||||
assert.equal(me.body.actor.id, actor.id);
|
||||
assert.equal(me.body.authMethod, "api-key");
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user