@wxn0brp/gate-warden 0.4.0 → 0.5.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 +5 -9
- package/dist/check.d.ts +32 -0
- package/dist/check.js +97 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -4
- package/dist/log.d.ts +2 -0
- package/dist/log.js +6 -0
- package/dist/mgr.d.ts +2 -3
- package/dist/mgr.js +1 -2
- package/dist/test/abac.test.d.ts +1 -0
- package/dist/test/abac.test.js +148 -0
- package/dist/test/act.test.d.ts +1 -0
- package/dist/test/act.test.js +85 -0
- package/dist/test/rbac.test.d.ts +1 -0
- package/dist/test/rbac.test.js +69 -0
- package/dist/types/check.d.ts +10 -0
- package/dist/types/check.js +1 -0
- package/dist/user.d.ts +1 -2
- package/dist/user.js +1 -2
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +10 -0
- package/dist/warden.d.ts +6 -5
- package/dist/warden.js +5 -116
- package/package.json +13 -11
package/README.md
CHANGED
|
@@ -13,9 +13,13 @@ Gate Warden is a lightweight access control library that supports ACL, RBAC, and
|
|
|
13
13
|
Install the library using npm or yarn:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
16
|
+
npm i @wxn0brp/gate-warden
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## DOCS
|
|
20
|
+
|
|
21
|
+
See [Docs](https://wxn0brp.github.io/gate-warden/)
|
|
22
|
+
|
|
19
23
|
## Usage
|
|
20
24
|
|
|
21
25
|
### GateWarden
|
|
@@ -76,14 +80,6 @@ Set the `debugLog` level in `GateWarden` to enable debug messages:
|
|
|
76
80
|
- `1`: Basic logs
|
|
77
81
|
- `2`: Detailed logs
|
|
78
82
|
|
|
79
|
-
## Project Structure
|
|
80
|
-
|
|
81
|
-
- **src/warden.ts:** Core access control logic.
|
|
82
|
-
- **src/user.ts:** User management.
|
|
83
|
-
- **src/mgr.ts:** Role and rule management.
|
|
84
|
-
- **src/types/system.ts:** Type definitions for roles, rules, and users.
|
|
85
|
-
- **src/log.ts:** Logging utilities.
|
|
86
|
-
|
|
87
83
|
## License
|
|
88
84
|
|
|
89
85
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CheckParams } from "./types/check.js";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a user has the given flag on the given entity by checking the entity's ACL.
|
|
4
|
+
* @param db The DB instance
|
|
5
|
+
* @param flag The flag to check
|
|
6
|
+
* @param user The user to check
|
|
7
|
+
* @param entityId The ID of the entity to check
|
|
8
|
+
* @returns If the user has the flag on the entity:
|
|
9
|
+
* - 1 if the user has the flag
|
|
10
|
+
* - 0 if the user does not have the flag
|
|
11
|
+
* - -1 if the entity does not have an ACL
|
|
12
|
+
*/
|
|
13
|
+
export declare function aclCheck({ db, entityId, flag, user }: CheckParams): Promise<number>;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a user has the given flag on the given entity by checking each role the user has.
|
|
16
|
+
* @param db The DB instance
|
|
17
|
+
* @param flag The flag to check
|
|
18
|
+
* @param user The user to check
|
|
19
|
+
* @param entityId The ID of the entity to check
|
|
20
|
+
* @returns If the user has the flag on the entity
|
|
21
|
+
*/
|
|
22
|
+
export declare function rbacCheck({ db, flag, user, entityId }: CheckParams): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* ABAC (Attribute-Based Access Control) check
|
|
25
|
+
* @param db The DB instance
|
|
26
|
+
* @param entityId The ID of the entity to check
|
|
27
|
+
* @param flag The flag to check
|
|
28
|
+
* @param user The user to check
|
|
29
|
+
* @param debugLog The debug log level
|
|
30
|
+
* @returns `true` if access is granted, `false` otherwise
|
|
31
|
+
*/
|
|
32
|
+
export declare function abacCheck({ db, entityId, flag, user, debugLog }: CheckParams): Promise<boolean>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import hasFieldsAdvanced from "@wxn0brp/db-core/utils/hasFieldsAdvanced";
|
|
2
|
+
import { COLORS } from "./log.js";
|
|
3
|
+
import { convertPath } from "./utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a user has the given flag on the given entity by checking the entity's ACL.
|
|
6
|
+
* @param db The DB instance
|
|
7
|
+
* @param flag The flag to check
|
|
8
|
+
* @param user The user to check
|
|
9
|
+
* @param entityId The ID of the entity to check
|
|
10
|
+
* @returns If the user has the flag on the entity:
|
|
11
|
+
* - 1 if the user has the flag
|
|
12
|
+
* - 0 if the user does not have the flag
|
|
13
|
+
* - -1 if the entity does not have an ACL
|
|
14
|
+
*/
|
|
15
|
+
export async function aclCheck({ db, entityId, flag, user }) {
|
|
16
|
+
if (!await db.issetCollection("acl/" + entityId))
|
|
17
|
+
return -1;
|
|
18
|
+
const rules = await db.find("acl/" + entityId, {
|
|
19
|
+
$or: [
|
|
20
|
+
{ uid: user._id },
|
|
21
|
+
{
|
|
22
|
+
$not: {
|
|
23
|
+
$exists: { "uid": true }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
if (rules.length === 0)
|
|
29
|
+
return -1;
|
|
30
|
+
for (const rule of rules) {
|
|
31
|
+
if (rule.p & flag)
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Checks if a user has the given flag on the given entity by checking each role the user has.
|
|
38
|
+
* @param db The DB instance
|
|
39
|
+
* @param flag The flag to check
|
|
40
|
+
* @param user The user to check
|
|
41
|
+
* @param entityId The ID of the entity to check
|
|
42
|
+
* @returns If the user has the flag on the entity
|
|
43
|
+
*/
|
|
44
|
+
export async function rbacCheck({ db, flag, user, entityId }) {
|
|
45
|
+
for (const role of user.roles) {
|
|
46
|
+
const rolesEntity = await db.find("role/" + role, { _id: entityId });
|
|
47
|
+
for (const entity of rolesEntity) {
|
|
48
|
+
if (entity.p & flag)
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* ABAC (Attribute-Based Access Control) check
|
|
56
|
+
* @param db The DB instance
|
|
57
|
+
* @param entityId The ID of the entity to check
|
|
58
|
+
* @param flag The flag to check
|
|
59
|
+
* @param user The user to check
|
|
60
|
+
* @param debugLog The debug log level
|
|
61
|
+
* @returns `true` if access is granted, `false` otherwise
|
|
62
|
+
*/
|
|
63
|
+
export async function abacCheck({ db, entityId, flag, user, debugLog }) {
|
|
64
|
+
if (!await db.issetCollection("abac/" + entityId))
|
|
65
|
+
return false;
|
|
66
|
+
const rules = await db.find("abac/" + entityId, { flag });
|
|
67
|
+
if (rules.length === 0)
|
|
68
|
+
return false;
|
|
69
|
+
for (const rule of rules) {
|
|
70
|
+
let authorized = true;
|
|
71
|
+
if (debugLog >= 1)
|
|
72
|
+
console.log(COLORS.blue + `[GW] ABAC rule: ${COLORS.yellow}${JSON.stringify(rule.condition)}${COLORS.blue} ` +
|
|
73
|
+
`-> checking...` + COLORS.reset);
|
|
74
|
+
for (const key in rule.condition) {
|
|
75
|
+
const expectedValue = rule.condition[key];
|
|
76
|
+
let actualValue;
|
|
77
|
+
if (key === "_")
|
|
78
|
+
actualValue = user;
|
|
79
|
+
else
|
|
80
|
+
actualValue = convertPath(user, key);
|
|
81
|
+
if (actualValue === undefined) {
|
|
82
|
+
authorized = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (!hasFieldsAdvanced(actualValue, expectedValue)) {
|
|
86
|
+
authorized = false;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (authorized) {
|
|
91
|
+
if (debugLog >= 1)
|
|
92
|
+
console.log(COLORS.green + `[GW] Access granted by this rule.` + COLORS.reset);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export * from "./warden.js";
|
|
2
|
+
export * from "./user.js";
|
|
3
|
+
export * from "./mgr.js";
|
|
4
4
|
import { AccessResult } from "./types/system.js";
|
|
5
|
-
export {
|
|
5
|
+
export type { AccessResult };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export { GateWarden, UserManager, WardenManager };
|
|
1
|
+
export * from "./warden.js";
|
|
2
|
+
export * from "./user.js";
|
|
3
|
+
export * from "./mgr.js";
|
package/dist/log.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Id from "@wxn0brp/db-core/types/Id";
|
|
1
2
|
export declare const COLORS: {
|
|
2
3
|
reset: string;
|
|
3
4
|
green: string;
|
|
@@ -5,3 +6,4 @@ export declare const COLORS: {
|
|
|
5
6
|
red: string;
|
|
6
7
|
blue: string;
|
|
7
8
|
};
|
|
9
|
+
export declare function logAccess(userId: Id, entityId: Id, via: string, debugLog: number): void;
|
package/dist/log.js
CHANGED
|
@@ -5,3 +5,9 @@ export const COLORS = {
|
|
|
5
5
|
red: "\x1b[31m",
|
|
6
6
|
blue: "\x1b[34m",
|
|
7
7
|
};
|
|
8
|
+
export function logAccess(userId, entityId, via, debugLog) {
|
|
9
|
+
if (debugLog < 1)
|
|
10
|
+
return;
|
|
11
|
+
console.log(`${COLORS.green}[GW] Access granted to ${COLORS.yellow}${entityId}${COLORS.green} via ` +
|
|
12
|
+
`${COLORS.yellow}${via}${COLORS.green} by ${COLORS.yellow}${userId}${COLORS.reset}`);
|
|
13
|
+
}
|
package/dist/mgr.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Id, ValtheraCompatible } from "@wxn0brp/db-core";
|
|
2
2
|
import { ABACRule, ACLRule, Role, RoleEntity } from "./types/system.js";
|
|
3
|
-
declare class WardenManager {
|
|
3
|
+
export declare class WardenManager {
|
|
4
4
|
private db;
|
|
5
5
|
constructor(db: ValtheraCompatible);
|
|
6
6
|
changeRoleNameToId(name: string): Promise<Id>;
|
|
7
|
-
addRole(role: Role): Promise<Role>;
|
|
7
|
+
addRole(role: Role | Omit<Role, "_id">): Promise<Role>;
|
|
8
8
|
addACLRule(entityId: string, p: number, uid?: Id): Promise<ACLRule>;
|
|
9
9
|
addRBACRule(role_id: string, entity_id: string, p: number): Promise<RoleEntity>;
|
|
10
10
|
addABACRule(entity_id: string, flag: number, condition: ABACRule["condition"]): Promise<ABACRule>;
|
|
@@ -13,4 +13,3 @@ declare class WardenManager {
|
|
|
13
13
|
removeRBACRule(roleId: string, entityId: string): Promise<boolean>;
|
|
14
14
|
removeABACRule(entityId: string, flag: number): Promise<boolean>;
|
|
15
15
|
}
|
|
16
|
-
export default WardenManager;
|
package/dist/mgr.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createMemoryValthera } from "@wxn0brp/db-core";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { abacCheck } from "../check.js";
|
|
4
|
+
describe("Access Control Checks", () => {
|
|
5
|
+
describe("abacCheck", () => {
|
|
6
|
+
it("should return false when entity has no ABAC collection", async () => {
|
|
7
|
+
const db = createMemoryValthera({});
|
|
8
|
+
const user = { _id: "user1", roles: [], attrib: { department: "engineering" } };
|
|
9
|
+
const result = await abacCheck({
|
|
10
|
+
db,
|
|
11
|
+
entityId: "entity1",
|
|
12
|
+
flag: 1,
|
|
13
|
+
user,
|
|
14
|
+
debugLog: 0
|
|
15
|
+
});
|
|
16
|
+
expect(result).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it("should return true when user attributes match ABAC rule", async () => {
|
|
19
|
+
const abacRules = [
|
|
20
|
+
{
|
|
21
|
+
flag: 1,
|
|
22
|
+
condition: {
|
|
23
|
+
attrib: {
|
|
24
|
+
department: "engineering"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
const db = createMemoryValthera({
|
|
30
|
+
"abac/entity1": abacRules
|
|
31
|
+
});
|
|
32
|
+
const user = { _id: "user1", roles: [], attrib: { department: "engineering" } };
|
|
33
|
+
const result = await abacCheck({
|
|
34
|
+
db,
|
|
35
|
+
entityId: "entity1",
|
|
36
|
+
flag: 1,
|
|
37
|
+
user,
|
|
38
|
+
debugLog: 0
|
|
39
|
+
});
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it("should return false when user attributes don't match ABAC rule", async () => {
|
|
43
|
+
const abacRules = [
|
|
44
|
+
{
|
|
45
|
+
flag: 1,
|
|
46
|
+
condition: {
|
|
47
|
+
attrib: {
|
|
48
|
+
department: "engineering"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
const db = createMemoryValthera({
|
|
54
|
+
"abac/entity1": abacRules
|
|
55
|
+
});
|
|
56
|
+
const user = { _id: "user1", roles: [], attrib: { department: "marketing" } };
|
|
57
|
+
const result = await abacCheck({
|
|
58
|
+
db,
|
|
59
|
+
entityId: "entity1",
|
|
60
|
+
flag: 1,
|
|
61
|
+
user,
|
|
62
|
+
debugLog: 0
|
|
63
|
+
});
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it("should handle nested attribute paths correctly", async () => {
|
|
67
|
+
const abacRules = [
|
|
68
|
+
{
|
|
69
|
+
flag: 1,
|
|
70
|
+
condition: {
|
|
71
|
+
"attrib.profile": {
|
|
72
|
+
level: 5
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
const db = createMemoryValthera({
|
|
78
|
+
"abac/entity1": abacRules
|
|
79
|
+
});
|
|
80
|
+
const user = {
|
|
81
|
+
_id: "user1",
|
|
82
|
+
roles: [],
|
|
83
|
+
attrib: {
|
|
84
|
+
profile: {
|
|
85
|
+
level: 5,
|
|
86
|
+
name: "test"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const result = await abacCheck({
|
|
91
|
+
db,
|
|
92
|
+
entityId: "entity1",
|
|
93
|
+
flag: 1,
|
|
94
|
+
user,
|
|
95
|
+
debugLog: 0
|
|
96
|
+
});
|
|
97
|
+
expect(result).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it("should return false when user attributes are undefined", async () => {
|
|
100
|
+
const abacRules = [
|
|
101
|
+
{
|
|
102
|
+
flag: 1,
|
|
103
|
+
condition: {
|
|
104
|
+
attrib: {
|
|
105
|
+
department: "engineering"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
const db = createMemoryValthera({
|
|
111
|
+
"abac/entity1": abacRules
|
|
112
|
+
});
|
|
113
|
+
const user = { _id: "user1", roles: [], attrib: {} }; // No department
|
|
114
|
+
const result = await abacCheck({
|
|
115
|
+
db,
|
|
116
|
+
entityId: "entity1",
|
|
117
|
+
flag: 1,
|
|
118
|
+
user,
|
|
119
|
+
debugLog: 0
|
|
120
|
+
});
|
|
121
|
+
expect(result).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it("should return true when checking user root properties with _", async () => {
|
|
124
|
+
const abacRules = [
|
|
125
|
+
{
|
|
126
|
+
flag: 1,
|
|
127
|
+
condition: {
|
|
128
|
+
_: {
|
|
129
|
+
_id: "user1"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
const db = createMemoryValthera({
|
|
135
|
+
"abac/entity1": abacRules
|
|
136
|
+
});
|
|
137
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
138
|
+
const result = await abacCheck({
|
|
139
|
+
db,
|
|
140
|
+
entityId: "entity1",
|
|
141
|
+
flag: 1,
|
|
142
|
+
user,
|
|
143
|
+
debugLog: 0
|
|
144
|
+
});
|
|
145
|
+
expect(result).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createMemoryValthera } from "@wxn0brp/db-core";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { aclCheck } from "../check.js";
|
|
4
|
+
describe("Access Control Checks", () => {
|
|
5
|
+
describe("aclCheck", () => {
|
|
6
|
+
it("should return -1 when entity has no ACL collection", async () => {
|
|
7
|
+
const db = createMemoryValthera({});
|
|
8
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
9
|
+
const result = await aclCheck({
|
|
10
|
+
db,
|
|
11
|
+
entityId: "entity1",
|
|
12
|
+
flag: 1,
|
|
13
|
+
user
|
|
14
|
+
});
|
|
15
|
+
expect(result).toBe(-1);
|
|
16
|
+
});
|
|
17
|
+
it("should return 1 when user has the specific flag in ACL", async () => {
|
|
18
|
+
const aclRules = [
|
|
19
|
+
{ uid: "user1", p: 5 } // User has flags 1 and 4 (1 | 4 = 5)
|
|
20
|
+
];
|
|
21
|
+
const db = createMemoryValthera({
|
|
22
|
+
"acl/entity1": aclRules
|
|
23
|
+
});
|
|
24
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
25
|
+
// Check for flag 1 (should be granted)
|
|
26
|
+
const result = await aclCheck({
|
|
27
|
+
db,
|
|
28
|
+
entityId: "entity1",
|
|
29
|
+
flag: 1,
|
|
30
|
+
user
|
|
31
|
+
});
|
|
32
|
+
expect(result).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
it("should return 0 when user does not have the flag in ACL", async () => {
|
|
35
|
+
const aclRules = [
|
|
36
|
+
{ uid: "user1", p: 4 } // User has only flag 4
|
|
37
|
+
];
|
|
38
|
+
const db = createMemoryValthera({
|
|
39
|
+
"acl/entity1": aclRules
|
|
40
|
+
});
|
|
41
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
42
|
+
// Check for flag 1 (should not be granted)
|
|
43
|
+
const result = await aclCheck({
|
|
44
|
+
db,
|
|
45
|
+
entityId: "entity1",
|
|
46
|
+
flag: 1,
|
|
47
|
+
user
|
|
48
|
+
});
|
|
49
|
+
expect(result).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
it("should return 1 when there is a public rule with the flag", async () => {
|
|
52
|
+
const aclRules = [
|
|
53
|
+
{ p: 3 } // Public rule with flags 1 and 2
|
|
54
|
+
];
|
|
55
|
+
const db = createMemoryValthera({
|
|
56
|
+
"acl/entity1": aclRules
|
|
57
|
+
});
|
|
58
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
59
|
+
// Check for flag 1 (should be granted via public rule)
|
|
60
|
+
const result = await aclCheck({
|
|
61
|
+
db,
|
|
62
|
+
entityId: "entity1",
|
|
63
|
+
flag: 1,
|
|
64
|
+
user
|
|
65
|
+
});
|
|
66
|
+
expect(result).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
it("should return -1 when no rules match the user or public access", async () => {
|
|
69
|
+
const aclRules = [
|
|
70
|
+
{ uid: "user2", p: 1 } // Different user has the flag
|
|
71
|
+
];
|
|
72
|
+
const db = createMemoryValthera({
|
|
73
|
+
"acl/entity1": aclRules
|
|
74
|
+
});
|
|
75
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
76
|
+
const result = await aclCheck({
|
|
77
|
+
db,
|
|
78
|
+
entityId: "entity1",
|
|
79
|
+
flag: 1,
|
|
80
|
+
user
|
|
81
|
+
});
|
|
82
|
+
expect(result).toBe(-1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createMemoryValthera } from "@wxn0brp/db-core";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { rbacCheck } from "../check.js";
|
|
4
|
+
describe("Access Control Checks", () => {
|
|
5
|
+
describe("rbacCheck", () => {
|
|
6
|
+
it("should return true when user has role with the required flag", async () => {
|
|
7
|
+
const rolesData = [
|
|
8
|
+
{ _id: "resource1", p: 5 } // Flags 1 and 4
|
|
9
|
+
];
|
|
10
|
+
const db = createMemoryValthera({
|
|
11
|
+
"role/admin": rolesData
|
|
12
|
+
});
|
|
13
|
+
const user = { _id: "user1", roles: ["admin"], attrib: {} };
|
|
14
|
+
// Check for flag 1 (should be granted by admin role)
|
|
15
|
+
const result = await rbacCheck({
|
|
16
|
+
db,
|
|
17
|
+
entityId: "resource1",
|
|
18
|
+
flag: 1,
|
|
19
|
+
user
|
|
20
|
+
});
|
|
21
|
+
expect(result).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("should return false when user has role without the required flag", async () => {
|
|
24
|
+
const rolesData = [
|
|
25
|
+
{ _id: "resource1", p: 4 } // Only flag 4
|
|
26
|
+
];
|
|
27
|
+
const db = createMemoryValthera({
|
|
28
|
+
"role/admin": rolesData
|
|
29
|
+
});
|
|
30
|
+
const user = { _id: "user1", roles: ["admin"], attrib: {} };
|
|
31
|
+
// Check for flag 1 (should not be granted)
|
|
32
|
+
const result = await rbacCheck({
|
|
33
|
+
db,
|
|
34
|
+
entityId: "resource1",
|
|
35
|
+
flag: 1,
|
|
36
|
+
user
|
|
37
|
+
});
|
|
38
|
+
expect(result).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it("should return false when user has no matching role-entity permissions", async () => {
|
|
41
|
+
const rolesData = [
|
|
42
|
+
{ _id: "resource2", p: 1 } // Different resource
|
|
43
|
+
];
|
|
44
|
+
const db = createMemoryValthera({
|
|
45
|
+
"role/admin": rolesData
|
|
46
|
+
});
|
|
47
|
+
const user = { _id: "user1", roles: ["admin"], attrib: {} };
|
|
48
|
+
// Check for resource1 when only resource2 permissions exist
|
|
49
|
+
const result = await rbacCheck({
|
|
50
|
+
db,
|
|
51
|
+
entityId: "resource1",
|
|
52
|
+
flag: 1,
|
|
53
|
+
user
|
|
54
|
+
});
|
|
55
|
+
expect(result).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it("should return false when user has no roles", async () => {
|
|
58
|
+
const db = createMemoryValthera({});
|
|
59
|
+
const user = { _id: "user1", roles: [], attrib: {} };
|
|
60
|
+
const result = await rbacCheck({
|
|
61
|
+
db,
|
|
62
|
+
entityId: "resource1",
|
|
63
|
+
flag: 1,
|
|
64
|
+
user
|
|
65
|
+
});
|
|
66
|
+
expect(result).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ValtheraCompatible } from "@wxn0brp/db-core";
|
|
2
|
+
import Id from "@wxn0brp/db-core/types/Id";
|
|
3
|
+
import { User } from "./system.js";
|
|
4
|
+
export interface CheckParams {
|
|
5
|
+
db: ValtheraCompatible;
|
|
6
|
+
entityId: Id;
|
|
7
|
+
flag: number;
|
|
8
|
+
user: User;
|
|
9
|
+
debugLog?: number;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/user.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Id, ValtheraCompatible } from "@wxn0brp/db-core";
|
|
2
2
|
import { User } from "./types/system.js";
|
|
3
|
-
declare class UserManager<A = any> {
|
|
3
|
+
export declare class UserManager<A = any> {
|
|
4
4
|
private db;
|
|
5
5
|
constructor(db: ValtheraCompatible);
|
|
6
6
|
/**
|
|
@@ -48,4 +48,3 @@ declare class UserManager<A = any> {
|
|
|
48
48
|
*/
|
|
49
49
|
updateAttributes(user_id: Id, attributes: Partial<A>): Promise<void>;
|
|
50
50
|
}
|
|
51
|
-
export default UserManager;
|
package/dist/user.js
CHANGED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function convertPath(user: any, path: string): any;
|
package/dist/utils.js
ADDED
package/dist/warden.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ValtheraCompatible } from "@wxn0brp/db-core";
|
|
2
|
-
import { AccessResult } from "./types/system.js";
|
|
3
|
-
declare
|
|
4
|
-
|
|
1
|
+
import { Id, ValtheraCompatible } from "@wxn0brp/db-core";
|
|
2
|
+
import { AccessResult, User } from "./types/system.js";
|
|
3
|
+
export declare function fetchUser(db: ValtheraCompatible, userId: Id): Promise<User>;
|
|
4
|
+
export declare function matchPermission(db: ValtheraCompatible, entityId: Id, flag: number, user: User, debugLog: number): Promise<AccessResult>;
|
|
5
|
+
export declare class GateWarden {
|
|
6
|
+
db: ValtheraCompatible;
|
|
5
7
|
debugLog: number;
|
|
6
8
|
constructor(db: ValtheraCompatible, debugLog?: number);
|
|
7
9
|
hasAccess(userId: string, entityId: string, flag: number): Promise<AccessResult>;
|
|
8
10
|
}
|
|
9
|
-
export default GateWarden;
|
package/dist/warden.js
CHANGED
|
@@ -1,122 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
function
|
|
4
|
-
if (debugLog < 1)
|
|
5
|
-
return;
|
|
6
|
-
console.log(`${COLORS.green}[GW] Access granted to ${COLORS.yellow}${entityId}${COLORS.green} via ` +
|
|
7
|
-
`${COLORS.yellow}${via}${COLORS.green} by ${COLORS.yellow}${userId}${COLORS.reset}`);
|
|
8
|
-
}
|
|
9
|
-
async function fetchUser(db, userId) {
|
|
1
|
+
import { abacCheck, aclCheck, rbacCheck } from "./check.js";
|
|
2
|
+
import { COLORS, logAccess } from "./log.js";
|
|
3
|
+
export async function fetchUser(db, userId) {
|
|
10
4
|
const user = await db.findOne("users", { _id: userId });
|
|
11
5
|
if (!user)
|
|
12
6
|
throw new Error("User not found");
|
|
13
7
|
return user;
|
|
14
8
|
}
|
|
15
|
-
|
|
16
|
-
* Checks if a user has the given flag on the given entity by checking the entity's ACL.
|
|
17
|
-
* @param db The DB instance
|
|
18
|
-
* @param flag The flag to check
|
|
19
|
-
* @param user The user to check
|
|
20
|
-
* @param entityId The ID of the entity to check
|
|
21
|
-
* @returns If the user has the flag on the entity:
|
|
22
|
-
* - 1 if the user has the flag
|
|
23
|
-
* - 0 if the user does not have the flag
|
|
24
|
-
* - -1 if the entity does not have an ACL
|
|
25
|
-
*/
|
|
26
|
-
async function aclCheck({ db, entityId, flag, user }) {
|
|
27
|
-
if (!await db.issetCollection("acl/" + entityId))
|
|
28
|
-
return -1;
|
|
29
|
-
const rules = await db.find("acl/" + entityId, {
|
|
30
|
-
$or: [
|
|
31
|
-
{ uid: user._id },
|
|
32
|
-
{
|
|
33
|
-
$not: {
|
|
34
|
-
$exists: { "uid": true }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
});
|
|
39
|
-
if (rules.length === 0)
|
|
40
|
-
return -1;
|
|
41
|
-
for (const rule of rules) {
|
|
42
|
-
if (rule.p & flag)
|
|
43
|
-
return 1;
|
|
44
|
-
}
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Checks if a user has the given flag on the given entity by checking each role the user has.
|
|
49
|
-
* @param db The DB instance
|
|
50
|
-
* @param flag The flag to check
|
|
51
|
-
* @param user The user to check
|
|
52
|
-
* @param entityId The ID of the entity to check
|
|
53
|
-
* @returns If the user has the flag on the entity
|
|
54
|
-
*/
|
|
55
|
-
async function rbacCheck({ db, flag, user, entityId }) {
|
|
56
|
-
for (const role of user.roles) {
|
|
57
|
-
const rolesEntity = await db.find("role/" + role, { _id: entityId });
|
|
58
|
-
for (const entity of rolesEntity) {
|
|
59
|
-
if (entity.p & flag)
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
function convertPath(user, path) {
|
|
66
|
-
const keys = path.split(".");
|
|
67
|
-
let data = user;
|
|
68
|
-
for (const key of keys) {
|
|
69
|
-
data = data?.[key];
|
|
70
|
-
if (data === undefined)
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
return data;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* ABAC (Attribute-Based Access Control) check
|
|
77
|
-
* @param db The DB instance
|
|
78
|
-
* @param entityId The ID of the entity to check
|
|
79
|
-
* @param flag The flag to check
|
|
80
|
-
* @param user The user to check
|
|
81
|
-
* @param debugLog The debug log level
|
|
82
|
-
* @returns `true` if access is granted, `false` otherwise
|
|
83
|
-
*/
|
|
84
|
-
async function abacCheck({ db, entityId, flag, user, debugLog }) {
|
|
85
|
-
if (!await db.issetCollection("abac/" + entityId))
|
|
86
|
-
return false;
|
|
87
|
-
const rules = await db.find("abac/" + entityId, { flag });
|
|
88
|
-
if (rules.length === 0)
|
|
89
|
-
return false;
|
|
90
|
-
for (const rule of rules) {
|
|
91
|
-
let authorized = true;
|
|
92
|
-
if (debugLog >= 1)
|
|
93
|
-
console.log(COLORS.blue + `[GW] ABAC rule: ${COLORS.yellow}${JSON.stringify(rule.condition)}${COLORS.blue} ` +
|
|
94
|
-
`-> checking...` + COLORS.reset);
|
|
95
|
-
for (const key in rule.condition) {
|
|
96
|
-
const expectedValue = rule.condition[key];
|
|
97
|
-
let actualValue;
|
|
98
|
-
if (key === "_")
|
|
99
|
-
actualValue = user;
|
|
100
|
-
else
|
|
101
|
-
actualValue = convertPath(user, key);
|
|
102
|
-
if (actualValue === undefined) {
|
|
103
|
-
authorized = false;
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
if (!hasFieldsAdvanced(actualValue, expectedValue)) {
|
|
107
|
-
authorized = false;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (authorized) {
|
|
112
|
-
if (debugLog >= 1)
|
|
113
|
-
console.log(COLORS.green + `[GW] Access granted by this rule.` + COLORS.reset);
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
async function matchPermission(db, entityId, flag, user, debugLog) {
|
|
9
|
+
export async function matchPermission(db, entityId, flag, user, debugLog) {
|
|
120
10
|
const checks = [
|
|
121
11
|
{ name: "ACL", method: aclCheck },
|
|
122
12
|
{ name: "RBAC", method: rbacCheck },
|
|
@@ -136,7 +26,7 @@ async function matchPermission(db, entityId, flag, user, debugLog) {
|
|
|
136
26
|
}
|
|
137
27
|
return { granted: false, via: null };
|
|
138
28
|
}
|
|
139
|
-
class GateWarden {
|
|
29
|
+
export class GateWarden {
|
|
140
30
|
db;
|
|
141
31
|
debugLog;
|
|
142
32
|
constructor(db, debugLog = 0) {
|
|
@@ -171,4 +61,3 @@ class GateWarden {
|
|
|
171
61
|
};
|
|
172
62
|
}
|
|
173
63
|
}
|
|
174
|
-
export default GateWarden;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wxn0brp/gate-warden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "wxn0brP",
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://github.com/wxn0brP/gate-warden",
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/
|
|
17
|
-
"@
|
|
18
|
-
"
|
|
19
|
-
"
|
|
16
|
+
"@types/bun": "*",
|
|
17
|
+
"@types/node": "*",
|
|
18
|
+
"@wxn0brp/db-core": "^0.2.7",
|
|
19
|
+
"tsc-alias": "*",
|
|
20
|
+
"typescript": "*"
|
|
20
21
|
},
|
|
21
22
|
"peerDependencies": {
|
|
22
|
-
"@wxn0brp/db-core": ">=0.
|
|
23
|
+
"@wxn0brp/db-core": ">=0.2.7"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"dist"
|
|
@@ -27,12 +28,13 @@
|
|
|
27
28
|
"exports": {
|
|
28
29
|
".": {
|
|
29
30
|
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js",
|
|
30
32
|
"import": "./dist/index.js"
|
|
31
33
|
},
|
|
32
34
|
"./*": {
|
|
33
|
-
"types": "./dist
|
|
34
|
-
"
|
|
35
|
+
"types": "./dist/*.d.ts",
|
|
36
|
+
"default": "./dist/*.js",
|
|
37
|
+
"import": "./dist/*.js"
|
|
35
38
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
+
}
|
|
40
|
+
}
|