Skip to content

feat(scim): SCIM 2.0 user provisioning with deprovision-kills-sessions - #73

Merged
PenguinzTech merged 2 commits into
v2.1.xfrom
feature/scim-provisioning
Jul 27, 2026
Merged

feat(scim): SCIM 2.0 user provisioning with deprovision-kills-sessions#73
PenguinzTech merged 2 commits into
v2.1.xfrom
feature/scim-provisioning

Conversation

@PenguinzTech

Copy link
Copy Markdown
Contributor

SCIM 2.0 server subset (RFC 7643/7644) so enterprise IdPs (Okta/Entra) can provision and deprovision Squawk users. /scim/v2 ServiceProviderConfig/ResourceTypes/Schemas (honest capability advertisement), Users CRUD with filter=userName eq, 1-based startIndex pagination, PATCH subset with proper scimType errors. Auth via dedicated bcrypt-hashed SCIM bearer tokens (minted once-visible by admins, admin:super + Enterprise tier gated, constant-time validation). Deprovisioning (DELETE or active=false) disables the user — refresh continuation dies via the user.active gate in the refresh flow (tested: the old refresh token stops working); short-TTL access tokens expire naturally. external_id column added for IdP correlation.

Tests: 201 green (30 new: auth 401s, CRUD cycle, filters/pagination incl. empty results, deprovision-kills-refresh proof, SCIM error shapes, license gating).


Stack note: stacks on #62 (feature/mfa-totp) alongside the SSO branch; auto-retargets as the stack merges. Contains alembic migration 010 — same parallel-numbering caveat as the batch-1 auth PRs.

🤖 Generated with Claude Code

Implements RFC 7643/7644 SCIM 2.0 subset for enterprise IdP (Okta/Entra)
user provisioning with:

- Dedicated SCIM bearer tokens (bcrypt-hashed, constant-time validation)
- POST /Users (JIT create, no password set), GET (read), PUT (replace),
  PATCH (partial update), DELETE (soft-delete/deprovisioning)
- Filtering (userName eq only) and pagination (1-based startIndex per spec)
- Deprovisioning: PATCH/DELETE active=false relies on refresh_access_token's
  user.active check (auth_service.py:257) to fail closed; access tokens expire
  naturally (~15min)
- Admin endpoints: /admin/tokens (mint/revoke) gated by admin:super scope
  and Enterprise license tier
- Static metadata: /ServiceProviderConfig, /ResourceTypes, /Schemas

Schema additions:
- auth_user: external_id column (unique, indexed) for SCIM provisioning
- scim_tokens: dedicated table for bearer tokens (id, token_hash, tenant,
  active, created_at, last_used_at)

Migration: 010_add_scim_provisioning creates new table and columns.

Comprehensive test coverage (30 tests):
- Token authentication (401/403 for invalid/revoked tokens)
- CRUD lifecycle (create, read, update, delete)
- Filtering and pagination per RFC 7644
- Deprovisioning confirms inactive users can't refresh
- Unsupported filter/path → proper SCIM error responses
- Enterprise license gate on token minting
- Admin scope (admin:super) gate on token endpoints

All 201 backend tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech PenguinzTech self-assigned this Jul 26, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @PenguinzTech, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

- app/__init__.py: combined scim_bp registration (this branch) with
  machine_clients_bp/oidc_trust_anchors_bp/audit_bp (already merged).
- app/middleware/auth.py: combined this branch's verify_jwt/has_scope
  helpers with the already-merged dpop_bound_token decorator --
  independent, unrelated functions.
- app/schema.py + tests/test_schema.py: scim_tokens table combined
  with machine_client/oidc_trust_anchor/dpop_replay/audit_event
  (all already merged).
- alembic: no chain fix needed this time -- 010_add_scim_provisioning
  already correctly points down_revision to 008_add_mfa_fields (its
  actual parent when this branch was authored off feature/mfa-totp),
  and that chain now correctly terminates the full linear graph as a
  single head.

Full manager suite: 323/323 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech
PenguinzTech merged commit b288b2a into v2.1.x Jul 27, 2026
3 of 6 checks passed
PenguinzTech added a commit that referenced this pull request Jul 27, 2026
- .gitleaks.toml: both branches independently created this file
  (add/add conflict). Kept origin/v2.1.x's version -- accumulated and
  already validated across #69/#71/#73/#77's resolutions.
- .pre-commit-config.yaml: same flake8/hadolint divergence resolved
  identically to prior merges this session (single .flake8 source of
  truth + flake8-bugbear; hadolint v2.14.0).
- app/schema.py: auth_user gained sso_provider/sso_subject (this
  branch) alongside external_id (#73 SCIM, already merged) -- both
  independent new columns, combined.
- app/services/scopes.py: sso:write/sso:admin (this branch) combined
  with audit:read (already merged, SystemAdmin-only).
- tests/test_schema.py: expected-tables set now covers sso_providers/
  sso_login_attempts alongside scim_tokens/machine_client/
  oidc_trust_anchor/dpop_replay/audit_event (all already merged).
- alembic: SSO's own chain (008_add_mfa_fields -> 009_add_sso_providers
  -> 010_add_sso_login_attempts -> 011_allow_null_password_hash) and
  SCIM's chain (008_add_mfa_fields -> 010_add_scim_provisioning) both
  forked from the same parent, authored independently off
  feature/mfa-totp. Re-chained 009_add_sso_providers to depend on
  010_add_scim_provisioning (SCIM's already-merged migration) instead,
  restoring a single linear head.

Full manager suite: 339/339 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant