Skip to content

Order Permissions

Owners of an order can invite other platform users as members and grant them a curated set of permissions controlling what they can do on that order. The order owner always has full access.

Admin access is not affected. Users with system permissions (ManageProvisioning, ViewAllOrders, etc.) and application tokens retain their existing blanket access.

Domain Design

OrderPermission enum

pub enum OrderPermission {
    ViewOrder,
    ViewTerminal,
    ToggleServer,
    BrowseFiles,
    ManageExternalAccess,
    ManageBillingAndResize,
}

ViewOrder is the baseline required to see the order at all. When granting any other permission the API auto-includes ViewOrder.

OrderMember entity

pub struct OrderMember {
    id: OrderMemberId,
    pub order_id: OrderId,
    pub user_id: UserId,
    pub permissions: Vec<OrderPermission>,
    pub created_at: DateTime<Utc>,
}

OrderMemberRepository

async fn find_by_id(id) -> Option<OrderMember>
async fn find_by_order_id(order_id) -> Vec<OrderMember>
async fn find_by_order_and_user(order_id, user_id) -> Option<OrderMember>
async fn save(member) -> OrderMember
async fn set_permissions(member_id, permissions)  // atomic replace
async fn delete(member_id)

Policy Changes

ReadOrderPolicy and ManageOrderPolicy are extended with one additional Allow branch for order members. The admin paths are checked first and are completely unaffected.

Updated ReadOrderPolicy

1. Application token → Allow
2. User with ViewAllOrders → Allow
3. user.id() == order.owner_id → Allow
4. member.has_permission(ViewOrder) → Allow  (NEW)
5. Otherwise → Deny

Updated ManageOrderPolicy

1. Application token → Allow
2. User with ManageProvisioning → Allow
3. user.id() == order.owner_id → Allow
4. member has the required OrderPermission → Allow  (NEW)
5. Otherwise → Deny

Per-Endpoint Member Permission Required

Endpoint Member permission
GET /orders/{id} ViewOrder
GET /orders/{id}/provisions ViewOrder
GET /orders/{id}/status ViewOrder
GET /orders/{id}/invoices ManageBillingAndResize
GET /orders/{id}/terminal (WS) ViewTerminal
POST /orders/{id}/toggle ToggleServer
POST /orders/{id}/resize ManageBillingAndResize
POST /orders/{id}/cancel ManageBillingAndResize
GET /orders/{id}/external-access ViewOrder
POST /orders/{id}/external-access ManageExternalAccess
File system endpoints BrowseFiles
Configure / reprovision / member management owner/admin only

Database Schema

CREATE TABLE order_members (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id   UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    user_id    UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (order_id, user_id)
);

CREATE TABLE order_member_permissions (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    member_id  UUID NOT NULL REFERENCES order_members(id) ON DELETE CASCADE,
    permission TEXT NOT NULL,
    granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

HTTP API

Routes mounted under /api/v1/orders.

GET /api/v1/orders/{id}/members

Lists all members with their permissions. Accessible to the order owner, members with ViewOrder, and admins.

[
  {
    "id": "<member_uuid>",
    "user_id": "<user_uuid>",
    "user_name": "alice",
    "permissions": ["view_order", "view_terminal", "toggle_server"],
    "created_at": "2026-03-12T10:00:00Z"
  }
]

POST /api/v1/orders/{id}/members

Adds a member. Owner or admin only.

{ "user_id": "<uuid>", "permissions": ["view_terminal", "toggle_server"] }

DELETE /api/v1/orders/{id}/members/{member_id}

Removes a member. Cascades to order_member_permissions via DB.

PUT /api/v1/orders/{id}/members/{member_id}/permissions

Replaces the full permission set for a member. ViewOrder is auto-included.

{ "permissions": ["browse_files"] }

Dashboard UI

A "Access" tab in the order detail screen, visible only to the order owner.

Permission labels

Enum variant Display label
view_order View
view_terminal Terminal
toggle_server Start / Stop
browse_files File browser
manage_external_access External ports
manage_billing_and_resize Billing & resize

View is always pre-checked and disabled when any other permission is selected.

Implementation Order

  1. OrderPermission enum + OrderMember entity + OrderMemberRepository trait + migration
  2. PgOrderMemberRepository
  3. HTTP endpoints + user search endpoint (GET /api/v1/users/search?q=)
  4. Extend ReadOrderPolicy + ManageOrderPolicy with member branch
  5. Dashboard UI — Access tab