Skip to content

feat: management apis for apps, envs, access#798

Open
rohan-chaturvedi wants to merge 41 commits into
mainfrom
api--apps-envs-accounts
Open

feat: management apis for apps, envs, access#798
rohan-chaturvedi wants to merge 41 commits into
mainfrom
api--apps-envs-accounts

Conversation

@rohan-chaturvedi
Copy link
Copy Markdown
Member

@rohan-chaturvedi rohan-chaturvedi commented Mar 9, 2026

Summary

This PR introduces a public REST API for managing Apps, Environments, Roles, Service Accounts, and Organisation Members/Invites, alongside a generic AuditEvent model that provides an org-scoped audit trail for all management operations.

Docs

Comprehensive documentation for all new API endpoints: phasehq/docs#213

New REST API endpoints

Resource Endpoints
Apps GET/POST /v1/apps/, GET/PUT/DELETE /v1/apps/:id/, PUT /v1/apps/:id/access/
Environments GET/POST /v1/apps/:id/environments/, GET/PUT/DELETE /v1/environments/:id/
Roles GET/POST /v1/roles/, GET/PUT/DELETE /v1/roles/:id/
Service Accounts GET/POST /v1/service-accounts/, GET/PUT/DELETE /v1/service-accounts/:id/, PUT /v1/service-accounts/:id/access/
Members GET/POST /v1/members/, GET/PUT/DELETE /v1/members/:id/, PUT /v1/members/:id/access/
Invites GET /v1/invites/, DELETE /v1/invites/:id/
Audit Logs GET /v1/audit-logs/

All endpoints use PhaseTokenAuthentication (User PAT or Service Account token), enforce RBAC via user_has_permission, apply IP allowlist middleware, and are rate-limited by plan.

AuditEvent model (backend/api/models.py)

New generic audit log model — distinct from SecretEvent — that captures every management action across the org:

  • Who: actor_type + actor_id + actor_metadata (denormalised at write time — survives member removal)
  • What: event_type (C/R/U/D/A) + resource_type + resource_id + resource_metadata
  • Diff: old_values / new_values JSON fields for before/after on updates
  • Context: ip_address, user_agent, timestamp
  • Three composite indexes for org-wide feed, entity timeline, and actor trail queries
  • Migrations 0118_auditevent + 0119_auditevent_invite_resource_type + 0120_invite_sa_inviter

Access management (server-side key wrapping)

Both /v1/members/:id/access/ and /v1/service-accounts/:id/access/ are declarative endpoints that atomically reconcile the full desired access state. For SSE-enabled apps, the server decrypts environment keys using the server keypair and re-wraps them for the target member/SA's identity key — no client-side crypto required.

Invite improvements

  • OrganisationMemberInvite.invited_by made nullable (migration 0120) so invites sent by service accounts don't require a member FK
  • New invited_by_service_account FK added — invite emails and serializer output now correctly attribute SA-originated invites
  • Invite acceptance page (/invite/[invite]/page.tsx) fixed: no longer crashes when invitedBy is null (SA-sent invite); falls back to SA name
  • validateOrganisationInvite GQL query updated to include invitedByServiceAccount { id name }

GraphQL audit log query

New auditLogs query on the schema with filters: organisationId, start/end timestamps, resourceType, resourceId, eventTypes, actorId, offset/limit. Returns AuditLogsResponseType with logs + approximate count.

Audit Logs UI (frontend/components/logs/AuditLogs.tsx)

New full-featured audit log viewer:

Screenshot From 2026-03-18 17-48-14
  • Infinite scroll / "load more" pagination
  • Filters: date range, resource type, event type, actor
  • Expandable event rows showing old_valuesnew_values diffs
  • Clickable resource links (navigates to the affected entity)
  • Correct actor display for both human members and service accounts — uses ApiAuditEventActorTypeChoices.Sa enum (not raw 'sa' string) to avoid GraphQL enum case mismatch
  • Resource type link routing uses ApiAuditEventResourceTypeChoices enum for the same reason

GraphQL mutation instrumentation

All existing GraphQL mutations for Apps, Environments, Roles, Service Accounts, Organisation Members, Network Policies, and Tokens now call log_audit_event() after each successful write.

Serializers

OrganisationMemberSerializer and OrganisationMemberInviteSerializer added to api/serializers.py. The members view no longer uses manual helper functions.

Permission parity

REST and GraphQL mutation checks are now in parity:

  • Owner role cannot be assigned via the role-update endpoint (must use ownership transfer flow)
  • Service Account tokens cannot assign global-access roles
  • Global-access members can only be updated by other global-access callers

