Authorizing Business Actions
For a team deciding whether to give its routes business names such as wms:warehouse.purge, and for platform authors who write DENYs and rate limits that must hold for services they do not own. A route map turns a request's method and path into an action and a typed resource. It also changes which statements apply to the request, and that is the part to know before you add one.
Do you need a map
http:<METHOD> on http:path:<path> is a complete authorization language: path patterns capture variables, conditions read them, and a permission can be written with "action": "http:GET" alone. A route map is a layer of names above it.
| Use a route map when | Because |
|---|---|
Policies must survive URL changes and versions (/v1, /v2) | The action name does not depend on the path |
| You list permissions in a console and grant them by name | The catalog is organized by action |
| Requests and code-level checks share one vocabulary | POST /orders/42/cancel and checker.check(ctx, "order:cancel", "order/42") are the same statement |
| You can go without one when | Because |
|---|---|
| The service is behind Spring Cloud Gateway | The gateway loads no route map (Gateway topologies) |
| A small service whose paths are its natural vocabulary | http:GET and http:path:/orders/{id} are enough |
| You check business actions only in code | AuthorizationChecker takes the action as it is |
Setting one up
A service loads the maps named by cosec.authorization.routes.location. Unset, it loads none, unless classpath:cosec-routes.json exists, in which case it fails at startup until the property names that file (or is set empty on purpose). The startup log lists each map, its service and its number of routes. The file, and generating it from OpenAPI, are in Route maps; a worked service is An app without spaces.
A request has one action
A request is either mapped, and then it is that route's service:verb on its typed resource, or unmapped, and then it is http:<METHOD> on its path. Never both. So:
- a statement on
service:verbnever applies to an unmapped request, and - a statement on
http:*orhttp:GETnever applies to a mapped one.
Rate limit rules match like a DENY, by action and resource, so the same holds for them: a rule on http:POST and http:path:/auth/login stops applying to the request once POST /auth/login has a route.
This matters most for the platform. A platform DENY for /admin/** is written on http:*. When a service team adds a route for an admin path, its requests are no longer http: requests, and the DENY no longer matches them. CoSec has no condition on the request path, so a platform author cannot write "closed whether or not mapped" once; the DENY is written for both actions.
An example
A route map gives DELETE /admin/warehouses/{id} the action wms:warehouse.purge:
{
"service": "wms",
"routes": [
{
"method": "GET",
"path": "/warehouses/{id}",
"action": "wms:warehouse.read",
"resource": "warehouse/{id}"
},
{
"method": "DELETE",
"path": "/admin/warehouses/{id}",
"action": "wms:warehouse.purge",
"resource": "warehouse/{id}"
}
]
}and the platform closes the admin paths with a DENY on http:*, while the admin role holds every permission of the app, wms:warehouse.purge included:
{
"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": "admin-closed",
"statements": [
{
"name": "admin-paths-are-closed",
"effect": "deny",
"action": "http:*",
"resource": "http:path:/admin/**"
}
]
}
],
"bindings": [
{
"id": "login",
"subject": "*",
"app": "*",
"space": "*",
"policy": "login"
},
{
"id": "control-api",
"subject": "*",
"app": "*",
"space": "*",
"policy": "control-api"
},
{
"id": "admin-closed",
"subject": "*",
"app": "*",
"space": "*",
"policy": "admin-closed"
}
]
}The scenarios in wiki/examples/route-maps show both sides:
| Request | Decision |
|---|---|
DELETE /admin/x, no route for it: http:DELETE on http:path:/admin/x | Deny: the platform DENY matches |
DELETE /admin/warehouses/7, mapped: wms:warehouse.purge on warehouse/7 | Permit: the DENY does not apply, and the role allows |
The fix is a second statement on the action, next to the first:
{
"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": "admin-closed",
"statements": [
{
"name": "admin-paths-are-closed",
"effect": "deny",
"action": "http:*",
"resource": "http:path:/admin/**"
},
{
"name": "purge-is-closed",
"effect": "deny",
"action": "wms:warehouse.purge"
}
]
}
],
"bindings": [
{
"id": "login",
"subject": "*",
"app": "*",
"space": "*",
"policy": "login"
},
{
"id": "control-api",
"subject": "*",
"app": "*",
"space": "*",
"policy": "control-api"
},
{
"id": "admin-closed",
"subject": "*",
"app": "*",
"space": "*",
"policy": "admin-closed"
}
]
}Now the mapped request is denied as well (scenarios-fixed in the example). Do the same for a rate limit rule: write it for the action, or for both.
A checklist
- Adding a route to a map? Review the platform DENYs and rate limit rules that name its path, and repeat them for its action.
- Writing a platform DENY or limit for services you do not own? Ask whether each has a map, and write the statement for the
httpform and for the actions that cover the path. - Seeing a request treated unexpectedly?
explainand the decision event show the action and resource the request was decided as (Explaining a decision).
Generated from OpenAPI
With cosec-openapi, springdoc and Spring Boot Actuator, cosecServiceGenerator/routes generates the map from the OpenAPI document springdoc scans, and cosecServiceGenerator/app the app document: a catalog of short-form permissions, one per action, with no policy to write by hand and no roles. The groups are not spaced; mark them spaced when the app uses spaces (Role permissions). Generating maps every operation, so every generated route leaves the http statements above behind.
Next: the same question at the edge, in Gateway topologies.