Modern applications often require authorization logic that goes beyond simple roles or static attributes. Access frequently depends on the specific item being accessed, the runtime state, or a combination of factors. Traditional models can struggle:
The Doh Permission System offers a pragmatic, context-aware alternative designed for modularity, clarity, and developer experience. It combines the best aspects of group-based management with dynamic, context-sensitive evaluation.
Feature | Doh Permission System | RBAC (Role-Based) | ABAC (Attribute-Based) | ACLs (Access Control Lists) |
---|---|---|---|---|
Granularity | High: action:context evaluated against runtime objects. |
Low: Based on broad user roles. | High: Based on arbitrary attributes. | High: Per-user, per-object. |
Flexibility | High: Composable groups, dynamic conditions (user/context), inheritance, negation. | Low: Static roles, limited context. | High: Flexible policy language. | Moderate: Grouping helps, but lists. |
Manageability | Moderate: Group composition requires thought, but modularity helps. Pod config allows overrides. | High: Simple role definitions. | Low: Complex policy management. | Low: Can become very large. |
Context Awareness | High: Centered around runtimeContext objects and definePermissionContext functions. |
Low: Usually none or minimal. | High: Via environment/resource attributes. | Moderate: Object-focused. |
Modularity/Extensibility | Very High: Modules define own actions/contexts/base groups. Apps compose these via inheritance/Pod config. | Low: Roles are usually global. | Moderate: Policies can be modularized. | Low: Lists are typically flat. |
Developer Experience | Clear Doh.permit checks. Modules provide working permission examples to build upon, reducing complexity vs. implementing ABAC/ACLs from scratch. Implicit actions simplify definitions. |
Simple: Easy to understand roles. | Complex: Requires policy expertise. | Tedious: Managing individual entries. |
The older approach using Users.permit()
middleware lacks context awareness and is being phased out:
// ❌ DEPRECATED: Using middleware without context
Router.AddRoute('/api/articles/:id', [Users.permit('edit:article')], handler);
Instead, rely on the global authentication middleware and perform direct checks with context:
// ✅ RECOMMENDED: Global middleware + direct contextual checks
Router.AddRoute('/api/articles/:id', [], async function(data, req, res, callback) {
const article = await Articles.getById(data.id);
// Pass the actual article object as runtime context
if (!await Doh.permit(req.user, 'edit:article', article)) {
return Router.SendJSON(res, { success: false, error: 'Insufficient permissions' }, 403, callback);
}
// Continue with article editing...
});
This approach allows for fine-grained permission logic based on the actual object being accessed.
A key advantage of Doh's system is its modularity. Instead of a single, monolithic authorization engine, Doh encourages a composable approach:
'publish'
, 'edit_profile'
), contexts ('blog_post'
, 'user_profile'
), and base permission groups ('blog_author_base'
, 'profile_owner_base'
) relevant to its specific domain.'senior_editor'
, 'community_manager'
) by inheriting from these module-provided base groups.Doh.pod.Users.groups
), allowing adjustments to inheritance or permissions without modifying the original module code.This approach means developers integrating a module get a working, self-contained permission model for that feature out-of-the-box. They can use it directly or easily compose it into their broader application roles, significantly reducing the effort compared to designing complex ABAC policies or ACL structures from the ground up.
read
, edit
). Defined implicitly by their use in permission strings.user_profile
, current_user_profile
, document_owner
). Defined via Doh.definePermissionContext(name, conditionFunctionOrInheritedName)
. The conditionFunction
receives (user, runtimeContext)
and returns true
if the context applies.action:context
(e.g., 'edit:user_profile'
). Negated via ~~
prefix (e.g., '~~delete:comment'
).Doh.definePermissionGroup(name, options)
.assignable
: (Boolean, default false
) Can this group be directly assigned to a user's groups
array?condition
: (Function | String | Undefined) Determines dynamic membership.condition(user)
: Static condition based only on user properties.condition(user, context)
: Contextual condition based on user and the runtime object.'groupName'
: Inherits the condition function from another group.inherits
: (Array) Names of groups to inherit permissions from.permissions
: (Array) List of permission strings ('action:context'
or '~~action:context'
).Doh.permit()
check (e.g., the specific blogPost
object).Doh.permit()
Use Doh.permit(user, actionPairString, runtimeContext)
to check permissions.
// Can the current user (req.user) publish this specific blogPost?
if (await Doh.permit(req.user, 'publish:blog_post', blogPost)) {
// Yes
}
Evaluation Flow:
runtimeContext
).'publish:blog_post'
) or a relevant wildcard ('*:*'
, 'publish:*'
, '*:blog_post'
) is negated. If yes -> DENY.(Negations take ultimate precedence).
Caution on Negations (~~
):
While negations are powerful, use them sparingly. Because they take ultimate precedence, a negated permission inherited from a base group cannot be easily overridden or granted back by a more specific group or context.
Recommendation: Prefer explicit grants. Rely on the absence of a granted permission to imply denial. Reserve negations primarily for specific, final overrides in application-level roles or configurations, rather than in base, potentially inheritable groups defined by modules.
Contexts and Groups form the core of your permission logic.
// --- Defining Contexts ---
Doh.definePermissionContext('user_profile', (user, context) => !!context?.username);
Doh.definePermissionContext('current_user_profile',
(user, context) => !!context?.username && user?.username === context.username);
Doh.definePermissionContext('document', (user, context) => !!context?.documentId);
// --- Defining Groups ---
// Static condition: Any user with a username
Doh.definePermissionGroup('authenticated', {
condition: (user) => !!user?.username,
permissions: ['read:user_profile']
});
// Contextual condition: User owns the document being checked
Doh.definePermissionGroup('document_owner', {
condition: (user, context) => context?.documentId && user?.id === context.ownerId,
permissions: ['edit:document', 'delete:document']
});
// Assignable role inheriting multiple capabilities
Doh.definePermissionGroup('editor', {
assignable: true,
inherits: ['authenticated', 'document_owner'], // Gets base read + owner rights
permissions: [
'publish:document',
]
});
Doh.pod.Users.groups
)You can define or extend group structures in pod.yaml
(or via Doh.Pod
). This is powerful for composing module-defined groups into application-specific roles.
permissions
and inherits
to existing groups.assignable
: You can define assignable: true
(or false
) for a group within the Pod configuration. If a group exists in both code and Pod, the assignable
value from the Pod takes precedence. If defined only in the Pod, it respects the assignable
value specified there (defaulting to false
if omitted).condition
: Cannot define condition
functions in Pods (as they require JavaScript code). If merging with a code-defined group, the code's condition
is retained.// Example: In pod.yaml or via Doh.Pod('my_app_perms', { Users: { groups: ... }})
Users: {
groups: {
'site_admin': {
// This Pod definition MERGES with any code definition for 'site_admin'
assignable: true, // Explicitly making it assignable via Pod
inherits: ['user_admin', 'content_admin', 'analytics_viewer'] // Adding inheritance
},
'new_pod_only_group': {
// This group only exists because of the Pod config
assignable: true, // Explicitly assignable
permissions: ['read:special_report']
}
'existing_code_group': {
// Assume 'existing_code_group' was defined in code without 'assignable: true'
assignable: true // Pod configuration makes it assignable
// It keeps its original condition (if any) and permissions from code,
// plus any permissions/inherits added here in the Pod.
}
}
}
(Remember: Managing user assignments to assignable groups within Pods is typically handled by other modules/tools like user_host
and doh poduser
).
The Doh Permission System includes powerful administrative tools to help you understand, manage, and troubleshoot your permission model:
/admin/permissions
)A comprehensive visualization and analysis tool for understanding your permission model at scale. The Permission Explorer provides:
/admin/users
)Note: Provided by the user_host
module - see doh_modules/user/user_host/README.md
for details
The User Admin Panel integrates seamlessly with the Permission System to provide:
// Ensure users have appropriate permissions to access admin tools
Doh.definePermissionContext('permission_explorer', (user, context) => {
return context && context.path === '/admin/permissions';
});
// In your route handler
Router.AddRoute('/admin/permissions', [], async function(data, req, res) {
if (!req.user) {
res.status(401).send('Authentication required');
return;
}
if (!await Doh.permit(req.user, 'view:permission_explorer', { path: '/admin/permissions' })) {
res.status(403).send('Permission denied');
return;
}
// Render the Permission Explorer
Router.SendHTML(res, 'permission_explorer_html', ['permission_explorer_page'], 'Permission Model Explorer');
});
The Permission Explorer provides REST APIs for external integrations:
// Get complete permission model data
const response = await fetch('/api/admin/permission-explorer/data');
const permissionData = await response.json();
// Simulate permission checks
const simulation = await fetch('/api/admin/permission-explorer/simulate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'john_doe',
permissionString: 'edit:document',
runtimeContext: { documentId: 123, ownerId: 456 }
})
});
// Get user's effective permissions
const userPerms = await fetch('/api/admin/permission-explorer/user-permissions/john_doe');
// Restrict admin tool access appropriately
Doh.definePermissionGroup('system_admin', {
assignable: true,
permissions: [
'view:permission_explorer',
'view:user_admin',
'manage:user_groups',
'simulate:permissions'
]
});