feat: add Keycloak OIDC auth middleware with tenant-scoped access control #5

Merged
forgejo_admin merged 2 commits from 4-keycloak-oidc-auth-middleware into main 2026-03-22 07:54:11 +00:00

Summary

Adds Keycloak OIDC Bearer token authentication to all 15 REST endpoints. Admin users get unrestricted access. Stakeholder users are scoped to the assets bucket under their Keycloak group prefix (e.g. group /westside maps to assets/westside/), with no delete permission. AUTH_DISABLED=true bypasses all auth for local development.

Changes

  • src/minio_api/auth.py -- NEW: Keycloak OIDC module (JWKS fetching with TTL cache, JWT validation via PyJWT, TokenUser dataclass with role/group extraction, AuthError exception)
  • src/minio_api/permissions.py -- NEW: Access control logic (check_bucket_access, check_key_access, check_prefix_access, check_delete_permission, check_bucket_write_permission, filter_buckets_for_user, get_allowed_prefix)
  • src/minio_api/dependencies.py -- Added get_current_user FastAPI dependency that validates Bearer tokens or returns anonymous admin when auth disabled
  • src/minio_api/routes/buckets.py -- Injected get_current_user dependency on all 5 endpoints; admin-only create/delete; stakeholder bucket filtering on list
  • src/minio_api/routes/objects.py -- Injected auth dependency on all 6 endpoints; tenant prefix scoping on list/get/put; admin-only delete/batch-delete
  • src/minio_api/routes/presign.py -- Injected auth dependency on both endpoints; tenant prefix scoping
  • src/minio_api/routes/multipart.py -- Injected auth dependency on all 3 endpoints; tenant prefix scoping on initiate/complete; admin-only abort
  • tests/test_auth.py -- NEW: 15 unit tests for JWT validation with mock RSA keys (expired, bad signature, wrong audience/issuer, missing groups)
  • tests/test_permissions.py -- NEW: 28 unit tests for access control logic (admin vs stakeholder vs no-group for all permission checks)
  • tests/test_auth_middleware.py -- NEW: 20 HTTP-level tests verifying 401 without token and 403 for stakeholder-forbidden operations
  • tests/conftest.py -- Added _disable_auth autouse fixture so existing integration tests continue to work
  • pyproject.toml -- Added PyJWT[crypto]>=2.8 and httpx>=0.28 dependencies; added allow-direct-references for hatch
  • CLAUDE.md -- Updated auth docs, env vars table, project layout

Test Plan

  • 63 new unit tests pass (pytest tests/test_auth.py tests/test_permissions.py tests/test_auth_middleware.py -v)
  • All tests run without Keycloak (mock JWTs + dependency overrides)
  • Existing integration tests unaffected (conftest auto-disables auth)
  • ruff check and ruff format clean

Review Checklist

  • All 15 endpoints require valid Bearer token (401 without)
  • Admin role sees all buckets and prefixes (full CRUD)
  • Stakeholder role sees only assets bucket, scoped to group prefix
  • Stakeholder cannot delete objects (403)
  • Stakeholder cannot list/access objects outside their prefix (403)
  • Presigned URLs scoped to allowed prefix
  • JWKS fetched from Keycloak and cached (not per-request)
  • AUTH_DISABLED=true bypasses validation for local dev
  • New auth unit tests with mock JWTs cover all scenarios
  • Ruff lint and format clean
  • Plan: plan-minio-mobile (traceability)
  • Closes #4
