Policy pack¶
The shipped Cedar policy pack and the loader that makes it an ordinary policy source. The pack
files themselves live under sentrykit/policy/pack/ and are read as data — they are never
imported. See policies for what each pattern does.
sentrykit.policy¶
The shipped Cedar policy pack, and the loader that makes it an ordinary policy source.
Re-exported from sentrykit.policy.loader so the pack is addressable as
sentrykit.policy — the pack files themselves live under sentrykit/policy/pack/ and are
read as data, never imported.
sentrykit.policy.loader¶
The shipped Cedar policy pack, loaded as an ordinary policy source.
The pack is data, not a second policy mechanism. It reaches a governor through exactly
the machinery a user’s own directory does — a PolicySource
registered under the sentrykit.policy_sources entry-point group, addressed by DSN,
unioned by CompositePolicySource:
install(policy_source=["pack://", "file://./policies"])
That union namespaces the pack’s declared ids as pack/<id>, so an audit record for a
decision the pack made names the pack, and a decision one of your own policies made names
your directory. Nothing about the pack is privileged: drop the DSN and it is not there.
Every pattern is attribute-driven
A shipped pack cannot know your role names, your resource types or your actions, so none of its policies name any. Each pattern reads the shape of the request instead:
the resource declares what it requires (
required_role,group,owner,emergency_only,emergency_role,conflicting_role,prepared_by);the grant store supplies the principal’s edges and attributes;
the context supplies
now,break_glass,on_behalf_ofandjustification.
So adopting a pattern is a matter of putting an attribute on an entity, which is data. The policy logic itself stays reviewed in version control and is never rewritten at runtime.
The schema is opt-in, and the reason matters
pack.cedarschema ships with the pack, but PackPolicySource.load() does not put
it on the bundle unless asked (pack://?schema=true). A composite concatenates its members’
schemas, and a Cedar engine built with a schema validates every request against it — so a
pack that contributed a schema by default would make every request naming an action the pack
never heard of fail to parse, in an application that was working a moment earlier. The pack’s
decisions are identical with and without the schema, so the default costs nothing and the
opt-in is a real choice rather than a workaround.
When you do opt in, your own schema must declare only what the pack does not: redeclaring
User, Role, Group or Resource is a Cedar schema parse error
(` `User` is declared twice `). Adding actions is safe, and an action whose context is
{} still type-checks against the pack’s context has now conditions.
Templates are linked here, not by Cedar
cedarpy 4.8.7 exposes no template-linking API at all, and an unlinked template in a
policy set is inert. link_template() therefore performs the
link on our side — textual substitution of ?principal and ?resource — and returns the
concrete policy and the matching GrantSpec together, since
a time-boxed grant is a rule and a piece of data at the same time.
- sentrykit.policy.loader.PACK_NAME: Final = 'pack'¶
Default source name, and therefore the namespace declared ids appear under in a union.
- sentrykit.policy.loader.PATTERNS: Final[Mapping[str, PackPattern]] = {'break-glass': PackPattern(name='break-glass', file='60-break-glass.cedar', summary='An emergency-only resource is refused outright unless the caller raises the flag, and is then reachable only by a principal entitled to the emergency, carrying a justification.', policy_ids=('emergency-resource-forbidden-without-flag', 'break-the-glass'), requires=('resource.emergency_only', 'resource.emergency_role', 'context.break_glass', 'context.justification'), obligations=('notify:secops', 'record:justification')), 'delegation': PackPattern(name='delegation', file='40-delegation.cedar', summary='A principal may act on behalf of its delegator, but only within the delegated scope and only until the delegation lapses.', policy_ids=('delegation-on-behalf-of', 'delegation-outside-scope-forbidden', 'delegation-expired-forbidden'), requires=('principal.delegated_by', 'principal.delegated_actions', 'principal.delegation_expires', 'resource.required_role', 'context.on_behalf_of', 'context.now'), obligations=('record:on_behalf_of',)), 'expiry': PackPattern(name='expiry', file='50-expiry.cedar', summary='An entitlement window that refuses the call outside its bounds.', policy_ids=('entitlement-expired-forbidden', 'entitlement-not-yet-valid-forbidden'), requires=('principal.entitlement_starts', 'principal.entitlement_expires', 'context.now'), obligations=()), 'groups': PackPattern(name='groups', file='20-groups.cedar', summary='A member of the group the resource belongs to may act on it, at any depth.', policy_ids=('group-membership',), requires=('resource.group',), obligations=()), 'jit-elevation': PackPattern(name='jit-elevation', file='80-jit-elevation.cedar', summary='A principal holds an elevated role for a window, and is refused once the window closes.', policy_ids=('jit-elevation', 'jit-elevation-expired-forbidden'), requires=('principal.elevation_role', 'principal.elevation_until', 'resource.required_role', 'context.now'), obligations=('notify:secops',)), 'ownership': PackPattern(name='ownership', file='30-ownership.cedar', summary='The owner of a resource may act on it; ownership is not inherited.', policy_ids=('resource-owner',), requires=('resource.owner',), obligations=()), 'rbac': PackPattern(name='rbac', file='10-rbac.cedar', summary='A principal holding the role the resource requires may act on it.', policy_ids=('rbac-required-role',), requires=('resource.required_role',), obligations=()), 'separation-of-duties': PackPattern(name='separation-of-duties', file='70-separation-of-duties.cedar', summary='A conflicting role is refused the resource outright, and nobody approves what they prepared themselves.', policy_ids=('sod-conflicting-role', 'sod-same-actor-approval'), requires=('resource.conflicting_role', 'resource.prepared_by'), obligations=())}¶
Every shipped pattern, keyed by name, in the order the files are concatenated.
Declared in Python rather than in a manifest file beside the policies because it is typed, and because a second file listing the first is a second thing to keep in step. The tests assert the table and the directory agree.
- sentrykit.policy.loader.SCHEMA_FILE: Final = 'pack.cedarschema'¶
The pack’s Cedar schema, shipped alongside the policies and loaded only on request.
- sentrykit.policy.loader.TEMPLATE_SUFFIX: Final = '.cedartemplate'¶
Templates live under
pack/templates/with this suffix, deliberately outside the*.cedarglob: an unlinked template can never permit anything, so loading one as if it were a policy would put something inert into a reviewed policy set.
- class sentrykit.policy.loader.LinkedPolicySource(links: Sequence[TemplateLink], *, name: str = 'linked')[source]¶
Bases:
objectLinked templates, as a policy source that composes like any other.
Held in memory rather than written to a directory, because a link is generated logic: it is derived from a reviewed template plus a piece of grant data, and round-tripping it through a file would invite someone to edit the derived half.
- Parameters:
links – The links to contribute, in order.
name – The source name, and therefore the namespace for the links’ ids in a union.
Example
link = link_template(pack_template("time-boxed-grant"), link_id="oncall-ana", principal='User::"ana"', resource='Resource::"paging"', expires_at=end_of_shift) install(policy_source=["pack://", LinkedPolicySource([link])])
- property name: str¶
Short name for this source.
- property links: tuple[TemplateLink, ...]¶
The links this source contributes, in order.
- load() PolicyBundle[source]¶
Build a bundle from the linked policies.
No links is not an error: “nobody currently holds a time-boxed grant” is the ordinary resting state of this source, and a union that refused to build in that state would take the whole policy set down every time the last link expired.
- Returns:
The bundle, with each link’s
link_idas its policy id.- Raises:
ConfigurationError – If the concatenated links do not parse.
- class sentrykit.policy.loader.PackPattern(name: str, file: str, summary: str, policy_ids: tuple[str, ...], requires: tuple[str, ...] = (), obligations: tuple[str, ...] = ())[source]¶
Bases:
objectOne shipped pattern: what it is, which file holds it, and what it declares.
- Variables:
name (str) – The pattern’s short name, as it is spelled in a
pack://<names>DSN.file (str) – The policy file, relative to the pack root.
summary (str) – One sentence, in authorization vocabulary, on what the pattern decides.
policy_ids (tuple[str, ...]) – The
@idannotations the file declares, in file order. These are the names a decision reports as determining, and<pack-name>/<id>once unioned.requires (tuple[str, ...]) – The entity attributes and context keys the pattern reads, so a host can tell what data adopting it needs. Spelled
resource.owner,context.now,principal.elevation_until.obligations (tuple[str, ...]) – The obligations the pattern’s permits carry, if any.
- class sentrykit.policy.loader.PackPolicySource(*, patterns: Sequence[str] = (), name: str = PACK_NAME, schema: bool = False, validate: bool = True)[source]¶
Bases:
objectThe shipped policy pack, as a
PolicySource.- Parameters:
patterns – Which patterns to include. Empty — the default — means every one of them. An unknown name is a configuration error rather than a silent omission, because a pattern that is quietly absent is a permit that quietly never fires.
name – The source name, which becomes the namespace for its policy ids in a union. Change it when you ship a second, differently-configured pack alongside.
schema – Whether to put
SCHEMA_FILEon the loaded bundle. Off by default; see the module docstring for why that default is the safe one.validate – Whether to type-check the pack against its own schema at load. Independent of
schema: validation happens against the shipped schema either way, and only the bundle’s schema is affected byschema.
Example
install(policy_source=["pack://ownership,rbac", "file://./policies"])
- classmethod from_dsn(dsn: DSN) PackPolicySource[source]¶
Build from
pack://[pattern,pattern]?name=...&schema=...&validate=....The DSN target is a comma-separated list of pattern names; an empty target means the whole pack.
packon its own parses aspack://and means the same thing.- Parameters:
dsn – The parsed address.
- Returns:
The source.
- Raises:
ConfigurationError – If a named pattern does not ship, or an option is malformed.
- property name: str¶
Short name for this source, used to namespace policy ids and tag provenance.
- property patterns: tuple[PackPattern, ...]¶
The patterns this source contributes, in concatenation order.
- load() PolicyBundle[source]¶
Read the shipped pack and build the bundle.
- Returns:
The bundle, with one provenance entry per policy naming the pattern file it came from as its
origin, andschemaset only when the source was built withschema=True.- Raises:
ConfigurationError – If the shipped pack is missing, unparseable, or fails validation against its own schema — all of which mean a broken install.
- current_hash() str[source]¶
Return the digest of the artifact
load()would return right now.Loads to compute it. The pack ships inside the distribution and cannot change under a running process, so a cheaper stat-based shortcut would be optimizing a poll nobody needs to make — and it would have to reproduce the schema opt-in, which is the one thing about this source’s digest that is not simply “what is on disk”.
- class sentrykit.policy.loader.TemplateLink(link_id: str, policy: str, grant: GrantSpec, template: str = '')[source]¶
Bases:
objectA linked template: one concrete policy, and the grant that records it as data.
Both halves describe the same authorization fact, for two different audiences. The
policyis reviewed logic a Cedar engine can decide against; thegrantis the store record an admin surface lists, an auditor queries, and a revocation targets.- Variables:
link_id (str) – The
@idwritten onto the generated policy, and the id the grant carries.policy (str) – Concrete Cedar policy text, with the template’s slots filled and an expiry condition appended when one was asked for.
grant (sentrykit.stores.base.GrantSpec) – The matching grant, carrying the same principal, subject and expiry.
template (str) – The template text the link was made from, kept for provenance.
- sentrykit.policy.loader.link_template(template: str, *, link_id: str, principal: str | EntityRef, resource: str | EntityRef, expires_at: datetime | None = None, granted_by: str | None = None, reason: str | None = None) TemplateLink[source]¶
Link a Cedar template into a concrete policy, and the grant that records it.
cedarpyhas no linking API, so the link is textual: every?principalbecomes the principal’s uid and every?resourcebecomes the resource’s, an@id(link_id)annotation is prepended, and — whenexpires_atis given — awhen { context has now && context.now <= <epoch> }condition is appended so the policy stops permitting on its own. Epoch seconds, becausecoerce_value()renders everydatetimethat way and Cedar has no ordering on strings; guarded byhas, because Cedar’s validator refuses an unconditional read of an optional context attribute and a link that evaluated correctly but failedsentrykit policy validatewould be a trap.- Parameters:
template – The template text, from
pack_template()or your own file.link_id – The id for this link. Must be unique across the policy set; it is what a decision reports and what an auditor joins on.
principal – Who the link is for.
resource – What it is over.
expires_at – When it stops applying.
Nonemeans it does not expire — which is a standing grant, not a time-boxed one, and is worth being deliberate about.granted_by – The actor making the grant, recorded on the grant.
reason – The justification, recorded on the grant.
- Returns:
The link.
- Raises:
ConfigurationError – If the template declares no slots, if a slot is left unfilled, or if
link_idis empty.
- sentrykit.policy.loader.load_pack(*, patterns: Sequence[str] = (), schema: bool = False, name: str = PACK_NAME) PolicyBundle[source]¶
Load the shipped pack directly, without going through a DSN.
- Parameters:
patterns – Which patterns to include; empty means all of them.
schema – Whether the bundle carries the pack’s schema.
name – The source name recorded on every provenance entry.
- Returns:
The bundle.
- Raises:
ConfigurationError – If a named pattern does not ship, or the install is broken.
- sentrykit.policy.loader.pack_root() Path[source]¶
Return the directory the shipped pack lives in.
Resolved from
__file__rather than throughimportlib.resources, because the pack is read as a directory byFilesystemPolicySourceandfiles()yields aTraversablethat is only a real path for the ordinary (non-zipped) install. sentrykit does not support running from a zipimport, and saying so here is better than a context manager that pretends otherwise.- Returns:
The absolute path of
sentrykit/policy/pack.
- sentrykit.policy.loader.pack_schema() str[source]¶
Return the pack’s Cedar schema text.
- Returns:
The contents of
pack.cedarschema.- Raises:
ConfigurationError – If the shipped schema is missing, which means a broken install.
- sentrykit.policy.loader.pack_template(name: str) str[source]¶
Return one shipped Cedar template’s text.
- Parameters:
name – The template’s name, with or without the
TEMPLATE_SUFFIXsuffix.- Returns:
The template text, with its
?principal/?resourceslots unfilled.- Raises:
ConfigurationError – If no such template ships with the pack.