Deny Permissions
Learn how to explicitly deny permissions to override role-based grants.
Overview
Deny Permissions is a powerful feature in Fire Shield v2.2.0 that allows you to explicitly revoke specific permissions from users, overriding their role-based permissions.
Key Concepts:
- Deny > Allow: Denied permissions ALWAYS take precedence over granted permissions
- User-Specific: Denies are applied per user, not per role
- Temporary Revocation: Great for temporary access restrictions without changing roles
- Wildcard Support: Can deny entire permission groups with wildcards
Why Use Deny Permissions?
Use Cases
Temporary Restrictions
- Suspend user's write access during investigation
- Revoke specific permissions temporarily without role change
Fine-Grained Control
- Remove one permission from a role without creating a new role
- Override role permissions for specific users
Security Incidents
- Quickly revoke access when user account is compromised
- Disable dangerous permissions immediately
Feature Flags
- Disable beta features for specific users
- Roll out features gradually
Compliance
- Enforce regulatory restrictions on specific users
- Implement separation of duties
Basic Usage
Deny a Permission
import { RBAC } from '@fire-shield/core';
const rbac = new RBAC();
rbac.createRole('editor', ['post:read', 'post:write', 'post:delete']);
const user = { id: 'user-123', roles: ['editor'] };
// User has all editor permissions initially
rbac.hasPermission(user, 'post:delete'); // ✅ true
// Deny delete permission for this specific user
rbac.denyPermission('user-123', 'post:delete');
// Now they can't delete, even though their role allows it
rbac.hasPermission(user, 'post:delete'); // ❌ false
rbac.hasPermission(user, 'post:write'); // ✅ true (still works)Allow Permission (Remove Deny)
// Restore the denied permission
rbac.allowPermission('user-123', 'post:delete');
// Permission restored to role default
rbac.hasPermission(user, 'post:delete'); // ✅ trueCheck If Permission Is Denied
// Check if a specific permission is denied for a user
const isDenied = rbac.isDenied('user-123', 'post:delete');
if (isDenied) {
console.log('This permission is explicitly denied');
}Get All Denied Permissions
// Get list of all denied permissions for a user
const deniedPermissions = rbac.getDeniedPermissions('user-123');
console.log('Denied permissions:', deniedPermissions);
// ['post:delete', 'user:write']Wildcard Denies
You can use wildcards to deny entire groups of permissions.
Deny All Permissions in a Resource
const rbac = new RBAC({ enableWildcards: true });
rbac.createRole('admin', ['user:*', 'post:*']);
const user = { id: 'admin-123', roles: ['admin'] };
// Deny all user-related permissions
rbac.denyPermission('admin-123', 'user:*');
// Admin can no longer access user operations
rbac.hasPermission(user, 'user:read'); // ❌ false
rbac.hasPermission(user, 'user:write'); // ❌ false
rbac.hasPermission(user, 'user:delete'); // ❌ false
// But can still access posts
rbac.hasPermission(user, 'post:read'); // ✅ true
rbac.hasPermission(user, 'post:write'); // ✅ trueDeny Specific Action Across Resources
// Deny all delete operations
rbac.denyPermission('user-123', '*:delete');
rbac.hasPermission(user, 'post:delete'); // ❌ false
rbac.hasPermission(user, 'user:delete'); // ❌ false
rbac.hasPermission(user, 'comment:delete'); // ❌ false
// Other actions still work
rbac.hasPermission(user, 'post:write'); // ✅ true
rbac.hasPermission(user, 'user:read'); // ✅ truePriority Rules
Deny Always Wins
Denied permissions ALWAYS take precedence, regardless of how permissions are granted.
const rbac = new RBAC();
rbac.createRole('admin', ['*']); // Admin has ALL permissions
const user = {
id: 'admin-123',
roles: ['admin'],
permissions: ['super:admin'] // Even with direct permission
};
// Deny one permission
rbac.denyPermission('admin-123', 'delete:database');
// Deny overrides role AND direct permissions
rbac.hasPermission(user, 'delete:database'); // ❌ false
rbac.hasPermission(user, 'create:user'); // ✅ truePriority Order:
- 🔴 Deny (highest priority)
- 🟡 Direct User Permissions
- 🟢 Role Permissions (lowest priority)
Multiple Denies
You can deny multiple permissions for the same user:
// Deny multiple permissions
rbac.denyPermission('user-123', 'post:delete');
rbac.denyPermission('user-123', 'user:delete');
rbac.denyPermission('user-123', 'comment:delete');
// Check all denies
const denied = rbac.getDeniedPermissions('user-123');
// ['post:delete', 'user:delete', 'comment:delete']Advanced Patterns
Temporary Suspension
function suspendUserWrites(userId: string) {
// Deny all write operations
rbac.denyPermission(userId, '*:write');
rbac.denyPermission(userId, '*:delete');
rbac.denyPermission(userId, '*:create');
console.log(`User ${userId} suspended from write operations`);
}
function restoreUserAccess(userId: string) {
// Remove all denies
const denied = rbac.getDeniedPermissions(userId);
for (const permission of denied) {
rbac.allowPermission(userId, permission);
}
console.log(`User ${userId} access restored`);
}
// Use it
suspendUserWrites('suspicious-user-123');
// Later, after investigation
restoreUserAccess('suspicious-user-123');Feature Flags
class FeatureFlags {
constructor(private rbac: RBAC) {}
disableFeature(userId: string, feature: string) {
this.rbac.denyPermission(userId, `feature:${feature}`);
}
enableFeature(userId: string, feature: string) {
this.rbac.allowPermission(userId, `feature:${feature}`);
}
isFeatureEnabled(user: RBACUser, feature: string): boolean {
return this.rbac.hasPermission(user, `feature:${feature}`);
}
}
// Usage
const flags = new FeatureFlags(rbac);
// Beta features are denied by default for some users
flags.disableFeature('user-123', 'ai-assistant');
flags.disableFeature('user-123', 'advanced-analytics');
// Enable for specific users
flags.enableFeature('beta-tester-456', 'ai-assistant');Compliance Enforcement
class ComplianceManager {
private rbac: RBAC;
enforceSODPolicy(userId: string) {
// Separation of Duties: Users can't approve their own requests
if (this.userCreatesRequests(userId)) {
// Deny approval permission
this.rbac.denyPermission(userId, 'request:approve');
}
}
enforceDataResidency(userId: string, region: string) {
// GDPR: EU users can only access EU data
if (region === 'EU') {
this.rbac.denyPermission(userId, 'data:us:*');
this.rbac.denyPermission(userId, 'data:asia:*');
}
}
}Emergency Lockdown
function emergencyLockdown(userId: string, reason: string) {
// Deny ALL permissions
rbac.denyPermission(userId, '*');
// Log the event
console.log(`EMERGENCY: User ${userId} locked down. Reason: ${reason}`);
// Send alert
sendSecurityAlert({
type: 'LOCKDOWN',
userId,
reason,
timestamp: new Date()
});
}
// Usage
emergencyLockdown('compromised-user-789', 'Suspected account breach');Framework Integration
React
import { useRBAC, useDenyPermission, useIsDenied } from '@fire-shield/react';
function UserManagement() {
const { can } = useRBAC();
const denyPermission = useDenyPermission();
const isDeniedDelete = useIsDenied('user:delete');
const handleSuspendUser = (userId: string) => {
// Deny critical permissions
denyPermission('user:delete');
denyPermission('user:write');
toast.success('User suspended');
};
return (
<div>
{isDeniedDelete && (
<Alert>Delete permission is currently denied</Alert>
)}
{can('user:suspend') && (
<Button onClick={() => handleSuspendUser('user-123')}>
Suspend User
</Button>
)}
</div>
);
}Vue
<template>
<div>
<!-- Show warning if permission is denied -->
<Alert v-if="isDeniedDelete">
Delete permission is currently denied
</Alert>
<!-- Suspend user button -->
<button
v-can="'user:suspend'"
@click="suspendUser('user-123')"
>
Suspend User
</button>
</div>
</template>
<script setup>
import { useDenyPermission, useIsDenied } from '@fire-shield/vue';
const deny Permission = useDenyPermission();
const isDeniedDelete = useIsDenied('user:delete');
function suspendUser(userId) {
denyPermission('user:delete');
denyPermission('user:write');
}
</script>Express
import { createExpressRBAC } from '@fire-shield/express';
const rbacMiddleware = createExpressRBAC(rbac, {
getUser: (req) => req.user
});
// Endpoint to deny permission
app.post('/admin/deny-permission', async (req, res) => {
const { userId, permission } = req.body;
// Verify admin
if (!rbac.hasPermission(req.user, 'admin:permissions')) {
return res.status(403).json({ error: 'Forbidden' });
}
// Deny permission
rbac.denyPermission(userId, permission);
res.json({ success: true });
});
// Check denied permissions
app.get('/users/:id/denied-permissions', async (req, res) => {
const denied = rbac.getDeniedPermissions(req.params.id);
res.json({ deniedPermissions: denied });
});API Reference
denyPermission(userId, permission)
Deny a specific permission for a user.
rbac.denyPermission(
userId: string, // User ID
permission: string // Permission to deny (supports wildcards)
): voidExample:
rbac.denyPermission('user-123', 'post:delete');
rbac.denyPermission('admin-456', 'user:*');allowPermission(userId, permission)
Remove a denied permission (restore to role default).
rbac.allowPermission(
userId: string, // User ID
permission: string // Permission to allow back
): voidExample:
rbac.allowPermission('user-123', 'post:delete');isDenied(userId, permission)
Check if a specific permission is denied for a user.
rbac.isDenied(
userId: string, // User ID
permission: string // Permission to check
): booleanReturns: true if permission is explicitly denied, false otherwise
Example:
if (rbac.isDenied('user-123', 'post:delete')) {
console.log('This permission is denied');
}getDeniedPermissions(userId)
Get all denied permissions for a user.
rbac.getDeniedPermissions(
userId: string // User ID
): string[]Returns: Array of denied permission strings
Example:
const denied = rbac.getDeniedPermissions('user-123');
// ['post:delete', 'user:write']Best Practices
1. Document Denies
Always document why a permission was denied:
interface PermissionDeny {
userId: string;
permission: string;
reason: string;
deniedAt: Date;
deniedBy: string;
}
class AuditedRBAC {
private denies: PermissionDeny[] = [];
denyWithReason(
userId: string,
permission: string,
reason: string,
deniedBy: string
) {
this.rbac.denyPermission(userId, permission);
this.denies.push({
userId,
permission,
reason,
deniedAt: new Date(),
deniedBy
});
}
getDenyReason(userId: string, permission: string): string | null {
const deny = this.denies.find(d =>
d.userId === userId && d.permission === permission
);
return deny?.reason || null;
}
}2. Time-Limited Denies
Implement automatic expiration:
class TemporaryDenies {
private rbac: RBAC;
private expirations = new Map<string, Date>();
denyUntil(userId: string, permission: string, until: Date) {
this.rbac.denyPermission(userId, permission);
const key = `${userId}:${permission}`;
this.expirations.set(key, until);
// Schedule removal
const timeout = until.getTime() - Date.now();
setTimeout(() => {
this.rbac.allowPermission(userId, permission);
this.expirations.delete(key);
}, timeout);
}
}
// Usage
const temp = new TemporaryDenies(rbac);
// Deny for 24 hours
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
temp.denyUntil('user-123', 'post:delete', tomorrow);3. Notify Users
Inform users when permissions are denied:
function denyWithNotification(
userId: string,
permission: string,
reason: string
) {
rbac.denyPermission(userId, permission);
// Send notification
sendEmail({
to: getUserEmail(userId),
subject: 'Permission Revoked',
body: `Your ${permission} permission has been revoked. Reason: ${reason}`
});
}4. Bulk Operations
Provide helpers for common deny scenarios:
function suspendAllWrites(userId: string) {
const writePermissions = ['*:write', '*:delete', '*:create', '*:update'];
for (const permission of writePermissions) {
rbac.denyPermission(userId, permission);
}
}
function removeAllDenies(userId: string) {
const denied = rbac.getDeniedPermissions(userId);
for (const permission of denied) {
rbac.allowPermission(userId, permission);
}
}5. Integration with Audit Logs
Track all deny operations:
const rbac = new RBAC({
auditLogger: {
log: (event) => {
if (event.action === 'deny_permission') {
console.log('Permission denied:', event);
database.logSecurityEvent(event);
}
}
}
});Common Patterns
Role Downgrade Without Role Change
// Temporarily reduce admin to read-only without changing their role
function downgradeToReadOnly(userId: string) {
rbac.denyPermission(userId, '*:write');
rbac.denyPermission(userId, '*:delete');
rbac.denyPermission(userId, '*:create');
rbac.denyPermission(userId, '*:update');
// They keep their role but can only read
}Progressive Access
// Start with minimal access, gradually enable features
function onboardNewUser(userId: string) {
// Deny advanced features initially
rbac.denyPermission(userId, 'feature:analytics');
rbac.denyPermission(userId, 'feature:export');
rbac.denyPermission(userId, 'feature:api');
// After 30 days, enable analytics
setTimeout(() => {
rbac.allowPermission(userId, 'feature:analytics');
}, 30 * 24 * 60 * 60 * 1000);
}Context-Based Restrictions
// Deny permissions based on context
function enforceWorkHours(userId: string) {
const hour = new Date().getHours();
// Deny sensitive operations outside work hours (9 AM - 5 PM)
if (hour < 9 || hour >= 17) {
rbac.denyPermission(userId, 'financial:*');
rbac.denyPermission(userId, 'user:delete');
} else {
rbac.allowPermission(userId, 'financial:*');
rbac.allowPermission(userId, 'user:delete');
}
}Comparison with Other Approaches
vs. Removing from Role
Deny Permission:
- ✅ Temporary and reversible
- ✅ Doesn't affect role definition
- ✅ User-specific
- ✅ Faster to implement
Remove from Role:
- ❌ Changes role structure
- ❌ Affects all users with that role
- ❌ Requires role management
- ✅ More permanent solution
vs. Creating New Role
Deny Permission:
- ✅ No role proliferation
- ✅ Easy to manage
- ✅ Quick to apply/remove
New Role:
- ❌ Role explosion
- ❌ Hard to maintain
- ✅ Better for permanent changes
Next Steps
- Wildcards - Pattern matching in permissions
- Audit Logging - Track permission changes
- Role Hierarchy - Organize roles effectively
- API Reference - Complete API documentation
