Check access at runtime
Check access from Python (client.policy), JavaScript (Policy), or Refine
(accessControlProvider, 1.3.7 and later).
Checks evaluate the authenticated caller configured on the client; don't
supply a principal override in application code.
A caller with organization access (an organization owner, admin, or member, a superuser, or an API key created by one of them) is allowed every action it asks about, without evaluating any policy. To check what an app user may do, build the client from that user's session token; see Act for a signed-in user.
Prerequisites#
- Configure the JavaScript client,
Python client, or Refine integration
for the target app with an authenticated caller. The examples call the
JavaScript client
taruviand the synchronous Python clientclient. - Identify a concrete policy resource such as
datatable:orders, an existing record identifier such asorder-123, and the proposed attributes for a new record. - Choose only actions supported by that resource. For Database, use the resource-action reference.
Check what the caller can do#
Keep existing-record checks separate from create checks. For an existing record,
use its real ID and only read, update, or delete. For a proposed create,
use ID new, action create, and the attributes that will be submitted.
new:i is the batch form, where i identifies each proposed record.
new represents the proposed record and must carry the submitted attributes
that the create policy evaluates.
The full Database action inventory is read, create, update, and delete.
Do not rely on the SDK's default candidate actions because that default includes
write, which is not a Database system action.
- Place the example in a request path whose configured client represents the authenticated caller.
- Replace the resource kind, ID, attributes, and candidate actions with the values reviewed for your policy.
- Treat every error as a denial, then test with both an allowed and a denied caller before relying on the result. Use site users for both: a caller with organization access is always allowed.
- JavaScript SDK
- Python SDK
- Refine
import {Policy} from '@taruvi/sdk';
import type {Resource} from '@taruvi/sdk';
const policy = new Policy(taruvi);
const existingResource: Resource = {
kind: 'datatable:orders', id: 'order-123', attr: {status: 'active'},
};
const proposedResource: Resource = {
kind: 'datatable:orders', id: 'new', attr: {status: 'draft', total: 12500},
};
const existingActions = ['read', 'update', 'delete'];
let readAllowed = false;
let createAllowed = false;
let visible: Resource[] = [];
let allowedActions: string[] = [];
try {
const result = await policy.checkResource([
{
resource: existingResource.kind,
recordId: existingResource.id,
attributes: existingResource.attr,
actions: ['read'],
},
{
resource: proposedResource.kind,
recordId: proposedResource.id,
attributes: proposedResource.attr,
actions: ['create'],
},
]);
readAllowed = result.results?.[0]?.actions?.read === 'EFFECT_ALLOW';
createAllowed = result.results?.[1]?.actions?.create === 'EFFECT_ALLOW';
visible = readAllowed ? [existingResource] : [];
allowedActions = await policy.getAllowedActions(existingResource, {
actions: existingActions,
});
} catch {
readAllowed = false;
createAllowed = false;
visible = [];
allowedActions = [];
}
checkResource takes resource, recordId, attributes, and actions for
each check. getAllowedActions takes a resource with kind, id, and attr.
Filter your input list after the corresponding check returns EFFECT_ALLOW.
import logging
from taruvi import TaruviError
logger = logging.getLogger(__name__)
existing_resource = {
"kind": "datatable:orders",
"id": "order-123",
"attr": {"status": "active"},
}
proposed_resource = {
"kind": "datatable:orders",
"id": "new",
"attr": {"status": "draft", "total": 12500},
}
database_actions = ["read", "create", "update", "delete"]
existing_actions = [action for action in database_actions if action != "create"]
create_actions = ["create"]
read_allowed = False
create_allowed = False
visible = []
allowed_actions = []
try:
read_result = client.policy.check_resources([
{"resource": existing_resource, "actions": ["read"]}
])
read_results = read_result.get("results") or []
read_effect = (
(read_results[0].get("actions") or {}).get("read")
if read_results else None
)
# EFFECT_DENY, a missing result, or a missing effect all remain denied.
read_allowed = read_effect == "EFFECT_ALLOW"
create_result = client.policy.check_resources([
{"resource": proposed_resource, "actions": create_actions}
])
create_results = create_result.get("results") or []
create_effect = (
(create_results[0].get("actions") or {}).get("create")
if create_results else None
)
create_allowed = create_effect == "EFFECT_ALLOW"
visible = client.policy.filter_allowed([existing_resource], ["read"])
allowed_actions = client.policy.get_allowed_actions(
existing_resource, existing_actions
)
except TaruviError as error:
# Covers ValidationError, AuthenticationError, AuthorizationError,
# ServiceUnavailableError, TimeoutError, ConnectionError, and ResponseError.
read_allowed = False
create_allowed = False
visible = []
allowed_actions = []
logger.warning("Policy check failed: %s", type(error).__name__)
check_resources returns a response dictionary. Each requested action maps to
EFFECT_ALLOW or EFFECT_DENY under results[].actions. A missing result or
effect must deny. filter_allowed returns only input resources for which every
requested action is EFFECT_ALLOW. get_allowed_actions returns allowed names
from the explicit existing-record candidate set. A create decision applies only
to the proposed new resource and its submitted attributes; it is not a create
grant for order-123.
Create accessControlProvider(taruvi) once and pass it to your <Refine> app
as shown in Refine access control.
useCan returns a query result directly; a missing, loading, failed, or denied
result must not enable an action.
import {useCan} from '@refinedev/core';
const existing = {
entityType: 'datatable:orders', id: 'order-123', status: 'active',
};
const read = useCan({resource: 'orders', action: 'read', params: existing});
const update = useCan({resource: 'orders', action: 'update', params: existing});
const remove = useCan({resource: 'orders', action: 'delete', params: existing});
const create = useCan({
resource: 'orders', action: 'create',
params: {
entityType: 'datatable:orders', id: 'new', status: 'draft', total: 12500,
},
});
const readAllowed = read.isSuccess && read.data.can === true;
const createAllowed = create.isSuccess && create.data.can === true;
const allowedActions = [
...(readAllowed ? ['read'] : []),
...(update.isSuccess && update.data.can === true ? ['update'] : []),
...(remove.isSuccess && remove.data.can === true ? ['delete'] : []),
];
Put policy attributes such as status and total directly in params.
The provider uses params.id as the record ID and removes entityType and the
Refine resource definition before forwarding the remaining fields as
attributes. Refine's show and list map to read, edit to update, and
clone to create; this example uses the Database action names explicitly.
Verification#
The expected result is a caller-bound read decision for order-123, a separate
create decision for the proposed new record, a conservatively filtered
resource list, and existing-record actions drawn only from the explicit set.
On an API, service, timeout, connection, or response error, treat the request
as denied. Do not log credentials, personal attributes, raw policy definitions, or tokens.
UI checks such as Refine's CanAccess and useCan only decide what to show; TaruviBase always
enforces policies on the server.
Review Security and limits and Troubleshooting before using the decision in a production request path.