Skip to content

Policy Language ​

For policy authors, reviewers and tool builders who need the exact meaning of a field. This page is the reference of the document format (schema: 2). How the fields combine into a decision is in the security model; how to change documents safely is in Testing policies.

All authorization data lives in three kinds of documents. A document is read strictly and compiled whole: an unknown field, a repeated key, a value of the wrong JSON type, a pattern or condition that does not compile, or a dangling reference makes the whole document fail. A document that fails is never installed, and every request that needs it fails closed: 503 unless a DENY matches (fail closed).

Documents ​

DocumentKeyOne perHolds
PlatformplatformdeploymentPolicies shared by every tenant, bindings for anyone (*) and for named principals, break-glass bindings, rate limit rules, and what only the platform sets: limits, attributes, boundaries.
Appapp:<appId>appThe app's catalog, its permission policies perm:<appId>/<permission>, system roles, role bindings, rate limit rules.
Tenanttenant:<tenantId>tenantThe tenant's own policies, its bindings (anyone, its principals, its roles), custom roles, its boundary, rate limit rules.

The key is how the control plane names a document. Every request reads the platform document and the document of its tenant (anonymous requests: tenant (0)). It reads the app document of its app only when the principal holds at least one role in the request's space, or when the tenant document binds one of the app's permission policies to anyone or to a principal; a request the blacklist blocks reads only the platform document. A document nobody published reads as empty. Which candidates apply, and in which order, is in the security model.

A tenant document:

json
{
  "schema": 2,
  "revision": 3,
  "publishedAt": "2026-11-01T08:00:00Z",
  "policies": [
    {
      "id": "order-reader",
      "statements": [
        {
          "name": "read-orders",
          "effect": "allow",
          "action": "order:read",
          "resource": "order/**"
        }
      ]
    }
  ],
  "bindings": [
    {
      "id": "support-reads-orders",
      "subject": { "role": "support" },
      "app": "order",
      "space": "",
      "policy": "order-reader"
    }
  ]
}
FieldTypeRequiredMeaning
schemaintegeryesThe format version: always 2. Any other value fails the document.
revisionintegeryesPositive. Each publish writes the next one.
publishedAtstringyesAn ISO-8601 instant with Z or an offset, such as 2026-11-01T08:00:00Z.
provenanceobjectnoHow the revision was published (Provenance). Written by the control plane and cosec-migrate.

When you submit a whole document to the control plane (:apply), the header must be present and valid, but the control plane writes its own revision, publishedAt and provenance when it publishes.

Sections by document ​

FieldPlatformAppTenant
policies, bindings, rateLimitsyesyesyes
catalogyes
rolesyesyes
limits, attributesyes
boundariesyes
boundaryyes

A field a document kind does not take fails that document.

Policies ​

A policy is a named list of statements. It does nothing until a binding or an attachment makes it apply to someone.

FieldTypeDefaultMeaning
idstringrequiredUnique in its document. See the rules below.
attachablebooleanfalseWhether a principal may attach it.
boundarybooleanfalseA platform boundary policy.
conditionconditionnoneGuards every statement of the policy. It cannot read path variables. It is evaluated at most once per request, and only after one of its statements matched the action and resource.
statementsarrayrequiredThe statements.

Policy ids:

  • perm:<appId>/<permission> names an app's permission policy. Only that app's document defines it, and it cannot be attachable (Role permissions).
  • cosec:blacklist, cosec:root, cosec:platform-admin, cosec:tenant-admin and cosec:tenant-viewer are built-in policies; no document defines them. The control plane also refuses new ids that start with cosec:, g: or r:, which are reserved for built-in policies and cosec-migrate.
  • A tenant policy must not reuse the id of a platform policy. The control plane refuses it. On the data plane, a binding or attachment that names such an id makes the request Indeterminate (503) unless a DENY matches or both policies have the same content.

Statements ​

A statement matches a request when one of its action patterns matches the action, one of its resource patterns matches the resource, its condition is true, and its policy's condition is true.