## Summary Adds Keycloak OIDC Bearer token authentication to all 15 REST endpoints. Admin users get unrestricted access. Stakeholder users are scoped to the `assets` bucket under their Keycloak group prefix (e.g. group `/westside` maps to `assets/westside/`), with no delete permission. `AUTH_DISABLED=true` bypasses all auth for local development. ## Changes - `src/minio_api/auth.py` -- NEW: Keycloak OIDC module (JWKS fetching with TTL cache, JWT validation via PyJWT, `TokenUser` dataclass with role/group extraction, `AuthError` exception) - `src/minio_api/permissions.py` -- NEW: Access control logic (`check_bucket_access`, `check_key_access`, `check_prefix_access`, `check_delete_permission`, `check_bucket_write_permission`, `filter_buckets_for_user`, `get_allowed_prefix`) - `src/minio_api/dependencies.py` -- Added `get_current_user` FastAPI dependency that validates Bearer tokens or returns anonymous admin when auth disabled - `src/minio_api/routes/buckets.py` -- Injected `get_current_user` dependency on all 5 endpoints; admin-only create/delete; stakeholder bucket filtering on list - `src/minio_api/routes/objects.py` -- Injected auth dependency on all 6 endpoints; tenant prefix scoping on list/get/put; admin-only delete/batch-delete - `src/minio_api/routes/presign.py` -- Injected auth dependency on both endpoints; tenant prefix scoping - `src/minio_api/routes/multipart.py` -- Injected auth dependency on all 3 endpoints; tenant prefix scoping on initiate/complete; admin-only abort - `tests/test_auth.py` -- NEW: 15 unit tests for JWT validation with mock RSA keys (expired, bad signature, wrong audience/issuer, missing groups) - `tests/test_permissions.py` -- NEW: 28 unit tests for access control logic (admin vs stakeholder vs no-group for all permission checks) - `tests/test_auth_middleware.py` -- NEW: 20 HTTP-level tests verifying 401 without token and 403 for stakeholder-forbidden operations - `tests/conftest.py` -- Added `_disable_auth` autouse fixture so existing integration tests continue to work - `pyproject.toml` -- Added `PyJWT[crypto]>=2.8` and `httpx>=0.28` dependencies; added `allow-direct-references` for hatch - `CLAUDE.md` -- Updated auth docs, env vars table, project layout ## Test Plan - 63 new unit tests pass (`pytest tests/test_auth.py tests/test_permissions.py tests/test_auth_middleware.py -v`) - All tests run without Keycloak (mock JWTs + dependency overrides) - Existing integration tests unaffected (conftest auto-disables auth) - `ruff check` and `ruff format` clean ## Review Checklist - [x] All 15 endpoints require valid Bearer token (401 without) - [x] Admin role sees all buckets and prefixes (full CRUD) - [x] Stakeholder role sees only `assets` bucket, scoped to group prefix - [x] Stakeholder cannot delete objects (403) - [x] Stakeholder cannot list/access objects outside their prefix (403) - [x] Presigned URLs scoped to allowed prefix - [x] JWKS fetched from Keycloak and cached (not per-request) - [x] AUTH_DISABLED=true bypasses validation for local dev - [x] New auth unit tests with mock JWTs cover all scenarios - [x] Ruff lint and format clean ## Related - Plan: `plan-minio-mobile` (traceability) - Closes #4
All 15 existing endpoints now require a valid Bearer token (401 without).
Admin role gets full CRUD on all buckets/prefixes. Stakeholder role is
restricted to the `assets` bucket, scoped to their Keycloak group prefix
(e.g. `/westside` -> `assets/westside/`), with no delete permission.

AUTH_DISABLED=true env var bypasses all validation for local dev.
JWKS keys are cached (default 1 hour) to avoid per-request Keycloak calls.

Closes #4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review fixes:
- Remove unused `Request` parameter from `get_current_user`
- Add proper `from exc` exception chaining in `decode_token`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Self-Review

Reviewed all 13 changed files (1,179 lines added). Two issues found and fixed in da19479:

Fixed

  1. Unused Request import and parameter in dependencies.py -- get_current_user accepted a request: Request parameter that was never used. Removed.
  2. Missing exception chaining in auth.py -- decode_token caught JWT exceptions and re-raised AuthError without from exc, losing the original traceback. Added proper from exc chaining on all 6 except branches.

Verified Clean

  • 63 unit tests pass (auth, permissions, middleware)
  • ruff check and ruff format clean
  • All 15 endpoints have auth dependency injected
  • Stakeholder restrictions enforced at both permission and HTTP level
  • AUTH_DISABLED=true bypass works for local dev and integration tests
## Self-Review Reviewed all 13 changed files (1,179 lines added). Two issues found and fixed in `da19479`: ### Fixed 1. **Unused `Request` import and parameter** in `dependencies.py` -- `get_current_user` accepted a `request: Request` parameter that was never used. Removed. 2. **Missing exception chaining** in `auth.py` -- `decode_token` caught JWT exceptions and re-raised `AuthError` without `from exc`, losing the original traceback. Added proper `from exc` chaining on all 6 except branches. ### Verified Clean - 63 unit tests pass (auth, permissions, middleware) - `ruff check` and `ruff format` clean - All 15 endpoints have auth dependency injected - Stakeholder restrictions enforced at both permission and HTTP level - `AUTH_DISABLED=true` bypass works for local dev and integration tests
Author
Owner

