API reference¶
The reference is generated from grantr's docstrings and type annotations. It documents each package's public API: the names in its __all__. The top-level package, grantr, re-exports the names most applications use.
grantr.core¶
grantr's model, and the rules a check and a grant follow: no I/O, no async.
Grants loads what a decision needs from storage, and decides with these.
PRINCIPAL_ID
module-attribute
¶
A service principal's id's form.
DEPLOYMENT
module-attribute
¶
The deployment, where the operator role is granted.
ITEMS
module-attribute
¶
The kinds that live in a workspace.
APPLICATION
module-attribute
¶
APPLICATION: Final = 'application'
The role of the application's own service principal: granted on the deployment, and the one deployment role that holds in every tenant.
BUILT_IN
module-attribute
¶
BUILT_IN: Final = frozenset(
{
"operator",
"application",
"owner",
"admin",
"member",
"editor",
"viewer",
"rule",
}
)
The built-in roles' names, which no custom role may take.
GRANTR_ACTIONS
module-attribute
¶
GRANTR_ACTIONS: Final = frozenset(
f"grantr:{name}"
for name in (
"create_tenant",
"delete_tenant",
"add_member",
"remove_member",
"manage_groups",
"define_role",
"grant",
"revoke",
"issue_key",
"revoke_key",
"restrict",
"manage_integrations",
"read_audit",
"grant_operator",
)
)
grantr's own catalog: the actions that change access and switch integrations, and read its audit log.
MEMBER
module-attribute
¶
MEMBER: Final = 'member'
The role that makes a user or service principal a member of a tenant, and lets them list what in it they may open.
OPERATOR
module-attribute
¶
grantr's actions the deployment's operators hold, and no one else.
AuditEvent
¶
AuditEvent = Annotated[
TenantCreated
| DeploymentTenantCreated
| TenantDeleted
| MemberAdded
| MemberRemoved
| GroupDefined
| GroupDeleted
| RoleDefined
| RoleDeleted
| Granted
| Revoked
| KeyIssued
| KeyRevoked
| Restricted
| Unrestricted
| IntegrationSet,
Field(discriminator=type),
]
A change to access, or to an integration's setting.
AuditType
¶
AuditType = Literal[
"tenant_created",
"deployment_tenant_created",
"tenant_deleted",
"member_added",
"member_removed",
"group_defined",
"group_deleted",
"role_defined",
"role_deleted",
"granted",
"revoked",
"key_issued",
"key_revoked",
"restricted",
"unrestricted",
"integration_set",
]
An audit event's type: what a read of the log may keep.
PrincipalId
¶
PrincipalId = Annotated[
str, StringConstraints(pattern=pattern)
]
A service principal: not empty, and with no :, ~ or whitespace. The ids the
libraries' own actors take hold a :, as relayr's code rules' do (reflexr:<rule>), so no
principal a tenant makes, and no key it issues, can pass for one (grantr ADR-0028).
TenantId
¶
TenantId = Annotated[
str, StringConstraints(pattern=pattern)
]
The isolation boundary, as the libraries name it: not empty, and with no ~, which
grantr's storage keeps for the deployment.
ListableKind
¶
ListableKind = Literal[
"workspace", "thread", "artifact", "rule"
]
The kinds grantr lists: those in a tenant that grants name.
Level
¶
Level = Literal["deployment", "tenant", "workspace", "item"]
Where a role is granted: a thread, an artifact and a rule are each an item.
Grantee
¶
Grantee = Annotated[
User | ServicePrincipal | RulePrincipal | Group,
Field(discriminator=kind),
]
Who a grant names.
Member
¶
Member = Annotated[
User | ServicePrincipal, Field(discriminator=kind)
]
Who belongs to a tenant, and to its groups.
Subject
¶
Subject = Annotated[
User | ServicePrincipal | RulePrincipal | Agent,
Field(discriminator=kind),
]
Who a check is asked about.
AuditEntry
pydantic-model
¶
AuditFilter
dataclass
¶
Which entries a read of the log keeps: of some types, by one subject, or every one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
types
|
Set[AuditType]
|
The event types to keep; every type when empty. |
frozenset()
|
by
|
Subject | None
|
The subject whose changes to keep, such as a user or a rule; anyone's when None. |
None
|
DeploymentTenantCreated
pydantic-model
¶
Bases: BaseModel
A tenant was created: the deployment's record of it, beside the tenant's own.
Its entry is in no tenant's log, the deployment's, and names who created the tenant, so an operator who isn't a member of the tenant it created is seen to have created it. The owner is a user's id, never an email.
Config:
frozen:True
Fields:
Granted
pydantic-model
¶
GroupDefined
pydantic-model
¶
GroupDeleted
pydantic-model
¶
IntegrationSet
pydantic-model
¶
Bases: BaseModel
An integration was turned on or off, on the tenant or in a workspace, or unset.
Config:
frozen:True
Fields:
workspace
pydantic-field
¶
workspace: WorkspaceId | None
The workspace it was set in; None for the tenant.
KeyIssued
pydantic-model
¶
KeyRevoked
pydantic-model
¶
Bases: BaseModel
A key was revoked.
Config:
frozen:True
Fields:
-
type(Literal['key_revoked']) -
principal(ServicePrincipal) -
key(str)
MemberAdded
pydantic-model
¶
MemberRemoved
pydantic-model
¶
Restricted
pydantic-model
¶
Revoked
pydantic-model
¶
RoleDefined
pydantic-model
¶
RoleDeleted
pydantic-model
¶
TenantCreated
pydantic-model
¶
TenantDeleted
pydantic-model
¶
Unrestricted
pydantic-model
¶
Forbidden
¶
Bases: GrantrError
The actor may not do this: it lacks the action, or holds less than it would give.
Invalid
¶
Bases: GrantrError
The command can't apply: an unknown tenant, role, group or key, or a duplicate.
Grant
pydantic-model
¶
Bases: BaseModel
A role, on a resource, to a grantee: in a tenant, or on the deployment.
Config:
frozen:True
Fields:
Validators:
-
_only_the_deployment_is_in_no_tenant
Membership
pydantic-model
¶
Visibility
pydantic-model
¶
IntegrationSetting
pydantic-model
¶
IntegrationState
pydantic-model
¶
Bases: BaseModel
An integration as an admin reads it: what it is, each level's setting, and the outcome.
Config:
frozen:True
Fields:
-
key(str) -
description(str) -
default(bool) -
installed(bool) -
tenant(IntegrationSetting | None) -
workspace(IntegrationSetting | None) -
enabled(bool)
installed
pydantic-field
¶
installed: bool
Whether the deployment installs it: if not, it is off everywhere.
workspace
pydantic-field
¶
workspace: IntegrationSetting | None
The workspace's setting; None if unset, and on the tenant.
enabled
pydantic-field
¶
enabled: bool
Whether it is on in the workspace; on the tenant, in a workspace that sets nothing.
of
classmethod
¶
of(
key: str,
*,
description: str,
default: bool,
installed: bool,
tenant: IntegrationSetting | None,
workspace: IntegrationSetting | None = None,
) -> Self
The state of an integration, with whether it is on worked out from its levels.
Key
pydantic-model
¶
Resource
pydantic-model
¶
Bases: BaseModel
A resource, by its kind, its workspace if it is in one, and its id if it is an item.
Config:
frozen:True
Fields:
-
kind(ResourceKind) -
workspace(WorkspaceId | None) -
id(str | None)
Validators:
-
_fields_match_the_kind
BuiltInRole
pydantic-model
¶
Catalog
dataclass
¶
Every action the application handed grantr, and the built-in roles over them.
built_in
instance-attribute
¶
Each built-in role's actions, by the level it is granted at and its name.
of
classmethod
¶
The catalog of the libraries' catalogs and grantr's.
Raises:
| Type | Description |
|---|---|
ValueError
|
If an action isn't |
view
¶
view() -> CatalogView
The catalog as a tenant's admins read it.
Each built-in role granted within a tenant comes in the order of where it is granted, with its kinds, then custom roles' actions and kinds.
role
¶
A role's actions, granted on a resource of this kind; None if it can't be there.
CatalogView
pydantic-model
¶
Bases: BaseModel
What a tenant's roles may be: the built-in ones, and what a custom one may hold.
It has each built-in role and where it is granted, the actions a custom role may hold, and where one may be granted. The deployment's roles, an operator's and the application's, are granted apart from any tenant, and aren't in it.
Config:
frozen:True
Fields:
-
roles(tuple[BuiltInRole, ...]) -
custom_actions(tuple[str, ...]) -
custom_kinds(tuple[ResourceKind, ...])
Role
pydantic-model
¶
Agent
pydantic-model
¶
Group
pydantic-model
¶
RulePrincipal
pydantic-model
¶
Bases: BaseModel
A reflexr rule, named by its workspace and rule: its runs act as it.
Config:
frozen:True
Fields:
-
kind(Literal['rule']) -
workspace(WorkspaceId) -
rule(str)
ServicePrincipal
pydantic-model
¶
Bases: BaseModel
A tenant's non-human principal: an integration, an MCP client or the application.
Config:
frozen:True
Fields:
-
kind(Literal['service_principal']) -
id(PrincipalId)
User
pydantic-model
¶
memberships
¶
memberships(
user: UserId, grants: Iterable[Grant]
) -> list[Membership]
A user's memberships, from the grants on tenants made to them directly, by tenant.
effective
¶
effective(
*,
installed: bool,
default: bool,
tenant: bool | None,
workspace: bool | None,
) -> bool
Whether an integration is on in a workspace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
installed
|
bool
|
Whether the deployment installs it. |
required |
default
|
bool
|
Whether it is on where neither the tenant nor the workspace set it. |
required |
tenant
|
bool | None
|
The tenant's setting; None if unset. |
required |
workspace
|
bool | None
|
The workspace's setting; None if unset. |
required |
grantr.grants¶
Grants over storage: checks, the libraries' hooks, and the commands that change access.
With the storage port, and its in-memory implementation.
SubjectOf
¶
SubjectOf = Callable[
[TenantId, WorkspaceId | None, A], Subject
]
Maps a library's actor, in a tenant and workspace, to the subject grantr checks.
Access
¶
Grants
¶
Grants(
storage: Storage,
*,
catalogs: Iterable[Iterable[str]] = (),
tracer_provider: TracerProvider | None = None,
meter_provider: MeterProvider | None = None,
slow_check: timedelta = SLOW_CHECK,
application: ServicePrincipal | None = None,
integrations: Mapping[str, Integration] = KNOWN,
installed: Collection[str] | None = None,
)
grantr: who may do what, and the commands that change it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
storage
|
Storage
|
Where access is kept. |
required |
catalogs
|
Iterable[Iterable[str]]
|
Each library's |
()
|
tracer_provider
|
TracerProvider | None
|
Where slow checks' spans go. Defaults to the global tracer provider. |
None
|
meter_provider
|
MeterProvider | None
|
Where the check counter goes. Defaults to the global meter provider. |
None
|
slow_check
|
timedelta
|
How long a check takes before it is recorded as a span. |
SLOW_CHECK
|
application
|
ServicePrincipal | None
|
The application's own service principal, which |
None
|
integrations
|
Mapping[str, Integration]
|
The integrations a tenant's admins switch, by key: lattice's,
|
KNOWN
|
installed
|
Collection[str] | None
|
The keys of those the deployment installs; every one if omitted. One it doesn't install is off everywhere, whatever is set. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If it installs an integration that |
check
async
¶
Whether a subject may take an action on a resource in a tenant.
hidden
async
¶
hidden(
tenant: TenantId,
subject: Subject,
workspace: WorkspaceId,
) -> Visibility
The threads and artifacts a subject may not see in a workspace.
A restricted thread or artifact is hidden from everyone but those granted a role on it, and those who may restrict it: the workspace's admins, and the tenant's owners and admins. The application's own principal sees everything.
resources
async
¶
The resources of a kind on which a subject may take an action.
Such as the workspaces a user can open. A workspace, thread, artifact or rule is known to grantr once a grant names it, or an item in it.
member
async
¶
member(tenant: TenantId, user: UserId) -> Membership | None
A user's membership of a tenant, or None if they aren't a member.
tenant_roles
async
¶
A user's or service principal's roles on a tenant, sorted; none if not a member.
workspaces
async
¶
workspaces(
tenant: TenantId, member: Member
) -> list[WorkspaceId]
The workspaces a member can open: those where it holds any library's open.
It reads the member's grants, and the tenant's custom roles if they name one, once.
memberships
async
¶
memberships(user: UserId) -> list[Membership]
Every tenant a user is a member of, with their roles in it.
authenticate
async
¶
authenticate(
key: str,
) -> tuple[TenantId, ServicePrincipal] | None
The tenant and service principal a key belongs to, or None if it isn't a valid key.
hooks
¶
The libraries' authorize and hidden hooks, for their Workspaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subject_of
|
SubjectOf[A]
|
Maps the library's actor to the subject to check, as the template does: a user to the user, an agent acting for a user to that user, and so on. |
required |
restrictions
async
¶
restrictions(
tenant: TenantId, workspace: WorkspaceId, *, by: Subject
) -> list[Resource]
The threads and artifacts restricted in a workspace.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
served_tenants
async
¶
Every tenant, for the deployment's own in-process tools, such as a startup task.
It checks no one: no surface reaches it, as none reaches provision_tenant.
Surfaces list tenants with tenants, for an operator.
tenants
async
¶
Every tenant, for an operator.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
members
async
¶
A tenant's members.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
groups
async
¶
A tenant's groups, by id, with their members.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
roles
async
¶
A tenant's custom roles.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
grants_in
async
¶
grants_in(
tenant: TenantId,
*,
by: Subject,
workspace: WorkspaceId | None = None,
) -> list[Grant]
Every grant in a tenant, or on a workspace and the items in it.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
keys
async
¶
A tenant's keys, which hold their secrets' digests and never the secrets.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
audit
async
¶
audit(
tenant: TenantId | None,
*,
by: Subject,
after_seq: int = 0,
before_seq: int | None = None,
limit: int | None = None,
last: int | None = None,
keep: AuditFilter | None = None,
) -> list[AuditEntry]
A tenant's audit log in the window after_seq < seq < before_seq, oldest first.
The deployment's with None. Of the entries keep keeps (of some types, by one
actor, or every one): the first limit, 100 with neither, or the last last. A
client pages back with last and its oldest entry's seq as before_seq, as
artifactr's log does, and catches up with its newest as after_seq: a page shorter
than it asked for holds all there is.
Raises:
| Type | Description |
|---|---|
Invalid
|
If it gives both |
Forbidden
|
If |
catalog
¶
catalog() -> CatalogView
What a tenant's roles may be: the built-in roles, and what a custom one may hold.
It is the deployment's, and anyone may read it.
first_operator
async
¶
first_operator(user: UserId) -> AuditEntry
Make a user the deployment's first operator, which no one grants.
Raises:
| Type | Description |
|---|---|
Invalid
|
If the deployment has an operator already. |
bootstrap
async
¶
bootstrap() -> list[AuditEntry]
Make the configured principal the one that holds the application role.
The application calls it as it starts; nothing else grants the role. It grants the role
to the principal Grants was made with, if it hasn't it, and revokes it from any
other, as when the application's principal is renamed, each in an audit entry made by
no one. Replicas starting together grant it once.
Returns:
| Type | Description |
|---|---|
list[AuditEntry]
|
The audit entries; none if nothing changed. |
Raises:
| Type | Description |
|---|---|
Invalid
|
If |
grant_operator
async
¶
grant_operator(user: UserId, *, by: Subject) -> AuditEntry
revoke_operator
async
¶
revoke_operator(user: UserId, *, by: Subject) -> AuditEntry
create_tenant
async
¶
create_tenant(
tenant: TenantId, *, owner: UserId, by: Subject
) -> AuditEntry
Create a tenant, with a user as its first owner.
The creation is recorded twice, in the same transaction: tenant_created in the
tenant's log, which this returns, and deployment_tenant_created in the deployment's,
which names the tenant and its owner, and by, so an operator who isn't a member of
the tenant is seen to have created it.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
Invalid
|
If the tenant exists, or its id is empty or has a |
provision_tenant
async
¶
provision_tenant(
tenant: TenantId,
*,
owner: UserId,
admins: Iterable[UserId] = (),
) -> list[AuditEntry]
Create a tenant with its owner, and give it the admins it lacks: the deployment's tools.
A tenant is created only if it is new, and each of admins who isn't yet a member is
made a member and an admin. make tenant and make migrate-tenants call it in
process; no surface reaches it, and its audit entries are made by no one. It removes no
one and changes no member's roles, so it can run again: a tenant that exists keeps its
owner, and gains the admins it lacks.
Returns:
| Type | Description |
|---|---|
list[AuditEntry]
|
The audit entries, in order: a new tenant's |
list[AuditEntry]
|
and last the deployment's |
list[AuditEntry]
|
nothing changed. |
Raises:
| Type | Description |
|---|---|
Invalid
|
If the tenant's id is empty or has a |
delete_tenant
async
¶
delete_tenant(
tenant: TenantId, *, by: Subject
) -> AuditEntry
Delete a tenant, and everything grantr holds for it but its audit log.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
add_member
async
¶
add_member(
tenant: TenantId, member: Member, *, by: Subject
) -> AuditEntry
remove_member
async
¶
remove_member(
tenant: TenantId, member: Member, *, by: Subject
) -> AuditEntry
define_group
async
¶
define_group(
tenant: TenantId,
group: Group,
members: frozenset[Member],
*,
by: Subject,
) -> AuditEntry
Create a group, or replace its members.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
Invalid
|
If one of the members isn't a member of it, or the change would leave the
tenant with no member holding |
delete_group
async
¶
delete_group(
tenant: TenantId, group: Group, *, by: Subject
) -> AuditEntry
define_role
async
¶
define_role(
tenant: TenantId, role: Role, *, by: Subject
) -> AuditEntry
delete_role
async
¶
delete_role(
tenant: TenantId, name: str, *, by: Subject
) -> AuditEntry
grant
async
¶
Grant a role on a resource in a tenant.
An operator may grant themselves any role in a tenant, which makes them a member of it if they aren't one; each step is in the tenant's audit log.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
Invalid
|
If there is no such group, the role can't be granted there, the grantee
isn't a member, or the grant exists; or, for an operator, no such tenant.
|
revoke
async
¶
Revoke a grant. Anyone may give up a grant of their own.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
Invalid
|
If there is no such grant, or it leaves no member holding |
restrict
async
¶
restrict(
tenant: TenantId,
on: Resource,
*,
restricted: bool = True,
by: Subject,
) -> AuditEntry
Restrict a thread or artifact to those granted a role on it, or lift its restriction.
Its workspace's admins, and the tenant's owners and admins, see it either way.
Raises:
| Type | Description |
|---|---|
Forbidden
|
If |
Invalid
|
If it isn't a thread or artifact, or is restricted, or not, already. |
issue_key
async
¶
issue_key(
tenant: TenantId,
principal: ServicePrincipal,
*,
by: Subject,
) -> tuple[str, AuditEntry]
revoke_key
async
¶
revoke_key(
tenant: TenantId, key: str, *, by: Subject
) -> AuditEntry
is_enabled
async
¶
Whether an integration is on in a tenant's workspace: the application's port.
It checks no one, as hidden doesn't: the package that asks it is about to act
for a caller it checks itself. A key the deployment doesn't install, or that it doesn't
know, is off, and so is every key while the store can't be read. Grants is
lattice-core's Integrations, and a bound is_enabled is relayr's Enabled.
An id that names no tenant, or an empty one that names no workspace, has everything off.
integrations_enabled
async
¶
integrations
async
¶
integrations(
tenant: TenantId,
*,
workspace: WorkspaceId | None = None,
by: Subject,
) -> list[IntegrationState]
set_integration
async
¶
set_integration(
tenant: TenantId,
key: str,
on: bool | None,
*,
workspace: WorkspaceId | None = None,
by: Subject,
) -> IntegrationState
Turn an integration on or off on the tenant or in a workspace, or unset it with None.
An unset level inherits the one above. Only a change is recorded: setting what is set already, or unsetting what isn't, writes and records nothing, and returns the state as it is. An integration the deployment doesn't install may be set, and stays off.
Raises:
| Type | Description |
|---|---|
Invalid
|
If no integration has the key, or the workspace's id is empty. |
Forbidden
|
If |
Hooks
¶
InMemoryStorage
¶
Bases: _Reads
Storage in this process's memory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
clock
|
Clock
|
Returns the time each audit entry is stamped with. |
_utc_now
|
transaction
async
¶
transaction(
tenant: TenantId | None,
) -> AsyncGenerator[Transaction]
Begin a change; one at a time, whatever the tenant.
tenant_grants
async
¶
The grants on tenants made to a user directly, in every tenant.
audit
async
¶
audit(
tenant: TenantId | None,
*,
after_seq: int = 0,
before_seq: int | None = None,
limit: int | None = None,
last: int | None = None,
keep: AuditFilter | None = None,
) -> list[AuditEntry]
A tenant's audit entries in the window after_seq < seq < before_seq, in order.
The deployment's with None. Of those keep keeps: the first limit, the last
last, still oldest first, or every one with neither.
Reads
¶
Bases: Protocol
What a decision reads: in a transaction, or outside one.
grants
async
¶
grants(
tenant: TenantId | None,
to: Grantee,
*,
on: Collection[Resource] | None = None,
) -> list[Grant]
The grants that apply to a grantee: made to it, or to a group it is in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
TenantId | None
|
The tenant, or |
required |
to
|
Grantee
|
The grantee. A group is in no group. |
required |
on
|
Collection[Resource] | None
|
Only the grants on these resources; |
None
|
granted
async
¶
Every grant on a resource, to anyone, as it was made; only of one role, if given.
grants_in
async
¶
grants_in(
tenant: TenantId,
*,
workspace: WorkspaceId | None = None,
) -> list[Grant]
Every grant in a tenant, or only those on a workspace and the items in it.
group
async
¶
A group's members, or None if there is no such group.
groups
async
¶
A tenant's groups, by id, with their members.
resources
async
¶
resources(
tenant: TenantId, kind: ListableKind
) -> list[Resource]
The workspaces, threads, artifacts or rules grantr knows in the tenant.
Those a grant names, or is in; and the threads and artifacts ever restricted, and their workspaces.
restricted
async
¶
restricted(
tenant: TenantId, workspace: WorkspaceId | None = None
) -> list[Resource]
The threads and artifacts restricted in a workspace, or in the whole tenant.
is_restricted
async
¶
Whether a thread or artifact is restricted.
integrations
async
¶
integrations(
tenant: TenantId, workspace: WorkspaceId | None = None
) -> dict[str, IntegrationSetting]
The integrations set on a tenant, with None, or in one of its workspaces, by key.
Storage
¶
Persistence for grantr: reads, transactions, and the reads that span tenants.
transaction
¶
transaction(
tenant: TenantId | None,
) -> AbstractAsyncContextManager[Transaction]
Begin a change to one tenant, or to the deployment's operators with None.
tenant_grants
async
¶
The grants on tenants made to a user directly, in every tenant.
audit
async
¶
audit(
tenant: TenantId | None,
*,
after_seq: int = 0,
before_seq: int | None = None,
limit: int | None = None,
last: int | None = None,
keep: AuditFilter | None = None,
) -> list[AuditEntry]
A tenant's audit entries in the window after_seq < seq < before_seq, in order.
The deployment's with None. Of those keep keeps: the first limit, the last
last, still oldest first, or every one with neither.
Transaction
¶
One atomic change to access, serialized with every other in its tenant.
It commits when its block exits normally, and leaves nothing behind when it raises.
delete_tenant
async
¶
delete_tenant(tenant: TenantId) -> None
Remove a tenant and everything in it, but its audit log.
remove_member
async
¶
Remove a member's grants, its places in groups, and its keys.
put_group
async
¶
Create a group, or replace its members.
remove_group
async
¶
Remove a group and its grants.
put_role
async
¶
Create a custom role, or replace its actions.
remove_role
async
¶
Remove a custom role and its grants.
set_restricted
async
¶
Restrict a thread or artifact, or lift its restriction.
set_integration
async
¶
set_integration(
tenant: TenantId,
workspace: WorkspaceId | None,
key: str,
setting: IntegrationSetting | None,
) -> None
Set an integration on a tenant, with None, or in a workspace; unset it with None.
remove_key
async
¶
Remove one of the tenant's keys, and return it; None if it has no such key.
append
async
¶
append(
tenant: TenantId | None,
by: Subject | None,
event: AuditEvent,
) -> AuditEntry
Append an event to the audit log, with the next sequence number and the time.
grantr.sql¶
SQL storage: grantr in PostgreSQL or SQLite, through SQLAlchemy 2's asyncio extension.
Install grantr[postgres] (asyncpg) or grantr[sqlite] (aiosqlite). Upgrade the database
with migrate, then give Grants a SqlStorage::
engine = create_async_engine("postgresql+asyncpg://localhost/app")
await migrate(engine)
grants = Grants(SqlStorage(engine), catalogs=[artifactr.ACTIONS, reflexr.ACTIONS])
For SQLite, create the engine with create_sqlite_engine. create_schema creates
the tables without migrations, for tests and prototypes.
SqlStorage
¶
SqlStorage(
engine: AsyncEngine,
*,
clock: Clock = _utc_now,
row_level_security: bool = False,
across_tenants: AsyncEngine | None = None,
)
Storage in a SQL database, through SQLAlchemy's asyncio extension.
Create the tables with migrate first. The same code runs on
PostgreSQL (with its default READ COMMITTED isolation) and on SQLite, whose engines
must come from create_sqlite_engine.
With row_level_security, each transaction tells PostgreSQL its tenant, and the
policies the migrations create show it only that tenant's rows, and the deployment's
(grantr.sql.tenancy). The three reads that span tenants, ACROSS_TENANTS, go
to across_tenants instead: an engine whose role the policies don't hold, such as the
tables' owner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
AsyncEngine
|
The database to use. The storage does not dispose of it. |
required |
clock
|
Clock
|
Returns the time each audit entry is stamped with. Defaults to the system clock in UTC. |
_utc_now
|
row_level_security
|
bool
|
Whether each transaction names its tenant to PostgreSQL. |
False
|
across_tenants
|
AsyncEngine | None
|
The engine for the reads that span tenants; required with
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
transaction
async
¶
transaction(
tenant: TenantId | None,
) -> AsyncGenerator[_Transaction]
Begin a change; it holds the tenant's row's lock until it ends.
A change to the deployment also takes the deployment log's lock, after it, as a tenant's creation does when it adds its entry there: the lock order is always the tenant's, then the log's.
Closing the session rolls back whatever it did not commit, and returns its connection.
grants
async
¶
grants(
tenant: TenantId | None,
to: Grantee,
*,
on: Collection[Resource] | None = None,
) -> list[Grant]
The grants that apply to a grantee: made to it, or to a group it is in.
granted
async
¶
Every grant on a resource, to anyone, as it was made; only of one role, if given.
grants_in
async
¶
grants_in(
tenant: TenantId,
*,
workspace: WorkspaceId | None = None,
) -> list[Grant]
Every grant in a tenant, or only those on a workspace and the items in it.
group
async
¶
A group's members, or None if there is no such group.
groups
async
¶
A tenant's groups, by id, with their members.
restricted
async
¶
restricted(
tenant: TenantId, workspace: WorkspaceId | None = None
) -> list[Resource]
The threads and artifacts restricted in a workspace, or in the whole tenant.
is_restricted
async
¶
Whether a thread or artifact is restricted.
integrations
async
¶
integrations(
tenant: TenantId, workspace: WorkspaceId | None = None
) -> dict[str, IntegrationSetting]
The integrations set on a tenant, with None, or in one of its workspaces, by key.
resources
async
¶
resources(
tenant: TenantId, kind: ListableKind
) -> list[Resource]
The workspaces, threads, artifacts or rules a grant in the tenant names, or is in.
tenant_grants
async
¶
The grants on tenants made to a user directly, in every tenant.
audit
async
¶
audit(
tenant: TenantId | None,
*,
after_seq: int = 0,
before_seq: int | None = None,
limit: int | None = None,
last: int | None = None,
keep: AuditFilter | None = None,
) -> list[AuditEntry]
A tenant's audit entries in the window after_seq < seq < before_seq, in order.
The deployment's with None. Of those keep keeps: the first limit, the last
last, still oldest first, or every one with neither; the last are read backwards
from the window's end. A single type, or a single actor, is one index's range; a type
and an actor together are an index's range and a filter. On PostgreSQL that holds for a
window's either end; SQLite may scan the tenant's index instead.
create_schema
async
¶
migrate
async
¶
migrate(
engine: AsyncEngine,
*,
application_role: str | None = None,
) -> None
Upgrade the database to grantr's latest schema, creating the tables if needed.
Run it when the application deploys or starts, as the tables' owner. It is safe to run on every start: a database that is already up to date is left alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
AsyncEngine
|
The database, as the tables' owner. |
required |
application_role
|
str | None
|
A role that owns nothing, which the application connects as with row-level security on: it is granted grantr's tables, on PostgreSQL. It must exist. |
None
|
create_sqlite_engine
¶
Create an async SQLite engine whose transactions take turns.
Every transaction on the engine, reads included, holds the database's write lock, so
transactions run one at a time, on the engine's one connection. A transaction waits for the
connection for up to the engine's pool_timeout (30 seconds), and for another process's
lock for up to the timeout connect argument of Python's sqlite3 (5 seconds unless
connect_args says otherwise). Use a database file (or a shared-cache memory database).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str | URL
|
A SQLite URL for an async driver, such as |
required |
**kwargs
|
Any
|
Passed to SQLAlchemy's |
{}
|
Returns:
| Type | Description |
|---|---|
AsyncEngine
|
The engine. Dispose of it when you are done. |
grantr.sql.tenancy¶
Row-level security: each transaction tells PostgreSQL its tenant (lattice RFC-0001).
With the option on, SqlStorage opens its sessions as
TenantSession, which sets SETTING to its tenant, for the transaction only, as
the transaction begins. The policies the migrations create on PostgreSQL then show each
transaction only its tenant's rows, and the deployment's. A pooled connection never carries a
tenant into its next transaction, and a session that names no tenant can't begin one.
lattice-core holds the tenant session, the policy and the grants, which every library's SQL storage shares (lattice ADR-0018); this module re-exports them.
CURRENT
module-attribute
¶
CURRENT: Final = (
f"current_setting('{TENANT_SETTING}', true)"
)
The transaction's tenant, in SQL: null, or empty, when it names none, which no row's is.
TENANT
module-attribute
¶
TENANT: Final = 'tenant_id'
The key of a TenantSession's tenant in its info.
SETTING
module-attribute
¶
SETTING: Final = 'lattice.tenant_id'
The PostgreSQL setting that holds a transaction's tenant.
TenantSession
¶
Bases: Session
A session that tells PostgreSQL its tenant as each transaction begins.
It sets TENANT_SETTING to the tenant in its info. SQLite has no row-level security,
so there it sets nothing, but a session must still name its tenant.
grant
¶
grant(
connection: Connection,
role: str,
tables: Collection[str],
) -> None
Let a role use some tables as storage does, and their sequences, on PostgreSQL.
It grants the role USAGE on the current schema, reading and writing each table, and
USAGE on the sequences the tables own. Granting again changes nothing.
tenant_policy
¶
The DDL that keeps a table's rows to their tenant, on PostgreSQL.
table and column go into the DDL as they are, unquoted, unlike grant's role
and tables: pass only trusted identifiers, such as a model's own table name. The policy is
enabled, not forced, so the tables' owner isn't held by it. A transaction
sees, changes and adds only rows whose column is its tenant; one that names none, or an
empty one, sees nothing.
grantr.fastapi¶
grantr's admin API, as a FastAPI router.
Mount it at /grantr/v1 and give it the application's authentication::
async def resolve_actor(connection: HTTPConnection) -> Caller:
if key := connection.headers.get("X-Api-Key"):
if (found := await grants.authenticate(key)) is None:
raise Unauthorized("unknown key")
tenant, principal = found
return Caller(principal, tenant)
user = await verify_token(connection.headers) # the application's own sign-in
return Caller(User(id=user.sub))
app.include_router(grantr_router(grants, resolve_actor=resolve_actor), prefix="/grantr/v1")
A refusal keeps the libraries' shape, {"type": ..., "message": ...}: 401 unauthorized
without a caller, 403 forbidden, 422 invalid, and 400 tenant_required when a caller
in several tenants names none. FastAPI's own 422 for a malformed request keeps its list.
STATUS_CODES
module-attribute
¶
STATUS_CODES: dict[str, int] = {
"unauthorized": 401,
"tenant_required": 400,
"forbidden": 403,
"invalid": 422,
}
The HTTP status for each refusal type.
ResolveActor
module-attribute
¶
Authenticates a request: returns its caller, or raises Unauthorized.
Caller
¶
Bases: NamedTuple
Who is calling, as resolve_actor authenticates them.
grantr_router
¶
grantr_router(
grants: Grants, *, resolve_actor: ResolveActor
) -> APIRouter
Build the admin API, for an application to mount at /grantr/v1.
Every command is made by the caller and checked by grantr, with its own actions. A caller whose key binds it to one tenant is refused every other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grants
|
Grants
|
What the API reads and changes. |
required |
resolve_actor
|
ResolveActor
|
Authenticates each request. |
required |
grantr.telemetry¶
grantr's telemetry, through the OpenTelemetry API only.
The API is the port, as in the libraries: grantr records under the grantr scope and never
configures the SDK, so with none configured, recording is a no-op.
- Checks are counted in
grantr.checks, by action, outcome and, for a refusal, its reason. An action in no catalog is counted asunknown, so the actions are the catalogs' and the reasonsReason's, and the counter's attributes are bounded however callers ask. Tenants and subjects are never metric attributes. - A slow check is a span,
grantr.check, recorded once it is over the threshold, from its start, with the action as asked: a check that is fast leaves no span. - Every audit entry is logged, at the info level on the
grantr.auditlogger, so the stack's logs hold a copy of the audit log.
SCOPE
module-attribute
¶
SCOPE: Final = 'grantr'
The instrumentation scope of grantr's spans and metrics.
UNKNOWN
module-attribute
¶
UNKNOWN: Final = 'unknown'
The action a check of an action in no catalog is counted under.
SLOW_CHECK
module-attribute
¶
How long a check takes before it is recorded as a span, by default.
audit_log
module-attribute
¶
Where each audit entry is logged.
Reason
¶
Reason = Literal[
"unknown_action", "no_grant", "store_unreadable"
]
Why a check was refused.
Telemetry
¶
Telemetry(
*,
tracer_provider: TracerProvider | None = None,
meter_provider: MeterProvider | None = None,
slow_check: timedelta = SLOW_CHECK,
)
The tracer and meter Grants records with.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tracer_provider
|
TracerProvider | None
|
Where spans go. Defaults to the global tracer provider. |
None
|
meter_provider
|
MeterProvider | None
|
Where metrics go. Defaults to the global meter provider. |
None
|
slow_check
|
timedelta
|
How long a check takes before it is recorded as a span. |
SLOW_CHECK
|
checked
¶
Count a check, and record it as a span if it was slow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
TenantId
|
The check's tenant, which only the span carries. |
required |
action
|
str
|
The action checked. |
required |
refused
|
Reason | None
|
Why it was refused, or None if it was allowed. |
required |
start
|
int
|
When it started, in nanoseconds since the epoch. |
required |
end
|
int
|
When it ended, in the same clock. |
required |
grantr.testing¶
What every role holds, as a table an application commits.
An application prints matrix over the catalogs it hands Grants
and its custom roles, commits the table, and a test compares the two. A library's new action, or
a change to a role, then changes the table, and the change is reviewed with the code.
matrix
¶
Every role against every action in the catalogs, as a Markdown table.
A column is a built-in role where it is granted, then each custom role. A built-in role granted on items holds there what it holds on their workspace, so it has one column.
Raises:
| Type | Description |
|---|---|
Invalid
|
If a custom role isn't valid against the catalogs. |