feat: management apis for apps, envs, access#798
Open
rohan-chaturvedi wants to merge 41 commits into
Open
Conversation
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>
Signed-off-by: Rohan Chaturvedi <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>
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>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
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
11 tasks
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
…_audit_fks_set_null leaves
…non-string email error
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
GET/POST /v1/apps/,GET/PUT/DELETE /v1/apps/:id/,PUT /v1/apps/:id/access/GET/POST /v1/apps/:id/environments/,GET/PUT/DELETE /v1/environments/:id/GET/POST /v1/roles/,GET/PUT/DELETE /v1/roles/:id/GET/POST /v1/service-accounts/,GET/PUT/DELETE /v1/service-accounts/:id/,PUT /v1/service-accounts/:id/access/GET/POST /v1/members/,GET/PUT/DELETE /v1/members/:id/,PUT /v1/members/:id/access/GET /v1/invites/,DELETE /v1/invites/:id/GET /v1/audit-logs/All endpoints use
PhaseTokenAuthentication(User PAT or Service Account token), enforce RBAC viauser_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:actor_type+actor_id+actor_metadata(denormalised at write time — survives member removal)event_type(C/R/U/D/A) +resource_type+resource_id+resource_metadataold_values/new_valuesJSON fields for before/after on updatesip_address,user_agent,timestamp0118_auditevent+0119_auditevent_invite_resource_type+0120_invite_sa_inviterAccess 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_bymade nullable (migration0120) so invites sent by service accounts don't require a member FKinvited_by_service_accountFK added — invite emails and serializer output now correctly attribute SA-originated invites/invite/[invite]/page.tsx) fixed: no longer crashes wheninvitedByis null (SA-sent invite); falls back to SA namevalidateOrganisationInviteGQL query updated to includeinvitedByServiceAccount { id name }GraphQL audit log query
New
auditLogsquery on the schema with filters:organisationId,start/endtimestamps,resourceType,resourceId,eventTypes,actorId,offset/limit. ReturnsAuditLogsResponseTypewithlogs+ approximatecount.Audit Logs UI (
frontend/components/logs/AuditLogs.tsx)New full-featured audit log viewer:
old_values→new_valuesdiffsApiAuditEventActorTypeChoices.Saenum (not raw'sa'string) to avoid GraphQL enum case mismatchApiAuditEventResourceTypeChoicesenum for the same reasonGraphQL 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
OrganisationMemberSerializerandOrganisationMemberInviteSerializeradded toapi/serializers.py. The members view no longer uses manual helper functions.Permission parity
REST and GraphQL mutation checks are now in parity:
Tests
Comprehensive test suites added for all five new REST view modules, following the
APIRequestFactory+@patchpattern (no DB required):test_apps_api.py— 765 linestest_environments_api.py— 780 linestest_roles_api.py— 498 linestest_service_accounts_api.py— 501 linestest_members_api.py— 1085 linestest_environments.py— 626 lines (utility function tests)Test plan
docker compose -f dev-docker-compose.yml exec backend pytest tests/ -v— all tests passAuditEventrows in DBauditLogsvia GraphQL and confirm events appear with correct actor, resource, and diff fieldsPUT /v1/members/:id/access/on an SSE app; confirmEnvironmentKeyrows are created