Skip to content

An App Without Spaces ​

For a team whose service is one app used by many tenants, with permissions that users get through roles. Spaces are not needed: everything happens in the default space. This page builds the app wms from the route map to the token, with documents and settings that run, and checks them with the test kit. The model is in Model.

What you need ​

An app without spaces
DependenciesThe Quick start's, and cosec-control once documents change at runtime
Configurationcosec.jwt.secret; cosec.authorization.scope.app-id: wms; cosec.authorization.routes.location: classpath:cosec-routes.json
Filescosec-routes.json in the service; platform.json and apps/wms.json published to Redis
ConceptsApp documents, the catalog, short-form permissions, system roles, route maps, tokens bound to an app
Left outSpaces: no spaced, no role@space, no CoSec-Space-Id

Map routes to actions ​

The service names its operations, so that permissions say wms:warehouse.edit and not PUT /warehouses/**:

json
{
  "service": "wms",
  "routes": [
    {
      "method": "GET",
      "path": "/warehouses",
      "action": "wms:warehouse.read",
      "resource": "warehouse"
    },
    {
      "method": "GET",
      "path": "/warehouses/{id}",
      "action": "wms:warehouse.read",
      "resource": "warehouse/{id}"
    },
    {
      "method": "PUT",
      "path": "/warehouses/{id}",
      "action": "wms:warehouse.edit",
      "resource": "warehouse/{id}"
    },
    {
      "method": "POST",
      "path": "/inbound/{id}/ship",
      "action": "wms:inbound.ship",
      "resource": "inbound/{id}"
    }
  ]
}

A request that matches a route is that route's action on its resource; any other request stays http:<METHOD> on its path. What a route map changes for the statements that apply is in Authorizing business actions.

Describe the app ​

The app document holds the catalog, whose entries that write an action are the permissions, and the system roles:

json
{
  "schema": 2,
  "revision": 1,
  "publishedAt": "2026-11-01T08:00:00Z",
  "catalog": {
    "groups": [
      {
        "name": "Warehouse",
        "permissions": [
          {
            "id": "warehouse.read",
            "name": "Read warehouses",
            "action": "wms:warehouse.read"
          },
          {
            "id": "warehouse.edit",
            "name": "Edit warehouses",
            "action": "wms:warehouse.edit"
          },
          {
            "id": "inbound.ship",
            "name": "Ship inbound orders",
            "action": "wms:inbound.ship",
            "resource": "inbound/{id}"
          }
        ]
      }
    ]
  },
  "roles": [
    {
      "id": "clerk",
      "name": "Clerk",
      "description": "Looks things up",
      "permissions": [
        "warehouse.read"
      ]
    },
    {
      "id": "manager",
      "name": "Manager",
      "permissions": [
        "warehouse.read",
        "warehouse.edit",
        "inbound.ship"
      ]
    },
    {
      "id": "admin",
      "name": "Administrator",
      "permissions": "*"
    }
  ]
}
  • Each entry with an action is the permission perm:wms/<id>: one ALLOW statement of that action and resource. No policy is written by hand (short-form permissions).
  • A role lists the permissions it holds. "*" is every permission the catalog defines, so the role follows the catalog as it grows; the diff and the approval of a catalog change show the new grants.
  • The group is not spaced, so each permission applies in the default space, the only one this app has.
  • The roles apply to every tenant. A tenant that needs its own role adds one: System and custom roles.

The platform document ​

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
            ]
          }
        }
      ]
    }
  ],
  "bindings": [
    {
      "id": "login",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "login"
    },
    {
      "id": "control-api",
      "subject": "*",
      "app": "*",
      "space": "*",
      "policy": "control-api"
    }
  ]
}

It permits only the login path and the control API's paths. Everything the app allows comes from its own document, through roles, so nothing here mentions wms.

Configure the service ​

yaml
cosec:
  jwt:
    secret: ${COSEC_JWT_SECRET}
  authorization:
    scope:
      app-id: wms
    routes:
      location: classpath:cosec-routes.json

scope.app-id makes wms the host's app: the service serves only tokens issued for wms, and reads the wms document for requests that carry a role. It is the app the service serves, never something the client says.

Publish, and issue tokens for the app ​

Publish the documents as in the Quick start, with both files in the report:

bash
cd documents
{ printf '{"accepted":true,"outputSha256":{'; sep=""
  for f in platform.json apps/wms.json; do
    printf '%s"%s":"%s"' "$sep" "$f" "$(shasum -a 256 "$f" | cut -d' ' -f1)"; sep=","   # Linux: sha256sum
  done; printf '}}\n'; } > report.json
cosec-migrate publish --to-redis redis://localhost:6379 --in .

The login says which app the token is for, and the roles the user holds in it:

kotlin
TokenSubject(id = user.id, app = "wms", tenant = user.tenant, roles = setOf("clerk"))

A token issued for another app, or for none, is refused by this service (Apps).

Check it ​

The scenarios in wiki/examples/app run against these documents on every build:

RequestTokenResult
GET /warehouses/7role clerk200: warehouse.read
PUT /warehouses/7role clerk403: the clerk does not hold warehouse.edit
POST /inbound/42/shiprole manager200
PUT /warehouses/7role admin200: "*"
GET /warehouses/7no role403
GET /warehouses/7, CoSec-Space-Id: wh-01role clerk403 scope-rejected: a space the token does not list
GET /warehouses/7role clerk, issued for pos403 scope-rejected

Test your own documents the same way, with cosec-testkit (Testing decisions).

Change it later ​

A new permission is a catalog entry and, usually, a line in a role. Change the app document through the control plane, with a principal of the (platform) tenant, as in Change a document later: putCatalog and putRole operations, or POST /documents/app:wms:apply with the whole document. Nothing is published before it has been checked, and the change shows what it adds to each role.

If it fails ​

Next: let tenants add roles, in System and custom roles.

Licensed under the Apache License, Version 2.0.