FieldTypeDefaultMeaning
namestring""Reported in decisions, traces and explain.
effect"allow" or "deny"required
actionaction pattern, or a non-empty array of themrequiredWrite "*" for every action.
resourceresource pattern or pattern object, or a non-empty array of themany resource
conditionconditionalways true
onMissing"indeterminate" or "nomatch""indeterminate"What an external attribute that cannot be fetched means (External attributes). Only for a condition that reads one.
onMissingConfirmedobjectnoneWho confirmed a DENY with onMissing: "nomatch", and in which change: {"by": {"tenant", "principal"}, "change"}. Only the control plane writes it.

A DENY cannot name a bare resource type such as order: it would cover only the resource with an empty id. Write order/** for every order. Such a statement does not compile.

Actions ​

PatternMatches
service:verbThat action only.
service:*Every verb of the service.
*Every action.

There is no other wildcard. Service and verb compare exactly, case included (http:get is not http:GET). Neither may be empty or contain :, *, whitespace or control characters.

Where a request's action comes from:

  • An HTTP request whose route is in the service's route map has that route's service:verb action and typed resource.
  • Any other HTTP request has the action http:<METHOD> (the method as received, such as http:GET) on the resource of type http whose id is its path. Statements on service:verb never match it, and statements on http:* never match a mapped request.
  • A check in code names the action and resource itself (Checks in code).

Route maps ​

A route map (cosec-routes.json, loaded from cosec.authorization.routes.location, Configuration) gives a service's HTTP routes their actions and typed resources:

json
{
  "service": "order",
  "routes": [
    { "method": "GET", "path": "/orders/{orderId}", "action": "order:read", "resource": "order/{orderId}" },
    { "method": "POST", "path": "/orders/{orderId}/cancel", "action": "order:cancel", "resource": "order/{orderId}" }
  ]
}

path is an HTTP path pattern. resource is a typed resource whose segments are literals or variables of path. A route map loads whole or not at all: a map with no routes, a malformed route, or two routes of one method that the same request could match, fails startup. A captured value that holds / (a decoded %2F) would change the resource id's segments, so such a request is refused with 400. A gateway loads no route map: its requests stay in the http namespace. cosecServiceGenerator/routes generates a route map from the service's OpenAPI document (Role permissions).

Resources ​

A resource is a type and an id: order and 42 for order/42, http and /orders/42 for an HTTP path.

Typed resources ​

type, or type/segment/.... Each segment is one of:

SegmentMatches
a literalthat text exactly, case included
*one non-empty segment
{name}one non-empty segment, captured as a path variable
**, last onlyany number of further segments, none included
Patternorder (empty id)order/42order/42/items
orderyes
order/*yes
order/{orderId}yes
order/**yesyesyes
order/*/itemsyes

Ids are split on / and compared as written: never decoded, never trimmed. The type cannot be http, and cannot contain :; no literal may contain *, ?, {, }, whitespace or control characters.

HTTP paths ​

http:path:<pattern> matches a resource of type http whose id is the request path, as the pipeline normalized it (Web integration). It matches what Spring's PathPattern matches, on this subset:

