@david.uhlir/permissions-guard
v0.0.14
Published
Permissions guard using async context
Maintainers
Readme
Permissions Guard
PermissionsGuard is a utility for managing and enforcing permission rules in an application. It provides a flexible way to define, check, and enforce permissions using AsyncLocalStorage to maintain context for the current execution flow. This package is ideal for applications that require fine-grained access control.
Features
- Define and enforce permission rules.
- Run code within a permission context.
- Validate ownership of entities.
- Decorators for enforcing permissions on methods.
- Flexible rule matching with wildcard and hierarchical path support.
Permission Rules
Permission rules are defined as hierarchical paths, similar to file system paths. This allows for fine-grained control over permissions. For example:
*: Matches any single segment of a rule (e.g.,entity/*matchesentity/readbut notentity/sub/read).**: Matches zero or more segments of a rule (e.g.,entity/**matchesentity/read,entity/sub/read, andentity)./: Matches only when no specific permissions are required (disabled for security reasons in most cases).entity: Matches only the exactentityrule.entity/*: Matches any single action underentity, such asentity/readorentity/write.entity/**: Matches all actions and sub-actions underentity, such asentity/read,entity/write, orentity/sub/action.
Wildcard Support
Wildcard rules allow for flexible matching:
*: Matches exactly one segment of a rule.**: Matches zero or more segments of a rule.
Examples
const rules = ['entity/*', 'entity/**', 'admin/**']
await PermissionsGuard.runWithPermissions(rules, 'user123', async () => {
await PermissionsGuard.checkRequiredPermissions(['entity/read']) // Matches 'entity/*'
await PermissionsGuard.checkRequiredPermissions(['entity/sub/read']) // Matches 'entity/**'
await PermissionsGuard.checkRequiredPermissions(['admin/manage']) // Matches 'admin/**'
try {
await PermissionsGuard.checkRequiredPermissions(['otherEntity/action']) // Does NOT match
} catch (err) {
console.error('Permission denied for otherEntity/action') // Expected behavior
}
})Installation
Install the package using npm or yarn:
npm install @david.uhlir/permissions-guard
# or
yarn add @david.uhlir/permissions-guardUsage
Basic Example
import { PermissionsGuard } from '@david.uhlir/permissions-guard'
// Define permission rules
const rules = ['entity/read', 'entity/write']
const owner = 'user123'
// Run a function within a permission context
await PermissionsGuard.runWithPermissions(rules, owner, async () => {
// Check if the current context has the required permissions
await PermissionsGuard.checkRequiredPermissions(['entity/read'])
// Check if the current context's owner matches a specific owner
await PermissionsGuard.checkRequiredOwner('user123')
console.log('Permissions validated successfully!')
})Using the @PermissionRequired Decorator
import { PermissionsGuard } from '@david.uhlir/permissions-guard'
class ExampleService {
@PermissionsGuard.PermissionRequired(['entity/write'])
async updateEntity() {
console.log('Entity updated!')
}
}
// Run the method within a permission context
await PermissionsGuard.runWithPermissions(['entity/write'], 'user123', async () => {
const service = new ExampleService()
await service.updateEntity() // This will succeed
})Rule Matching with Wildcards and Hierarchical Paths
Permission rules support wildcards (*) and hierarchical paths for flexible matching:
const rules = ['entity/*'] // Grants all permissions under "entity"
await PermissionsGuard.runWithPermissions(rules, 'user123', async () => {
await PermissionsGuard.checkRequiredPermissions(['entity/read']) // Matches
await PermissionsGuard.checkRequiredPermissions(['entity/write']) // Matches
try {
await PermissionsGuard.checkRequiredPermissions(['otherEntity/action']) // Does NOT match
} catch (err) {
console.error('Permission denied for otherEntity/action') // Expected behavior
}
})Using rule parameters
Permissions can also use context variables. Its useful for checking ownership of entities.
Example:
// Define permission rules
const ownerPermissions = ['entity/read', 'entity/:ownedEntities/read']
const owner = 'user123'
const ownedEntities = [
'entity1',
'entity2',
]
// Run a function within a permission context
await PermissionsGuard.runWithPermissions(ownerPermissions, owner, async () => {
const readingId = 'entity1'
await PermissionsGuard.checkRequiredPermissions([`entity/${readingId}/read`])
}, { ownedEntities } )API Documentation
PermissionsGuard
PermissionRequired(required: PermissionRule[])
A decorator to enforce required permissions on a method.
- Parameters:
required(PermissionRule[]): Array of required permission rules.
- Usage:
@PermissionsGuard.PermissionRequired(['entity/write']) async updateEntity() { // ... }
runWithPermissions<T>(rules: PermissionRule[], owner: OwnerType, callback: () => Promise<T>, variables: Record<string, string[]> = {}, inheritRule?: boolean): Promise<T>
Runs a callback within a permission context.
- Parameters:
rules(PermissionRule[]): Array of permission rules to set in the context.owner(OwnerType): Owner of the context.callback(Function): Callback function to execute within the context.variablesContext variables that can be used in rulesinheritRuleAllows nested rules to extend actual context
- Returns: The result of the callback function.
runWithPermissionsBypassOwner<T>(rules: PermissionRule[], callback: () => Promise<T>, variables: Record<string, string[]> = {}): Promise<T>
Runs a callback within an administrative permission context. This allows access to entities or actions that are typically restricted to administrators.
Parameters:
rules(PermissionRule[]): Array of permission rules to set in the context.callback(Function): Callback function to execute within the administrative context.variablesContext variables that can be used in rules
Returns: The result of the callback function.
Example:
const adminRules = ['entity/read', 'entity/write', 'admin/manage'] await PermissionsGuard.runWithPermissionsBypassOwner(adminRules, async () => { // Admin can access restricted resources await PermissionsGuard.checkRequiredPermissions(['admin/manage']) await PermissionsGuard.checkRequiredOwner('user456') // Admin can access entities owned by others })
getContext(): PermissionsGuardContextMetadata<OwnerType> | undefined
Retrieves the current permission context, including both the rules and the owner.
Returns: An object containing the current context's permission rules and owner, or
undefinedif no context exists.Example:
const context = PermissionsGuard.getContext() if (context) { console.log('Rules:', context.rules) console.log('Owner:', context.owner) } else { console.log('No context exists.') }
checkRequiredPermissions(required: PermissionRule[]): Promise<void>
Checks if the current context has the required permissions.
- Parameters:
required(PermissionRule[]): Array of required permission rules.
- Throws:
PermissionErrorif no context exists or required permissions are not matched.
checkRequiredOwner(entityOwner: OwnerType): Promise<void>
Checks if the current context's owner matches the specified entity owner.
- Parameters:
entityOwner(OwnerType): The owner of the entity to check against.
- Throws:
PermissionErrorif the owners do not match.
Error Handling
PermissionError
Thrown when a user is unauthorized or lacks the required permissions.
PermissionRuleError
Thrown when required permission rules are not matched.
- Details:
required: Array of required rules.notMatched: Array of unmatched rules.
License
This package is licensed under the MIT License.
