Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
bc741ec
fix(rbac): auto-create RLS policy on user creation
KinshukSS2 May 30, 2026
3455702
feat(auth): implement identity linking & JIT provisioning for OIDC users
KinshukSS2 Jun 8, 2026
9c06c5b
feat(auth): password update endpoint with OIDC guard & input validation
KinshukSS2 Jun 8, 2026
5631ed2
feat(auth): role re-assignment endpoint with last-admin lockout & sta…
KinshukSS2 Jun 8, 2026
a5f6faa
refactor(auth): pivot role & user management to application-layer cre…
KinshukSS2 Jun 10, 2026
c3d0a7a
fix(rbac): kill split-brain DDL — SET LOCAL ROLE, bcrypt, pure UPDATE
KinshukSS2 Jun 14, 2026
1400f1f
fix(rbac): remove missed REVOKE/GRANT DDL from update/user.py
KinshukSS2 Jun 14, 2026
e88aebf
feat(auth): Phase 0 — application-layer auth foundation
KinshukSS2 Jul 13, 2026
064aa2e
fix: pin bcrypt for passlib compatibility and resolve endpoint indent…
KinshukSS2 Jul 13, 2026
f29fc8d
fix(rbac): kill split-brain DDL — SET LOCAL ROLE, bcrypt, pure UPDATE
KinshukSS2 Jun 14, 2026
0807bbc
feat(audit): PR 2 — append-only AuditLog table and logging helper
KinshukSS2 Jul 13, 2026
6bb597c
fix(audit): guard REVOKE/GRANT against missing roles in 003_audit_log…
KinshukSS2 Jul 13, 2026
0fa431e
feat(auth): add restricted-access self-registration endpoint
KinshukSS2 Jul 16, 2026
24fb58b
feat(auth): PR 4 — admin policy-approval endpoint (Path B, Step 5)
KinshukSS2 Jul 16, 2026
57fbdc5
fix(admin-approval): guard RLS call with savepoint for app-layer users
KinshukSS2 Jul 16, 2026
dc03b5d
feat(public-access): PR 5 — Path A public data access via RLS
KinshukSS2 Jul 17, 2026
d0dcabd
fix(public-access): remove orphaned RESET ROLE from $value path
KinshukSS2 Jul 17, 2026
c68a3f1
fix(rbac): resolve SET LOCAL ROLE conflict and fix dangling if syntax…
KinshukSS2 Jul 27, 2026
0577ed6
fix(login): restore REDIS guards, ttl_from_exp clamp, and optional He…
KinshukSS2 Jul 27, 2026
8dd4e3e
feat(auth): enforce password strength on initial registration
KinshukSS2 Jul 27, 2026
e52cbee
feat(auth): move password strength rule to shared validate_password_s…
KinshukSS2 Jul 27, 2026
8734352
fix(auth): close rejection-bypass gap in approval and activation endp…
KinshukSS2 Jul 27, 2026
4913efd
feat(auth): cyclical approval workflow — admin rejection + re-applica…
KinshukSS2 Jul 27, 2026
c225c88
fix(auth): enforce username format validation on self-registration
KinshukSS2 Jul 27, 2026
ae1d0d4
fix(auth): explicit error on OIDC username collision, not a silent de…
KinshukSS2 Jul 27, 2026
d497967
docs(rbac): mark POLICY_FN_MAP as legacy scaffolding pending ODRL mig…
KinshukSS2 Jul 27, 2026
acf3e7b
test(auth): update test_password_update.py for the new 8-char/digit/s…
KinshukSS2 Jul 27, 2026
d14dd89
chore: stop tracking .env, ignore personal scratch/demo artifacts
KinshukSS2 Aug 23, 2026
7026976
feat(auth): external identity providers + session-scoped RLS fix
KinshukSS2 Aug 23, 2026
47265ec
Merge upstream/main into feat/public-access
KinshukSS2 Aug 23, 2026
cf190c5
Merge feat/public-access (with upstream/main) into external-identity
KinshukSS2 Aug 23, 2026
8265bd1
fix: make this branch self-contained and importable
KinshukSS2 Aug 23, 2026
9a87b5c
fix(rbac): POST /Policies crashes for viewer/editor/obs_manager/senso…
KinshukSS2 Aug 23, 2026
79879ee
test: add a live regression test for the 007 RLS fix
KinshukSS2 Aug 23, 2026
78609a4
feat(auth): let applicants state a requested role at registration
KinshukSS2 Aug 23, 2026
ff8e2cb
fix(auth): sanitize OIDC-derived usernames before storage
KinshukSS2 Aug 23, 2026
77bc025
fix(deploy): wire external-auth config into production docker-compose…
KinshukSS2 Aug 24, 2026
9f794f7
test: remove test_rls_policy_creation.py, a false-confidence test
KinshukSS2 Aug 24, 2026
a11fad4
Merge feat/public-access into feat/external-identity-providers-rls-fix
KinshukSS2 Aug 24, 2026
43a84ed
fix(auth): DELETE /Users deactivates instead of hard-deleting
KinshukSS2 Aug 24, 2026
e5487e5
Merge feat/public-access into feat/external-identity-providers-rls-fix
KinshukSS2 Aug 24, 2026
03f8642
fix(auth): auto-suffix colliding OIDC usernames, flag possible duplic…
KinshukSS2 Aug 24, 2026
5e66727
test(oidc): fix external-provider tests broken by the auto-suffix commit
KinshukSS2 Aug 24, 2026
e2c052e
fix(rbac): PATCH/PUT no longer report 200 when RLS silently blocks th…
KinshukSS2 Aug 24, 2026
815b929
fix(audit): log ADMIN_APPROVAL when an OIDC user is activated
KinshukSS2 Aug 26, 2026
36dd9d1
fix(auth): run SET LOCAL ROLE guest before the PUBLIC_READ audit write
KinshukSS2 Aug 26, 2026
7e5a768
Merge remote-tracking branch 'origin/feat/public-access' into feat/ex…
KinshukSS2 Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 0 additions & 192 deletions .env

