Security

Access Control

Attribute-Based Access Control (ABAC) for published services and entity data.

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.

PlacementEnforcement Point
Service methodBefore invocation -- the call is rejected if the policy fails
Entity decoratorAt 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

OperatorDescriptionExample
==Equalsparticipant.department == 'engineering'
!=Not equalsentity.status != 'archived'
<Less thanorder.amount < 50000
>Greater thanentity.priority > 3
<=Less than or equaltransfer.amount <= participant.transferLimit
>=Greater than or equalentity.score >= 80
containsCollection membershipparticipant.roles contains 'admin'
inValue in setentity.status in ['active', 'pending']
existsField presenceentity.approvedBy exists
likePattern matchentity.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

TypeSyntaxExample
StringSingle quotes'finance'
IntegerDigits50000
DecimalDigits with dot3.14
Booleantrue / falsetrue

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 PathResolves To
transfer.amountThe amount field of the first argument
approval.approvedThe approved field of the second argument
participant.rolesThe 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

GroupOperations Covered
allCreatesave, bulkSave
allReadfindById, findByIds, findAll, search, count, countByQuery
allUpdateupdate, bulkUpdate
allDeletedeleteById, 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 call sharePhoto
  • 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.

Copyright © 2026