@flusys/nestjs-shared 2.0.0 → 3.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/README.md +851 -275
- package/cjs/classes/api-controller.class.js +4 -4
- package/cjs/decorators/require-permission.decorator.js +7 -3
- package/cjs/exceptions/permission.exception.js +2 -2
- package/cjs/guards/permission.guard.js +66 -65
- package/cjs/interfaces/permission.interface.js +1 -1
- package/cjs/utils/html-sanitizer.util.js +9 -9
- package/cjs/utils/request.util.js +2 -1
- package/classes/api-controller.class.d.ts +3 -3
- package/decorators/require-permission.decorator.d.ts +3 -2
- package/exceptions/permission.exception.d.ts +1 -1
- package/fesm/classes/api-controller.class.js +5 -5
- package/fesm/decorators/require-permission.decorator.js +18 -65
- package/fesm/exceptions/permission.exception.js +2 -2
- package/fesm/guards/permission.guard.js +66 -65
- package/fesm/interfaces/permission.interface.js +1 -3
- package/fesm/utils/request.util.js +2 -1
- package/guards/permission.guard.d.ts +2 -4
- package/interfaces/permission.interface.d.ts +13 -8
- package/package.json +2 -2
|
@@ -72,11 +72,11 @@ function _ts_param(paramIndex, decorator) {
|
|
|
72
72
|
// Apply PermissionGuard to check permissions from cache
|
|
73
73
|
decorators.push((0, _common.UseGuards)(_guards.JwtAuthGuard, _guards.PermissionGuard));
|
|
74
74
|
decorators.push((0, _swagger.ApiBearerAuth)());
|
|
75
|
-
// Check for complex
|
|
76
|
-
if (security.
|
|
77
|
-
decorators.push((0, _decorators.
|
|
75
|
+
// Check for complex logic first
|
|
76
|
+
if (security.logic) {
|
|
77
|
+
decorators.push((0, _decorators.RequirePermissionLogic)(security.logic));
|
|
78
78
|
} else if (security.permissions && security.permissions.length > 0) {
|
|
79
|
-
if (security.operator === '
|
|
79
|
+
if (security.operator === 'OR') {
|
|
80
80
|
decorators.push((0, _decorators.RequireAnyPermission)(...security.permissions));
|
|
81
81
|
} else {
|
|
82
82
|
// AND is default and most secure
|
|
@@ -17,16 +17,20 @@ _export(exports, {
|
|
|
17
17
|
},
|
|
18
18
|
get RequirePermissionCondition () {
|
|
19
19
|
return RequirePermissionCondition;
|
|
20
|
+
},
|
|
21
|
+
get RequirePermissionLogic () {
|
|
22
|
+
return RequirePermissionLogic;
|
|
20
23
|
}
|
|
21
24
|
});
|
|
22
25
|
const _common = require("@nestjs/common");
|
|
23
26
|
const _constants = require("../constants");
|
|
24
27
|
const RequirePermission = (...permissions)=>(0, _common.SetMetadata)(_constants.PERMISSIONS_KEY, {
|
|
25
28
|
permissions,
|
|
26
|
-
operator: '
|
|
29
|
+
operator: 'AND'
|
|
27
30
|
});
|
|
28
31
|
const RequireAnyPermission = (...permissions)=>(0, _common.SetMetadata)(_constants.PERMISSIONS_KEY, {
|
|
29
32
|
permissions,
|
|
30
|
-
operator: '
|
|
33
|
+
operator: 'OR'
|
|
31
34
|
});
|
|
32
|
-
const
|
|
35
|
+
const RequirePermissionLogic = (logic)=>(0, _common.SetMetadata)(_constants.PERMISSIONS_KEY, logic);
|
|
36
|
+
const RequirePermissionCondition = RequirePermissionLogic;
|
|
@@ -30,8 +30,8 @@ let PermissionSystemUnavailableException = class PermissionSystemUnavailableExce
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
let InsufficientPermissionsException = class InsufficientPermissionsException extends _common.ForbiddenException {
|
|
33
|
-
constructor(missingPermissions, operator = '
|
|
34
|
-
const message = operator === '
|
|
33
|
+
constructor(missingPermissions, operator = 'AND'){
|
|
34
|
+
const message = operator === 'OR' ? `Requires at least one of: ${missingPermissions.join(', ')}` : `Missing required permissions: ${missingPermissions.join(', ')}`;
|
|
35
35
|
super({
|
|
36
36
|
success: false,
|
|
37
37
|
message,
|
|
@@ -55,8 +55,6 @@ let PermissionGuard = class PermissionGuard {
|
|
|
55
55
|
context.getClass()
|
|
56
56
|
]);
|
|
57
57
|
if (!permissionConfig) return true;
|
|
58
|
-
const { permissions: requiredPermissions, operator } = this.normalizePermissionConfig(permissionConfig);
|
|
59
|
-
if (!requiredPermissions || requiredPermissions.length === 0) return true;
|
|
60
58
|
const request = context.switchToHttp().getRequest();
|
|
61
59
|
const user = request.user;
|
|
62
60
|
if (!user) throw new _common.UnauthorizedException('Authentication required');
|
|
@@ -69,90 +67,93 @@ let PermissionGuard = class PermissionGuard {
|
|
|
69
67
|
this.logger.warn(`No permissions found (userId: ${user.id})`, 'PermissionGuard');
|
|
70
68
|
throw new _permissionexception.NoPermissionsFoundException();
|
|
71
69
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
this.validateSimplePermissions(requiredPermissions, userPermissions, operator);
|
|
70
|
+
const logicNode = this.normalizeToLogicNode(permissionConfig);
|
|
71
|
+
if (!logicNode) return true;
|
|
72
|
+
const result = this.evaluateLogicNode(logicNode, userPermissions);
|
|
73
|
+
if (!result.passed) {
|
|
74
|
+
this.logger.warn(`Permission denied (userId: ${user.id})`, 'PermissionGuard');
|
|
75
|
+
throw new _permissionexception.InsufficientPermissionsException(result.missingPermissions, result.operator);
|
|
80
76
|
}
|
|
81
77
|
return true;
|
|
82
78
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
operator: config.operator || 'and'
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
validateSimplePermissions(requiredPermissions, userPermissions, operator) {
|
|
94
|
-
if (operator === 'or') {
|
|
95
|
-
const hasAny = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
96
|
-
if (!hasAny) throw new _permissionexception.InsufficientPermissionsException(requiredPermissions, 'or');
|
|
97
|
-
} else {
|
|
98
|
-
const hasAll = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
99
|
-
if (!hasAll) {
|
|
100
|
-
const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
101
|
-
throw new _permissionexception.InsufficientPermissionsException(missing, 'and');
|
|
102
|
-
}
|
|
79
|
+
normalizeToLogicNode(config) {
|
|
80
|
+
// string - single permission
|
|
81
|
+
if (typeof config === 'string') {
|
|
82
|
+
return config ? {
|
|
83
|
+
type: 'action',
|
|
84
|
+
actionId: config
|
|
85
|
+
} : null;
|
|
103
86
|
}
|
|
87
|
+
// string[] - treat as AND of multiple permissions
|
|
88
|
+
if (Array.isArray(config)) {
|
|
89
|
+
if (config.length === 0) return null;
|
|
90
|
+
if (config.length === 1) return {
|
|
91
|
+
type: 'action',
|
|
92
|
+
actionId: config[0]
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
type: 'group',
|
|
96
|
+
operator: 'AND',
|
|
97
|
+
children: config.map((p)=>({
|
|
98
|
+
type: 'action',
|
|
99
|
+
actionId: p
|
|
100
|
+
}))
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// SimplePermissionConfig - { permissions: string[], operator: 'AND'|'OR' }
|
|
104
|
+
if ('permissions' in config && Array.isArray(config.permissions)) {
|
|
105
|
+
const simple = config;
|
|
106
|
+
if (simple.permissions.length === 0) return null;
|
|
107
|
+
if (simple.permissions.length === 1) return {
|
|
108
|
+
type: 'action',
|
|
109
|
+
actionId: simple.permissions[0]
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
type: 'group',
|
|
113
|
+
operator: simple.operator || 'AND',
|
|
114
|
+
children: simple.permissions.map((p)=>({
|
|
115
|
+
type: 'action',
|
|
116
|
+
actionId: p
|
|
117
|
+
}))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ILogicNode
|
|
121
|
+
return config;
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
evaluateLogicNode(node, userPermissions) {
|
|
124
|
+
if (node.type === 'action') {
|
|
125
|
+
const passed = this.hasPermission(userPermissions, node.actionId);
|
|
126
|
+
return {
|
|
127
|
+
passed,
|
|
128
|
+
missingPermissions: passed ? [] : [
|
|
129
|
+
node.actionId
|
|
130
|
+
],
|
|
131
|
+
operator: 'AND'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Group node
|
|
135
|
+
const { operator, children } = node;
|
|
136
|
+
// SECURITY: Fail-closed - deny access when no children configured
|
|
137
|
+
if (!children || children.length === 0) {
|
|
113
138
|
return {
|
|
114
139
|
passed: false,
|
|
115
|
-
message: 'No permissions configured - access denied by default',
|
|
116
140
|
missingPermissions: [],
|
|
117
141
|
operator
|
|
118
142
|
};
|
|
119
143
|
}
|
|
120
144
|
const results = [];
|
|
121
|
-
const failureDetails = [];
|
|
122
145
|
const missingPermissions = [];
|
|
123
|
-
if (permissions.length > 0) {
|
|
124
|
-
if (operator === 'or') {
|
|
125
|
-
const hasAny = permissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
126
|
-
results.push(hasAny);
|
|
127
|
-
if (!hasAny) {
|
|
128
|
-
failureDetails.push(`needs one of: [${permissions.join(', ')}]`);
|
|
129
|
-
missingPermissions.push(...permissions);
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
const hasAll = permissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
133
|
-
results.push(hasAll);
|
|
134
|
-
if (!hasAll) {
|
|
135
|
-
const missing = permissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
136
|
-
failureDetails.push(`missing: [${missing.join(', ')}]`);
|
|
137
|
-
missingPermissions.push(...missing);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
146
|
for (const child of children){
|
|
142
|
-
const childResult = this.
|
|
147
|
+
const childResult = this.evaluateLogicNode(child, userPermissions);
|
|
143
148
|
results.push(childResult.passed);
|
|
144
149
|
if (!childResult.passed) {
|
|
145
|
-
failureDetails.push(`(${childResult.message})`);
|
|
146
150
|
missingPermissions.push(...childResult.missingPermissions);
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
|
-
|
|
150
|
-
const passed = operator === 'or' ? results.some((r)=>r) : results.every((r)=>r);
|
|
151
|
-
const message = passed ? 'OK' : `Denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
|
|
153
|
+
const passed = operator === 'OR' ? results.some((r)=>r) : results.every((r)=>r);
|
|
152
154
|
return {
|
|
153
155
|
passed,
|
|
154
|
-
|
|
155
|
-
missingPermissions,
|
|
156
|
+
missingPermissions: passed ? [] : missingPermissions,
|
|
156
157
|
operator
|
|
157
158
|
};
|
|
158
159
|
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* HTML Sanitizer Utilities
|
|
3
|
-
*
|
|
4
|
-
* Provides functions for escaping HTML content to prevent XSS attacks.
|
|
5
|
-
* Use these utilities when interpolating user-provided variables into HTML content.
|
|
6
|
-
*/ /**
|
|
7
|
-
* HTML entity mapping for escaping special characters
|
|
8
|
-
*/ "use strict";
|
|
1
|
+
"use strict";
|
|
9
2
|
Object.defineProperty(exports, "__esModule", {
|
|
10
3
|
value: true
|
|
11
4
|
});
|
|
@@ -23,7 +16,14 @@ _export(exports, {
|
|
|
23
16
|
return escapeHtmlVariables;
|
|
24
17
|
}
|
|
25
18
|
});
|
|
26
|
-
|
|
19
|
+
/**
|
|
20
|
+
* HTML Sanitizer Utilities
|
|
21
|
+
*
|
|
22
|
+
* Provides functions for escaping HTML content to prevent XSS attacks.
|
|
23
|
+
* Use these utilities when interpolating user-provided variables into HTML content.
|
|
24
|
+
*/ /**
|
|
25
|
+
* HTML entity mapping for escaping special characters
|
|
26
|
+
*/ const HTML_ESCAPE_MAP = {
|
|
27
27
|
'&': '&',
|
|
28
28
|
'<': '<',
|
|
29
29
|
'>': '>',
|
|
@@ -19,6 +19,7 @@ _export(exports, {
|
|
|
19
19
|
return parseDurationToMs;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
|
+
const _config = require("@flusys/nestjs-core/config");
|
|
22
23
|
const _constants = require("../constants");
|
|
23
24
|
/** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
|
|
24
25
|
s: 1000,
|
|
@@ -45,7 +46,7 @@ function isBrowserRequest(req) {
|
|
|
45
46
|
function buildCookieOptions(req) {
|
|
46
47
|
const hostname = req.hostname || '';
|
|
47
48
|
const origin = req.headers.origin || '';
|
|
48
|
-
const isProduction =
|
|
49
|
+
const isProduction = _config.envConfig.isProduction();
|
|
49
50
|
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
50
51
|
const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
|
|
51
52
|
let domain;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { BulkResponseDto, DeleteDto, FilterAndPaginationDto, GetByIdBodyDto, ListResponseDto, MessageResponseDto, SingleResponseDto } from '../dtos';
|
|
2
2
|
import { Identity } from '../entities';
|
|
3
|
-
import { ILoggedUserInfo,
|
|
3
|
+
import { ILoggedUserInfo, IPermissionLogic, IService } from '../interfaces';
|
|
4
4
|
import { Type } from '@nestjs/common';
|
|
5
5
|
export type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getAll' | 'update' | 'updateMany' | 'delete';
|
|
6
6
|
export type SecurityLevel = 'public' | 'jwt' | 'permission';
|
|
7
7
|
export interface EndpointSecurity {
|
|
8
8
|
level: SecurityLevel;
|
|
9
9
|
permissions?: string[];
|
|
10
|
-
operator?:
|
|
11
|
-
|
|
10
|
+
operator?: 'AND' | 'OR';
|
|
11
|
+
logic?: IPermissionLogic;
|
|
12
12
|
}
|
|
13
13
|
export type ApiSecurityConfig = {
|
|
14
14
|
[K in ApiEndpoint]?: EndpointSecurity | SecurityLevel;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IPermissionLogic } from '../interfaces/permission.interface';
|
|
2
2
|
export declare const RequirePermission: (...permissions: string[]) => import("@nestjs/common").CustomDecorator<string>;
|
|
3
3
|
export declare const RequireAnyPermission: (...permissions: string[]) => import("@nestjs/common").CustomDecorator<string>;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const RequirePermissionLogic: (logic: IPermissionLogic) => import("@nestjs/common").CustomDecorator<string>;
|
|
5
|
+
export declare const RequirePermissionCondition: (logic: IPermissionLogic) => import("@nestjs/common").CustomDecorator<string>;
|
|
@@ -3,7 +3,7 @@ export declare class PermissionSystemUnavailableException extends InternalServer
|
|
|
3
3
|
constructor(message?: string);
|
|
4
4
|
}
|
|
5
5
|
export declare class InsufficientPermissionsException extends ForbiddenException {
|
|
6
|
-
constructor(missingPermissions: string[], operator?: '
|
|
6
|
+
constructor(missingPermissions: string[], operator?: 'AND' | 'OR');
|
|
7
7
|
}
|
|
8
8
|
export declare class NoPermissionsFoundException extends ForbiddenException {
|
|
9
9
|
constructor();
|
|
@@ -25,7 +25,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { CurrentUser, Public, RequirePermission, RequireAnyPermission,
|
|
28
|
+
import { CurrentUser, Public, RequirePermission, RequireAnyPermission, RequirePermissionLogic } from '../decorators';
|
|
29
29
|
import { DeleteDto, FilterAndPaginationDto, GetByIdBodyDto } from '../dtos';
|
|
30
30
|
import { JwtAuthGuard, PermissionGuard } from '../guards';
|
|
31
31
|
import { IdempotencyInterceptor, SetCreatedByOnBody, SetDeletedByOnBody, SetUpdateByOnBody, Slug } from '../interceptors';
|
|
@@ -62,11 +62,11 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
|
|
|
62
62
|
// Apply PermissionGuard to check permissions from cache
|
|
63
63
|
decorators.push(UseGuards(JwtAuthGuard, PermissionGuard));
|
|
64
64
|
decorators.push(ApiBearerAuth());
|
|
65
|
-
// Check for complex
|
|
66
|
-
if (security.
|
|
67
|
-
decorators.push(
|
|
65
|
+
// Check for complex logic first
|
|
66
|
+
if (security.logic) {
|
|
67
|
+
decorators.push(RequirePermissionLogic(security.logic));
|
|
68
68
|
} else if (security.permissions && security.permissions.length > 0) {
|
|
69
|
-
if (security.operator === '
|
|
69
|
+
if (security.operator === 'OR') {
|
|
70
70
|
decorators.push(RequireAnyPermission(...security.permissions));
|
|
71
71
|
} else {
|
|
72
72
|
// AND is default and most secure
|
|
@@ -1,75 +1,28 @@
|
|
|
1
1
|
import { SetMetadata } from '@nestjs/common';
|
|
2
2
|
import { PERMISSIONS_KEY } from '../constants';
|
|
3
|
-
/**
|
|
4
|
-
* Decorator to require specific permissions for a route
|
|
5
|
-
*
|
|
6
|
-
* By default uses AND logic (user must have ALL permissions).
|
|
7
|
-
* This is the most common and secure pattern.
|
|
8
|
-
*
|
|
9
|
-
* @param permissions - One or more permission keys required (AND logic)
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* // Require single permission
|
|
13
|
-
* @RequirePermission('users.read')
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* // Require ALL permissions (AND)
|
|
17
|
-
* @RequirePermission('users.read', 'users.write')
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // For OR logic, use RequireAnyPermission instead
|
|
21
|
-
* @RequireAnyPermission('admin.access', 'manager.access')
|
|
22
|
-
*/ export const RequirePermission = (...permissions)=>SetMetadata(PERMISSIONS_KEY, {
|
|
3
|
+
/** Require ALL permissions (AND logic) - most secure pattern */ export const RequirePermission = (...permissions)=>SetMetadata(PERMISSIONS_KEY, {
|
|
23
4
|
permissions,
|
|
24
|
-
operator: '
|
|
5
|
+
operator: 'AND'
|
|
25
6
|
});
|
|
26
|
-
/**
|
|
27
|
-
* Decorator to require ANY of the specified permissions (OR logic)
|
|
28
|
-
*
|
|
29
|
-
* User must have at least ONE of the specified permissions.
|
|
30
|
-
* Less secure than RequirePermission (AND), use carefully.
|
|
31
|
-
*
|
|
32
|
-
* @param permissions - User must have at least ONE of these permissions
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* @RequireAnyPermission('admin.access', 'manager.access')
|
|
36
|
-
*/ export const RequireAnyPermission = (...permissions)=>SetMetadata(PERMISSIONS_KEY, {
|
|
7
|
+
/** Require ANY permission (OR logic) - use carefully */ export const RequireAnyPermission = (...permissions)=>SetMetadata(PERMISSIONS_KEY, {
|
|
37
8
|
permissions,
|
|
38
|
-
operator: '
|
|
9
|
+
operator: 'OR'
|
|
39
10
|
});
|
|
40
11
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Supports building complex permission trees:
|
|
44
|
-
* - AND/OR operators
|
|
45
|
-
* - Nested children for complex logic
|
|
46
|
-
*
|
|
47
|
-
* @param condition - Complex permission condition
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* // Simple: User needs 'admin' OR 'manager'
|
|
51
|
-
* @RequirePermissionCondition({
|
|
52
|
-
* operator: 'or',
|
|
53
|
-
* permissions: ['admin', 'manager'],
|
|
54
|
-
* })
|
|
55
|
-
*
|
|
12
|
+
* Require complex permission logic using ILogicNode tree
|
|
56
13
|
* @example
|
|
57
|
-
* //
|
|
58
|
-
* @
|
|
59
|
-
* operator: 'and',
|
|
60
|
-
* permissions: ['users.read'],
|
|
61
|
-
* children: [
|
|
62
|
-
* { operator: 'or', permissions: ['admin', 'manager'] }
|
|
63
|
-
* ]
|
|
64
|
-
* })
|
|
65
|
-
*
|
|
14
|
+
* // Single permission
|
|
15
|
+
* @RequirePermissionLogic('users.read')
|
|
66
16
|
* @example
|
|
67
|
-
* //
|
|
68
|
-
* @
|
|
69
|
-
* operator: '
|
|
70
|
-
*
|
|
71
|
-
* {
|
|
72
|
-
*
|
|
73
|
-
*
|
|
17
|
+
* // Complex: users.read AND (admin OR manager)
|
|
18
|
+
* @RequirePermissionLogic({
|
|
19
|
+
* type: 'group', operator: 'AND', children: [
|
|
20
|
+
* { type: 'action', actionId: 'users.read' },
|
|
21
|
+
* { type: 'group', operator: 'OR', children: [
|
|
22
|
+
* { type: 'action', actionId: 'admin' },
|
|
23
|
+
* { type: 'action', actionId: 'manager' },
|
|
24
|
+
* ]},
|
|
25
|
+
* ],
|
|
74
26
|
* })
|
|
75
|
-
*/ export const
|
|
27
|
+
*/ export const RequirePermissionLogic = (logic)=>SetMetadata(PERMISSIONS_KEY, logic);
|
|
28
|
+
/** @deprecated Use RequirePermissionLogic instead */ export const RequirePermissionCondition = RequirePermissionLogic;
|
|
@@ -13,8 +13,8 @@ import { ForbiddenException, InternalServerErrorException } from '@nestjs/common
|
|
|
13
13
|
/**
|
|
14
14
|
* Exception thrown when user lacks required permissions
|
|
15
15
|
*/ export class InsufficientPermissionsException extends ForbiddenException {
|
|
16
|
-
constructor(missingPermissions, operator = '
|
|
17
|
-
const message = operator === '
|
|
16
|
+
constructor(missingPermissions, operator = 'AND'){
|
|
17
|
+
const message = operator === 'OR' ? `Requires at least one of: ${missingPermissions.join(', ')}` : `Missing required permissions: ${missingPermissions.join(', ')}`;
|
|
18
18
|
super({
|
|
19
19
|
success: false,
|
|
20
20
|
message,
|
|
@@ -45,8 +45,6 @@ export class PermissionGuard {
|
|
|
45
45
|
context.getClass()
|
|
46
46
|
]);
|
|
47
47
|
if (!permissionConfig) return true;
|
|
48
|
-
const { permissions: requiredPermissions, operator } = this.normalizePermissionConfig(permissionConfig);
|
|
49
|
-
if (!requiredPermissions || requiredPermissions.length === 0) return true;
|
|
50
48
|
const request = context.switchToHttp().getRequest();
|
|
51
49
|
const user = request.user;
|
|
52
50
|
if (!user) throw new UnauthorizedException('Authentication required');
|
|
@@ -59,90 +57,93 @@ export class PermissionGuard {
|
|
|
59
57
|
this.logger.warn(`No permissions found (userId: ${user.id})`, 'PermissionGuard');
|
|
60
58
|
throw new NoPermissionsFoundException();
|
|
61
59
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else {
|
|
69
|
-
this.validateSimplePermissions(requiredPermissions, userPermissions, operator);
|
|
60
|
+
const logicNode = this.normalizeToLogicNode(permissionConfig);
|
|
61
|
+
if (!logicNode) return true;
|
|
62
|
+
const result = this.evaluateLogicNode(logicNode, userPermissions);
|
|
63
|
+
if (!result.passed) {
|
|
64
|
+
this.logger.warn(`Permission denied (userId: ${user.id})`, 'PermissionGuard');
|
|
65
|
+
throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
|
|
70
66
|
}
|
|
71
67
|
return true;
|
|
72
68
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
operator: config.operator || 'and'
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
validateSimplePermissions(requiredPermissions, userPermissions, operator) {
|
|
84
|
-
if (operator === 'or') {
|
|
85
|
-
const hasAny = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
86
|
-
if (!hasAny) throw new InsufficientPermissionsException(requiredPermissions, 'or');
|
|
87
|
-
} else {
|
|
88
|
-
const hasAll = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
89
|
-
if (!hasAll) {
|
|
90
|
-
const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
91
|
-
throw new InsufficientPermissionsException(missing, 'and');
|
|
92
|
-
}
|
|
69
|
+
normalizeToLogicNode(config) {
|
|
70
|
+
// string - single permission
|
|
71
|
+
if (typeof config === 'string') {
|
|
72
|
+
return config ? {
|
|
73
|
+
type: 'action',
|
|
74
|
+
actionId: config
|
|
75
|
+
} : null;
|
|
93
76
|
}
|
|
77
|
+
// string[] - treat as AND of multiple permissions
|
|
78
|
+
if (Array.isArray(config)) {
|
|
79
|
+
if (config.length === 0) return null;
|
|
80
|
+
if (config.length === 1) return {
|
|
81
|
+
type: 'action',
|
|
82
|
+
actionId: config[0]
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
type: 'group',
|
|
86
|
+
operator: 'AND',
|
|
87
|
+
children: config.map((p)=>({
|
|
88
|
+
type: 'action',
|
|
89
|
+
actionId: p
|
|
90
|
+
}))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// SimplePermissionConfig - { permissions: string[], operator: 'AND'|'OR' }
|
|
94
|
+
if ('permissions' in config && Array.isArray(config.permissions)) {
|
|
95
|
+
const simple = config;
|
|
96
|
+
if (simple.permissions.length === 0) return null;
|
|
97
|
+
if (simple.permissions.length === 1) return {
|
|
98
|
+
type: 'action',
|
|
99
|
+
actionId: simple.permissions[0]
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
type: 'group',
|
|
103
|
+
operator: simple.operator || 'AND',
|
|
104
|
+
children: simple.permissions.map((p)=>({
|
|
105
|
+
type: 'action',
|
|
106
|
+
actionId: p
|
|
107
|
+
}))
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ILogicNode
|
|
111
|
+
return config;
|
|
94
112
|
}
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
evaluateLogicNode(node, userPermissions) {
|
|
114
|
+
if (node.type === 'action') {
|
|
115
|
+
const passed = this.hasPermission(userPermissions, node.actionId);
|
|
116
|
+
return {
|
|
117
|
+
passed,
|
|
118
|
+
missingPermissions: passed ? [] : [
|
|
119
|
+
node.actionId
|
|
120
|
+
],
|
|
121
|
+
operator: 'AND'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Group node
|
|
125
|
+
const { operator, children } = node;
|
|
126
|
+
// SECURITY: Fail-closed - deny access when no children configured
|
|
127
|
+
if (!children || children.length === 0) {
|
|
103
128
|
return {
|
|
104
129
|
passed: false,
|
|
105
|
-
message: 'No permissions configured - access denied by default',
|
|
106
130
|
missingPermissions: [],
|
|
107
131
|
operator
|
|
108
132
|
};
|
|
109
133
|
}
|
|
110
134
|
const results = [];
|
|
111
|
-
const failureDetails = [];
|
|
112
135
|
const missingPermissions = [];
|
|
113
|
-
if (permissions.length > 0) {
|
|
114
|
-
if (operator === 'or') {
|
|
115
|
-
const hasAny = permissions.some((p)=>this.hasPermission(userPermissions, p));
|
|
116
|
-
results.push(hasAny);
|
|
117
|
-
if (!hasAny) {
|
|
118
|
-
failureDetails.push(`needs one of: [${permissions.join(', ')}]`);
|
|
119
|
-
missingPermissions.push(...permissions);
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
const hasAll = permissions.every((p)=>this.hasPermission(userPermissions, p));
|
|
123
|
-
results.push(hasAll);
|
|
124
|
-
if (!hasAll) {
|
|
125
|
-
const missing = permissions.filter((p)=>!this.hasPermission(userPermissions, p));
|
|
126
|
-
failureDetails.push(`missing: [${missing.join(', ')}]`);
|
|
127
|
-
missingPermissions.push(...missing);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
136
|
for (const child of children){
|
|
132
|
-
const childResult = this.
|
|
137
|
+
const childResult = this.evaluateLogicNode(child, userPermissions);
|
|
133
138
|
results.push(childResult.passed);
|
|
134
139
|
if (!childResult.passed) {
|
|
135
|
-
failureDetails.push(`(${childResult.message})`);
|
|
136
140
|
missingPermissions.push(...childResult.missingPermissions);
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
|
-
|
|
140
|
-
const passed = operator === 'or' ? results.some((r)=>r) : results.every((r)=>r);
|
|
141
|
-
const message = passed ? 'OK' : `Denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
|
|
143
|
+
const passed = operator === 'OR' ? results.some((r)=>r) : results.every((r)=>r);
|
|
142
144
|
return {
|
|
143
145
|
passed,
|
|
144
|
-
|
|
145
|
-
missingPermissions,
|
|
146
|
+
missingPermissions: passed ? [] : missingPermissions,
|
|
146
147
|
operator
|
|
147
148
|
};
|
|
148
149
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { envConfig } from '@flusys/nestjs-core/config';
|
|
1
2
|
import { CLIENT_TYPE_HEADER } from '../constants';
|
|
2
3
|
/** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
|
|
3
4
|
s: 1000,
|
|
@@ -30,7 +31,7 @@ import { CLIENT_TYPE_HEADER } from '../constants';
|
|
|
30
31
|
*/ export function buildCookieOptions(req) {
|
|
31
32
|
const hostname = req.hostname || '';
|
|
32
33
|
const origin = req.headers.origin || '';
|
|
33
|
-
const isProduction =
|
|
34
|
+
const isProduction = envConfig.isProduction();
|
|
34
35
|
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
35
36
|
const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
|
|
36
37
|
let domain;
|
|
@@ -10,10 +10,8 @@ export declare class PermissionGuard implements CanActivate {
|
|
|
10
10
|
private readonly logger;
|
|
11
11
|
constructor(reflector: Reflector, cache?: HybridCache, config?: PermissionGuardConfig, logger?: ILogger);
|
|
12
12
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private isNestedCondition;
|
|
16
|
-
private evaluateCondition;
|
|
13
|
+
private normalizeToLogicNode;
|
|
14
|
+
private evaluateLogicNode;
|
|
17
15
|
private getUserPermissions;
|
|
18
16
|
private buildPermissionCacheKey;
|
|
19
17
|
private hasPermission;
|