Access Control
Overview
Kinotic provides a unified policy expression language for controlling access to both published service methods and entity data. The same @AbacPolicy expression syntax works in both contexts — the platform routes enforcement to the appropriate layer based on where the policy is applied.
| Placement | Enforcement Point |
|---|---|
| Service method | Before invocation -- the call is rejected if the policy fails |
| Entity decorator | At the data layer -- unauthorized records are never returned |
Expression Language
Policy expressions use a simple, developer-friendly syntax with dotted attribute paths, comparison operators, and boolean logic.
Attribute Paths
Expressions reference attributes using dotted notation. The root identifier determines what the path resolves against:
participant— the authenticated caller (roles, department, limits, etc.)context— the request environment (time, IP, etc.)- Any other root — resolved against method parameter names (service methods) or the entity being accessed (entity decorators)
Operators
| Operator | Description | Example |
|---|---|---|
== | Equals | participant.department == 'engineering' |
!= | Not equals | entity.status != 'archived' |
< | Less than | order.amount < 50000 |
> | Greater than | entity.priority > 3 |
<= | Less than or equal | transfer.amount <= participant.transferLimit |
>= | Greater than or equal | entity.score >= 80 |
contains | Collection membership | participant.roles contains 'admin' |
in | Value in set | entity.status in ['active', 'pending'] |
exists | Field presence | entity.approvedBy exists |
like | Pattern match | entity.email like '*@kinotic.ai' |
Boolean Logic
Combine conditions with and, or, and not. Parentheses override default precedence. Keywords are case-insensitive.
Precedence (highest to lowest): comparisons, not, and, or
participant.roles contains 'finance' and order.amount < 50000
entity.status in ['active', 'pending'] or participant.department == entity.department
not entity.deleted == true
(participant.roles contains 'admin' or participant.roles contains 'manager')
and entity.classification != 'top-secret'
Literals
| Type | Syntax | Example |
|---|---|---|
| String | Single quotes | 'finance' |
| Integer | Digits | 50000 |
| Decimal | Digits with dot | 3.14 |
| Boolean | true / false | true |
Service Method Policies
Apply @AbacPolicy to published service methods to enforce authorization before the call reaches the service. The platform evaluates the policy against the method's arguments and the caller's identity.
import { AbacPolicy, Publish } from '@kinotic-ai/core'
@Publish('com.example')
class OrderService {
@AbacPolicy("participant.roles contains 'finance' and order.amount < 50000")
placeOrder(order: Order): void {
// Only reached if the caller has the 'finance' role
// AND the order amount is under 50,000
}
@AbacPolicy("participant.roles contains 'finance'")
@AbacPolicy("transfer.amount <= participant.transferLimit")
transferFunds(transfer: Transfer, approval: Approval): void {
// Multiple policies are combined with AND semantics —
// both must be satisfied
}
}
Parameter Name Resolution
In service method policies, non-reserved root identifiers are resolved against method parameter names. For a method like transferFunds(transfer: Transfer, approval: Approval):
| Expression Path | Resolves To |
|---|---|
transfer.amount | The amount field of the first argument |
approval.approved | The approved field of the second argument |
participant.roles | The caller's roles from the security context |
Entity Policies
Apply @AbacPolicy to entities via @EntityServiceDecorators to enforce authorization at the data layer. Unauthorized records are never returned -- the platform filters them before they reach your code.
import { Entity, AutoGeneratedId } from '@kinotic-ai/persistence'
import { EntityServiceDecorators, $AbacPolicy } from '@kinotic-ai/persistence'
@EntityServiceDecorators({
allRead: [
$AbacPolicy("entity.sharedWith contains participant.id")
],
allCreate: [
$AbacPolicy("participant.roles contains 'editor'")
],
allDelete: [
$AbacPolicy("participant.roles contains 'admin'")
]
})
@Entity()
export class Photo {
@AutoGeneratedId
id: string | null = null
title: string = ''
ownerId: string = ''
sharedWith: string[] = []
}
Operation Groups
| Group | Operations Covered |
|---|---|
allCreate | save, bulkSave |
allRead | findById, findByIds, findAll, search, count, countByQuery |
allUpdate | update, bulkUpdate |
allDelete | deleteById, deleteByQuery |
You can also target individual operations:
@EntityServiceDecorators({
findAll: [
$AbacPolicy("entity.status in ['active', 'pending']")
],
deleteById: [
$AbacPolicy("participant.roles contains 'admin'")
]
})
Dynamic Resource Sharing
Entity policies enable user-driven access control without creating dynamic policies. The policies themselves stay static — only the entity data changes.
Example: Photo sharing
The policy entity.sharedWith contains participant.id never changes. When a user shares a photo, the application updates the photo's sharedWith array. The next time anyone queries photos, the policy filters results to only include photos shared with the caller.
class PhotoService {
async sharePhoto(photoId: string, userId: string): Promise<void> {
const photo = await this.photoEntityService.findById(photoId)
photo.sharedWith.push(userId)
await this.photoEntityService.update(photo)
}
// No authorization code here — the EntityService policy handles it
async getMyPhotos(): Promise<Photo[]> {
return await this.photoEntityService.findAll({ page: 0, size: 100 })
}
}
Combining Both Layers
Service method policies and entity policies work together:
// Service-level enforcement — can this caller use this service at all?
@Publish('com.example')
class PhotoService {
@AbacPolicy("participant.roles contains 'user'")
async sharePhoto(photoId: string, userId: string): Promise<void> {
const photo = await this.photoEntityService.findById(photoId)
photo.sharedWith.push(userId)
await this.photoEntityService.update(photo)
}
}
// Persistence enforcement — which photos can this caller see?
@EntityServiceDecorators({
allRead: [
$AbacPolicy("entity.ownerId == participant.id or entity.sharedWith contains participant.id")
],
allCreate: [
$AbacPolicy("participant.roles contains 'user'")
],
allDelete: [
$AbacPolicy("entity.ownerId == participant.id")
]
})
@Entity()
export class Photo {
@AutoGeneratedId
id: string | null = null
ownerId: string = ''
sharedWith: string[] = []
}
In this example:
- The platform ensures only authenticated users with the
'user'role can callsharePhoto - The persistence layer ensures users only see photos they own or that are shared with them
- Only the photo owner can delete their photos
- The service code contains zero authorization logic — the platform handles it
Role-Based Access Control
ABAC policies can express pure role-based checks:
// Only admins
@AbacPolicy("participant.roles contains 'admin'")
// Admin or manager
@AbacPolicy("participant.roles contains 'admin' or participant.roles contains 'manager'")
// Must have both roles
@AbacPolicy("participant.roles contains 'finance' and participant.roles contains 'approver'")
RBAC is a subset of ABAC — no separate role-checking mechanism is needed.