Tests

Comprehensive test suites added for all five new REST view modules, following the APIRequestFactory + @patch pattern (no DB required):

  • test_apps_api.py — 765 lines
  • test_environments_api.py — 780 lines
  • test_roles_api.py — 498 lines
  • test_service_accounts_api.py — 501 lines
  • test_members_api.py — 1085 lines
  • test_environments.py — 626 lines (utility function tests)

Test plan

  • docker compose -f dev-docker-compose.yml exec backend pytest tests/ -v — all tests pass
  • Create an app, environment, role, service account, and member via REST API; verify AuditEvent rows in DB
  • Query auditLogs via GraphQL and confirm events appear with correct actor, resource, and diff fields
  • Send an invite via a Service Account token; confirm invite email shows SA name and acceptance page renders correctly
  • Grant member access via PUT /v1/members/:id/access/ on an SSE app; confirm EnvironmentKey rows are created
  • Verify audit log UI shows SA actors correctly (robot icon + SA name, not "User")
  • Verify resource type links in audit log UI navigate to correct pages

Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Comment thread backend/api/views/apps.py Fixed
Comment thread backend/api/views/environments.py Fixed
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Comment thread backend/api/views/service_accounts.py Dismissed
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: Rohan Chaturvedi <rohan.chaturvedi@protonmail.com>
Comment thread backend/api/views/apps.py Dismissed
Comment thread backend/api/views/environments.py Dismissed
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
…ution for various actions

Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
@rohan-chaturvedi rohan-chaturvedi added frontend Change in frontend code backend updates migrations This PR adds new migrations that update the database schema labels Mar 18, 2026
@rohan-chaturvedi rohan-chaturvedi marked this pull request as ready for review March 18, 2026 12:20
@rohan-chaturvedi rohan-chaturvedi added the enhancement New feature or request label Mar 18, 2026
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
rohan-chaturvedi and others added 11 commits March 20, 2026 21:25
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
… display

Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
- Add validate_text_field(): rejects non-string types, strips HTML tags
  (XSS defence-in-depth via django.utils.html.strip_tags), enforces
  max length
- Add validate_email_address(): uses Django's built-in EmailValidator
- Fix get_token_type(): handle empty/malformed bearer tokens that caused
  500 IndexError
user_can_access_app() and user_can_access_environment() did bare .get()
calls that threw unhandled DoesNotExist when a user from org B accessed
a resource belonging to org A. Wrap in try/except and return False so
the auth layer returns 403 instead of crashing.

Same fix applied to service_account_can_access_environment().
- Owner member's role cannot be changed via the API (all actors blocked,
  not just self-update). Directs to the ownership transfer flow.
- Owner member cannot be removed via the API (all actors blocked).
- SAs cannot modify or remove members with global-access roles (e.g.
  Admin). Previously the global-access check only applied to User tokens.
- Member invite email validated with Django's EmailValidator (previously
  accepted arbitrary strings like "not-an-email").
- Unsupported HTTP methods now return 405 instead of 403.
- Prevent enabling global_access on a role that has service accounts
  assigned. Previously an SA's role could be updated to global_access
  after the SA was created, bypassing the creation-time check.
- Add _normalize_permissions() to accept both camelCase (appPermissions,
  globalAccess) and snake_case (app_permissions, global_access) keys,
  enabling GET response round-tripping back to POST/PUT.
- Apply validate_text_field() to name and description fields.
- Unsupported HTTP methods now return 405 instead of 403.
…counts

- Apps: validate name/description with validate_text_field() (rejects
  non-string types, strips HTML tags, enforces length limits)
- Service accounts: same validation for name, plus token_name field
  which was previously unsanitized
- Environments: unsupported HTTP methods now return 405 instead of 403
- All three views: MethodNotAllowed replaces PermissionDenied for
  unsupported HTTP methods
nimish-ks and others added 10 commits April 8, 2026 17:22
PUT /v1/service-accounts/:id/ with only role_id (no name) crashed with
UnboundLocalError because the audit log check referenced `name` which
is only bound inside the `if raw_name is not None` block. Changed to
reference `raw_name` which is always bound from request.data.get().
# Conflicts:
#	backend/api/utils/access/roles.py
#	backend/backend/urls.py
fix: security and validation hardening for REST API management endpoints
…ole-id tests so Owner-protection check does not pre-empt them
Signed-off-by: Rohan Chaturvedi <rohan.chaturvedi@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend enhancement New feature or request frontend Change in frontend code updates migrations This PR adds new migrations that update the database schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants