Skip to content

feat(api): API keys for programmatic access#75

Open
PenguinzTech wants to merge 81 commits into
mainfrom
fix/tenant-team-isolation
Open

feat(api): API keys for programmatic access#75
PenguinzTech wants to merge 81 commits into
mainfrom
fix/tenant-team-isolation

Conversation

@PenguinzTech

Copy link
Copy Markdown
Contributor

Summary

Adds issuable/revocable API keys for programmatic API access. API keys are tenant-scoped and support optional expiration, constant-time hash verification, and scope restrictions.

Changes

  • app/api_keys.py: New module with issue/list/revoke endpoints
  • app/auth.py: Extended auth_required to support X-API-Key header authentication
  • app/db_schema.py: Added ApiKey SQLAlchemy model with tenant/user FKs
  • app/models.py: Added api_keys PyDAL table definition (both PostgreSQL/MySQL and SQLite)
  • app/config.py: Added API_KEY_SECRET configuration (required in production)
  • app/init.py: Registered api_keys blueprint
  • tests/test_api_keys.py: 18 comprehensive tests covering creation, auth, revocation, expiration
  • migrations/versions/90a835023d20_api_keys.py: Alembic migration

Schema

api_keys table:

  • id (PK)
  • tenant_id (FK to tenants, CASCADE)
  • owner_user_id (FK to auth_user, CASCADE)
  • name (string)
  • key_prefix (string, for display/lookup)
  • key_hash (string, unique, HMAC-SHA256 — only stored, never raw key)
  • scopes (text, space-separated, must be subset of owner's scopes)
  • created_at (timestamp)
  • last_used_at (timestamp, nullable)
  • expires_at (timestamp, nullable)
  • revoked_at (timestamp, nullable)

Security

  • Raw API keys (format: sk_live_<token_urlsafe(32)>) generated with secrets module
  • Only key prefix (first 8 chars) stored for display
  • Full key hashed using HMAC-SHA256 (server secret + key)
  • Constant-time comparison prevents timing attacks
  • Scope escalation rejected (scopes must be subset of owner's scopes)
  • Revoked/expired keys blocked at authentication time
  • Rate limited on endpoint (configurable via RATE_LIMIT_API_KEYS)
  • Tenant-scoped enforcement prevents cross-tenant access
  • Masked logging prevents raw key leakage

Test Coverage

  • 18 tests all passing
  • Full suite: 707 tests passed, 90.35% coverage (exceeds 90% gate)

Migration

  • ID: 90a835023d20
  • Down revision: ed63edc73ca1 (MFA baseline)
  • Up/down both tested and working

Stacked On

#71 (feature/mfa-totp)

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

PenguinzTech and others added 30 commits September 7, 2025 11:55
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add .version file monitoring to all build workflows
- Implement epoch64 timestamp-based naming (alpha/beta-<epoch64>)
- Add version-based release naming (vX.X.X-alpha/beta)
- Add auto pre-release creation on .version changes
- Add security scanning (gosec for Go, bandit for Python, npm audit for Node.js)
- Create comprehensive docs/WORKFLOWS.md
- Update docs/STANDARDS.md with CI/CD section
- Update CLAUDE.md with CI/CD section and pre-commit checklist

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Flask Backend (services/flask-backend/):
- Quart async web framework with Hypercorn ASGI server
- Flask-Security-Too authentication with PyDAL datastore
- Pydantic schemas for request/response validation
- JWT-based auth with access/refresh tokens
- User management API with RBAC (admin, maintainer, viewer)
- Health endpoints (/readyz, /livez, /healthz)
- Prometheus metrics integration
- Security headers middleware

Shared Libraries (shared/py_libs/):
- crypto: Argon2id/bcrypt hashing, AES-256-GCM encryption, secure tokens
- security: rate limiting, CSRF, audit logging, sanitization, headers
- validation: string, password, network, datetime validators
- http: async HTTP client with retry logic

API Test Suite (tests/api/flask-backend/):
- run-tests.sh: orchestrates build, unit, API, and load tests
- test_endpoints.py: 15 API endpoint tests
- test_load.py: performance tests with configurable concurrency
- 21 pytest unit tests for schemas and endpoints

All 36 tests passing (21 unit + 15 API endpoint tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…sk-backend

Resolved two critical issues preventing flask-backend from starting in production:

1. Flask-Principal Compatibility: Disabled Flask-Principal initialization as it
   conflicts with Quart's async context model. Flask-Principal's synchronous
   before_request handlers cause "Working outside of application context" errors.
   The app now runs with JWT-based authentication only.

2. CORS Configuration: Fixed "Cannot allow credentials with wildcard allowed origins"
   error by conditionally disabling credentials when using wildcard CORS origins,
   as required by CORS security specifications.

These fixes enable flask-backend to successfully deploy to Kubernetes and pass
health checks. All pods are now running and healthy in the beta cluster.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The useAuth hook initialized with isLoading=true but checkAuth() was never
called on app mount, causing the app to show "Loading..." indefinitely.

Added useEffect in App.tsx to call checkAuth() on mount, which checks for
existing auth tokens and sets isLoading=false, allowing the app to proceed
to the login page or dashboard.

This fixes the issue where users saw an infinite loading screen at
https://current.penguintech.io

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add logo to README.md with centered display
- Add logo to login page
- Add clickable logo home button in sidebar (top left)
- Add favicon.ico, favicon.svg for browser tabs
- Add logo192.png, logo512.png for PWA support
- Add manifest.json for progressive web app
- Update index.html with proper meta tags and favicon links
- Save logo files to docs/screenshots/ for documentation

Logo appears in:
- README.md (centered at top)
- Login page (above sign-in form)
- Sidebar home button (top left, clickable to return to dashboard)
- Browser favicon/tab icon
- PWA app icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements three-tier role-based access control system per CLAUDE.md
and STANDARDS.md requirements:

**Three Organizational Tiers:**
- Global: Organization-wide roles (admin, maintainer, viewer)
- Team: Per-team roles (team_admin, team_maintainer, team_viewer)
- Resource: Per-resource roles (owner, editor, resource_viewer)

**OAuth2-Style Scopes:**
- users:read, users:write, users:admin
- teams:read, teams:write, teams:admin
- urls:read, urls:write, urls:delete, urls:admin
- analytics:read, analytics:admin
- settings:read, settings:write
- system:admin

**Database Schema:**
- scopes: All available permission scopes
- teams: Team/group management
- team_members: Team membership
- role_scopes: Role-to-scope mappings
- user_role_assignments: User roles at specific levels (global/team/resource)
- custom_roles: User-defined roles

**New API Endpoints:**
- GET /api/v1/scopes - List all scopes
- GET /api/v1/roles - List roles with scopes
- POST /api/v1/roles/custom - Create custom role
- DELETE /api/v1/roles/<id> - Delete custom role
- POST /api/v1/users/<id>/roles - Assign role at level
- GET /api/v1/users/<id>/roles - Get role assignments
- GET /api/v1/teams - List teams
- POST /api/v1/teams - Create team
- GET/PUT/DELETE /api/v1/teams/<id> - Team CRUD
- POST/DELETE /api/v1/teams/<id>/members - Manage members

**Permission Enforcement:**
- @require_scope decorator for endpoint protection
- Supports team_id_param and resource_id_param for scoped checks
- Hierarchical permission checking (global → team → resource)

**Files Modified:**
- services/flask-backend/app/rbac.py (NEW): Complete RBAC implementation
- services/flask-backend/app/teams.py (NEW): Team management APIs
- services/flask-backend/app/roles.py (NEW): Role/scope management APIs
- services/flask-backend/app/models.py: RBAC integration
- services/flask-backend/app/__init__.py: Register blueprints
- docs/APP_STANDARDS.md: Full RBAC documentation

This provides enterprise-grade permissions management with custom
role creation and fine-grained access control.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
**Fixes:**
1. Fixed auth_required import in teams.py and roles.py
   - Changed from non-existent token_required to auth_required decorator

2. Fixed WebUI Dockerfile for project root context
   - Updated paths to services/webui/* when building from project root
   - Ensures correct file copying during multi-stage build

3. Fixed docker-compose.yml webui context
   - Changed webui context from ./services/webui to . (project root)
   - Matches flask-backend context pattern for consistency

**Testing:**
- Local smoke tests passed with docker-compose
- Flask backend running and healthy
- WebUI building and serving correctly
- All API endpoints requiring authentication properly
- Health checks passing on all services

**Verification:**
- curl http://localhost:5002/healthz - ✅ healthy
- curl http://localhost:3008/healthz - ✅ healthy
- curl http://localhost:3008/ - ✅ serving React app
- curl http://localhost:5002/api/v1/scopes - ✅ requires auth

Ready for Kubernetes deployment.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Security Updates:
- Fixed react-router-dom XSS vulnerability (GHSA-2w69-qvjg-hvjx)
- Updated @remix-run/router from 1.23.1 to 1.23.2
- Updated react-router from 6.30.2 to 6.30.3
- Updated react-router-dom from 6.30.2 to 6.30.3
- All npm audit vulnerabilities resolved (0 vulnerabilities)

Testing:
- Added comprehensive E2E smoke test suite (tests/smoke/test_smoke.py)
- Tests cover: Docker Compose, service health, Flask API, WebUI, assets
- All 7/7 smoke tests passing

Security Review Results:
- npm audit: All vulnerabilities fixed
- bandit: No actionable issues (8 false positives)
- Dependabot: No open alerts
- Smoke tests: All passing after fixes

Version bump: v1.0.0 → v1.0.1.1768501253

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Applied black code formatter to fix linting failures:
- services/flask-backend/app/ (10 files)
- tests/smoke/test_smoke.py

All code now complies with black formatting standards.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Applied black code formatter to all Python files:
- shorturl-app/ (14 files)
- tests/ (9 files)

All Python code now complies with black formatting standards.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed import ordering in all Python files:
- services/flask-backend/app/ (9 files)
- tests/ (10 files)
- shorturl-app/ (10 files)

All imports now comply with isort standards.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Re-applied black formatting to files modified by isort.
isort and black are now both satisfied.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added pyproject.toml with isort configuration using black profile.
This ensures isort and black work together without conflicts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated CI workflow to use isort with black profile for compatibility.
This prevents conflicts between isort and black formatting.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
PenguinzTech and others added 29 commits March 9, 2026 09:17
CI/CD:
- Add push-images job to build.yml: pushes beta-latest to ghcr.io on
  every push to v*.x branches, gamma-latest on main
- Add packages:write permission for GITHUB_TOKEN
- Add v*.x to push trigger so release branches fire the pipeline
- Enforce --cov-fail-under=90 on pytest step

deploy-beta.sh:
- Default to SKIP_BUILD=true; CI/CD owns image builds
- Use beta-latest as default tag (CI's rolling alias)
- --build flag retained as emergency-only local override
- Update help text and warnings accordingly

Helm charts (all 3 services):
- Add missing serviceaccount.yaml template (was causing 19-day-old
  FailedCreate loop: "serviceaccount flask-backend not found")

E2E tests (Playwright):
- New: tests/e2e/api/team-members-extended.spec.ts (6 tests)
- New: tests/e2e/api/oauth-errors.spec.ts (8 tests)
- Edit: auth-validation.spec.ts +3 bearer token edge case tests
- Edit: playwright.config.ts — JSON+HTML reporters for CI artifacts
- Edit: users.spec.ts, roles-crud.spec.ts, permissions.spec.ts,
  oidc.spec.ts, users-validation.spec.ts — expanded coverage
- Add: global-setup.ts for test environment initialization

Flask backend:
- Unit test suite: test_auth, test_models, test_rbac, test_users,
  test_config, test_async_db, test_schemas_extended, test_rbac_unit
- pytest.ini added for consistent test discovery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-formatted by black + isort --profile black to match CI linting
standards. No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI installs latest black (26.x); local was pinned to 24.x. Upgraded
locally and reformatted the two files that differ between versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Docker FROM lines: add @sha256 digests for all external base images
- GitHub Actions: pin uses: to commit SHAs (not mutable version tags)
- Trivy: standardize to trivy-action@v0.35.0 + trivy-version=v0.69.3
- setup-trivy: pinned to v0.2.6 SHA
- package.json: remove ^ and ~ version prefixes (exact versions)
- requirements.txt: flag files needing pip-compile --generate-hashes
- README/docs: update Trivy version references

Follows updated immutable dependency standards in .claude/rules/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Layout.tsx: Add ml-64 to main element to prevent fixed sidebar overlap
- UserDetail.tsx: Remove loading skeleton, add 50ms render delay for Playwright sync
- Users.tsx: Extract API error message in duplicate email catch block
- api.ts: Remove window.location.href redirect on 401 refresh failure

Tests now pass: 246/248 E2E (all API tests + 159 passing)
Remaining: 2 UI timing issues (dashboard-tabs, user-detail name field)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Key fixes:
- ProtectedRoute: remove useEffect checkAuth() that caused 401 cascade
  and navigation away from pages mid-test
- api.ts: add externalApi instance without auth interceptors for Go
  backend calls; remove window.location redirect on 401 refresh failure
- useApi.ts: use externalApi for goApi so Go 401s don't clear Flask tokens
- Dashboard.tsx: wrap helloApi/goApi calls in separate try-catch; add
  role="tabpanel" to tab content div so Playwright locator resolves
  immediately instead of blocking 10s
- UserDetail.tsx: guard against null user during initial load to prevent
  React crash on user.created_at access; add aria-label/name/testid attrs
- E2E tests: switch all URLs to NodePort (30008/30002) for stable access
  without kubectl port-forwards; fix flaky user-detail name field test
  to use waitFor instead of count() snapshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-format 5 files to pass CI lint checks (black + isort --profile black).
No logic changes — formatting only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pytest, pytest-cov, pytest-asyncio, and requests are all pinned with
hashes in requirements.txt already. Appending them without hashes to
pip install caused failure because hash-checking mode requires ALL
packages to have hashes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- roles.py: fix scope extraction — rs["scopes"]["name"] → rs["name"]
  (select(db.scopes.name) returns flat dicts, not nested)
- users.py: fix get_user_role_assignments permission — users:read scope
  (held by viewers via *:read) was too permissive; tighten to users:admin
- tests/test_hello.py: add tests for /hello, /hello/protected, /scopes
- tests/test_user_roles.py: add tests for POST/GET /users/{id}/roles
- tests/test_users.py: add test for empty-body PUT → 400

Coverage: 88.15% → 90.28%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker Compose is deprecated per standards. Replace with:
- kind cluster with extraPortMappings (30008/webui, 30002/flask)
- k8s/kustomize/overlays/ci/ overlay with NodePort services,
  SQLite config, and CI-appropriate secrets
- Kustomize base manifests co-located in k8s/kustomize/base/
  (more conventional than referencing ../../manifests/)
- go-backend replicas set to 0 in CI (not built in E2E job)

Workflow now: create kind cluster → build images → kind load →
kubectl apply -k → wait for deployments → playwright tests →
delete cluster

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kustomize strategic merge fails when mixing stringData (patch) with
data (base) in the same Secret. Convert CI secrets-patch.yaml to use
data: with base64-encoded values to match the base secret format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace nginx Ingress with Gateway API HTTPRoute targeting the shared
Gateway. Routes: flask-backend:5000 (/api), webui:3000 (/).
Part of dal2-beta nginx-ingress EOL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Simplify build.yml workflow with improved logic and reduced duplication
- Update version-release.yml trigger condition
- Add image.digest pins to Helm values (flask-backend, go-backend, webui)
- Remove unused node_modules file (puppeteer-core CdpSession.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…startup

Fixes two critical findings:
- C1: JWT_SECRET_KEY no longer falls back to the dev SECRET_KEY default in
  production. ProductionConfig now requires it via env var and validate()
  rejects the dev default, closing an admin-token forgery vector.
- C2: create_app() now calls ProductionConfig.validate() at startup (guarded
  by hasattr so Dev/Testing configs are unaffected), so the dead secret-guard
  actually runs and the app boot-fails on default/missing secrets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tokens off URL

Fixes account-takeover and token-leak findings in the OIDC/SSO consumer:
- OC1: callback now validates the id_token (JWKS signature, iss/aud/exp/nonce)
  instead of blindly trusting the userinfo response; identity derives from
  validated claims. Adds PKCE (S256) + nonce to authorize/callback.
- OC2: auto-link/auto-provision gated on email_verified (+ OIDC_AUTO_LINK_
  VERIFIED_EMAIL flag, default off); no more silent email-based account linking.
- OH4: access/refresh tokens returned as httpOnly Secure SameSite cookies, not
  in the redirect URL query string.
- OH5: identity linking only via authenticated OIDC round-trip; removed
  body-supplied external_sub/external_email linking.

Adds nullable columns oidc_providers.issuer, user_oidc_links.email_verified.
_encrypt_secret fail-open and update_provider gating deferred to A5.

Full backend suite: 403 passed, 1 skipped, 90.16% coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…H3/H1)

Closes IDOR: a global viewer holds global tenants:read/teams:read, so the
scope gate passed for everyone and routes then loaded any object by id with
no membership check — any user could read any org's tenants/teams + member
emails across tenants.

- Adds membership helpers in rbac.py; each object-scoped tenant/team route now
  requires the caller to be a global admin OR a member of that specific
  tenant/team (admin ops require tenant/team admin scope, resolved with the id
  so tenant/team-scoped roles are actually consulted).
- create_team validates the body tenant_id against caller membership (no more
  cross-tenant writes).
- Regression tests: non-member viewer 403, member 200, global admin 200,
  foreign-tenant create 403.

Also gitignore /worktrees/ (repo-local worktree convention).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…follow-up)

The initial isolation fix guarded only read routes; write/admin routes still had
just a global scope gate, so a user with global teams:write/tenants:write (e.g. a
global maintainer) could modify/delete any team/tenant and grant roles across orgs
— privilege escalation. (Also restores object-scoped authz that had regressed when
team_id_param was dropped from the decorators.)

- Adds _is_team_admin / _is_tenant_admin helpers (global admin scope OR team/tenant-
  scoped admin role for THAT object).
- update/delete/add-member/remove-member on both teams.py and tenants.py now require
  the caller to be an admin of that specific team/tenant (or global admin).
- add_team_member/add_tenant_member especially: they grant roles incl. *_admin, so
  non-admins of the object are now blocked.
- Regression tests: global maintainer (no admin role) → 403 on every write route;
  team/tenant admin → 200/201.

Full suite: 423 passed, 90.10% coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 20000 lines

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