Skip to content

A Tenant Owner ​

For a team whose identity system already knows who owns each tenant, and that wants CoSec to follow it. The owner is attached to the principal by the token (an attachment) instead of being bound in a document, so granting or transferring ownership is a change in the identity system and never a change to a CoSec document. This page builds it, and shows what an attachment does and does not give.

What you need ​

A tenant owner
DocumentsAn attachable policy in the platform document; a binding of the owner role to the control plane's built-in cosec:tenant-admin in each tenant document
TokensThe owner's token carries policies: [tenant-owner]; the token for the control plane carries the role owner
ConceptsAttachments, cosec:tenant-admin, the grant analysis

The documents ​

The platform document defines the policy owners get. It is attachable, so a token may list it, and it is narrow: all actions of the app wms, and nothing else.

json
{
  "schema": 2,
  "revision": 1,
  "publishedAt": "2026-11-01T08:00:00Z",
  "policies": [
    {
      "id": "login",
      "statements": [
        {
          "name": "login",
          "effect": "allow",
          "action": "http:POST",
          "resource": "http:path:/auth/login"
        }
      ]
    },
    {
      "id": "control-api",
      "statements": [
        {
          "name": "reach-the-control-api",
          "effect": "allow",
          "action": [
            "http:GET",
            "http:POST",
            "http:DELETE"
          ],
          "resource": "http:path:/cosec/control/v1/**",
          "condition": {
            "eq": [
              "subject.authenticated",
              true
            ]
          }
        }
      ]
    },
    {
      "id": "tenant-owner",
      "attachable": true,
      "statements": [
        {
          "name": "owner-of-the-tenant",
          "effect": "allow",
          "action": "wms:*"
        }
      ]
    }
  ],
  "bindings": [
    {
      "id": "login",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "login"
    },
    {
      "id": "control-api",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "control-api"
    }
  ]
}

Each tenant document binds the role owner once, to the built-in policy that lets a tenant administer its own document, in the control plane's app:

json
{
  "schema": 2,
  "revision": 1,
  "publishedAt": "2026-11-01T08:00:00Z",
  "bindings": [
    {
      "id": "owners-administer",
      "subject": {
        "role": "owner"
      },
      "app": "cosec-control",
      "space": "",
      "policy": "cosec:tenant-admin"
    }
  ]
}

The app document is the one of An app without spaces. The two bindings are written when the tenant is created, and never again: who is an owner is the token's business.

The identity system issues the owner two tokens. For the app, policies lists the attachment:

kotlin
TokenSubject(id = "bob", app = "wms", tenant = "acme", policies = setOf("tenant-owner"))

and for the control plane, issued for its app, the role:

kotlin
TokenSubject(id = "bob", app = "cosec-control", tenant = "acme", roles = setOf("owner"))

Issue a token with tenant-owner only for a user the identity system says owns the tenant. An attachment is trusted as the token says it, which is the point, and also why the token's issuer decides ownership.

Check it ​

The scenarios in wiki/examples/tenant-owner run on every build:

CallerRequestResult
bob, attachment tenant-owneredit a warehousePermit: no role and no binding for bob
bob, attachment tenant-ownerthe same, in space wh-01403 scope-rejected: an attachment is not a role in a space
bob, attachment tenant-ownercosec:write on acme's documentDeny: the attachment covers wms, not the control plane
carol, no role, no attachmentedit a warehouseDeny
bob, role owner, token for cosec-controlcosec:read on acme's documentPermit: cosec:tenant-admin
bob, role owner in tenant globexcosec:read on globex's documentDeny: only acme's document binds the role

An attachment applies in every app and, by itself, only in the default space: a space still needs a role in it. Give an owner who works in spaces the roles there too, or answer for them from a MembershipProvider.

Why an attachment ​

The grant analysis, which decides whether an administrator may give what a change grants, counts what the author holds. An attached policy counts for every app and every space, so an owner who holds wms:* through an attachment covers any role of wms the tenant creates, with no approver. An administrator who holds permissions through roles is held to the spaces they hold the roles in, so granting a spaced permission in all spaces goes to approval (What a grantor needs).

So that the owner's reach stays what you intend:

  • write the attached policy with the actions of the app, not *: an attachment applies in every app, so * would match the control plane's cosec: actions as well;
  • keep cosec:approve and the platform document with the (platform) tenant.

Next: keep CoSec's roles in step with an identity system, in Roles from an external source.

Licensed under the Apache License, Version 2.0.