Skip to content

feat: identity risk + assurance metadata (CoSAI + NIST IAL, closes #80)#122

Merged
rsharath merged 3 commits intomainfrom
add-identity-risk-ial-metadata
May 6, 2026
Merged

feat: identity risk + assurance metadata (CoSAI + NIST IAL, closes #80)#122
rsharath merged 3 commits intomainfrom
add-identity-risk-ial-metadata

Conversation

@rsharath
Copy link
Copy Markdown
Contributor

@rsharath rsharath commented May 5, 2026

Summary

Closes #80. Adds three optional classification fields on domain.Identity aligned with the CoSAI Agentic IAM capability–risk matrix (§3.2) and NIST SP 800-63 Identity Assurance Levels (referenced in §3.5):

Field Values (besides "" = unclassified) Standard
capability_tier low, high CoSAI Agentic IAM §3.2
risk_tier low, high CoSAI Agentic IAM §3.2
ial ial1, ial2, ial3 NIST SP 800-63 (§3.5 ref)

These 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.sql adds three nullable VARCHAR(20) columns with per-column CHECK constraints:

ALTER TABLE identities
    ADD COLUMN capability_tier VARCHAR(20) CHECK (capability_tier IS NULL OR capability_tier IN ('low','high')),
    ADD COLUMN risk_tier       VARCHAR(20) CHECK (risk_tier       IS NULL OR risk_tier       IN ('low','high')),
    ADD COLUMN ial             VARCHAR(20) CHECK (ial             IS NULL OR ial             IN ('ial1','ial2','ial3'));

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 nullzero so an empty Go string round-trips as SQL NULL — essential because "" would otherwise violate the CHECK constraint.

API surface

POST /identities and PATCH /identities/{id} accept all three fields with huma enum: schema tags. Bad values surface as 422 Unprocessable Entity from the schema validator before the request reaches the service. The service-layer domain.ValidCapabilityTier / domain.ValidRiskTier / domain.ValidIAL functions 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.

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)

  • Shield / highflame-policy consumption of the new fields in authorization decisions.
  • Identity docs page mapping the tiers to specific CoSAI §3.2 cells and IAL to specific NIST SP 800-63 levels.

Test plan

  • go vet ./... clean
  • go build ./... clean
  • Full integration suite green ~10s, including four new tests:
    • TestRegisterIdentityWithRiskMetadata — 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 that capability_tier updates don't disturb risk_tier / ial.

Spec references

…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)
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/handler/identity.go Outdated
Comment thread internal/handler/identity.go Outdated
Comment thread internal/handler/identity.go Outdated
Comment thread internal/service/identity.go
Comment thread internal/service/identity.go
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.
@rsharath rsharath merged commit cbe515c into main May 6, 2026
10 checks passed
@rsharath rsharath deleted the add-identity-risk-ial-metadata branch May 6, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identity risk/assurance metadata (tier + IAL)

2 participants