Skip to content

Permission Model ​

middag-account implements a multi-tenant permission system based on organization membership, role hierarchy, and scoped permissions. Every data access is bounded by the X-Middag-Organization header.

Organization Boundary ​

All queries filter by organization_id. Data from one organization never leaks to another. The boundary is enforced at multiple layers:

  1. OrganizationMiddleware -- Validates the X-Middag-Organization header on every org-scoped request
  2. PermissionsMiddleware -- Verifies the user is a collaborator of the target organization
  3. Repository layer -- Every query method requires organization_id as a parameter
HTTP Request
  -> AuthMiddleware (validate JWT, set WP_User)
  -> PermissionsMiddleware (check role + scopes)
  -> OrganizationMiddleware (validate org membership)
  -> Controller (execute with org-scoped context)

X-Middag-Organization Header ​

Required on all endpoints that operate on organization-scoped data. The value is the integer ID of the organization.

GET /wp-json/middag-account/v1/entitlements
Authorization: Bearer eyJ...
X-Middag-Organization: 42

Validation steps:

  1. Header present -- else 400 Bad Request
  2. Organization exists -- else 404 Not Found
  3. User is a collaborator of the organization -- else 403 Forbidden
  4. Organization context is set in the request for downstream use

Role Hierarchy ​

Roles determine the base access level within an organization. Defined in RoleHierarchy enum:

RoleWeightCan Manage MembersCan Alter OrgCan Delete Org
owner100Yes (all)YesYes
admin80Yes (member/guest)Yes (scoped)No
member50NoNoNo
guest20NoNoNo
pending0NoNoNo

A role outranks another if its weight is higher. The outranks() method compares two roles:

php
RoleHierarchy::Owner->outranks(RoleHierarchy::Admin); // true
RoleHierarchy::Member->outranks(RoleHierarchy::Admin); // false

Permission Scopes ​

Fine-grained permissions are controlled by scopes assigned to each collaborator. Defined in PermissionScope enum:

ScopeControls Access To
organizationOrganization settings, profile
financesInvoices, tax invoices, billing
ordersWooCommerce orders, refunds
licensesSoftware license management
ticketsSupport tickets, service requests
quotesQuote viewing, accept/reject
contractsContract viewing, PDF download
documentsOrganization documents
downloadsProduct downloads

Each scope maps to a boolean field on the CollaboratorEntity:

php
$collaborator->canManageFinances;  // true/false
$collaborator->canManageOrders;    // true/false

The owner role has implicit access to all scopes. Admin and member roles require explicit scope assignment.

Route Protection ​

Every REST route declares its required role and scopes. The PermissionsMiddleware enforces these at request time:

Route: GET /invoices
Required scope: finances
Required role: member (minimum)

Check:
1. Load collaborator for (user_id, organization_id)
2. Verify role >= member (weight >= 50)
3. Verify collaborator has "finances" scope
4. If scope missing -> 403 "Scope not authorized: finances"

Custom WordPress Capabilities ​

The plugin registers custom capabilities via user_has_cap filter. These map to the collaborator's role and scopes within the organization context, bridging the middag permission model with WordPress's native capability system.

JWT Scopes ​

The JWT payload includes the user's scopes for the default organization:

json
{
    "sub": 42,
    "org": 15,
    "roles": [
        "admin"
    ],
    "scopes": [
        "organization",
        "finances",
        "orders",
        "licenses"
    ]
}

The scopes array in the JWT represents the collaborator's permissions. A wildcard "*" grants access to all scopes.

X-Middag-Company Header ​

The optional X-Middag-Company header (middag_br or middag_global) routes requests to the correct legal entity for dual-entity operations (Stripe accounts, tax systems). It does not affect permission checks.