feat: identity risk + assurance metadata (CoSAI + NIST IAL, closes #80)#122
Merged
feat: identity risk + assurance metadata (CoSAI + NIST IAL, closes #80)#122
Conversation
…oses #80 Adds three optional metadata fields to identities so operators can classify agents against the CoSAI Agentic IAM capability–risk matrix and NIST SP 800-63 Identity Assurance Levels. These become the hooks for future default-policy selection (stricter TTL / mandatory attestation / human-in-the-loop for high-risk tier). ## New fields domain.Identity: CapabilityTier string // "" | "low" | "high" (CoSAI §3.2) RiskTier string // "" | "low" | "high" (CoSAI §3.2) IAL string // "" | "ial1"|"ial2"|"ial3" (NIST SP 800-63) All three are optional; empty string means "unclassified" and is the default for existing rows after the migration. The bun column tags use `nullzero` so an empty Go string round-trips as SQL NULL — necessary because the per-column CHECK constraints enforce "NULL or one of the enum values," and "" would otherwise violate them. ## Migration `016_identity_risk_metadata.up.sql` adds three nullable VARCHAR(20) columns + three CHECK constraints. The matching down migration drops the constraints first, then the columns. ## API surface POST /identities and PATCH /identities/{id} accept the new fields with huma `enum:` tags, so invalid values surface as 422 from the schema validator before the request reaches the service. The service-layer ValidCapabilityTier / ValidRiskTier / ValidIAL functions in domain/ are defense-in-depth for callers that bypass the schema (programmatic Go consumers). GET /identities and GET /identities/{id} return the values in JSON; unset fields are omitted via `omitempty`. ## Out of scope (separate follow-ups per #80) - highflame-shield / highflame-policy changes to consume these fields in policy decisions. - A docs section in the identity reference mapping the tiers to the CoSAI §3.2 cells and IAL to NIST SP 800-63. ## Test plan - [x] go vet ./... clean - [x] go build ./... clean - [x] Full integration suite green ~10s, including: - TestRegisterIdentityWithRiskMetadata (round-trip register → GET) - TestRegisterIdentityRiskMetadataDefaults (omit → unclassified) - TestRegisterIdentityRejectsInvalidRiskMetadata (422 on bad enums) - TestUpdateIdentityRiskMetadata (PATCH + re-classification)
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces risk and assurance metadata fields—capability tier, risk tier, and Identity Assurance Level (IAL)—to the identity domain, following CoSAI and NIST standards. The changes include database migrations, API handler updates, service-layer validation, and integration tests. The feedback focuses on refactoring the error handling to use standard Go error wrapping with errors.Is instead of fragile string prefix checks in the handlers, which would also allow for the removal of the strings package import.
Service-layer enum validations now wrap ErrInvalidIdentityField via fmt.Errorf "%w: ..." so the handler can map them to 400 with a single errors.Is check (consistent with the SPIFFE path-segment validation already in place). Collapses three redundant strings.HasPrefix branches in the handlers, drops the now-unused strings import, and adds the missing ErrInvalidIdentityField check on the update path that previously returned 500 for caller-fixable input. Addresses Gemini review on PR #122.
saucam
approved these changes
May 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #80. Adds three optional classification fields on
domain.Identityaligned with the CoSAI Agentic IAM capability–risk matrix (§3.2) and NIST SP 800-63 Identity Assurance Levels (referenced in §3.5):""= unclassified)capability_tierlow,highrisk_tierlow,highialial1,ial2,ial3These become the hooks for future default-policy selection — stricter TTL / mandatory attestation / human-in-the-loop for high-risk-tier agents, etc. They're explicitly the schema + API + docs slice; consumption inside Shield / Policy is a separate follow-up per #80's "out of scope."
Schema
migrations/016_identity_risk_metadata.up.sqladds three nullableVARCHAR(20)columns with per-column CHECK constraints:Matching down migration drops constraints first, then columns. Existing rows default to NULL ("unclassified") and the migration applies cleanly without backfill.
The bun column tags use
nullzeroso an empty Go string round-trips as SQL NULL — essential because""would otherwise violate the CHECK constraint.API surface
POST /identitiesandPATCH /identities/{id}accept all three fields with humaenum:schema tags. Bad values surface as 422 Unprocessable Entity from the schema validator before the request reaches the service. The service-layerdomain.ValidCapabilityTier/domain.ValidRiskTier/domain.ValidIALfunctions are defense-in-depth for callers that bypass the schema (programmatic Go consumers).GET /identitiesandGET /identities/{id}return the values in JSON; unset fields are omitted viaomitempty.The fields are NOT auto-cleared by sending
""through the API: the enum schema rejects""as not-an-enum-value. Once classified, an agent stays classified until explicitly re-classified to another valid value. SQL-layer clear via direct DB access remains possible if an operator really needs it; that's an admin-tool problem, not an API contract.Out of scope (separate follow-ups per #80)
Test plan
go vet ./...cleango build ./...cleanTestRegisterIdentityWithRiskMetadata— register → GET round-trip with valid values.TestRegisterIdentityRiskMetadataDefaults— omit → response excludes the fields entirely (no"capability_tier": ""noise).TestRegisterIdentityRejectsInvalidRiskMetadata— three subtests for unknown values across all three fields; asserts 4xx (400 or 422), specifically NOT a 500 from a CHECK violation leaking back.TestUpdateIdentityRiskMetadata— PATCH-style update + re-classification, with a sibling assertion thatcapability_tierupdates don't disturbrisk_tier/ial.Spec references