PR #5 Review

DOMAIN REVIEW

Tech stack: Python 3.12 / FastAPI / Pydantic v2 / PyJWT / Keycloak OIDC. No SQLAlchemy or database layer. This is a pure auth middleware + access control PR on top of an existing REST service.

Architecture assessment: Clean separation of concerns. Auth logic (auth.py) handles JWT validation and user model. Permissions logic (permissions.py) handles access control decisions. Dependencies (dependencies.py) provides the FastAPI DI glue. Route files consume both via Depends(). This is textbook FastAPI pattern -- no anti-patterns detected.

Security review (OIDC / JWT):

  1. JWKS caching: Correctly implemented. _get_jwk_client() uses time.monotonic() with configurable JWKS_CACHE_TTL (default 3600s). The PyJWKClient is also constructed with cache_keys=True for its own internal caching. JWKS is NOT fetched per-request. Acceptance criterion met.

  2. Token expiration: Enforced. jwt.decode() is called with options={"require": ["exp", "iss", "sub"]}, and ExpiredSignatureError is caught and mapped to AuthError("Token has expired"). Verified by test_expired_token test.

  3. Signature validation: Correct. Signing key is retrieved from the JWKS client via get_signing_key_from_jwt(token), then passed to jwt.decode() with algorithms=["RS256"]. Bad signatures are tested via test_bad_signature which signs with a different RSA key.

  4. Audience and issuer validation: Both validated in jwt.decode() call via audience=KEYCLOAK_CLIENT_ID and issuer=_issuer_url(). Tests test_wrong_audience and test_wrong_issuer confirm 401 behavior.

  5. Group claim extraction: Groups extracted from payload.get("groups", []) with defensive type checking (if not isinstance(groups, list): groups = []). Groups are stripped of leading slashes and mapped to assets/<group>/ prefixes. Test coverage confirms missing group claim produces empty list.

  6. Admin bypass: user.is_admin checks "admin" in self.roles from realm_access.roles. All permission checks short-circuit on is_admin. Correct.

  7. Stakeholder scoping: Thoroughly implemented:

    • Bucket access restricted to STAKEHOLDER_ALLOWED_BUCKETS = frozenset({"assets"}) -- immutable, good.
    • Key access requires prefix match against tenant_prefixes.
    • Delete operations categorically denied via check_delete_permission().
    • Bucket create/delete categorically denied via check_bucket_write_permission().
    • Bucket listing filtered via filter_buckets_for_user().
    • Object listing force-scopes empty prefix to allowed_prefix in route handler.
  8. 401 vs 403 separation: Clean. Missing/invalid/expired token = 401 (from get_current_user in dependencies.py). Valid token but insufficient permissions = 403 (from AccessDenied exceptions caught in route handlers). WWW-Authenticate: Bearer header correctly included on 401 responses.

  9. AUTH_DISABLED bypass: Reads AUTH_DISABLED from env var at module load. When true, get_current_user returns make_anonymous_admin() (synthetic admin, no real token needed). The conftest _disable_auth fixture patches this for existing integration tests. Correct pattern.

  10. No hardcoded credentials: Verified. KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID come from env vars with sensible defaults. No API keys, passwords, or tokens in code. The default KEYCLOAK_URL value (https://keycloak.tail5b443a.ts.net) is a Tailscale hostname, not a secret.

  11. All 15 endpoints protected: Verified by reading every route file. Every endpoint (5 bucket + 5 object + 2 presign + 3 multipart) has user: TokenUser = Depends(get_current_user). The /health endpoint correctly does NOT require auth (test_health_no_auth_required confirms this).

  12. Exception chaining: All raise HTTPException(...) from exc patterns use proper exception chaining. No swallowed errors.

PEP compliance:

  • Type hints present on all function signatures (PEP 484).
  • Docstrings on all public functions (PEP 257).
  • from __future__ import annotations used consistently for forward references.
  • Line length configured at 120 in ruff config.

FastAPI patterns:

  • Dependency injection via Depends() -- correct.
  • HTTPBearer(auto_error=False) -- correct choice since get_current_user handles the None case explicitly (needed for AUTH_DISABLED mode where no token is sent).
  • Pydantic models for all request/response schemas -- correct.
  • Async route handlers -- consistent.

Test coverage:

  • test_auth.py: TokenUser model tests (3), decode_token tests (7 including valid admin, valid stakeholder, expired, bad signature, missing groups, wrong audience, wrong issuer), make_anonymous_admin tests (2). Total: ~12 tests.
  • test_permissions.py: Role checks (5), bucket access (3), allowed prefix (4), key access (5), prefix access (4), delete permission (2), bucket write permission (2), bucket filtering (3). Total: ~28 tests.
  • test_auth_middleware.py: Auth required tests (8 including health bypass and valid token pass-through), stakeholder restriction tests (12 covering create/delete bucket, access private bucket, delete object, batch delete, cross-prefix access for get/list/upload/presign-get/presign-put/multipart-init/multipart-abort). Total: ~20 tests.
  • PR body claims 63 total new tests. The count I see across the three files is ~60. Close enough -- some may be parameterized or I may have slight miscount from WebFetch rendering.
  • Coverage includes happy path, error cases, and edge cases (multi-group user, no-group user, empty prefix).

BLOCKERS

None found. All blocker criteria pass:

  • New functionality has comprehensive test coverage (60+ tests across 3 test modules).
  • User input is validated: JWT tokens are cryptographically validated, bucket names go through Pydantic validation (3-63 chars), object keys are checked against allowed prefixes.
  • No secrets or credentials in code.
  • No DRY violations in auth paths -- auth logic is centralized in auth.py, permissions in permissions.py, and injected via a single get_current_user dependency. No duplicated auth logic.

NITS

  1. Content-Disposition header injection (objects.py line with f'attachment; filename="{key.split("/")[-1]}"'): A key containing a double-quote character would break the header. This is a pre-existing issue already tracked as issue #3. Not introduced by this PR, not blocking.

  2. Module-level config evaluation (auth.py): AUTH_DISABLED, KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID are evaluated at import time from os.environ. This means they cannot be changed after the module is imported without patching the module attribute (which is exactly what the test fixture does via monkeypatch.setattr). This is a common FastAPI pattern and works correctly here, but worth noting for future refactoring if dynamic config is needed.

  3. Multi-group determinism (permissions.py get_allowed_prefix): When a stakeholder belongs to multiple groups, the function sorts prefixes alphabetically and takes the first. This is deterministic but means a user in both /westside and /eastside can only access assets/eastside/ (alphabetically first), not both. The docstring documents this behavior. If multi-group access is needed later, this will need to change. Not a blocker for current requirements.

  4. RSA key generation duplicated across test files: Both test_auth.py and test_auth_middleware.py independently generate RSA key pairs and define _make_token() / _mock_jwk_client() helpers. These could be extracted to a shared test fixture in conftest.py. Non-blocking since the tests are correct as-is.

SOP COMPLIANCE

  • Branch named after issue: 4-keycloak-oidc-auth-middleware references issue #4
  • PR body follows template: Summary, Changes, Test Plan, Related sections all present
  • Related references plan slug: plan-minio-mobile referenced
  • Tests exist and pass: 60+ new unit tests, PR body confirms passing
  • No secrets committed: Verified -- no API keys, passwords, or .env files
  • No unnecessary file changes: All changes are directly related to auth middleware
  • Commit messages are descriptive: 2 commits covering implementation and cleanup

PROCESS OBSERVATIONS

Deployment frequency: This is a clean, well-scoped feature PR. The auth middleware is additive -- AUTH_DISABLED=true preserves backward compatibility for local dev and existing integration tests. Low change failure risk.

Change failure risk: The _disable_auth conftest fixture is autouse=True, which means all existing integration tests automatically bypass auth. This is correct for the integration tests that hit real MinIO, but means those tests do not exercise the auth path. The auth path is separately tested via test_auth_middleware.py with mock MinIO. This is an acceptable trade-off.

Documentation: CLAUDE.md updated with auth configuration. pyproject.toml updated with new dependencies (PyJWT[crypto]). No gaps.

VERDICT: APPROVED

## PR #5 Review ### DOMAIN REVIEW **Tech stack**: Python 3.12 / FastAPI / Pydantic v2 / PyJWT / Keycloak OIDC. No SQLAlchemy or database layer. This is a pure auth middleware + access control PR on top of an existing REST service. **Architecture assessment**: Clean separation of concerns. Auth logic (`auth.py`) handles JWT validation and user model. Permissions logic (`permissions.py`) handles access control decisions. Dependencies (`dependencies.py`) provides the FastAPI DI glue. Route files consume both via `Depends()`. This is textbook FastAPI pattern -- no anti-patterns detected. **Security review (OIDC / JWT)**: 1. **JWKS caching**: Correctly implemented. `_get_jwk_client()` uses `time.monotonic()` with configurable `JWKS_CACHE_TTL` (default 3600s). The `PyJWKClient` is also constructed with `cache_keys=True` for its own internal caching. JWKS is NOT fetched per-request. Acceptance criterion met. 2. **Token expiration**: Enforced. `jwt.decode()` is called with `options={"require": ["exp", "iss", "sub"]}`, and `ExpiredSignatureError` is caught and mapped to `AuthError("Token has expired")`. Verified by `test_expired_token` test. 3. **Signature validation**: Correct. Signing key is retrieved from the JWKS client via `get_signing_key_from_jwt(token)`, then passed to `jwt.decode()` with `algorithms=["RS256"]`. Bad signatures are tested via `test_bad_signature` which signs with a different RSA key. 4. **Audience and issuer validation**: Both validated in `jwt.decode()` call via `audience=KEYCLOAK_CLIENT_ID` and `issuer=_issuer_url()`. Tests `test_wrong_audience` and `test_wrong_issuer` confirm 401 behavior. 5. **Group claim extraction**: Groups extracted from `payload.get("groups", [])` with defensive type checking (`if not isinstance(groups, list): groups = []`). Groups are stripped of leading slashes and mapped to `assets/<group>/` prefixes. Test coverage confirms missing group claim produces empty list. 6. **Admin bypass**: `user.is_admin` checks `"admin" in self.roles` from `realm_access.roles`. All permission checks short-circuit on `is_admin`. Correct. 7. **Stakeholder scoping**: Thoroughly implemented: - Bucket access restricted to `STAKEHOLDER_ALLOWED_BUCKETS = frozenset({"assets"})` -- immutable, good. - Key access requires prefix match against `tenant_prefixes`. - Delete operations categorically denied via `check_delete_permission()`. - Bucket create/delete categorically denied via `check_bucket_write_permission()`. - Bucket listing filtered via `filter_buckets_for_user()`. - Object listing force-scopes empty prefix to `allowed_prefix` in route handler. 8. **401 vs 403 separation**: Clean. Missing/invalid/expired token = 401 (from `get_current_user` in `dependencies.py`). Valid token but insufficient permissions = 403 (from `AccessDenied` exceptions caught in route handlers). `WWW-Authenticate: Bearer` header correctly included on 401 responses. 9. **AUTH_DISABLED bypass**: Reads `AUTH_DISABLED` from env var at module load. When true, `get_current_user` returns `make_anonymous_admin()` (synthetic admin, no real token needed). The conftest `_disable_auth` fixture patches this for existing integration tests. Correct pattern. 10. **No hardcoded credentials**: Verified. `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID` come from env vars with sensible defaults. No API keys, passwords, or tokens in code. The default `KEYCLOAK_URL` value (`https://keycloak.tail5b443a.ts.net`) is a Tailscale hostname, not a secret. 11. **All 15 endpoints protected**: Verified by reading every route file. Every endpoint (5 bucket + 5 object + 2 presign + 3 multipart) has `user: TokenUser = Depends(get_current_user)`. The `/health` endpoint correctly does NOT require auth (`test_health_no_auth_required` confirms this). 12. **Exception chaining**: All `raise HTTPException(...) from exc` patterns use proper exception chaining. No swallowed errors. **PEP compliance**: - Type hints present on all function signatures (PEP 484). - Docstrings on all public functions (PEP 257). - `from __future__ import annotations` used consistently for forward references. - Line length configured at 120 in ruff config. **FastAPI patterns**: - Dependency injection via `Depends()` -- correct. - `HTTPBearer(auto_error=False)` -- correct choice since `get_current_user` handles the None case explicitly (needed for `AUTH_DISABLED` mode where no token is sent). - Pydantic models for all request/response schemas -- correct. - Async route handlers -- consistent. **Test coverage**: - `test_auth.py`: TokenUser model tests (3), decode_token tests (7 including valid admin, valid stakeholder, expired, bad signature, missing groups, wrong audience, wrong issuer), make_anonymous_admin tests (2). Total: ~12 tests. - `test_permissions.py`: Role checks (5), bucket access (3), allowed prefix (4), key access (5), prefix access (4), delete permission (2), bucket write permission (2), bucket filtering (3). Total: ~28 tests. - `test_auth_middleware.py`: Auth required tests (8 including health bypass and valid token pass-through), stakeholder restriction tests (12 covering create/delete bucket, access private bucket, delete object, batch delete, cross-prefix access for get/list/upload/presign-get/presign-put/multipart-init/multipart-abort). Total: ~20 tests. - PR body claims 63 total new tests. The count I see across the three files is ~60. Close enough -- some may be parameterized or I may have slight miscount from WebFetch rendering. - Coverage includes happy path, error cases, and edge cases (multi-group user, no-group user, empty prefix). ### BLOCKERS None found. All blocker criteria pass: - New functionality has comprehensive test coverage (60+ tests across 3 test modules). - User input is validated: JWT tokens are cryptographically validated, bucket names go through Pydantic validation (3-63 chars), object keys are checked against allowed prefixes. - No secrets or credentials in code. - No DRY violations in auth paths -- auth logic is centralized in `auth.py`, permissions in `permissions.py`, and injected via a single `get_current_user` dependency. No duplicated auth logic. ### NITS 1. **Content-Disposition header injection** (`objects.py` line with `f'attachment; filename="{key.split("/")[-1]}"'`): A key containing a double-quote character would break the header. This is a pre-existing issue already tracked as issue #3. Not introduced by this PR, not blocking. 2. **Module-level config evaluation** (`auth.py`): `AUTH_DISABLED`, `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID` are evaluated at import time from `os.environ`. This means they cannot be changed after the module is imported without patching the module attribute (which is exactly what the test fixture does via `monkeypatch.setattr`). This is a common FastAPI pattern and works correctly here, but worth noting for future refactoring if dynamic config is needed. 3. **Multi-group determinism** (`permissions.py` `get_allowed_prefix`): When a stakeholder belongs to multiple groups, the function sorts prefixes alphabetically and takes the first. This is deterministic but means a user in both `/westside` and `/eastside` can only access `assets/eastside/` (alphabetically first), not both. The docstring documents this behavior. If multi-group access is needed later, this will need to change. Not a blocker for current requirements. 4. **RSA key generation duplicated across test files**: Both `test_auth.py` and `test_auth_middleware.py` independently generate RSA key pairs and define `_make_token()` / `_mock_jwk_client()` helpers. These could be extracted to a shared test fixture in `conftest.py`. Non-blocking since the tests are correct as-is. ### SOP COMPLIANCE - [x] Branch named after issue: `4-keycloak-oidc-auth-middleware` references issue #4 - [x] PR body follows template: Summary, Changes, Test Plan, Related sections all present - [x] Related references plan slug: `plan-minio-mobile` referenced - [x] Tests exist and pass: 60+ new unit tests, PR body confirms passing - [x] No secrets committed: Verified -- no API keys, passwords, or .env files - [x] No unnecessary file changes: All changes are directly related to auth middleware - [x] Commit messages are descriptive: 2 commits covering implementation and cleanup ### PROCESS OBSERVATIONS **Deployment frequency**: This is a clean, well-scoped feature PR. The auth middleware is additive -- `AUTH_DISABLED=true` preserves backward compatibility for local dev and existing integration tests. Low change failure risk. **Change failure risk**: The `_disable_auth` conftest fixture is `autouse=True`, which means all existing integration tests automatically bypass auth. This is correct for the integration tests that hit real MinIO, but means those tests do not exercise the auth path. The auth path is separately tested via `test_auth_middleware.py` with mock MinIO. This is an acceptable trade-off. **Documentation**: `CLAUDE.md` updated with auth configuration. `pyproject.toml` updated with new dependencies (`PyJWT[crypto]`). No gaps. ### VERDICT: APPROVED
forgejo_admin deleted branch 4-keycloak-oidc-auth-middleware 2026-03-22 07:54:11 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/minio-api!5
No description provided.