Identity Linking & JIT Provisioning for OIDC Users#188
Open
KinshukSS2 wants to merge 2 commits into
Open
Conversation
Closes Issue istSOS#28 — eliminates two-step user provisioning. After POST /Users, a newly created user had no RLS policy and could not access any data until an administrator separately called POST /Policies. This commit fixes that by automatically calling the appropriate policy function inside the same transaction as user creation. Changes: - api/app/v1/endpoints/create/user.py * Add module-level _POLICY_FN_MAP (viewer/editor/obs_manager/sensor). * Capture app_role before get_db_role_for_rbac() remaps it, so the correct policy function can be dispatched. * After GRANT, call sensorthings.<role>_policy([username], policyname) with policyname = '{username}_default'. Administrator is skipped — admins bypass RLS by privilege, not by policy. * Policy functions already exist in the DB (istsos_auth.sql); no migration required. - api/app/v1/endpoints/functions.py * Add docstrings to _validate_role_identifier() and set_role(). - api/app/v1/endpoints/create/data_array_observation.py * Import shared set_role() helper (was already using the correct upstream version; this import makes the dependency explicit). - api/tests/test_rls_policy_creation.py (new) * Tests: correct policy function per role, administrator exclusion, naming convention, users_ as text[]. - api/tests/test_rbac_set_role_safety.py (new) * Tests: identifier validation, injection rejection, shared helper usage in data_array_observation.
- Add auth_provider and external_sub_id columns to sensorthings."User"
via idempotent migration (001_identity_linking.sql) with a partial
unique index on (auth_provider, external_sub_id) WHERE auth_provider
IS NOT NULL, so local password users are completely unaffected.
- Introduce PENDING_ROLE sentinel in rbac_roles.py. The 'pending' state
is intentionally absent from VALID_RBAC_ROLES so it can never be
assigned through the public API; existing role validation is unchanged.
- Gate pending accounts in get_current_user() (oauth.py): after the DB
lookup, any user with role='pending' immediately receives HTTP 403
'Account pending admin activation' before any SET ROLE or handler
body is reached.
- Add oidc_user_crud.py with create_pending_oidc_user() and
get_user_by_provider_sub(). The insert function hardcodes role to
PENDING_ROLE and contains zero DDL (no CREATE ROLE / CREATE USER),
giving new OIDC accounts zero PostgreSQL footprint until activation.
- Add POST /Users/{id}/activate endpoint (activate_user.py), restricted
to administrators. Runs UPDATE role, CREATE ROLE NOLOGIN IN ROLE,
GRANT, and RLS policy assignment inside a single transaction so a
failed step leaves the user still 'pending' with no partial state.
- Register activate_user router in api.py inside the AUTHORIZATION guard.
Local password users (POST /Users) are completely unaffected; no changes
were made to create/user.py.
Relates-to: GSoC 2026 Identity Linking architecture
Contributor
Author
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.
Identity Linking & JIT Provisioning for OIDC Users
Why
When a user logs in through an external provider, we cannot automatically provision a PostgreSQL database role for them — an administrator needs to vet and approve the account first.
This PR implements a secure application-level "Waiting Room" (Pending State). New external users immediately receive an internal database row for identity tracking, but they are completely blocked at the middleware layer with an HTTP 403 and have zero database footprint (no PostgreSQL role created) until an administrator explicitly activates them.
Architectural Flexibility:
This design strictly decouples Authentication (identifying the user) from Authorization (managing database permissions via RLS). By handling the identity mapping through a shared
SECRET_KEYand token validation, this plumbing naturally supports both authentication models simultaneously:Importantly, the existing local-password registration flow (
POST /Users) remains 100% untouched and acts as a safe fallback.What changed
database/migrations/001_identity_linking.sqlauth_provider+external_sub_idcolumns and a partial unique index tosensorthings."User"api/app/rbac_roles.pyPENDING_ROLE = "pending"sentinel — absent fromVALID_RBAC_ROLESso it can never be set via the APIapi/app/oauth.pyget_current_user()raisesHTTP 403for pending users before any handler orSET ROLEis reachedapi/app/db/oidc_user_crud.pycreate_pending_oidc_user()— pure INSERT, role hardcoded topending, zero DDLapi/app/v1/endpoints/create/activate_user.pyPOST /Users/{id}/activate— admin only; runs UPDATE +CREATE ROLE NOLOGIN+ RLS policy in one transactionapi/app/v1/api.pyHow to test