Skip to content

Identity Linking & JIT Provisioning for OIDC Users#188

Open
KinshukSS2 wants to merge 2 commits into
istSOS:mainfrom
KinshukSS2:feat/identity-linking-jit-provisioning
Open

Identity Linking & JIT Provisioning for OIDC Users#188
KinshukSS2 wants to merge 2 commits into
istSOS:mainfrom
KinshukSS2:feat/identity-linking-jit-provisioning

Conversation

@KinshukSS2

Copy link
Copy Markdown
Contributor

Identity Linking & JIT Provisioning for OIDC Users

GSoC 2026 — RBAC & Authentication

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_KEY and token validation, this plumbing naturally supports both authentication models simultaneously:

  • Internal Auth: If istSOS4 handles its own internal OIDC login callback routes.
  • External Gateway Proxy: If we shift authentication completely to the external STAC/DCAT connector project. The connector simply handles the login, mints a token using the shared key, and drops the user into this exact pending pipeline.

Importantly, the existing local-password registration flow (POST /Users) remains 100% untouched and acts as a safe fallback.

What changed

File Change
database/migrations/001_identity_linking.sql Adds auth_provider + external_sub_id columns and a partial unique index to sensorthings."User"
api/app/rbac_roles.py Adds PENDING_ROLE = "pending" sentinel — absent from VALID_RBAC_ROLES so it can never be set via the API
api/app/oauth.py get_current_user() raises HTTP 403 for pending users before any handler or SET ROLE is reached
api/app/db/oidc_user_crud.py create_pending_oidc_user() — pure INSERT, role hardcoded to pending, zero DDL
api/app/v1/endpoints/create/activate_user.py POST /Users/{id}/activate — admin only; runs UPDATE + CREATE ROLE NOLOGIN + RLS policy in one transaction
api/app/v1/api.py Registers the new router

How to test

# 1. Apply the migration
psql $DATABASE_URL -f database/migrations/001_identity_linking.sql

# 2. Simulate a JIT insert — alice lands in pending with no PG role
python3 -c "
import asyncio
from api.app.db.oidc_user_crud import create_pending_oidc_user
user = asyncio.run(create_pending_oidc_user('alice', 'alice@example.com', 'google', 'sub-123'))
print(user['role'])  # → pending
"

# 3. Confirm no PG role was created
psql $DATABASE_URL -c "SELECT rolname FROM pg_roles WHERE rolname = 'alice';"
# → 0 rows

# 4. Try any endpoint with alice's token — should be blocked
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $ALICE_TOKEN" \
  http://localhost:8018/v1.1/Things
# → 403

# 5. Admin activates alice
curl -X POST http://localhost:8018/v1.1/Users/<id>/activate \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "viewer"}'
# → 200 OK, PG role created, RLS policy applied

# 6. Regression — existing local user creation still works
curl -X POST http://localhost:8018/v1.1/Users \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "localuser", "password": "pass", "role": "editor"}'
# → 201 Created

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
@KinshukSS2

Copy link
Copy Markdown
Contributor Author

This PR builds on top of the RLS policy logic introduced in #185 . Please merge #185 first. Once it lands in main, I will quickly rebase this branch so it is ready to be merged.

@KinshukSS2 KinshukSS2 changed the title Feat/identity linking jit provisioning Identity Linking & JIT Provisioning for OIDC Users Jun 8, 2026
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