This file was deleted.

57 changes: 57 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,60 @@ NETWORK=0
# EXTENT - a two-dimensional bounding box enclosing a geometry or collection of geometries is used instead.
# Default: CONVEX_HULL
ST_AGGREGATE=CONVEX_HULL

# ---------------------------------------------------------------------------
# External identity providers (app/oidc_providers.py)
#
# Each provider only registers if BOTH its CLIENT_ID and CLIENT_SECRET
# resolve to a non-empty value -- leave any provider's pair unset to
# disable just that one. Get these from each provider's own developer/app
# console; the redirect URI to register there is
# {HOSTNAME}{SUBPATH}{VERSION}/auth/{provider}/callback (e.g.
# https://your-domain.example/istsos4/v1.1/auth/google/callback).
# ---------------------------------------------------------------------------

# GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET
# MICROSOFT_DISCOVERY_URL: Optional. Defaults to the multi-tenant "any
# Microsoft account" endpoint
# (.../organizations/v2.0 restricts to work/school
# accounts only -- only change this if that's
# actually what you want).
# Default: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_DISCOVERY_URL=

# GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# ORCID_CLIENT_ID / ORCID_CLIENT_SECRET
# ORCID_DISCOVERY_URL: Optional. Defaults to ORCID's production endpoint --
# only set this to point at ORCID's sandbox
# environment for testing.
# Default: https://orcid.org/.well-known/openid-configuration
ORCID_CLIENT_ID=
ORCID_CLIENT_SECRET=
ORCID_DISCOVERY_URL=

# EDUID_CLIENT_ID / EDUID_CLIENT_SECRET (SWITCH edu-ID)
EDUID_CLIENT_ID=
EDUID_CLIENT_SECRET=

# FORWARDED_ALLOW_IPS: The IP address (or CIDR range) of the reverse proxy
# fronting this deployment. uvicorn only trusts the
# X-Forwarded-Proto header -- needed to build correct
# https:// external-login callback URLs -- from
# requests whose immediate peer address matches this.
# Leaving this at its default means uvicorn trusts
# only itself (127.0.0.1), which will NOT work behind
# a real reverse proxy or load balancer: set this to
# that proxy's actual IP/CIDR once you know it. Do
# not set this to "*" (trust every peer) outside of
# local development.
# Default: 127.0.0.1
FORWARDED_ALLOW_IPS=127.0.0.1
21 changes: 20 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,24 @@
docs/mkdocs/site
database/schemaspy

# Virtualenvs & toolchain
.venv/
venv/
env/
.python-version