ElementMatches
a literal segmentthat segment
?one character
* within a segmentzero or more characters, as in /files/*.pdf
* as a segmentone segment
{name}one segment, captured
**, or {*name}any number of segments, captured by {*name} as the rest of the path; only first or last, at most one per pattern. /admin/** also matches /admin.

These do not compile: {name:regex}, an empty pattern, a segment that can match an empty value other than * alone (such as {a}{b} or *{a}), {} or {*}, and text that holds half of a surrogate pair without the other half (a character outside the Basic Multilingual Plane, such as an emoji, is written whole).

A resource may be a pattern object with options, for http:path: patterns only:

json
{ "pattern": "http:path:/Reports/{id}", "caseSensitive": false, "decodeAndParseSegments": true }
OptionDefaultMeaning
caseSensitivetruefalse compares literals ignoring ASCII case.
decodeAndParseSegmentstruetrue matches each segment by its percent-decoded value without ; parameters; false by its text as received, except that %2F reads as /.

Path pattern bounds ​

What matching a document's patterns costs one request is bounded (DOC-4), as conditions are (Limits). A pattern over a bound does not compile:

BoundLimit
An http:path: patternat most 2,048 characters after http:path:, the longest path the pipeline accepts
A typed resource patternat most 2,058 characters
A segment that mixes *, ? or {name} with other text, such as report-*.pdfat most 16 parts: each *, ? and {name}, and each run of text between them. Consecutive * count once: a**b is a*b
All such segments of one document, across its policies, boundaries and rate limit rulesat most 2,048 glob units in all: each segment's parts and the characters of its text, so report-*.pdf is 3 parts and 11 characters, 14 units. A segment after a leading ** or {*name} counts twice, and so does a rate limit rule whose key reads a captured variable

A literal segment, * or {name} as a whole segment, ** and {*name} cost nothing: only a segment mixing them with text is matched by searching, and its text may be compared at every position of the request's segment, so a character costs as much as a part. A segment after a leading ** is tried against two segments, and a rule keyed by a captured variable is matched and then captured: hence they count twice. The bounds are fixed in 8.0. On the data plane a stored document over them is installed as failed, so the requests that read it fail closed.

Each policy is evaluated at most once per pass for each document that binds it, plus one evaluation for the attachments, however many of its bindings apply (ALG-9). So the budget bounds what matching one document's patterns costs a request: a document that spends all of it on patterns that are candidates for every path costs about 0.5 ms per request at worst, and about twice that when its policies are attached as well, for a path segment of 256 characters, the longest the pipeline accepts (GlobBoundBenchmark, its worst inputs, on a CI runner). The worst is a segment of surrogate pairs, such as emoji, against ? and *; with ASCII it is about 0.33 ms. It allocates the values it captures: about 0.25 MB per request with ASCII values, and about 0.6 MB with values outside Latin-1. A shadow window evaluates its candidate document as well, within a budget of its own.

Path variables ​

{name} in a typed or HTTP pattern, and {*name} in an HTTP pattern, capture a variable. Only the statement whose pattern captured it can read it, as resource.var.<name>, in its own condition or a rate limit rule's key:

json
{
  "name": "own-profile",
  "effect": "allow",
  "action": "http:*",
  "resource": "http:path:/users/{userId}/**",
  "condition": { "eq": ["resource.var.userId", { "attr": "subject.id" }] }
}
  • Every resource pattern of the statement must capture the variable it reads, or the statement does not compile: a variable some pattern does not capture would read as absent, and a DENY written against it would silently miss. A policy condition cannot read any variable.
  • A name is a Java identifier that may also contain -, and is captured once per pattern.
  • A captured value is the segment's decoded value ({*name}: the rest of the path). A value is only ever compared: it never becomes part of a pattern, so a principal id of ** cannot widen anything.

Conditions ​

A condition is a JSON object with exactly one operator; ignoreCase may stand beside eq and prefix. Every attribute path and every operand type is checked when the document compiles.

json
{ "and": [
  { "eq": ["subject.authenticated", true] },
  { "in": ["scope.space", ["team-a", "team-b"]] },
  { "not": { "cidr": ["environment.ip", ["10.0.0.0/8"]] } },
  { "eq": ["resource.var.ownerId", { "attr": "subject.id" }] }
] }

Operators ​

OperatorFormTrue whenAttribute types
eq[attribute, literal]the value equals the literal. With "ignoreCase": true (String only), ignoring ASCII case. An Ip attribute compares with an address.String, Bool, Ip
eq[attribute, {"attr": attribute}]both values are present and equal; both attributes have the same typeString, Bool, Ip
in[attribute, [literals]]the value is one of the literals: 1 to maxInValues of themString, Bool, Ip
in[literal, attribute]the set holds the string literalSet
prefix[attribute, "text"]the value starts with the text; ignoreCase allowed. On a Labels attribute, whole labels: com.example matches example.com and api.example.com, not badexample.com.String, Labels
cidr[attribute, ["block", ...]]the address is in one of the IPv4 or IPv6 blocks: 1 to maxCidrBlocksIp
time{...}the request time is in the window (Time conditions)
exists"attribute"the attribute is present (for Labels, its host is)any
and[conditions]all are true; at least one, left to right, stops at the first false
or[conditions]any is true; at least one, left to right, stops at the first true
notconditionthe condition is false

Literals are JSON strings or booleans, never numbers; a string is never read as a boolean. There is no true or false condition: leave condition out instead.

Attributes ​

AttributeTypeValue
subject.idStringThe principal id; absent for an anonymous principal.
subject.tenantStringThe request's tenant: the principal's, (0) when anonymous.
subject.authenticatedBoolWhether a credential was accepted.
subject.rolesSetThe principal's role ids, in any space.
subject.scopedRolesSetIts roles as claimed: role in the default space, role@space in another.
subject.attributes.<name>StringThe token's attributes claim entry, as text (Authentication).
scope.tenantStringSame as subject.tenant.
scope.appStringThe request's app; absent when it has none.
scope.spaceStringThe request's space; "" for the default space.
scope.rolesSetThe principal's roles in the request's space.
action.service, action.verbStringThe two parts of the action.
resource.type, resource.idStringThe two parts of the resource.
resource.attributes.<name>StringAttributes of the resource. Neither the request pipeline nor the documented checks set any: an object-level rule puts its facts in the resource id (Object-level rules).
resource.var.<name>StringA path variable.
environment.ipIpThe client address (Web integration).
environment.timeInstantThe request time; only time reads it.
environment.deviceStringThe CoSec-Device-Id parameter; absent when empty.
environment.blockedBoolWhether the blacklist blocks the request.
http.header.<name>StringThe request header <name>; absent when the request has none. See Headers.
http.origin, http.refererStringThe Origin and Referer headers, read as http.header.* is.
http.origin.host, http.referer.hostStringThe host of the Origin and Referer URLs, in lower case; absent when the value is not a URL with a host.
http.origin.host.labels, http.referer.host.labelsLabelsThat host's labels, reversed (api.example.com is com.example.api), in lower case; for prefix.
request.attributes.<name>StringWhat the host knows about the request besides HTTP. The gateway sets request.attributes.cosec.audit.routeId, the id of the route the request came through, and request.attributes.cosec.identity.forward: true on a route that receives the forwarded identity (a lb:// route, or one whose metadata says cosec-identity: true), false on any other; WebFlux and WebMvc set none.
ext.<provider>.<name>declaredAn external attribute.

A path outside this table does not compile.

Headers ​

A header name is an ASCII token (RFC 9110: letters, digits and !#$%&'*+-.^_`|~), and any other name does not compile. Names are case-insensitive, folding ASCII letters only, on the document and the request side alike: http.header.X-Team is http.header.x-team, and no Unicode case mapping makes two names meet.

No attribute reference can read a header that carries a credential: http.header.authorization, proxy-authorization, cookie, set-cookie, x-api-key and cosec-identity, in any case. That covers a condition and a rate limit rule's key alike. A document with such a reference does not compile, through the control plane and cosec-migrate alike. A DENY with prefix on authorization would let a document's author learn another principal's token one character at a time, and a key on it would count requests per token. cosec-identity is what a gateway forwards to the services behind it; at the gateway it is only the client's claim. The list is fixed (ConditionChecker.CREDENTIAL_HEADERS), plus the header the host reads API keys from, when cosec.pipeline.api-key-header renames it: the starter compiles every document, on the data plane and in the control plane, with that header refused too, and the pipeline never reads it for a condition.

A header sent more than once reads as its value when every copy is the same. When the copies differ, the header has no single value, and the first or last copy never decides: the pipeline answers the request 400 invalid-request before evaluating it, when any statement, policy condition, boundary or rate limit key of the documents the request is decided on reads that header (PIP-12). It does so whatever the statement's effect and whether it could apply to the request, so the answer tells a client neither that a DENY reads the header nor which.

Matching roles ​

Roles grant permissions through role bindings, not conditions. When a condition needs a role, test the set:

json
{ "in": ["auditor", "scope.roles"] }

scope.roles holds the roles of the request's space only, which is usually what you mean; subject.roles holds a role whatever space it is in; subject.scopedRoles holds role@space strings. An anonymous principal has no roles.

Absent values ​

An attribute is absent when the request has no value for it, such as subject.id of an anonymous principal or scope.app of a request without an app.

  • eq, in, prefix, cidr and time are false for an absent value; exists is false.
  • Absent is equal to nothing: not to "", and not to another absent value.
  • not is plain negation, so not over a comparison with an absent value is true. A DENY written as {"not": {"eq": [...]}} also matches requests where the attribute is absent.

Time conditions ​

time reads environment.time.

FieldMeaning
notBeforeISO date-time; the condition is false before it. Without an offset, read in zone.
notAfterISO date-time; false from it on. Must be after notBefore.
zoneTime zone id: UTC (the default), an offset such as +08:00 or UTC+8, or a region such as Asia/Shanghai.
daysOfWeekNon-empty array of MON … SUN, spelled exactly so: any other case or name does not compile.
from, toTime of day HH:mm, HH:mm:ss or HH:mm:ss.fff; both or neither, and not equal. The window is [from, to). When from is after to it crosses midnight, and its early-morning part belongs to the day it started on.

At least one of notBefore, notAfter, daysOfWeek or from/to is required.

json
{ "time": { "zone": "Asia/Shanghai", "daysOfWeek": ["MON", "TUE", "WED", "THU", "FRI"], "from": "09:00", "to": "18:00" } }

External attributes and onMissing ​

ext.<provider>.<name> attributes come from an AttributeProvider (Extending), fetched only for requests whose candidate statements read them. Their type is declared: string, bool, stringSet or ip. A path no provider and no platform attributes entry declares does not compile. Every instance compiles against both its own providers and the platform's attributes, and so does the control plane when it checks a change; the providers must therefore be the same on every host (Testing policies).

A provider may return no value (absent, as above), or fail, time out or not be asked: then the attribute is missing, and the statement's onMissing decides:

onMissingDENYALLOW
indeterminate (default)The decision is Indeterminate (503) unless another DENY matches.Skipped. If nothing else permits, the decision is Indeterminate (503), not a deny.
nomatchThe DENY does not match: it lapses while the attribute is missing. Compiles only with onMissingConfirmed, which the control plane writes when the change's author confirms it (Testing policies).Skipped, and the request is decided without it.

A missing attribute never turns a Deny or Indeterminate into a Permit, except through a confirmed nomatch DENY.

Limits ​

Every condition is bounded when it compiles. The platform document sets the bounds for every document (limits); absent ones are the defaults.

LimitDefaultBounds
maxNodes64Nodes of one condition.
maxDepth8Nesting depth of one condition.
maxInValues1024Literals of one in, and days of one time.
maxCidrBlocks256Blocks of one cidr.
maxLiteralBytes1024UTF-8 bytes of one literal or attribute path.
maxCost256The sum of node weights of one condition: and, or, not, exists 1; eq, prefix 2; in, cidr 4; time 8.

Bindings ​

A binding says: subject is subject to policy in app and space.

FieldTypeDefaultMeaning
idstringrequiredUnique in its document. The control plane refuses new ids that start with cosec:, g: or r:.
subject"*" or objectrequiredWho: see below.
appstringrequiredAn app id, or * for any app. Never empty.
spacestringrequiredA space id, "" for the default space, or * for any space. A space id beginning with ( is CoSec's, and a binding in one does not compile.
policystringrequiredThe policy id.
expiresAtstringnoneAn ISO-8601 instant. From then on the binding does not apply.
breakGlassbooleanfalseA break-glass binding (security model).

app and space compare exactly, case included; * means any and only that.

SubjectPlatformAppTenant
"*": anyone, anonymous principals includedyesyes
{"principal": "<id>", "tenant": "<tenant>"}yes, tenant requiredyes, tenant may be left out (this tenant)
{"role": "<id>"}, optional "tenant"yes; no tenant: any tenantyes; no tenant: this tenant

A binding that names another tenant in a tenant document, an app document binding scoped to another app, and a role binding in the platform document do not compile: they could never apply, and a DENY would be lost silently.

What a binding may name in policy:

  • a policy of its own document;
  • in an app or tenant document, a platform policy. It is looked up per request: if the platform no longer has it, requests it applies to are Indeterminate (503) unless a DENY matches. The control plane refuses to publish a reference that does not resolve, and refuses to remove a platform policy another document refers to;
  • an app's permission policy: only in that app's document, or in a tenant document with app set to that app (Role permissions);
  • cosec:root, only from a break-glass binding; cosec:platform-admin, cosec:tenant-admin and cosec:tenant-viewer, which authorize the control plane. cosec:platform-admin is bound only in the platform document and the platform tenant's own, (platform): a tenant or app document that binds it does not compile.

A boundary policy and cosec:blacklist are never bound. A break-glass binding is only in the platform document, binds a principal of the (platform) tenant, and has expiresAt. A document binds at most 1,000 principals: grant users through roles in their token or a MembershipProvider instead.

Attachments ​

A principal's memberships can list policies to attach directly: the token's policies claim, or a MembershipProvider (Authentication). An attachment applies in every app and space. The policy must be attachable: true and defined in the principal's tenant document or in the platform document. An attachment that names any other policy is missing, and the request is Indeterminate (503) unless a DENY matches. Anonymous principals have no attachments.

Catalogs ​

An app document's catalog lists the permissions the app offers, for consoles and generators. An entry that writes action also defines its permission: the policy perm:<appId>/<id>, one ALLOW statement. No other entry takes part in evaluation.

FieldTypeMeaning
groupsarray, requiredGroups of permissions.
groups[].namestring, required
groups[].descriptionstring
groups[].spacedboolean, default falseWhere a permission written with action applies: in any space a role holding it applies in; without it, only in the default space, by the condition { "eq": ["scope.space", ""] }.
groups[].permissionsarray, required
groups[].permissions[].idstring, requiredThe permission id; its policy is perm:<appId>/<id>.
groups[].permissions[].name, .descriptionstring
groups[].permissions[].actionaction pattern or non-empty arrayDefines the permission. An entry with action and a policy of its id do not compile; the id must not be repeated.
groups[].permissions[].resourceas a statement's resource; needs actionAbsent: any resource.

Nothing checks that an entry without action has a policy, or the reverse; such an entry is only for display and no role can list it. See Role permissions.

Roles ​

An app document (system roles) and a tenant document (custom roles) may hold roles: who gets which permissions, written once. Role permissions has the concept and its examples.

FieldDocumentsTypeMeaning
idbothstring, requiredUnique in the document; not *, without @.
apptenantstring, requiredThe app the role belongs to; a valid app id.
spacetenantstringAbsent: any space. Otherwise only that space, "" being the default space; not reserved.
name, descriptionbothstringFor display.
permissionsbotharray of ids, or "*" in an app documentThe permissions of the role; possibly none.

A role derives one binding per permission, role:<roleId>@<app>:<permission>, of policy perm:<app>/<permission>, subject the role (of any tenant in an app document, of its tenant in a tenant document), app the role's app and space the role's space or *, after the hand-written bindings. A collision with a hand-written binding does not compile, and role: is reserved to these bindings: a change may not put one. A document derives at most 50,000.

Rate limit rules ​

rateLimits is an ordered array of rules. A permitted request spends one permit of every rule it matches (Rate limiting).

FieldTypeDefaultMeaning
idstringrequiredLetters, digits, ., _ and -. Unique in its document.
actionaction pattern or non-empty arrayrequiredWhich requests count.
resourceresource pattern or non-empty arrayany resource
conditionconditionalways
onMissing"indeterminate" or "nomatch""indeterminate"For an external attribute that cannot be fetched: indeterminate leaves it to strictFailure; nomatch skips the rule. nomatch compiles only in the platform document: rate limit rules have no confirmation, unlike a DENY.
keyattribute pathone shared bucketA String or Ip attribute whose value names the bucket. resource.var.<name> must be captured by every resource pattern.
permitsPerSecondnumberrequiredGreater than 0.
windowSecondsinteger1At least 1.
strictFailurebooleanfalseWhether a limiter that cannot count refuses the request (503).

A rule matches as a DENY statement does: action, resource and condition, with its own path variables; a bare resource type does not compile.

Platform sections ​

Limits and attributes ​

limits sets the condition limits every document compiles against. attributes declares the external attributes documents may read:

json
{ "path": "ext.geo.country", "type": "string" }

path is ext.<provider>.<name>; type is string, bool, stringSet or ip; each path once. When a platform revision changes either section, every instance compiles the app and tenant documents again; a document that no longer compiles fails closed. The control plane warns about this when it checks such a change.

Boundaries ​

A boundary caps what a tenant can be permitted. A policy with "boundary": true is a boundary: only in the platform document, only ALLOW statements, at most 64 of them, never attachable, never bound. boundaries.default names the boundary that holds every tenant; a tenant document's boundary names the platform boundary that holds that tenant instead. The (platform) tenant has none. A permit then stands only if a statement of the tenant's boundary also matches; otherwise it becomes a Deny, or Indeterminate when a boundary statement failed or the named boundary is missing.

A platform document:

json
{
  "schema": 2,
  "revision": 12,
  "publishedAt": "2026-11-01T08:00:00Z",
  "limits": { "maxCost": 128 },
  "attributes": [{ "path": "ext.geo.country", "type": "string" }],
  "boundaries": { "default": "boundary-standard" },
  "policies": [
    {
      "id": "deny-admin-outside-office",
      "statements": [
        {
          "effect": "deny",
          "action": "*",
          "resource": "http:path:/admin/**",
          "condition": { "not": { "cidr": ["environment.ip", ["10.0.0.0/8"]] } }
        }
      ]
    },
    {
      "id": "public-health",
      "statements": [{ "effect": "allow", "action": "http:GET", "resource": "http:path:/actuator/health" }]
    },
    {
      "id": "boundary-standard",
      "boundary": true,
      "statements": [{ "effect": "allow", "action": ["http:*", "order:*"] }]
    }
  ],
  "bindings": [
    { "id": "deny-admin", "subject": "*", "app": "*", "space": "*", "policy": "deny-admin-outside-office" },
    { "id": "health", "subject": "*", "app": "*", "space": "*", "policy": "public-health" },
    {
      "id": "ops-platform-admin",
      "subject": { "principal": "ops-1", "tenant": "(platform)" },
      "app": "*",
      "space": "*",
      "policy": "cosec:platform-admin"
    }
  ]
}

Provenance ​

The control plane and cosec-migrate record how each revision was published. It decides nothing.

FieldMeaning
parent{"revision", "digest"} of the head this revision replaced (digest: SHA-256: and 64 lowercase hex digits, the store's digest); absent for the first revision.
changeThe change that published it.
kindPUBLISH, ROLLBACK, BREAK_GLASS, EXPIRE, MIGRATE or BOOTSTRAP.
publishedBy{"tenant", "principal"}.
approvedByApprovers, when the change needed approval.
shadow{"window", "skipped"}: the shadow window the change went through, or that it was skipped.

Publishing documents ​

Documents are stored in Redis and written only through the control plane (the cosec-control module) or cosec-migrate publish and bootstrap; see Storage. The control plane checks and compiles every change before it publishes it, and can try it on live traffic first: see Testing policies.

JSON Schema ​

schema/ holds JSON Schemas for editor completion and checks in CI:

FileValidates
cosec-snapshot-document.schema.jsona platform, app or tenant document
cosec-binding.schema.jsonone binding
cosec-catalog.schema.jsonan app's catalog
definitions.schema.jsonshared definitions
problem.schema.jsonan error response body (Web integration)

A document is closed, so do not add a $schema field to it: map the schema to your files in the editor instead, for example in VS Code's settings.json:

json
{
  "json.schemas": [
    {
      "fileMatch": ["policies/*.json"],
      "url": "https://raw.githubusercontent.com/Ahoo-Wang/CoSec/refs/heads/main/schema/cosec-snapshot-document.schema.json"
    }
  ]
}

The files are generated from the format descriptors in cosec-runtime (DocumentFormats) and never edited by hand. A test keeps them and the document reader in agreement: what one accepts, the other accepts.

Passing the schema is necessary, not sufficient. JSON Schema cannot express, among others: which of the three documents a file is; unique ids and references between documents; whether a time zone exists, and dates such as February 30; a time window whose from equals its to, and notBefore after notAfter; path variable scope; the attribute catalog and the types of condition operands. Compile the document to check those (Testing policies).

Next: grant permissions by role instead of per principal, in Role permissions.

Licensed under the Apache License, Version 2.0.