Add AccountType to TokenInfo

- TokenInfo now includes AccountType ("human" or "system") from the
  MCIAS validate response
- Required for policy engines (MCR, Metacrypt) that match on account type
- Mock MCIAS in tests updated to return account_type
- New assertion in TestValidateToken verifies AccountType is populated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 17:45:12 -07:00
parent bbf491f343
commit ceb10ce102
2 changed files with 27 additions and 15 deletions

View File

@@ -73,6 +73,11 @@ type TokenInfo struct {
// Username is the MCIAS username (the "sub" claim). // Username is the MCIAS username (the "sub" claim).
Username string Username string
// AccountType is the MCIAS account type: "human" or "system".
// Used by policy engines that need to distinguish interactive users
// from service accounts.
AccountType string
// Roles is the set of MCIAS roles assigned to the account. // Roles is the set of MCIAS roles assigned to the account.
Roles []string Roles []string
@@ -193,10 +198,11 @@ func (a *Authenticator) ValidateToken(token string) (*TokenInfo, error) {
} }
var resp struct { var resp struct {
Valid bool `json:"valid"` Valid bool `json:"valid"`
Sub string `json:"sub"` Sub string `json:"sub"`
Username string `json:"username"` Username string `json:"username"`
Roles []string `json:"roles"` AccountType string `json:"account_type"`
Roles []string `json:"roles"`
} }
status, err := a.doJSON(http.MethodPost, "/v1/token/validate", status, err := a.doJSON(http.MethodPost, "/v1/token/validate",
map[string]string{"token": token}, &resp) map[string]string{"token": token}, &resp)
@@ -209,9 +215,10 @@ func (a *Authenticator) ValidateToken(token string) (*TokenInfo, error) {
} }
info := &TokenInfo{ info := &TokenInfo{
Username: resp.Username, Username: resp.Username,
Roles: resp.Roles, AccountType: resp.AccountType,
IsAdmin: hasRole(resp.Roles, "admin"), Roles: resp.Roles,
IsAdmin: hasRole(resp.Roles, "admin"),
} }
if info.Username == "" { if info.Username == "" {
info.Username = resp.Sub info.Username = resp.Sub

View File

@@ -54,18 +54,20 @@ func mockMCIAS(t *testing.T) *httptest.Server {
case "tok-admin-123": case "tok-admin-123":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{ _ = json.NewEncoder(w).Encode(map[string]interface{}{
"valid": true, "valid": true,
"sub": "uuid-admin", "sub": "uuid-admin",
"username": "admin", "username": "admin",
"roles": []string{"admin", "user"}, "account_type": "human",
"roles": []string{"admin", "user"},
}) })
case "tok-user-456": case "tok-user-456":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{ _ = json.NewEncoder(w).Encode(map[string]interface{}{
"valid": true, "valid": true,
"sub": "uuid-user", "sub": "uuid-user",
"username": "alice", "username": "alice",
"roles": []string{"user"}, "account_type": "human",
"roles": []string{"user"},
}) })
case "tok-expired": case "tok-expired":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -154,6 +156,9 @@ func TestValidateToken(t *testing.T) {
if info.Username != "admin" { if info.Username != "admin" {
t.Fatalf("Username = %q, want %q", info.Username, "admin") t.Fatalf("Username = %q, want %q", info.Username, "admin")
} }
if info.AccountType != "human" {
t.Fatalf("AccountType = %q, want %q", info.AccountType, "human")
}
if !info.IsAdmin { if !info.IsAdmin {
t.Fatal("IsAdmin = false, want true") t.Fatal("IsAdmin = false, want true")
} }