Add identity and target authorization package

OIDC validation, principal mapping, remora:v1 roles, and current-authz
filtering with kind acceptance on Keycloak and ZITADEL.
This commit is contained in:
ms2k 2026-07-16 18:27:42 +09:00
commit bf028161ae
3 changed files with 3153 additions and 21 deletions

View file

@ -0,0 +1,40 @@
apiVersion: batch/v1
kind: Job
metadata:
name: @@JOB_NAME@@
namespace: remora-testbed
spec:
activeDeadlineSeconds: 600
backoffLimit: 0
ttlSecondsAfterFinished: 300
template:
spec:
automountServiceAccountToken: false
enableServiceLinks: false
restartPolicy: Never
dnsPolicy: None
dnsConfig:
nameservers: [10.96.0.53]
searches: [remora-testbed.svc.cluster.local, svc.cluster.local, cluster.local]
options:
- {name: ndots, value: "5"}
containers:
- name: tests
image: @@TEST_IMAGE@@
imagePullPolicy: Never
envFrom:
- configMapRef: {name: @@PROFILE_NAME@@}
env:
- name: REMORA_IDENTITY_REQUIRE
value: "1"
volumeMounts:
- {name: fixture-profile, mountPath: /run/remora-fixture, readOnly: true}
resources:
requests: {cpu: 250m, memory: 256Mi}
limits: {cpu: "2", memory: 1Gi}
volumes:
- name: fixture-profile
configMap:
name: @@PROFILE_NAME@@-file
items:
- {key: profile.env, path: profile.env}

View file

