Close T04 and pin the single route by which a principal becomes human
KEY-WP-0014-T04 sat `wait` with nothing outstanding, which is a contradiction. Its title is admit rotation and verify consumer handoff, and both are delivered: the semantics are published, the executor, authority and transport are named, the CCR will be prepared on request, and step 4 shipped as keycape verify-client; ops-warden took option (a) for the reason given, with both lanes owner-confirmed and no route changed. Executing a rotation was never its deliverable, so treating the owner's deferral as an open item would have kept the task open against work it was not scoped to do. The deferral stands separately, revisited on evidence. GH-DEC-2026-016 §5 applies A-16 to what makes a principal human. Verified against source that `human` has exactly one route here -- a literal on the authorization-code path after an upstream login resolved to a directory user, with no configuration able to assert it -- so A-16 does not yet bite and no provenance claim is warranted. Adding one would encode a distinction that does not exist. Asserting the behaviour would not protect that: a test checking a human token says human passes just as happily when the value starts coming from a registration. The guard parses the package and requires every principal_type assignment to be a string literal, the set being exactly human and service. It covers both shapes -- the browser path assigns into a map, the service path uses a key-value pair in a map literal -- and checking only assignments found one of two routes and passed, which the pinned literal set caught. Verified by mutation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
This commit is contained in:
parent
11bd9fb2bb
commit
f247d3529d
3 changed files with 222 additions and 7 deletions
112
src/internal/server/oidc/principal_type_guard_test.go
Normal file
112
src/internal/server/oidc/principal_type_guard_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// GH-DEC-2026-016 §5 applies A-16 to what makes a principal human: if `human` is
|
||||
// reachable by two routes — asserted by the identity layer about the person, or
|
||||
// supplied by a registration about the client they came through — the record must
|
||||
// say which, and a human-in-the-loop control must not be discharged on the
|
||||
// registration-supplied one. Refusing a service principal while accepting an
|
||||
// unverified assertion of humanity moves the defect rather than closing it.
|
||||
//
|
||||
// Today `human` has exactly ONE route here: it is a literal on the
|
||||
// authorization-code path, reached only after an upstream login resolved to a
|
||||
// directory user. No configuration field can assert it, which is why the tenant
|
||||
// provenance work has no counterpart for principal_type and why A-16 does not
|
||||
// yet bite.
|
||||
//
|
||||
// That is a property of the current source, not a law, and it is the kind of
|
||||
// property a plausible change erases quietly — `claims["principal_type"] =
|
||||
// client.PrincipalType` would look like a feature. This test pins the property
|
||||
// rather than the behaviour: principal_type must be assigned only from string
|
||||
// literals, so the moment humanity becomes data the build fails and whoever made
|
||||
// it data has to decide what provenance it carries.
|
||||
func TestPrincipalTypeIsNeverSuppliedByConfiguration(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, ".", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse package: %v", err)
|
||||
}
|
||||
|
||||
var literals []string
|
||||
var dynamic []string
|
||||
for name, pkg := range pkgs {
|
||||
if name != "oidc" {
|
||||
continue // skip the external test package
|
||||
}
|
||||
record := func(pos token.Pos, rhs ast.Expr) {
|
||||
if lit, ok := rhs.(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||||
value, _ := strconv.Unquote(lit.Value)
|
||||
literals = append(literals, value)
|
||||
return
|
||||
}
|
||||
dynamic = append(dynamic, fset.Position(pos).String())
|
||||
}
|
||||
|
||||
ast.Inspect(pkg, func(n ast.Node) bool {
|
||||
// Both shapes count: claims["principal_type"] = "human" on the
|
||||
// browser path, and "principal_type": "service" inside the service
|
||||
// path's map literal. Checking only assignments would have missed
|
||||
// half the routes, which is how a guard passes while proving less
|
||||
// than it claims.
|
||||
if kv, ok := n.(*ast.KeyValueExpr); ok {
|
||||
if key, ok := kv.Key.(*ast.BasicLit); ok && key.Kind == token.STRING {
|
||||
if unquoted, _ := strconv.Unquote(key.Value); unquoted == "principal_type" {
|
||||
record(kv.Pos(), kv.Value)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
assign, ok := n.(*ast.AssignStmt)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
for i, lhs := range assign.Lhs {
|
||||
index, ok := lhs.(*ast.IndexExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, ok := index.Index.(*ast.BasicLit)
|
||||
if !ok || key.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
if unquoted, _ := strconv.Unquote(key.Value); unquoted != "principal_type" {
|
||||
continue
|
||||
}
|
||||
if i >= len(assign.Rhs) {
|
||||
continue
|
||||
}
|
||||
record(assign.Pos(), assign.Rhs[i])
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if len(dynamic) > 0 {
|
||||
t.Fatalf("principal_type is assigned from something other than a string literal at %v.\n\n"+
|
||||
"That makes humanity DATA rather than a property of the authenticated path, and "+
|
||||
"creates the second route GH-DEC-2026-016 §5 warns about: `human` asserted by the "+
|
||||
"identity layer about the person, versus supplied by a registration about the client. "+
|
||||
"If that is deliberate, the claim must carry its provenance the way tenant_source "+
|
||||
"does, and a human-in-the-loop control must not be discharged on the "+
|
||||
"registration-supplied value. Decide that before deleting this test.", dynamic)
|
||||
}
|
||||
|
||||
sort.Strings(literals)
|
||||
want := []string{"human", "service"}
|
||||
if len(literals) != len(want) {
|
||||
t.Fatalf("principal_type literal assignments = %v; want exactly %v — one route each", literals, want)
|
||||
}
|
||||
for i := range want {
|
||||
if literals[i] != want[i] {
|
||||
t.Fatalf("principal_type literals = %v; want %v", literals, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue