Skip to content

ABAC Authorization

Loquent uses Attribute-Based Access Control (ABAC) to authorize every API endpoint. Permissions are declared in Rust, serialized as JSON in the database, loaded into the Session at authentication, and checked at the handler layer via the Resource trait.

resources.rs (define_resources! macro)
→ generates: InstanceAction, InstancePermission, CollectionPermission, Resource marker
→ Permission enum serializes as "Resource:Level:Variant"
Session (server-side)
→ carries: user, organization, member_id, permissions, is_super_admin, is_owner
Resource trait (resource_trait.rs)
→ has_instance_permission() — checks access to a specific entity
→ has_collection_permission() — checks access to a collection (list/create)

Permissions serialize as colon-separated strings:

"Agent:Instance:Delete" → delete a specific agent
"Agent:Collection:List" → list the org's agents
"Contact:Instance:ViewOwn" → view contacts assigned to the session's member
"ContactNote:Collection:ListOwn" → list notes authored by the session user

The root Permission enum deserializes each string:

src/bases/auth/types/resources.rs
let perm: Permission = serde_json::from_str(r#""Agent:Instance:Delete""#).unwrap();
// → Permission::Agent(AgentPermission::Instance(AgentInstancePermission::Delete))

Invalid strings are silently skipped during session loading — the system won’t reject a login because of an unrecognized permission value.

All resources are declared in src/bases/auth/types/resources.rs using a single macro call:

define_resources! {
Agent {
Actions { View, Update, Delete }
Instance { View, Update, Delete }
Collection { List, Create }
}
ContactNote {
Actions { View, Update, Delete }
Instance { View, Update, Delete, ViewOwn, UpdateOwn, DeleteOwn }
Collection { List, ListOwn, Create }
}
Settings {
Actions { View, Update }
Instance { View, Update }
// No Collection block — Settings is a singleton per org
}
// ... 15 resources total
}

For each resource block, the macro generates:

Generated typeExample
<R>InstanceActionAgentInstanceAction::Delete
<R>InstancePermissionAgentInstancePermission::Delete
<R>CollectionPermissionAgentCollectionPermission::List
<R>PermissionAgentPermission::Instance(...) / AgentPermission::Collection(...)
<R>Resource (server only)AgentResource — implements Resource trait
ResourceInstance permissionsCollection permissions
ContactView, Update, Delete, ViewAssigned, UpdateAssigned, DeleteAssignedList, ListAssigned, Create
ContactNoteUpdate, Delete, UpdateOwn, DeleteOwnList, Create
AgentView, Update, DeleteList, Create
SettingsView, Update
RoleView, Update, DeleteList, Create
MemberView, Update, RemoveList
AnalyzerView, Update, DeleteList, Create
CallView, ViewAssigned, UpdateList, ListAssigned, MakeCall
DashboardViewOrg, ViewOwn
KnowledgeView, Update, DeleteList, Create
MessageViewList, Create, ViewAttachment, UploadAttachment
PhoneView, UpdateList, Create
TextAgentView, Update, DeleteList, Create
TaskView, Update, DeleteList, Create
PlanView, Approve, RejectList, Create
PlanTemplateView, Update, DeleteList, Create
TwilioView, UpdateList, Create
NotificationView, MarkReadList
ReportSend
AssistantUse

The Session struct is populated via the get_auth_state_service and used as an Axum extractor:

src/bases/auth/types/session_type.rs
pub struct Session {
pub id: Uuid,
pub user: User,
pub organization: Organization,
pub member_id: Uuid,
pub permissions: Vec<Permission>,
pub is_super_admin: bool,
pub is_owner: bool,
}

Permissions are loaded from the role.permissions JSONB column for all roles assigned to the member. Unknown permission strings are filtered out silently (no login failure).

Every <R>Resource struct implements the Resource trait to define instance-level authorization logic:

src/bases/auth/types/resource_trait.rs
pub trait Resource: ResourceCore {
type Instance: ResourceInstance; // carries org_id + any ownership fields
/// Custom logic: does `permission` grant `action` on `instance`?
fn check_instance(
session: &Session,
action: Self::InstanceAction,
instance: &Self::Instance,
permission: &Self::InstancePermission,
) -> bool;
/// Enforcement chain — call this from handlers.
fn has_instance_permission(
session: &Session,
action: Self::InstanceAction,
instance: &Self::Instance,
) -> bool { /* see below */ }
/// Collection check — call this from list/create handlers.
fn has_collection_permission(
session: &Session,
permission: Self::CollectionPermission,
) -> bool { /* see below */ }
}
1. is_super_admin? → allow (cross-org access)
2. instance.org_id != session.org_id? → deny (hard org-scope)
3. is_owner? → allow (full access within org)
4. iterate session.permissions:
try_from<Permission> → if matches resource, call check_instance → allow on first match
5. no match → deny (403)
1. is_super_admin || is_owner? → allow
2. session.permissions contains the required CollectionPermission? → allow
3. no match → deny (403)

Resources like ContactNote implement check_instance with ownership logic:

// Pseudocode showing the pattern for ContactNote
fn check_instance(
session: &Session,
action: ContactNoteInstanceAction,
instance: &ContactNoteInstance,
permission: &ContactNoteInstancePermission,
) -> bool {
let is_author = session.user.id == instance.author_id;
match action {
ContactNoteInstanceAction::View => match permission {
ContactNoteInstancePermission::View => true,
ContactNoteInstancePermission::ViewOwn => is_author,
_ => false,
},
ContactNoteInstanceAction::Delete => match permission {
ContactNoteInstancePermission::Delete => true,
ContactNoteInstancePermission::DeleteOwn => is_author,
_ => false,
},
// ...
}
}

A member with only ContactNote:Instance:ViewOwn can view notes they authored, but gets 403 on notes from other team members.

// In an API handler
#[get("/api/agents/{id}", session: Session)]
pub async fn get_agent_api(id: String) -> Result<AgentData, HttpError> {
let db = db_client().await.or_internal_server_error("DB error")?;
// 1. Fetch the instance first (needed for org_id)
let agent = AgentEntity::find_by_id(id)
.one(&db).await
.or_internal_server_error("query failed")?
.or_not_found("Agent")?;
// 2. Build the authorization instance
let instance = AgentInstance { org_id: agent.organization_id };
// 3. Check permission
if !AgentResource::has_instance_permission(&session, AgentInstanceAction::View, &instance) {
return Err(HttpError::forbidden("Insufficient permissions"));
}
Ok(AgentData::from(agent))
}
#[get("/api/agents", session: Session)]
pub async fn get_agents_api() -> Result<Vec<AgentData>, HttpError> {
// Check before querying
if !AgentResource::has_collection_permission(
&session,
AgentCollectionPermission::List,
) {
return Err(HttpError::forbidden("Insufficient permissions"));
}
let db = db_client().await.or_internal_server_error("DB error")?;
let agents = AgentEntity::find()
.filter(AgentColumn::OrganizationId.eq(session.organization.id))
.all(&db).await
.or_internal_server_error("query failed")?;
Ok(agents.into_iter().map(AgentData::from).collect())
}

All handlers return HttpError with proper status codes using these extension methods on Option and Result:

MethodStatus
.or_forbidden("msg")403
.or_not_found("Entity")404
.or_bad_request("msg")400
.or_internal_server_error("msg")500
  1. Declare it in src/bases/auth/types/resources.rs:
define_resources! {
// ... existing resources ...
Report {
Actions { Send }
Instance { Send }
}
}
  1. Define the instance struct and implement ResourceInstance:
pub struct ReportInstance {
pub org_id: Uuid,
}
impl ResourceInstance for ReportInstance {
fn org_id(&self) -> Uuid { self.org_id }
}
  1. Implement check_instance for ReportResource:
impl Resource for ReportResource {
type Instance = ReportInstance;
fn check_instance(
_session: &Session,
action: ReportInstanceAction,
_instance: &ReportInstance,
permission: &ReportInstancePermission,
) -> bool {
match action {
ReportInstanceAction::Send => matches!(permission, ReportInstancePermission::Send),
}
}
}
  1. Use it in handlers with ReportResource::has_instance_permission(...).
src/bases/auth/
├── types/
│ ├── resources.rs # define_resources! call — all 15 resources
│ ├── define_resources_macro.rs # macro definition
│ ├── resource_trait.rs # Resource, ResourceCore, ResourceInstance traits
│ ├── session_type.rs # Session struct + FromRequestParts impl
│ └── ...
├── services/
│ └── get_auth_state_service.rs # loads Session from cookie + DB
└── api/
└── get_session_api.rs # maps Session → ClientSession for the client