@ -451,11 +451,26 @@ reset_lane_target_store() {
aws --endpoint-url "$endpoint" s3api create-bucket --bucket "$bucket" >&2 || return
}
# Remora storage role for fixture lanes: target-main / target-bucket / models.
remora_fixture_storage_role() {
local b64url
b64url() {
printf '%s' "$1" | openssl base64 -A | tr '+/' '-_' | tr -d '='
}
printf 'remora:v1:storage|%s|%s|%s\n' \
"$(b64url target-main)" \
"$(b64url target-bucket)" \
"$(b64url models)"
}
reset_keycloak_lane() {
local lane="$1"
local realm="remora-lane-$lane"
local username="remora-user-$lane"
local client_id="remora-client-$lane"
local storage_role
local client_uuid
storage_role="$(remora_fixture_storage_role)"
if kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh get "realms/$realm" \
--no-config \
@ -473,7 +488,26 @@ reset_keycloak_lane() {
--user remora-admin \
--password synthetic-keycloak \
--set "realm=$realm" \
--set enabled=true >&2 || return
--set enabled=true \
--set registrationEmailAsUsername=false \
--set verifyEmail=false \
--set resetPasswordAllowed=false \
--set loginWithEmailAllowed=true \
--set duplicateEmailsAllowed=false \
--set sslRequired=none >&2 || return
# Keycloak 26 enables VERIFY_PROFILE by default; that blocks ROPC with
# "Account is not fully set up".
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh update authentication/required-actions/VERIFY_PROFILE \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--set enabled=false \
--set defaultAction=false >&2 || true
# Public client with direct access grants so package Jobs can mint ROPC tokens.
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create clients \
--no-config \
@ -485,7 +519,38 @@ reset_keycloak_lane() {
--set "clientId=$client_id" \
--set publicClient=true \
--set standardFlowEnabled=true \
--set 'redirectUris=["http://localhost/callback"]' >&2 || return
--set directAccessGrantsEnabled=true \
--set 'redirectUris=["http://localhost/callback"]' \
--set 'attributes={"access.token.lifespan":"3600"}' >&2 || return
client_uuid="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh get clients \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--query "clientId=$client_id" \
--fields id \
--format csv \
--noquotes)" || return
client_uuid="$(printf '%s' "$client_uuid" | tr -d '\r' | head -n1)"
[[ -n "$client_uuid" ]] || return
# Include this client in access-token `aud` so mandatory audience checks pass.
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create "clients/$client_uuid/protocol-mappers/models" \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--set name=remora-audience \
--set protocol=openid-connect \
--set protocolMapper=oidc-audience-mapper \
--set 'config."included.client.audience"='"$client_id" \
--set 'config."id.token.claim"=false' \
--set 'config."access.token.claim"=true' >&2 || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create roles \
--no-config \
@ -495,6 +560,26 @@ reset_keycloak_lane() {
--password synthetic-keycloak \
-r "$realm" \
--set name=test-user >&2 || return
# Client roles are the only Keycloak claim source Remora trusts (resource_access).
# Use -b JSON so `|` in storage roles is not percent-encoded by --set.
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create "clients/$client_uuid/roles" \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
-b "{\"name\":\"remora:v1:admin\"}" >&2 || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create "clients/$client_uuid/roles" \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
-b "{\"name\":\"$storage_role\"}" >&2 || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create users \
--no-config \
@ -506,7 +591,35 @@ reset_keycloak_lane() {
--set "username=$username" \
--set enabled=true \
--set emailVerified=true \
--set "email=$username@remora.test" >&2 || return
--set "email=$username@remora.test" \
--set 'requiredActions=[]' >&2 || return
local user_id
user_id="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh get users \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--query "username=$username" \
--fields id \
--format csv \
--noquotes)" || return
user_id="$(printf '%s' "$user_id" | tr -d '\r' | head -n1)"
[[ -n "$user_id" ]] || return
# Clear any required actions and mark email verified for ROPC.
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh update "users/$user_id" \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--set emailVerified=true \
--set 'requiredActions=[]' \
--set enabled=true >&2 || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh set-password \
--no-config \
@ -515,8 +628,9 @@ reset_keycloak_lane() {
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--username "$username" \
--new-password Synthetic-OIDC-1! >&2 || return
--userid "$user_id" \
--new-password Synthetic-OIDC-1! \
--temporary=false >&2 || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh add-roles \
--no-config \
@ -527,11 +641,30 @@ reset_keycloak_lane() {
-r "$realm" \
--uusername "$username" \
--rolename test-user >&2 || return
printf '%s\t%s\t%s\t%s\n' \
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh add-roles \
--no-config \
--server http://127.0.0.1:8080 \
--realm master \
--user remora-admin \
--password synthetic-keycloak \
-r "$realm" \
--uusername "$username" \
--cclientid "$client_id" \
--rolename 'remora:v1:admin' \
--rolename "$storage_role" >&2 || return
# issuer \t tenant \t client_id \t username \t storage_role \t project_id \t user_id \t mgmt_token
# issuer \t tenant \t client_id \t username \t storage_role \t project_id \t user_id \t mgmt_token \t user_key_b64
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"http://keycloak.remora.test:8080/realms/$realm" \
"$realm" \
"$client_id" \
"$username"
"$username" \
"$storage_role" \
"" \
"" \
"" \
""
}
zitadel_request() {
@ -551,7 +684,7 @@ zitadel_request() {
--silent \
--show-error \
--request "$method" \
--header "Host: zitadel.remora.test" \
--header "Host: zitadel.remora.test:8080" \
--header "Authorization: Bearer $token" \
--header "Content-Type: application/json"
if test -n "$organization"; then
@ -577,11 +710,15 @@ reset_zitadel_lane() {
local lane="$1"
local tenant="remora-lane-$lane"
local username="remora-user-$lane"
local storage_role
local response
local organization_id
local project_id
local user_id
local client_id
local mgmt_token
local grant_id
storage_role="$(remora_fixture_storage_role)"
cleanup_zitadel_lane "$lane" || return
response="$(zitadel_request /management/v1/orgs "{\"name\":\"$tenant\"}")" || return
organization_id="$(json_string_field "$response" id)" || return
@ -591,25 +728,74 @@ reset_zitadel_lane() {
"/management/v1/projects/$project_id/roles" \
'{"roleKey":"test-user","displayName":"Test User","group":"fixture"}' \
"$organization_id" >/dev/null || return
zitadel_request \
"/management/v1/projects/$project_id/roles" \
'{"roleKey":"remora:v1:admin","displayName":"Remora Admin","group":"remora"}' \
"$organization_id" >/dev/null || return
zitadel_request \
"/management/v1/projects/$project_id/roles" \
"{\"roleKey\":\"$storage_role\",\"displayName\":\"Remora Storage\",\"group\":\"remora\"}" \
"$organization_id" >/dev/null || return
# Native app: PKCE public client with JWT access tokens and project role assertion.
response="$(zitadel_request \
"/management/v1/projects/$project_id/apps/oidc" \
"{\"name\":\"$tenant\",\"redirectUris\":[\"http://localhost/callback\"],\"postLogoutRedirectUris\":[\"http://localhost/logout\"],\"responseTypes\":[\"OIDC_RESPONSE_TYPE_CODE\"],\"grantTypes\":[\"OIDC_GRANT_TYPE_AUTHORIZATION_CODE\",\"OIDC_GRANT_TYPE_REFRESH_TOKEN\"],\"appType\":\"OIDC_APP_TYPE_USER_AGENT\",\"authMethodType\":\"OIDC_AUTH_METHOD_TYPE_NONE\",\"version\":\"OIDC_VERSION_1_0\",\"devMode\":true,\"accessTokenType\":\"OIDC_TOKEN_TYPE_JWT\",\"accessTokenRoleAssertion\":true}" \
"{\"name\":\"$tenant\",\"redirectUris\":[\"http://localhost/callback\"],\"postLogoutRedirectUris\":[\"http://localhost/logout\"],\"responseTypes\":[\"OIDC_RESPONSE_TYPE_CODE\"],\"grantTypes\":[\"OIDC_GRANT_TYPE_AUTHORIZATION_CODE\",\"OIDC_GRANT_TYPE_REFRESH_TOKEN\"],\"appType\":\"OIDC_APP_TYPE_NATIVE\",\"authMethodType\":\"OIDC_AUTH_METHOD_TYPE_NONE\",\"version\":\"OIDC_VERSION_1_0\",\"devMode\":true,\"accessTokenType\":\"OIDC_TOKEN_TYPE_JWT\",\"accessTokenRoleAssertion\":true,\"idTokenRoleAssertion\":true,\"idTokenUserinfoAssertion\":true}" \
"$organization_id")" || return
client_id="$(json_string_field "$response" clientId)" || return
response="$(zitadel_request \
/management/v1/users/human \
"{\"userName\":\"$username\",\"profile\":{\"firstName\":\"Remora\",\"lastName\":\"Lane $lane\",\"displayName\":\"Remora Lane $lane\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_UNSPECIFIED\"},\"email\":{\"email\":\"$username@remora.test\",\"isEmailVerified\":true},\"password\":\"Synthetic-OIDC-1!\",\"passwordChangeRequired\":false}" \
"{\"userName\":\"$username\",\"profile\":{\"firstName\":\"Remora\",\"lastName\":\"Lane $lane\",\"displayName\":\"Remora Lane $lane\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_UNSPECIFIED\"},\"email\":{\"email\":\"$username@remora.test\",\"isEmailVerified\":true},\"initialPassword\":\"Synthetic-OIDC-1!\"}" \
"$organization_id")" || return
user_id="$(json_string_field "$response" userId)" || return
zitadel_request \
# Set a permanent password for Session API checks (initialPassword alone is not enough on all versions).
if ! zitadel_request \
"/management/v1/users/$user_id/password" \
'{"password":"Synthetic-OIDC-1!","noChangeRequired":true}' \
"$organization_id" >/dev/null; then
# Fallback field name used by older management API payloads.
zitadel_request \
"/management/v1/users/$user_id/password" \
'{"password":"Synthetic-OIDC-1!","changeRequired":false}' \
"$organization_id" >/dev/null || return
fi
response="$(zitadel_request \
"/management/v1/users/$user_id/grants" \
"{\"projectId\":\"$project_id\",\"roleKeys\":[\"test-user\"]}" \
"{\"projectId\":\"$project_id\",\"roleKeys\":[\"test-user\",\"remora:v1:admin\",\"$storage_role\"]}" \
"$organization_id")" || return
# Machine user + application key for JWT-bearer access tokens with project roles.
local machine_name="remora-machine-$lane"
# Bearer defaults to opaque tokens; Remora validates only JWKS-signed JWTs.
response="$(zitadel_request \
/management/v1/users/machine \
"{\"userName\":\"$machine_name\",\"name\":\"Remora Machine $lane\",\"description\":\"JWT bearer minting\",\"accessTokenType\":\"ACCESS_TOKEN_TYPE_JWT\"}" \
"$organization_id")" || return
local machine_id
machine_id="$(json_string_field "$response" userId)" || return
zitadel_request \
"/management/v1/users/$machine_id/grants" \
"{\"projectId\":\"$project_id\",\"roleKeys\":[\"remora:v1:admin\",\"$storage_role\"]}" \
"$organization_id" >/dev/null || return
printf '%s\t%s\t%s\t%s\n' \
response="$(zitadel_request \
"/management/v1/users/$machine_id/keys" \
'{"type":"KEY_TYPE_JSON","expirationDate":"2099-01-01T00:00:00Z"}' \
"$organization_id")" || return
local key_details_b64
key_details_b64="$(json_string_field "$response" keyDetails)" || return
mgmt_token="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/zitadel --container provisioner -- \
cat /bootstrap/admin.pat)" || return
mgmt_token="$(printf '%s' "$mgmt_token" | tr -d '\r\n')"
# issuer \t tenant \t client_id \t username \t storage_role \t project_id \t user_id \t mgmt_token \t key_details_b64
# user_id is the machine user used for JWT-bearer minting (and grant loss).
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
http://zitadel.remora.test:8080 \
"$organization_id" \
"$client_id" \
"$username"
"$username" \
"$storage_role" \
"$project_id" \
"$machine_id" \
"$mgmt_token" \
"$key_details_b64"
}
reset_lane_oidc_provider() {
@ -767,6 +953,11 @@ write_lane_profile() {
local oidc_tenant=
local oidc_client_id=
local oidc_username=
local oidc_storage_role=
local oidc_project_id=
local oidc_user_id=
local oidc_mgmt_token=
local oidc_user_key_b64=
local oidc_profile
local target_endpoint=
local target_bucket=
@ -787,7 +978,9 @@ write_lane_profile() {
fi
if [[ "$oidc" != - ]]; then
oidc_profile="$(reset_lane_oidc_provider "$oidc" "$lane")" || return
IFS=$'\t' read -r oidc_issuer oidc_tenant oidc_client_id oidc_username <<< "$oidc_profile"
IFS=$'\t' read -r oidc_issuer oidc_tenant oidc_client_id oidc_username \
oidc_storage_role oidc_project_id oidc_user_id oidc_mgmt_token oidc_user_key_b64 \
<<< "$oidc_profile"
fi
if [[ "$target" != - ]]; then
target_bucket="remora-lane-$lane"
@ -820,6 +1013,11 @@ write_lane_profile() {
"REMORA_OIDC_CLIENT_ID=$oidc_client_id" \
"REMORA_OIDC_USERNAME=$oidc_username" \
"REMORA_OIDC_PASSWORD=Synthetic-OIDC-1!" \
"REMORA_OIDC_STORAGE_ROLE=$oidc_storage_role" \
"REMORA_OIDC_PROJECT_ID=$oidc_project_id" \
"REMORA_OIDC_USER_ID=$oidc_user_id" \
"REMORA_OIDC_MGMT_TOKEN=$oidc_mgmt_token" \
"REMORA_OIDC_USER_KEY_B64=$oidc_user_key_b64" \
"REMORA_TARGET_PROVIDER=$target" \
"REMORA_TARGET_ENDPOINT=$target_endpoint" \
"REMORA_TARGET_BUCKET=$target_bucket" \
@ -847,6 +1045,11 @@ write_lane_profile() {
--from-literal="REMORA_OIDC_CLIENT_ID=$oidc_client_id" \
--from-literal="REMORA_OIDC_USERNAME=$oidc_username" \
--from-literal=REMORA_OIDC_PASSWORD=Synthetic-OIDC-1! \
--from-literal="REMORA_OIDC_STORAGE_ROLE=$oidc_storage_role" \
--from-literal="REMORA_OIDC_PROJECT_ID=$oidc_project_id" \
--from-literal="REMORA_OIDC_USER_ID=$oidc_user_id" \
--from-literal="REMORA_OIDC_MGMT_TOKEN=$oidc_mgmt_token" \
--from-literal="REMORA_OIDC_USER_KEY_B64=$oidc_user_key_b64" \
--from-literal="REMORA_TARGET_PROVIDER=$target" \
--from-literal="REMORA_TARGET_ENDPOINT=$target_endpoint" \
--from-literal="REMORA_TARGET_BUCKET=$target_bucket" \

50
tests/kind/run-identity-job.sh Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
source "$ROOT/tests/kind/lib.sh"
USER_KUBECONFIG_BEFORE="$(user_kubeconfig_digest)"
SUCCEEDED=false
OWNS_FIXTURE=false
cleanup() {
local status=$?
trap - EXIT INT TERM
if [[ "${REMORA_TESTBED_RETAIN:-0}" = 1 ]] && [[ "$SUCCEEDED" != true ]]; then
printf 'retained kubeconfig: %s\n' "$KUBECONFIG_PATH"
printf 'retained context: %s\n' "$CONTEXT"
exit "$status"
fi
if [[ "$OWNS_FIXTURE" = true ]]; then
destroy_fixture || status=1
fi
if [[ "$(user_kubeconfig_digest)" != "$USER_KUBECONFIG_BEFORE" ]]; then
printf 'user kubeconfig changed unexpectedly\n' >&2
status=1
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
acquire_fixture_lock
REMORA_REAL_NERDCTL="$(command -v nerdctl)"
export REMORA_REAL_NERDCTL
"$REMORA_REAL_NERDCTL" --namespace default build \
--file "$ROOT/Dockerfile.test" \
--target kind-identity-runner \
--tag remora-kind-identity:local \
"$ROOT"
OWNS_FIXTURE=true
setup_fixture durable oidc
load_local_image remora-kind-identity:local
run_job_matrix \
identity-authz \
"$TESTBED/identity-job.yaml.in" \
remora-kind-identity:local \
durable oidc
SUCCEEDED=true
printf 'identity and target authorization package test Job passed\n'