# Editors
.vscode
.vscode
.codex

# Personal scratch/demo artifacts -- meeting prep and manual verification
# scripts, not part of the real test suite (that's under api/tests/).
# Never intended to be committed.
MEETING_WALKTHROUGH.md
EXTERNAL_AUTH_MEETING.md
SWAGGER_DEMO_MEETING.md
demo.py
test_access_matrix.py
test_admin_rejection.py
test_dataset_scoping.py
test_security_boundaries.py
113 changes: 113 additions & 0 deletions api/app/db/audit_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright 2025 SUPSI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Database helper for writing to the sensorthings."AuditLog" table.

Design decisions
----------------
* ``log_audit_event`` accepts a pre-acquired asyncpg connection (``conn``)
rather than acquiring one from the pool itself. This keeps the function
composable: callers can include the audit INSERT in the same transaction
as the action being logged, guaranteeing atomicity.

* asyncpg does not auto-serialize Python ``dict`` to PostgreSQL JSONB.
Passing a raw dict raises ``TypeError: cannot convert dict to PostgreSQL
type``. The payload is serialised with ``json.dumps`` and the SQL
parameter is cast explicitly as ``$6::jsonb``, following the pattern
established in oidc_user_crud.py lines 104-106.

* ``None`` payload is passed through as-is; asyncpg maps Python ``None``
to SQL ``NULL`` correctly without any special handling.

* The function is intentionally fire-and-log: it raises on hard DB errors
so callers can decide whether to swallow or propagate. Callers that
treat audit logging as best-effort should wrap the call in try/except.
"""

import json
import logging

logger = logging.getLogger(__name__)

# Valid action_type values — must match the CHECK constraint in
# database/migrations/003_audit_log.sql and 004_admin_rejection.sql.
AUDIT_ACTION_PUBLIC_READ = "PUBLIC_READ"
AUDIT_ACTION_RESTRICTED_REQUEST = "RESTRICTED_REQUEST"
AUDIT_ACTION_ADMIN_APPROVAL = "ADMIN_APPROVAL"
AUDIT_ACTION_ADMIN_REJECTION = "ADMIN_REJECTION"


async def log_audit_event(
conn,
action_type: str,
actor_id: int | None = None,
dataset_id: str | None = None,
odrl_policy_id: str | None = None,
payload: dict | None = None,
) -> None:
"""Insert a single row into sensorthings."AuditLog".

The table is append-only (UPDATE and DELETE are revoked at the DB level),
so this function only ever issues an INSERT.

Args:
conn: A live asyncpg connection (or transaction connection).
The caller is responsible for acquiring and releasing it.
action_type: One of AUDIT_ACTION_* constants above. Must satisfy
the CHECK constraint in the migration or the INSERT
will raise a CheckViolationError.
actor_id: Primary key of the sensorthings."User" row that
triggered the event. Pass ``None`` for anonymous
(unauthenticated) actions such as PUBLIC_READ.
dataset_id: Human-readable or URI identifier for the STAC dataset
being accessed or requested. Optional.
odrl_policy_id: Identifier of the ODRL policy document associated
with a RESTRICTED_REQUEST or ADMIN_APPROVAL event.
Optional.
payload: Arbitrary JSON-serialisable metadata dict. Optional.
asyncpg does not auto-serialise dicts to JSONB; this
function handles that conversion internally.

Raises:
asyncpg.CheckViolationError: if ``action_type`` is not one of the
three permitted values.
asyncpg.ForeignKeyViolationError: if ``actor_id`` does not reference
an existing User row.
Exception: any other asyncpg / database error.
"""
# Serialise the payload dict to a JSON string so asyncpg can bind it as
# JSONB. Passing None through unchanged maps to SQL NULL.
payload_json: str | None = json.dumps(payload) if payload is not None else None

await conn.execute(
"""
INSERT INTO sensorthings."AuditLog"
(actor_id, action_type, dataset_id, odrl_policy_id, payload)
VALUES
($1, $2, $3, $4, $5::jsonb)
""",
actor_id,
action_type,
dataset_id,
odrl_policy_id,
payload_json,
)

logger.info(
"AuditLog: action=%r actor_id=%r dataset=%r policy=%r",
action_type,
actor_id,
dataset_id,
odrl_policy_id,
)
Loading
Loading