@faryzal2020/v-perms 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +527 -0
- package/package.json +41 -0
- package/src/adapters/BaseAdapter.js +262 -0
- package/src/adapters/PrismaAdapter.js +476 -0
- package/src/adapters/index.js +5 -0
- package/src/core/CacheManager.js +151 -0
- package/src/core/PermissionChecker.js +203 -0
- package/src/core/PermissionManager.js +410 -0
- package/src/core/errors.js +92 -0
- package/src/index.js +73 -0
- package/src/prisma/schema.prisma +86 -0
- package/src/utils/logger.js +64 -0
- package/src/utils/wildcard.js +44 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { generateWildcardPatterns } from '../utils/wildcard.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Core permission checking logic with caching and wildcard support
|
|
5
|
+
*/
|
|
6
|
+
class PermissionChecker {
|
|
7
|
+
constructor(adapter, cacheManager, logger) {
|
|
8
|
+
this.adapter = adapter;
|
|
9
|
+
this.cache = cacheManager;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a user has a specific permission
|
|
15
|
+
* @param {string} userId
|
|
16
|
+
* @param {string} permissionKey
|
|
17
|
+
* @returns {Promise<boolean>}
|
|
18
|
+
*/
|
|
19
|
+
async checkPermission(userId, permissionKey) {
|
|
20
|
+
this.logger.debug('checkPermission:', userId, permissionKey);
|
|
21
|
+
|
|
22
|
+
// Check cache first
|
|
23
|
+
const cached = await this.cache.get('user', userId, permissionKey);
|
|
24
|
+
if (cached !== null) {
|
|
25
|
+
this.logger.debug('Cache hit:', cached);
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = await this._checkPermissionUncached(userId, permissionKey);
|
|
30
|
+
|
|
31
|
+
// Cache result
|
|
32
|
+
await this.cache.set('user', result, userId, permissionKey);
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a role has a specific permission
|
|
39
|
+
* @param {string} roleId
|
|
40
|
+
* @param {string} permissionKey
|
|
41
|
+
* @returns {Promise<boolean>}
|
|
42
|
+
*/
|
|
43
|
+
async checkRolePermission(roleId, permissionKey) {
|
|
44
|
+
this.logger.debug('checkRolePermission:', roleId, permissionKey);
|
|
45
|
+
|
|
46
|
+
// Check cache first
|
|
47
|
+
const cached = await this.cache.get('role', roleId, permissionKey);
|
|
48
|
+
if (cached !== null) {
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await this._checkRolePermissionUncached(roleId, permissionKey);
|
|
53
|
+
|
|
54
|
+
// Cache result
|
|
55
|
+
await this.cache.set('role', result, roleId, permissionKey);
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check permission without using cache
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
async _checkPermissionUncached(userId, permissionKey) {
|
|
65
|
+
// 1. Check user-specific permissions (highest priority)
|
|
66
|
+
const userPerm = await this.adapter.getUserPermission(userId, permissionKey);
|
|
67
|
+
if (userPerm !== null) {
|
|
68
|
+
this.logger.debug('User direct permission:', userPerm.granted);
|
|
69
|
+
return userPerm.granted;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check user wildcards
|
|
73
|
+
const wildcardPatterns = generateWildcardPatterns(permissionKey);
|
|
74
|
+
for (const pattern of wildcardPatterns) {
|
|
75
|
+
const userWildcard = await this.adapter.getUserPermission(userId, pattern);
|
|
76
|
+
if (userWildcard !== null) {
|
|
77
|
+
this.logger.debug('User wildcard match:', pattern, userWildcard.granted);
|
|
78
|
+
return userWildcard.granted;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Get all roles with inheritance
|
|
83
|
+
const allRoles = await this._getUserRolesWithInheritance(userId);
|
|
84
|
+
this.logger.debug('User roles (with inheritance):', allRoles.map(r => r.name));
|
|
85
|
+
|
|
86
|
+
// 3. Check each role's permissions (by priority)
|
|
87
|
+
for (const role of allRoles) {
|
|
88
|
+
const rolePerm = await this.adapter.getRolePermission(role.id, permissionKey);
|
|
89
|
+
if (rolePerm !== null) {
|
|
90
|
+
this.logger.debug('Role direct permission:', role.name, rolePerm.granted);
|
|
91
|
+
return rolePerm.granted;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check role wildcards
|
|
95
|
+
for (const pattern of wildcardPatterns) {
|
|
96
|
+
const roleWildcard = await this.adapter.getRolePermission(role.id, pattern);
|
|
97
|
+
if (roleWildcard !== null) {
|
|
98
|
+
this.logger.debug('Role wildcard match:', role.name, pattern, roleWildcard.granted);
|
|
99
|
+
return roleWildcard.granted;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Default deny
|
|
105
|
+
this.logger.debug('No permission found, default deny');
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check role permission without using cache
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
async _checkRolePermissionUncached(roleId, permissionKey) {
|
|
114
|
+
// Check direct permission
|
|
115
|
+
const rolePerm = await this.adapter.getRolePermission(roleId, permissionKey);
|
|
116
|
+
if (rolePerm !== null) {
|
|
117
|
+
return rolePerm.granted;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check wildcards
|
|
121
|
+
const wildcardPatterns = generateWildcardPatterns(permissionKey);
|
|
122
|
+
for (const pattern of wildcardPatterns) {
|
|
123
|
+
const roleWildcard = await this.adapter.getRolePermission(roleId, pattern);
|
|
124
|
+
if (roleWildcard !== null) {
|
|
125
|
+
return roleWildcard.granted;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check inherited roles
|
|
130
|
+
const inheritedRoles = await this._getRoleInheritanceRecursive(roleId);
|
|
131
|
+
for (const inheritedRole of inheritedRoles) {
|
|
132
|
+
const inheritedPerm = await this.adapter.getRolePermission(inheritedRole.id, permissionKey);
|
|
133
|
+
if (inheritedPerm !== null) {
|
|
134
|
+
return inheritedPerm.granted;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const pattern of wildcardPatterns) {
|
|
138
|
+
const inheritedWildcard = await this.adapter.getRolePermission(inheritedRole.id, pattern);
|
|
139
|
+
if (inheritedWildcard !== null) {
|
|
140
|
+
return inheritedWildcard.granted;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get all roles for a user including inherited roles
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
async _getUserRolesWithInheritance(userId) {
|
|
153
|
+
const directRoles = await this.adapter.getUserRoles(userId);
|
|
154
|
+
const allRoles = new Map();
|
|
155
|
+
|
|
156
|
+
const collectRoles = async (roleId, visited = new Set()) => {
|
|
157
|
+
if (visited.has(roleId)) return;
|
|
158
|
+
visited.add(roleId);
|
|
159
|
+
|
|
160
|
+
const role = await this.adapter.getRole(roleId);
|
|
161
|
+
if (!role) return;
|
|
162
|
+
|
|
163
|
+
allRoles.set(role.id, role);
|
|
164
|
+
|
|
165
|
+
const inheritedRoles = await this.adapter.getRoleInheritance(roleId);
|
|
166
|
+
for (const inherited of inheritedRoles) {
|
|
167
|
+
await collectRoles(inherited.inheritsFromId, visited);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const role of directRoles) {
|
|
172
|
+
await collectRoles(role.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Sort by priority (highest first)
|
|
176
|
+
return Array.from(allRoles.values()).sort((a, b) => b.priority - a.priority);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all inherited roles recursively
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
async _getRoleInheritanceRecursive(roleId, visited = new Set()) {
|
|
184
|
+
if (visited.has(roleId)) return [];
|
|
185
|
+
visited.add(roleId);
|
|
186
|
+
|
|
187
|
+
const inheritedRoles = await this.adapter.getRoleInheritance(roleId);
|
|
188
|
+
const allInherited = [];
|
|
189
|
+
|
|
190
|
+
for (const inherited of inheritedRoles) {
|
|
191
|
+
const role = await this.adapter.getRole(inherited.inheritsFromId);
|
|
192
|
+
if (role) {
|
|
193
|
+
allInherited.push(role);
|
|
194
|
+
const deeper = await this._getRoleInheritanceRecursive(inherited.inheritsFromId, visited);
|
|
195
|
+
allInherited.push(...deeper);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return allInherited;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default PermissionChecker;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RoleNotFoundError,
|
|
3
|
+
PermissionNotFoundError,
|
|
4
|
+
} from './errors.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* High-level API for managing permissions, roles, and users
|
|
8
|
+
*/
|
|
9
|
+
class PermissionManager {
|
|
10
|
+
constructor(adapter, checker, cacheManager, logger) {
|
|
11
|
+
this.adapter = adapter;
|
|
12
|
+
this.checker = checker;
|
|
13
|
+
this.cache = cacheManager;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ==================== Permission Operations ====================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a new permission
|
|
21
|
+
* @param {string} key - Permission key (e.g., 'endpoint.users.list')
|
|
22
|
+
* @param {string|null} description - Description of the permission
|
|
23
|
+
* @param {string|null} category - Category for grouping
|
|
24
|
+
* @returns {Promise<Object>}
|
|
25
|
+
*/
|
|
26
|
+
async createPermission(key, description = null, category = null) {
|
|
27
|
+
this.logger.debug('createPermission:', key, description, category);
|
|
28
|
+
return await this.adapter.createPermission({ key, description, category });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Delete a permission
|
|
33
|
+
* @param {string} permissionKey
|
|
34
|
+
* @returns {Promise<boolean>}
|
|
35
|
+
*/
|
|
36
|
+
async deletePermission(permissionKey) {
|
|
37
|
+
this.logger.debug('deletePermission:', permissionKey);
|
|
38
|
+
return await this.adapter.deletePermission(permissionKey);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List all permissions
|
|
43
|
+
* @returns {Promise<Array>}
|
|
44
|
+
*/
|
|
45
|
+
async listPermissions() {
|
|
46
|
+
return await this.adapter.listAllPermissions();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a permission by key
|
|
51
|
+
* @param {string} permissionKey
|
|
52
|
+
* @returns {Promise<Object|null>}
|
|
53
|
+
*/
|
|
54
|
+
async getPermission(permissionKey) {
|
|
55
|
+
return await this.adapter.getPermission(permissionKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ==================== Role Operations ====================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new role
|
|
62
|
+
* @param {string} name - Unique role name
|
|
63
|
+
* @param {string|null} description - Role description
|
|
64
|
+
* @param {number} priority - Role priority (higher = more important)
|
|
65
|
+
* @param {boolean} isDefault - Whether to auto-assign to new users
|
|
66
|
+
* @returns {Promise<Object>}
|
|
67
|
+
*/
|
|
68
|
+
async createRole(name, description = null, priority = 0, isDefault = false) {
|
|
69
|
+
this.logger.debug('createRole:', name, description, priority, isDefault);
|
|
70
|
+
return await this.adapter.createRole({ name, description, priority, isDefault });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete a role
|
|
75
|
+
* @param {string} roleIdOrName - Role ID or name
|
|
76
|
+
* @returns {Promise<boolean>}
|
|
77
|
+
*/
|
|
78
|
+
async deleteRole(roleIdOrName) {
|
|
79
|
+
this.logger.debug('deleteRole:', roleIdOrName);
|
|
80
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
81
|
+
if (!role) {
|
|
82
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Invalidate cache for all users with this role
|
|
86
|
+
await this.cache.invalidateRole(role.id);
|
|
87
|
+
|
|
88
|
+
return await this.adapter.deleteRole(role.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* List all roles
|
|
93
|
+
* @returns {Promise<Array>}
|
|
94
|
+
*/
|
|
95
|
+
async listRoles() {
|
|
96
|
+
return await this.adapter.listAllRoles();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a role by ID or name
|
|
101
|
+
* @param {string} roleIdOrName
|
|
102
|
+
* @returns {Promise<Object|null>}
|
|
103
|
+
*/
|
|
104
|
+
async getRole(roleIdOrName) {
|
|
105
|
+
return await this._resolveRole(roleIdOrName);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update a role
|
|
110
|
+
* @param {string} roleIdOrName
|
|
111
|
+
* @param {Object} data - Fields to update
|
|
112
|
+
* @returns {Promise<Object>}
|
|
113
|
+
*/
|
|
114
|
+
async updateRole(roleIdOrName, data) {
|
|
115
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
116
|
+
if (!role) {
|
|
117
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await this.cache.invalidateRole(role.id);
|
|
121
|
+
return await this.adapter.updateRole(role.id, data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==================== Assignment Operations ====================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Assign permission to role or user
|
|
128
|
+
* @param {string} permissionKey - Permission key (supports wildcards)
|
|
129
|
+
* @param {string} targetId - Role ID/name or User ID
|
|
130
|
+
* @param {string} targetType - 'role' or 'user'
|
|
131
|
+
* @returns {Promise<Object>}
|
|
132
|
+
*/
|
|
133
|
+
async assignPermission(permissionKey, targetId, targetType = 'role') {
|
|
134
|
+
this.logger.debug('assignPermission:', permissionKey, targetId, targetType);
|
|
135
|
+
|
|
136
|
+
if (targetType === 'role') {
|
|
137
|
+
const role = await this._resolveRole(targetId);
|
|
138
|
+
if (!role) {
|
|
139
|
+
throw new RoleNotFoundError(targetId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await this.cache.invalidateRole(role.id);
|
|
143
|
+
return await this.adapter.assignPermissionToRole(permissionKey, role.id, true);
|
|
144
|
+
} else if (targetType === 'user') {
|
|
145
|
+
await this.cache.invalidateUser(targetId);
|
|
146
|
+
return await this.adapter.assignPermissionToUser(permissionKey, targetId, true);
|
|
147
|
+
} else {
|
|
148
|
+
throw new Error(`Invalid targetType: ${targetType}. Must be 'role' or 'user'.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Ban/deny permission (set granted=false)
|
|
154
|
+
* @param {string} permissionKey
|
|
155
|
+
* @param {string} targetId - Role ID/name or User ID
|
|
156
|
+
* @param {string} targetType - 'role' or 'user'
|
|
157
|
+
* @returns {Promise<Object>}
|
|
158
|
+
*/
|
|
159
|
+
async banPermission(permissionKey, targetId, targetType = 'role') {
|
|
160
|
+
this.logger.debug('banPermission:', permissionKey, targetId, targetType);
|
|
161
|
+
|
|
162
|
+
if (targetType === 'role') {
|
|
163
|
+
const role = await this._resolveRole(targetId);
|
|
164
|
+
if (!role) {
|
|
165
|
+
throw new RoleNotFoundError(targetId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await this.cache.invalidateRole(role.id);
|
|
169
|
+
return await this.adapter.assignPermissionToRole(permissionKey, role.id, false);
|
|
170
|
+
} else if (targetType === 'user') {
|
|
171
|
+
await this.cache.invalidateUser(targetId);
|
|
172
|
+
return await this.adapter.assignPermissionToUser(permissionKey, targetId, false);
|
|
173
|
+
} else {
|
|
174
|
+
throw new Error(`Invalid targetType: ${targetType}. Must be 'role' or 'user'.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Remove permission assignment
|
|
180
|
+
* @param {string} permissionKey
|
|
181
|
+
* @param {string} targetId - Role ID/name or User ID
|
|
182
|
+
* @param {string} targetType - 'role' or 'user'
|
|
183
|
+
* @returns {Promise<boolean>}
|
|
184
|
+
*/
|
|
185
|
+
async removePermission(permissionKey, targetId, targetType = 'role') {
|
|
186
|
+
this.logger.debug('removePermission:', permissionKey, targetId, targetType);
|
|
187
|
+
|
|
188
|
+
if (targetType === 'role') {
|
|
189
|
+
const role = await this._resolveRole(targetId);
|
|
190
|
+
if (!role) {
|
|
191
|
+
throw new RoleNotFoundError(targetId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await this.cache.invalidateRole(role.id);
|
|
195
|
+
return await this.adapter.removePermissionFromRole(permissionKey, role.id);
|
|
196
|
+
} else if (targetType === 'user') {
|
|
197
|
+
await this.cache.invalidateUser(targetId);
|
|
198
|
+
return await this.adapter.removePermissionFromUser(permissionKey, targetId);
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error(`Invalid targetType: ${targetType}. Must be 'role' or 'user'.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Assign role to user
|
|
206
|
+
* @param {string} roleIdOrName
|
|
207
|
+
* @param {string} userId
|
|
208
|
+
* @returns {Promise<Object>}
|
|
209
|
+
*/
|
|
210
|
+
async assignRole(roleIdOrName, userId) {
|
|
211
|
+
this.logger.debug('assignRole:', roleIdOrName, userId);
|
|
212
|
+
|
|
213
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
214
|
+
if (!role) {
|
|
215
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await this.cache.invalidateUser(userId);
|
|
219
|
+
return await this.adapter.assignRoleToUser(userId, role.id);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Remove role from user
|
|
224
|
+
* @param {string} roleIdOrName
|
|
225
|
+
* @param {string} userId
|
|
226
|
+
* @returns {Promise<boolean>}
|
|
227
|
+
*/
|
|
228
|
+
async removeRole(roleIdOrName, userId) {
|
|
229
|
+
this.logger.debug('removeRole:', roleIdOrName, userId);
|
|
230
|
+
|
|
231
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
232
|
+
if (!role) {
|
|
233
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await this.cache.invalidateUser(userId);
|
|
237
|
+
return await this.adapter.removeRoleFromUser(userId, role.id);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Set role inheritance (roleId inherits from inheritFromRoleId)
|
|
242
|
+
* @param {string} roleIdOrName
|
|
243
|
+
* @param {string} inheritFromRoleIdOrName
|
|
244
|
+
* @param {number} priority
|
|
245
|
+
* @returns {Promise<Object>}
|
|
246
|
+
*/
|
|
247
|
+
async setRoleInheritance(roleIdOrName, inheritFromRoleIdOrName, priority = 0) {
|
|
248
|
+
this.logger.debug('setRoleInheritance:', roleIdOrName, inheritFromRoleIdOrName, priority);
|
|
249
|
+
|
|
250
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
251
|
+
if (!role) {
|
|
252
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const inheritFromRole = await this._resolveRole(inheritFromRoleIdOrName);
|
|
256
|
+
if (!inheritFromRole) {
|
|
257
|
+
throw new RoleNotFoundError(inheritFromRoleIdOrName);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await this.cache.invalidateRole(role.id);
|
|
261
|
+
return await this.adapter.setRoleInheritance(role.id, inheritFromRole.id, priority);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Remove role inheritance
|
|
266
|
+
* @param {string} roleIdOrName
|
|
267
|
+
* @param {string} inheritFromRoleIdOrName
|
|
268
|
+
* @returns {Promise<boolean>}
|
|
269
|
+
*/
|
|
270
|
+
async removeRoleInheritance(roleIdOrName, inheritFromRoleIdOrName) {
|
|
271
|
+
this.logger.debug('removeRoleInheritance:', roleIdOrName, inheritFromRoleIdOrName);
|
|
272
|
+
|
|
273
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
274
|
+
if (!role) {
|
|
275
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const inheritFromRole = await this._resolveRole(inheritFromRoleIdOrName);
|
|
279
|
+
if (!inheritFromRole) {
|
|
280
|
+
throw new RoleNotFoundError(inheritFromRoleIdOrName);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await this.cache.invalidateRole(role.id);
|
|
284
|
+
return await this.adapter.removeRoleInheritance(role.id, inheritFromRole.id);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ==================== Check Operations ====================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if user/role has permission
|
|
291
|
+
* @param {string} targetId - User ID or Role ID
|
|
292
|
+
* @param {string} permissionKey
|
|
293
|
+
* @param {string} targetType - 'user' or 'role'
|
|
294
|
+
* @returns {Promise<boolean>}
|
|
295
|
+
*/
|
|
296
|
+
async checkPermission(targetId, permissionKey, targetType = 'user') {
|
|
297
|
+
if (targetType === 'user') {
|
|
298
|
+
return await this.checker.checkPermission(targetId, permissionKey);
|
|
299
|
+
} else if (targetType === 'role') {
|
|
300
|
+
return await this.checker.checkRolePermission(targetId, permissionKey);
|
|
301
|
+
} else {
|
|
302
|
+
throw new Error(`Invalid targetType: ${targetType}. Must be 'user' or 'role'.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ==================== Query Operations ====================
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get all roles assigned to a user
|
|
310
|
+
* @param {string} userId
|
|
311
|
+
* @returns {Promise<Array>}
|
|
312
|
+
*/
|
|
313
|
+
async getUserRoles(userId) {
|
|
314
|
+
return await this.adapter.getUserRoles(userId);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get all permissions for a user (direct + role-based)
|
|
319
|
+
* @param {string} userId
|
|
320
|
+
* @returns {Promise<Object>} - { direct: [], fromRoles: [] }
|
|
321
|
+
*/
|
|
322
|
+
async getUserPermissions(userId) {
|
|
323
|
+
const directPermissions = await this.adapter.getUserDirectPermissions(userId);
|
|
324
|
+
const roles = await this.adapter.getUserRoles(userId);
|
|
325
|
+
|
|
326
|
+
const rolePermissions = [];
|
|
327
|
+
for (const role of roles) {
|
|
328
|
+
const permissions = await this.adapter.getRolePermissions(role.id);
|
|
329
|
+
rolePermissions.push({
|
|
330
|
+
role: role.name,
|
|
331
|
+
roleId: role.id,
|
|
332
|
+
permissions,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
direct: directPermissions,
|
|
338
|
+
fromRoles: rolePermissions,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get all permissions assigned to a role
|
|
344
|
+
* @param {string} roleIdOrName
|
|
345
|
+
* @returns {Promise<Array>}
|
|
346
|
+
*/
|
|
347
|
+
async getRolePermissions(roleIdOrName) {
|
|
348
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
349
|
+
if (!role) {
|
|
350
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
351
|
+
}
|
|
352
|
+
return await this.adapter.getRolePermissions(role.id);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get roles that a role inherits from
|
|
357
|
+
* @param {string} roleIdOrName
|
|
358
|
+
* @returns {Promise<Array>}
|
|
359
|
+
*/
|
|
360
|
+
async getRoleInheritance(roleIdOrName) {
|
|
361
|
+
const role = await this._resolveRole(roleIdOrName);
|
|
362
|
+
if (!role) {
|
|
363
|
+
throw new RoleNotFoundError(roleIdOrName);
|
|
364
|
+
}
|
|
365
|
+
return await this.adapter.getRoleInheritance(role.id);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ==================== Cache Operations ====================
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Invalidate cache for a specific user
|
|
372
|
+
* @param {string} userId
|
|
373
|
+
*/
|
|
374
|
+
async invalidateUserCache(userId) {
|
|
375
|
+
return await this.cache.invalidateUser(userId);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Invalidate cache for a specific role
|
|
380
|
+
* @param {string} roleId
|
|
381
|
+
*/
|
|
382
|
+
async invalidateRoleCache(roleId) {
|
|
383
|
+
return await this.cache.invalidateRole(roleId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clear entire cache
|
|
388
|
+
*/
|
|
389
|
+
async clearAllCache() {
|
|
390
|
+
return await this.cache.clear();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ==================== Helper Methods ====================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Resolve role by ID or name
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
async _resolveRole(roleIdOrName) {
|
|
400
|
+
// First try as ID
|
|
401
|
+
let role = await this.adapter.getRole(roleIdOrName);
|
|
402
|
+
if (role) return role;
|
|
403
|
+
|
|
404
|
+
// Then try as name
|
|
405
|
+
role = await this.adapter.getRoleByName(roleIdOrName);
|
|
406
|
+
return role;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export default PermissionManager;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for permission-related errors
|
|
3
|
+
*/
|
|
4
|
+
class PermissionError extends Error {
|
|
5
|
+
constructor(message, code, details = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'PermissionError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when a user is not found
|
|
15
|
+
*/
|
|
16
|
+
class UserNotFoundError extends PermissionError {
|
|
17
|
+
constructor(userId) {
|
|
18
|
+
super(`User not found: ${userId}`, 'USER_NOT_FOUND', { userId });
|
|
19
|
+
this.name = 'UserNotFoundError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Error thrown when a role is not found
|
|
25
|
+
*/
|
|
26
|
+
class RoleNotFoundError extends PermissionError {
|
|
27
|
+
constructor(roleId) {
|
|
28
|
+
super(`Role not found: ${roleId}`, 'ROLE_NOT_FOUND', { roleId });
|
|
29
|
+
this.name = 'RoleNotFoundError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when a permission is not found
|
|
35
|
+
*/
|
|
36
|
+
class PermissionNotFoundError extends PermissionError {
|
|
37
|
+
constructor(permissionKey) {
|
|
38
|
+
super(`Permission not found: ${permissionKey}`, 'PERMISSION_NOT_FOUND', { permissionKey });
|
|
39
|
+
this.name = 'PermissionNotFoundError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Error thrown when a role is already assigned to a user
|
|
45
|
+
*/
|
|
46
|
+
class RoleAlreadyAssignedError extends PermissionError {
|
|
47
|
+
constructor(userId, roleId) {
|
|
48
|
+
super(`Role already assigned to user`, 'ROLE_ALREADY_ASSIGNED', { userId, roleId });
|
|
49
|
+
this.name = 'RoleAlreadyAssignedError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Error thrown when a permission already exists
|
|
55
|
+
*/
|
|
56
|
+
class PermissionAlreadyExistsError extends PermissionError {
|
|
57
|
+
constructor(permissionKey) {
|
|
58
|
+
super(`Permission already exists: ${permissionKey}`, 'PERMISSION_EXISTS', { permissionKey });
|
|
59
|
+
this.name = 'PermissionAlreadyExistsError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Error thrown when a role already exists
|
|
65
|
+
*/
|
|
66
|
+
class RoleAlreadyExistsError extends PermissionError {
|
|
67
|
+
constructor(roleName) {
|
|
68
|
+
super(`Role already exists: ${roleName}`, 'ROLE_EXISTS', { roleName });
|
|
69
|
+
this.name = 'RoleAlreadyExistsError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Error thrown when circular inheritance is detected
|
|
75
|
+
*/
|
|
76
|
+
class CircularInheritanceError extends PermissionError {
|
|
77
|
+
constructor(roleId, inheritsFromId) {
|
|
78
|
+
super(`Circular inheritance detected`, 'CIRCULAR_INHERITANCE', { roleId, inheritsFromId });
|
|
79
|
+
this.name = 'CircularInheritanceError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
PermissionError,
|
|
85
|
+
UserNotFoundError,
|
|
86
|
+
RoleNotFoundError,
|
|
87
|
+
PermissionNotFoundError,
|
|
88
|
+
RoleAlreadyAssignedError,
|
|
89
|
+
PermissionAlreadyExistsError,
|
|
90
|
+
RoleAlreadyExistsError,
|
|
91
|
+
CircularInheritanceError,
|
|
92
|
+
};
|