Add approval:read to informed-decision-sitting-requester (source only)
Some checks failed
Authentication acceptance / acceptance (push) Failing after 56s
Authentication acceptance / provider-contract (push) Successful in 13s
Build and Publish Container Image / build-and-push (push) Successful in 26s

informed-decision's present_existing intake (INFD-WP-0010, 0e66a33) must GET
an existing approval before presenting it. This grants read only: no consume,
no approve, and the audience, subject, tenant and secret stay the same. The
tests pin the exact scope pair and keep the published JSON equal to the
source row.

The live registry is not changed. docs/sitting-requester-read-scope.md has the
record diff and the founder-run keycape-client.py remove/add/restart steps,
and lists the railiance-platform and informed-decision copies that still pin
create-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
tegwick 2026-11-01 00:34:07 +01:00
parent 655d8d31b3
commit 82b471fc36
5 changed files with 202 additions and 5 deletions

View file

@ -4,9 +4,12 @@ import (
"keycape/internal/config"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"gopkg.in/yaml.v3"
)
func TestServiceRegistrationAudienceAndScopeIsolation(t *testing.T) {
@ -184,14 +187,31 @@ func TestSittingRequesterRegistrationShapeIsExact(t *testing.T) {
if client.Tenant != "tenant:platform" || client.Audience != "approval-engine" {
t.Errorf("tenant/audience = %q %q", client.Tenant, client.Audience)
}
if len(client.AllowedScopes) != 1 || client.AllowedScopes[0] != "approval:create" {
t.Fatalf("scopes = %v; want only approval:create", client.AllowedScopes)
// create to file its own sittings; read (2026-11-01, INFD-WP-0010) to GET an
// existing approval before presenting it. Never consume or approve.
if !reflect.DeepEqual(client.AllowedScopes, []string{"approval:create", "approval:read"}) {
t.Fatalf("scopes = %v; want exactly approval:create, approval:read", client.AllowedScopes)
}
for _, scope := range client.AllowedScopes {
if scope == "approval:consume" || scope == "approval:approve" || scope == "approval:read" {
if scope == "approval:consume" || scope == "approval:approve" || scope == "openid" {
t.Fatalf("excess sitting-requester scope %q", scope)
}
}
if len(client.Roles) != 1 || client.Roles[0] != "informed-decision-sitting-requester" || client.TokenLifetime != "15m" {
t.Errorf("roles/lifetime = %v %q", client.Roles, client.TokenLifetime)
}
// The machine-readable copy that the live apply reads must equal the source row.
raw, err := os.ReadFile("../../../docs/sitting-requester-registration.json")
if err != nil {
t.Fatal(err)
}
var published config.ClientConfig // JSON is YAML; decode by the struct's yaml tags
if err := yaml.Unmarshal(raw, &published); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(published, *client) {
t.Fatalf("docs/sitting-requester-registration.json drifted from the source row:\n%+v\n%+v", published, *client)
}
if len(client.RedirectURIs) != 0 {
t.Errorf("redirect URIs = %v; sitting-requester is not a browser client", client.RedirectURIs)
}

View file

@ -63,7 +63,7 @@ func TestIdentityFactsCommittedServiceClients(t *testing.T) {
"secrets-engine-approval": {"approval:read", "approval:consume"},
"secrets-engine-requester": {"approval:create"},
"approval-engine-operator": {"approval:create", "approval:read", "approval:approve", "approval:revoke", "approval:supersede", "approval:observe", "approval:emit"},
"informed-decision-sitting-requester": {"approval:create"},
"informed-decision-sitting-requester": {"approval:create", "approval:read"},
"secrets-engine-openbao": {"openbao:login"},
}
seen := 0