- Remove sqlite DATABASE_URL from Dockerfile, docker-compose.yml, and Helm configmap; add Postgres service to base docker-compose - Remove SQLite persistence PVC, usePostgres helper, and conditional volume/strategy logic from Helm chart - Remove dual-dialect branches in run.py (keep Postgres-only SQL) - Remove SQLite backup endpoint and frontend button - Add Postgres container to dev.sh alongside Keycloak - Update config.py default to postgresql:// - Update .env.example files, README, CLAUDE.md, and notes - Tests remain on in-memory SQLite for speed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 KiB
16 KiB
CLAUDE.md — OSA Management Suite
Project Overview
Internal management tool with four domains: Projects, Users, Licenses, and Certificates. Includes a feedback system, sponsorship tracking, email notifications, and admin panel. Monorepo with a Flask backend and React frontend, deployed to Kubernetes via Helm.
Repository Structure
backend/ Python Flask API
app/
auth/ Session-based auth + Keycloak OIDC login (login_required, project_access_required decorators)
projects/ Project CRUD, member management (KC-based), sponsorship, verification, group management
users/ User CRUD, ProjectUser join model with billing types
licenses/ License CRUD with file upload/download
certs/ Cert CRUD with encrypted key + passphrase storage, format conversion
dashboard/ Stats (including sponsorship metrics), expiring items, audit log (limited to 10 most recent)
downtime/ Downtime Tracker CRUD (application outage logging with enclave, scope, planned/unplanned)
feedback/ Feedback CRUD with screenshot storage and status workflow
settings/ App-level settings (expiry thresholds, notification config, sponsorship, work hours) with key-value store
keycloak.py Keycloak Admin API client (users, groups, child groups, membership, sponsor attribute)
keycloak_permissions.py Derives app permissions from KC group memberships (cached in session)
email.py SMTP email client (send_email helper, configurable via env vars)
crypto.py Fernet encrypt/decrypt helpers
audit.py AuditLog model + log_audit() helper
config.py All config via env vars (includes SMTP_* settings)
seed.py Seeds projects, licenses, and certs into local DB (users are Keycloak-only)
run.py Entry point (db.create_all() on startup, manual ALTER TABLE for schema changes)
frontend/ React + TypeScript
src/
pages/ One file per route (login, dashboard, projects-list, project-detail,
users-list, user-detail, licenses-list, certs-list, downtime-list, admin)
components/
ui/ shadcn/ui primitives (do not edit manually)
shared/ Reusable components (data-table, stat-card, status-badge, activity-feed, etc.)
projects/ Project-specific components (group-management for KC groups, project-form)
users/ User-specific components (columns, form)
licenses/ License-specific components
certs/ Cert-specific components (cert-form with passphrase reveal)
downtime/ Downtime components (downtime-columns with work-hours classification, downtime-form)
dashboard/ Dashboard widgets (stats-overview with sponsorship metrics, expiring-items, recent-activity)
feedback/ Feedback modal (screenshot attach with auto-scaling)
layout/ App shell, sidebar (with admin nav), topbar, protected-route
hooks/ React Query hooks (use-projects, use-users, use-project-users,
use-project-members, use-licenses, use-certs, use-downtime,
use-feedback, use-settings, use-keycloak-groups, use-keycloak-sync)
contexts/ Auth context provider (is_admin flag, permissions helpers, OIDC logout)
lib/ API client (axios), utils, constants
chart/osa-suite/ Helm chart for Kubernetes deployment
scripts/ Keycloak seeding (seed-keycloak.py — users, groups, memberships, sponsors)
Running Locally
./dev.sh # Starts backend (:5001) + frontend (:5173) + Keycloak (:8180) locally
./demo.sh # Same as above but everything in containers (no local deps needed)
Login: admin / admin, or any seed user via "Sign in with Keycloak" (password: password)
Building & Deploying
./build.sh # Builds Docker containers
helm install osa ./chart/osa-suite --set secrets.fernetKey=... --set secrets.secretKey=...
Development Commands
# Backend
cd backend && source .venv/bin/activate && python run.py
# Frontend
cd frontend && npm run dev # Dev server
cd frontend && npm run build # Production build (also runs tsc)
Architecture Conventions
Backend
- Flask blueprints: one per domain (auth, projects, users, licenses, certs, dashboard, downtime, feedback, settings)
- All API routes prefixed with
/api/ - All list endpoints return wrapped objects:
{"projects": [...]},{"licenses": [...]},{"feedback": [...]}, etc. - Single-item endpoints return:
{"project": {...}},{"feedback": {...}}, etc. @login_requireddecorator on all routes except POST /api/auth/login and OIDC endpoints@project_access_required(min_privilege)decorator enforces project-level permissions (read/write/admin) via session-cached Keycloak group memberships@admin_requireddecorator on project creation, user create/update/delete/api/auth/loginand/api/auth/mereturnis_adminflag,auth_method, andpermissionsobject/api/auth/refresh-permissionsre-fetches KC groups and updates session cache- OIDC endpoints:
/api/auth/oidc/available,/api/auth/oidc/login,/api/auth/oidc/callback - OIDC callback derives permissions from KC groups via
keycloak_permissions.derive_permissions()and caches insession["kc_permissions"] - Authlib OAuth client registered conditionally when
KEYCLOAK_OIDC_CLIENT_SECRETis set - Group management API:
/api/projects/<key>/groups(list/create/delete child groups),/api/projects/<key>/groups/<id>/members(list/add/remove members) - Member management API (KC-based):
/api/projects/<key>/members(list/add/remove members from KC groups),/api/projects/<key>/members/<id>/context(other project memberships),/api/projects/<key>/members/<id>/remove(advanced removal with sponsorship actions) - Sponsorship API:
/api/projects/<key>/sponsor/<user_id>(POST to sponsor, DELETE to release) - Verification API:
POST /api/projects/<key>/verify(updateslast_verified_atandlast_verified_by) - Cert passphrase API:
GET /api/certs/<id>/passphrase(returns decrypted passphrase, separate from main cert endpoint for security) - Models use SQLAlchemy with PostgreSQL as the only supported database
- No migrations directory — uses
db.create_all()on startup with manual ALTER TABLE for schema changes inrun.py - Audit logging via
log_audit()on all CRUD and export operations - Cert private keys and passphrases encrypted at rest with Fernet; cert PEM stored unencrypted (public data)
- License and cert status computed via hybrid properties, not stored columns. License status: archived → pending (no purchase date or future purchase date) → perpetual → expired → expiring_soon → active
- Feedback screenshots stored as JSON array of base64 strings in the database
- Downtime Tracker API:
/api/downtime(list/create/update/delete). Fields: application, start_time, end_time, cause, lessons_learned, resolution, enclave (IL5/IL6 comma-separated), scope (disabled/limited), planned (boolean). Search spans application, cause, lessons_learned, resolution, submitted_by. - Settings stored as key-value pairs via the Setting model (expiry thresholds, notification config, sponsorship, work hours, etc.)
- Work hours settings:
work_hours_start,work_hours_end,work_hours_timezone— used by downtime tracker to classify events as during/after work hours - Email notifications via SMTP (configurable per-alert recipients, disabled when SMTP_HOST not set)
Frontend
- React Query (TanStack Query) for all server state — no Redux/Zustand
- TanStack Table for all data tables via shared
DataTablecomponent - shadcn/ui components in
components/ui/— installed via CLI, do not hand-edit - Hooks pattern:
use-projects.tsexportsuseProjects(),useCreateProject(), etc. use-project-members.tsexportsuseProjectMembers(),useMemberRemovalContext(),useRemoveMember(),useSponsorUser(),useReleaseSponsor()- Axios client in
lib/api.tswithwithCredentials: trueand 401 interceptor - Vite proxies
/apito backend in dev; nginx proxies in production - Toast notifications (sonner) for all mutations
- Status badges color-coded: green (active/valid), amber (expiring_soon), red (expired), indigo (perpetual), blue (onboarding)
- Feedback status badges: blue (unread), gray (read), amber (assigned), green (addressed)
- Sponsor status badges on project detail: green (sponsored here), blue (sponsored elsewhere), red (unsponsored with deletion countdown)
- Admin sidebar link (Settings icon) visible only when
user.is_adminis true - Shared
ActivityFeedcomponent used on project detail and user detail pages - Project memberships editable inline via Select dropdowns for billing type on both project detail and user detail pages
- Keycloak group management UI on project detail pages (split-panel: groups with app filter on left, members on right)
- Members tab on project detail shows KC-based members with sponsor status and action buttons (sponsor/release/remove)
- Cert form includes passphrase reveal button (fetches decrypted passphrase on demand)
- Dashboard stats overview includes sponsorship metrics (sponsored count, unsponsored count with warning badge)
- Downtime Tracker page with filterable table (application, enclave, scope, planned), work-hours classification column (during/after based on configurable work hours settings), and inline create/edit/delete
- Admin settings page uses unified
SettingRowlayout across grouped cards (Thresholds & Scheduling, Project & User Defaults, Security)
Naming
- Backend: snake_case for Python, kebab-case for URL paths
- Frontend: PascalCase for components, camelCase for hooks/utils, kebab-case for filenames
- Database columns: snake_case, matching the JSON API field names exactly
Key Design Decisions
- Database — PostgreSQL is required. Helm chart supports bundled Postgres or external Postgres (e.g., RDS). Local dev uses a Postgres container started by
dev.sh. Tests use in-memory SQLite for speed. - Dual auth: local + Keycloak OIDC — hardcoded admin/admin always works. When
KEYCLOAK_OIDC_CLIENT_SECRETis set, a "Sign in with Keycloak" button appears on the login page. OIDC callback finds-or-creates a local User bykeycloak_idthen email. - Admin access — determined by
VELA-mgmt-adminKeycloak group membership or hardcoded admin username. Controls sidebar admin link visibility. - Permission model — Keycloak child groups (
{PROJECT}-mgmt-read/write/admin) replace the oldProjectUser.privilegescolumn. Permissions are derived at login from KC group memberships and cached insession["kc_permissions"]. VELA group membership grants global escalation: VELA read = view all, VELA write = edit all, VELA admin = full admin. Hardcoded admin bypasses all checks. - Keycloak group hierarchy — Top-level groups per project (e.g.,
ACME). Child groups per app and permission:{PROJECT}-{app}-{level}(e.g.,ACME-bitbucket-read,ACME-mgmt-write). Standard apps: bitbucket, srm, coverity, mgmt — each with read/write/admin. Custom child groups also supported. - Sponsorship model — Keycloak
sponsorattribute (single-valued) on each user stores the project key that sponsors them. LocalUser.unsponsored_sincetracks when sponsorship was cleared. Configurableunsponsored_deletion_dayssetting (default 7) controls the deletion countdown. Sponsorship release disables the user in KC, marksunsponsored_since, and sends notification email. - Billing types —
core,collaborator,standingon ProjectUser. Business rule: users can only becoreon one project (enforced at route level with conflict warnings and confirmation flow). - Users are Keycloak-only — Users are seeded exclusively in Keycloak via
scripts/seed-keycloak.py. The backendseed.pyonly seeds projects, licenses, and certs into the local DB. LocalUserrecords are created on first OIDC login. The KC seeder assigns users to mgmt child groups plus random additional app child groups (bitbucket, srm, coverity) with deterministic randomness. - Auto-assignment — Adding a user to a project auto-adds them to the
{PROJECT}-mgmt-readKC child group. Removing a user removes them from all child groups. Creating a project auto-creates all standard child groups in KC. - Member removal workflow — Three actions available:
remove_permissions(KC groups only),release_sponsorship(clear sponsor, disable in KC, start deletion countdown),release_and_remove(both). Context endpoint provides other project memberships for informed decisions. - Stale KC ID healing — When adding a member to a group, if the stored
keycloak_idis stale (404), the system auto-looks up the user by email in KC and updates the stored ID. - Row-level filtering —
Project.for_user()returns only projects the user is a member of (based on KCvisible_projectsfrom session cache, or all if global admin / hardcoded admin). Licenses and dashboard stats scoped similarly. Certificates are global (not project-scoped). - Project verification — Projects have
last_verified_atandlast_verified_byfields for compliance/audit tracking, updated via verify endpoint. - Project metadata — Projects have
service,agency,cost_centerfields, contact fields with email subfields (bfm, pm, admin), and a configurableratefield (default 300). - Feedback system — users submit feedback from project detail pages with optional screenshots (auto-scaled to 1200x900 max). Admins manage via admin page with status workflow (unread → read → assigned → addressed).
- Configurable expiry thresholds — "expiring soon" window for licenses and certs is configurable via admin settings (stored in settings table), not hardcoded.
- Email notifications — SMTP-based with per-alert recipient configuration. Supports notifications for license/cert expiry, new feedback, user changes, and sponsorship release. Disabled when
SMTP_HOSTis not set. - Cert encryption — Fernet key persists to
.fernet_keyfile in dev, env var in production. Changing the key makes existing encrypted data unrecoverable. - Cert passphrase storage — PKCS12 import passphrases optionally stored encrypted (Fernet) in
passphrase_encryptedcolumn. Retrieved via separate endpoint for security. PKCS12 export still without passphrase (explicit product decision). - Downtime Tracker — Logs application outage events with start/end times, cause, resolution, and lessons learned. Enclave field supports multi-select (IL5, IL6) stored as comma-separated string, serialized as array in API. Scope (disabled/limited) and planned (boolean) classify the nature of outages. Work-hours classification computed client-side using
date-fns-tzagainst configurable work hours settings. - Backend replicas — scale freely with Postgres.
Stubs (Not Yet Implemented)
components/certs/aws-secrets-stub.tsx— AWS Secrets Manager sync
Security Notes
- Never commit
.env,.fernet_key,.keycloak-secret,.keycloak-oidc-secret, or*.dbfiles SECRET_KEYuses random bytes in dev; must be set via env var in production for session persistence- All error responses use generic messages; details logged server-side only
- File uploads validated by extension allowlist (licenses) and size limit (50MB global)
- Security headers set via
@app.after_request: nosniff, frame deny, referrer policy - Cert passphrases and private keys encrypted at rest; decrypted only via dedicated endpoints