@burdenoff/be-sdk 2026.510.79
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 +173 -0
- package/configs/eslint/index.js +492 -0
- package/configs/tsconfig/tsconfig.json +53 -0
- package/dist/EventQueueFactory-6NMSGECR.js +8 -0
- package/dist/EventQueueFactory-6NMSGECR.js.map +1 -0
- package/dist/auth/index.d.ts +66 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.js +6 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/captcha/index.d.ts +1 -0
- package/dist/captcha/index.js +4 -0
- package/dist/captcha/index.js.map +1 -0
- package/dist/chunk-2HANKZZM.js +245 -0
- package/dist/chunk-2HANKZZM.js.map +1 -0
- package/dist/chunk-2SD5L2F2.js +816 -0
- package/dist/chunk-2SD5L2F2.js.map +1 -0
- package/dist/chunk-3O2RQOPV.js +1068 -0
- package/dist/chunk-3O2RQOPV.js.map +1 -0
- package/dist/chunk-4B43DZIS.js +560 -0
- package/dist/chunk-4B43DZIS.js.map +1 -0
- package/dist/chunk-4XAEES25.js +2444 -0
- package/dist/chunk-4XAEES25.js.map +1 -0
- package/dist/chunk-4XGGWW36.js +1405 -0
- package/dist/chunk-4XGGWW36.js.map +1 -0
- package/dist/chunk-5LXDPGDH.js +245 -0
- package/dist/chunk-5LXDPGDH.js.map +1 -0
- package/dist/chunk-AI7L22ZJ.js +472 -0
- package/dist/chunk-AI7L22ZJ.js.map +1 -0
- package/dist/chunk-CBZNP6RF.js +286 -0
- package/dist/chunk-CBZNP6RF.js.map +1 -0
- package/dist/chunk-ELLX3K54.js +613 -0
- package/dist/chunk-ELLX3K54.js.map +1 -0
- package/dist/chunk-F5XEAGHQ.js +1875 -0
- package/dist/chunk-F5XEAGHQ.js.map +1 -0
- package/dist/chunk-FNZEQOJY.js +628 -0
- package/dist/chunk-FNZEQOJY.js.map +1 -0
- package/dist/chunk-GTEMQXCA.js +516 -0
- package/dist/chunk-GTEMQXCA.js.map +1 -0
- package/dist/chunk-HIWTWIGQ.js +226 -0
- package/dist/chunk-HIWTWIGQ.js.map +1 -0
- package/dist/chunk-IYE2NTRX.js +764 -0
- package/dist/chunk-IYE2NTRX.js.map +1 -0
- package/dist/chunk-J26YGR2U.js +1484 -0
- package/dist/chunk-J26YGR2U.js.map +1 -0
- package/dist/chunk-JG2FTZPI.js +509 -0
- package/dist/chunk-JG2FTZPI.js.map +1 -0
- package/dist/chunk-JVBTNSPG.js +1551 -0
- package/dist/chunk-JVBTNSPG.js.map +1 -0
- package/dist/chunk-L7ZMN2FJ.js +12 -0
- package/dist/chunk-L7ZMN2FJ.js.map +1 -0
- package/dist/chunk-M5IMU3US.js +473 -0
- package/dist/chunk-M5IMU3US.js.map +1 -0
- package/dist/chunk-MLW2HPS5.js +1267 -0
- package/dist/chunk-MLW2HPS5.js.map +1 -0
- package/dist/chunk-MNCGUYLJ.js +269 -0
- package/dist/chunk-MNCGUYLJ.js.map +1 -0
- package/dist/chunk-MOCUBSTW.js +373 -0
- package/dist/chunk-MOCUBSTW.js.map +1 -0
- package/dist/chunk-NGMEI42S.js +103 -0
- package/dist/chunk-NGMEI42S.js.map +1 -0
- package/dist/chunk-NUB5GQTU.js +270 -0
- package/dist/chunk-NUB5GQTU.js.map +1 -0
- package/dist/chunk-OVSYMNCI.js +145 -0
- package/dist/chunk-OVSYMNCI.js.map +1 -0
- package/dist/chunk-QLVKMWFA.js +94 -0
- package/dist/chunk-QLVKMWFA.js.map +1 -0
- package/dist/chunk-SAG6ZYWQ.js +89 -0
- package/dist/chunk-SAG6ZYWQ.js.map +1 -0
- package/dist/chunk-SAK2JCA7.js +198 -0
- package/dist/chunk-SAK2JCA7.js.map +1 -0
- package/dist/chunk-SBS4S2PJ.js +100 -0
- package/dist/chunk-SBS4S2PJ.js.map +1 -0
- package/dist/chunk-SNEOLWXT.js +148 -0
- package/dist/chunk-SNEOLWXT.js.map +1 -0
- package/dist/chunk-TKBD2ONF.js +564 -0
- package/dist/chunk-TKBD2ONF.js.map +1 -0
- package/dist/chunk-UBXFIIAW.js +127 -0
- package/dist/chunk-UBXFIIAW.js.map +1 -0
- package/dist/chunk-VI4VSNTI.js +57 -0
- package/dist/chunk-VI4VSNTI.js.map +1 -0
- package/dist/chunk-VWCN5PMD.js +268 -0
- package/dist/chunk-VWCN5PMD.js.map +1 -0
- package/dist/chunk-XC2YD6CF.js +913 -0
- package/dist/chunk-XC2YD6CF.js.map +1 -0
- package/dist/chunk-XE4OXN2W.js +10 -0
- package/dist/chunk-XE4OXN2W.js.map +1 -0
- package/dist/chunk-YMZEUTD2.js +110 -0
- package/dist/chunk-YMZEUTD2.js.map +1 -0
- package/dist/chunk-Z4IRLBWO.js +758 -0
- package/dist/chunk-Z4IRLBWO.js.map +1 -0
- package/dist/client/internal-client.d.ts +7 -0
- package/dist/client/internal-client.js +5 -0
- package/dist/client/internal-client.js.map +1 -0
- package/dist/client-CSjIrYKS.d.ts +77 -0
- package/dist/config-UKE2E3BW.js +4 -0
- package/dist/config-UKE2E3BW.js.map +1 -0
- package/dist/crypto/index.d.ts +1 -0
- package/dist/crypto/index.js +4 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/database/index.d.ts +117 -0
- package/dist/database/index.js +7 -0
- package/dist/database/index.js.map +1 -0
- package/dist/dataloader/index.d.ts +1 -0
- package/dist/dataloader/index.js +4 -0
- package/dist/dataloader/index.js.map +1 -0
- package/dist/esm-K6H7EIWK.js +1450 -0
- package/dist/esm-K6H7EIWK.js.map +1 -0
- package/dist/event-queue/index.d.ts +6 -0
- package/dist/event-queue/index.js +10 -0
- package/dist/event-queue/index.js.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.js +9 -0
- package/dist/export/index.js.map +1 -0
- package/dist/feature-flags/index.d.ts +1 -0
- package/dist/feature-flags/index.js +4 -0
- package/dist/feature-flags/index.js.map +1 -0
- package/dist/gateway/index.d.ts +4 -0
- package/dist/gateway/index.js +8 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/security/index.d.ts +2 -0
- package/dist/gateway/security/index.js +5 -0
- package/dist/gateway/security/index.js.map +1 -0
- package/dist/graph/index.d.ts +9 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph-emitter-Bk0FqD53.d.ts +47 -0
- package/dist/graphql/index.d.ts +2 -0
- package/dist/graphql/index.js +7 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/services/index.d.ts +24 -0
- package/dist/graphql/services/index.js +5 -0
- package/dist/graphql/services/index.js.map +1 -0
- package/dist/graphql-BAVrUgBF.d.ts +22 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +5 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n-DA6IPnYy.d.ts +101 -0
- package/dist/index-4gKQKZ2B.d.ts +71 -0
- package/dist/index-B18rrFru.d.ts +42 -0
- package/dist/index-B70b5mYS.d.ts +33 -0
- package/dist/index-BMq3lFjj.d.ts +247 -0
- package/dist/index-BODgvRtL.d.ts +316 -0
- package/dist/index-BS2idbnr.d.ts +874 -0
- package/dist/index-BV3G48cH.d.ts +124 -0
- package/dist/index-CHbVw5M7.d.ts +73 -0
- package/dist/index-Cq_xNjcv.d.ts +602 -0
- package/dist/index-CrQgidpx.d.ts +77 -0
- package/dist/index-CyVEg86m.d.ts +309 -0
- package/dist/index-DOcBnmMe.d.ts +166 -0
- package/dist/index-DSOpj8DI.d.ts +17 -0
- package/dist/index-DW9q22GH.d.ts +75 -0
- package/dist/index-DlFDFyWD.d.ts +55 -0
- package/dist/index-DoK0bjhH.d.ts +607 -0
- package/dist/index-Dtf6FBaD.d.ts +199 -0
- package/dist/index-KVWGHMbS.d.ts +415 -0
- package/dist/index-PaojfSdd.d.ts +71 -0
- package/dist/index-RU_M5c74.d.ts +163 -0
- package/dist/index-UhkH2eaB.d.ts +192 -0
- package/dist/index-e_18DzAg.d.ts +18 -0
- package/dist/index-jOuh_TKn.d.ts +59 -0
- package/dist/index.d.ts +584 -0
- package/dist/index.js +2626 -0
- package/dist/index.js.map +1 -0
- package/dist/init/index.d.ts +2 -0
- package/dist/init/index.js +12 -0
- package/dist/init/index.js.map +1 -0
- package/dist/instrumentation/index.d.ts +3 -0
- package/dist/instrumentation/index.js +5 -0
- package/dist/instrumentation/index.js.map +1 -0
- package/dist/layer/index.d.ts +2 -0
- package/dist/layer/index.js +6 -0
- package/dist/layer/index.js.map +1 -0
- package/dist/layer-DDip18KY.d.ts +67 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +4 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/notifications/index.d.ts +4 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/observability/index.d.ts +3 -0
- package/dist/observability/index.js +5 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/pino-logger-RYLGXPHZ.js +5 -0
- package/dist/pino-logger-RYLGXPHZ.js.map +1 -0
- package/dist/prisma/generator/index.d.ts +1 -0
- package/dist/prisma/generator/index.js +228 -0
- package/dist/prisma/generator/index.js.map +1 -0
- package/dist/prisma/index.d.ts +5 -0
- package/dist/prisma/index.js +12 -0
- package/dist/prisma/index.js.map +1 -0
- package/dist/prisma-extension-krXnYnrN.d.ts +30 -0
- package/dist/rbac/index.d.ts +116 -0
- package/dist/rbac/index.js +7 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/sdk-THO3IWEE.js +136 -0
- package/dist/sdk-THO3IWEE.js.map +1 -0
- package/dist/server/index.d.ts +359 -0
- package/dist/server/index.js +884 -0
- package/dist/server/index.js.map +1 -0
- package/dist/telemetry/index.d.ts +83 -0
- package/dist/telemetry/index.js +209 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/temporal/index.d.ts +148 -0
- package/dist/temporal/index.js +6 -0
- package/dist/temporal/index.js.map +1 -0
- package/dist/tracker/index.d.ts +5 -0
- package/dist/tracker/index.js +9 -0
- package/dist/tracker/index.js.map +1 -0
- package/dist/types-Bo2wkjSP.d.ts +35 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/workspace-guard-LHBNK4TG.js +5 -0
- package/dist/workspace-guard-LHBNK4TG.js.map +1 -0
- package/package.json +241 -0
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
import { ValidationError, PermissionDeniedError } from './chunk-SAG6ZYWQ.js';
|
|
2
|
+
import { DEFAULT_GATEWAY_ROUTING } from './chunk-NGMEI42S.js';
|
|
3
|
+
import { logger } from './chunk-CBZNP6RF.js';
|
|
4
|
+
import { __name } from './chunk-XE4OXN2W.js';
|
|
5
|
+
import { GraphQLError } from 'graphql';
|
|
6
|
+
import { GraphQLClient } from 'graphql-request';
|
|
7
|
+
|
|
8
|
+
var LRUCache = class LRUCache2 {
|
|
9
|
+
static {
|
|
10
|
+
__name(this, "LRUCache");
|
|
11
|
+
}
|
|
12
|
+
cache = /* @__PURE__ */ new Map();
|
|
13
|
+
maxSize;
|
|
14
|
+
ttl;
|
|
15
|
+
constructor(maxSize, ttl) {
|
|
16
|
+
this.maxSize = maxSize;
|
|
17
|
+
this.ttl = ttl;
|
|
18
|
+
}
|
|
19
|
+
get(key) {
|
|
20
|
+
const entry = this.cache.get(key);
|
|
21
|
+
if (!entry) return void 0;
|
|
22
|
+
if (Date.now() > entry.expiry) {
|
|
23
|
+
this.cache.delete(key);
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
this.cache.set(key, entry);
|
|
28
|
+
return entry.value;
|
|
29
|
+
}
|
|
30
|
+
set(key, value) {
|
|
31
|
+
if (this.cache.size >= this.maxSize) {
|
|
32
|
+
const firstKey = this.cache.keys().next().value;
|
|
33
|
+
if (firstKey !== void 0) {
|
|
34
|
+
this.cache.delete(firstKey);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this.cache.set(key, {
|
|
38
|
+
value,
|
|
39
|
+
expiry: Date.now() + this.ttl
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.cache.clear();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Invalidate all entries whose key starts with the given prefix.
|
|
47
|
+
* Used to evict all cached permissions for a specific actor.
|
|
48
|
+
*/
|
|
49
|
+
invalidateByPrefix(prefix) {
|
|
50
|
+
let count = 0;
|
|
51
|
+
for (const key of [
|
|
52
|
+
...this.cache.keys()
|
|
53
|
+
]) {
|
|
54
|
+
if (typeof key === "string" && key.startsWith(prefix)) {
|
|
55
|
+
this.cache.delete(key);
|
|
56
|
+
count++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var RBACClient = class RBACClient2 {
|
|
63
|
+
static {
|
|
64
|
+
__name(this, "RBACClient");
|
|
65
|
+
}
|
|
66
|
+
client;
|
|
67
|
+
cache;
|
|
68
|
+
config;
|
|
69
|
+
constructor(config) {
|
|
70
|
+
this.client = new GraphQLClient(config.rbacServiceUrl);
|
|
71
|
+
this.config = {
|
|
72
|
+
failClosed: config.failClosed !== false,
|
|
73
|
+
layer: config.layer || "wspace"
|
|
74
|
+
};
|
|
75
|
+
if (config.enableCaching !== false) {
|
|
76
|
+
this.cache = new LRUCache(
|
|
77
|
+
config.cacheMaxSize || 1e4,
|
|
78
|
+
config.cacheTTL || 6e4
|
|
79
|
+
// 1 minute
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse permission string into resourceType and action
|
|
85
|
+
* @example "project:read" => { resourceType: "project", action: "read" }
|
|
86
|
+
* @example "rbac:role:create" => { resourceType: "rbac", action: "role:create" }
|
|
87
|
+
*/
|
|
88
|
+
parsePermission(permission) {
|
|
89
|
+
const colonIndex = permission.indexOf(":");
|
|
90
|
+
if (colonIndex === -1) {
|
|
91
|
+
return {
|
|
92
|
+
resourceType: permission,
|
|
93
|
+
action: "read"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
resourceType: permission.substring(0, colonIndex),
|
|
98
|
+
action: permission.substring(colonIndex + 1)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if actor has permission using the new PBAC checkPermission query
|
|
103
|
+
*/
|
|
104
|
+
async checkPermission(actorId, actorType, scopeId, permission, resourceId) {
|
|
105
|
+
const { resourceType, action } = this.parsePermission(permission);
|
|
106
|
+
const cacheKey = `${actorId}:${actorType}:${scopeId}:${resourceType}:${action}:${resourceId || ""}`;
|
|
107
|
+
if (this.cache) {
|
|
108
|
+
const cached = this.cache.get(cacheKey);
|
|
109
|
+
if (cached !== void 0) {
|
|
110
|
+
logger.debug("[RBAC Directive] Cache hit", {
|
|
111
|
+
actorId,
|
|
112
|
+
actorType,
|
|
113
|
+
scopeId,
|
|
114
|
+
resourceType,
|
|
115
|
+
action,
|
|
116
|
+
resourceId,
|
|
117
|
+
hasPermission: cached
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
hasPermission: cached,
|
|
121
|
+
reason: cached ? "Cached: Permission granted" : "Cached: Permission denied"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const CHECK_PERMISSION_QUERY = `
|
|
126
|
+
query CheckPermission($input: PermissionCheckInput!) {
|
|
127
|
+
checkPermission(input: $input) {
|
|
128
|
+
allowed
|
|
129
|
+
decisionPath
|
|
130
|
+
roleName
|
|
131
|
+
roleLevel
|
|
132
|
+
overrideType
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
const input = {
|
|
137
|
+
actorId,
|
|
138
|
+
actorType,
|
|
139
|
+
scopeId,
|
|
140
|
+
resourceType,
|
|
141
|
+
action,
|
|
142
|
+
resourceId
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
logger.debug("[RBAC Directive] Checking permission via RBAC service (PBAC)", {
|
|
146
|
+
input,
|
|
147
|
+
layer: this.config.layer
|
|
148
|
+
});
|
|
149
|
+
const result = await this.client.request(CHECK_PERMISSION_QUERY, {
|
|
150
|
+
input
|
|
151
|
+
});
|
|
152
|
+
const hasPermission = result.checkPermission.allowed;
|
|
153
|
+
const decisionPath = result.checkPermission.decisionPath;
|
|
154
|
+
if (this.cache) {
|
|
155
|
+
this.cache.set(cacheKey, hasPermission);
|
|
156
|
+
}
|
|
157
|
+
logger.debug("[RBAC Directive] Permission check result", {
|
|
158
|
+
actorId,
|
|
159
|
+
resourceType,
|
|
160
|
+
action,
|
|
161
|
+
hasPermission,
|
|
162
|
+
decisionPath
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
hasPermission,
|
|
166
|
+
reason: decisionPath
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.error("[RBAC Directive] RBAC service error", {
|
|
170
|
+
error,
|
|
171
|
+
input,
|
|
172
|
+
layer: this.config.layer
|
|
173
|
+
});
|
|
174
|
+
if (this.config.failClosed) {
|
|
175
|
+
return {
|
|
176
|
+
hasPermission: false,
|
|
177
|
+
reason: "RBAC service unavailable - access denied (fail-closed)"
|
|
178
|
+
};
|
|
179
|
+
} else {
|
|
180
|
+
logger.warn("[RBAC Directive] Fail-open mode: allowing access despite RBAC error");
|
|
181
|
+
return {
|
|
182
|
+
hasPermission: true,
|
|
183
|
+
reason: "RBAC service unavailable - access allowed (fail-open)"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Clear permission cache (useful for testing or when permissions change)
|
|
190
|
+
*/
|
|
191
|
+
clearCache() {
|
|
192
|
+
this.cache?.clear();
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Invalidate cached permissions for a specific actor.
|
|
196
|
+
* Call this when an actor's roles or permissions change to avoid stale cache hits.
|
|
197
|
+
*
|
|
198
|
+
* @param actorId - The actor whose cached permissions should be evicted
|
|
199
|
+
* @returns Number of cache entries invalidated
|
|
200
|
+
*/
|
|
201
|
+
invalidateForActor(actorId) {
|
|
202
|
+
if (!this.cache) return 0;
|
|
203
|
+
const count = this.cache.invalidateByPrefix(`${actorId}:`);
|
|
204
|
+
if (count > 0) {
|
|
205
|
+
logger.info("[RBAC Directive] Invalidated cached permissions for actor", {
|
|
206
|
+
actorId,
|
|
207
|
+
entriesRemoved: count
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the configured layer
|
|
214
|
+
*/
|
|
215
|
+
getLayer() {
|
|
216
|
+
return this.config.layer;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
function getValueAtPath(obj, path) {
|
|
220
|
+
const parts = path.split(".");
|
|
221
|
+
let current = obj;
|
|
222
|
+
for (const part of parts) {
|
|
223
|
+
if (current === null || current === void 0) {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
if (typeof current !== "object") {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
current = current[part];
|
|
230
|
+
}
|
|
231
|
+
return typeof current === "string" ? current : void 0;
|
|
232
|
+
}
|
|
233
|
+
__name(getValueAtPath, "getValueAtPath");
|
|
234
|
+
function getDirectiveArgument(directive, name) {
|
|
235
|
+
const arg = directive.arguments?.find((a) => a.name.value === name);
|
|
236
|
+
if (arg && arg.value.kind === "StringValue") {
|
|
237
|
+
return arg.value.value;
|
|
238
|
+
}
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
241
|
+
__name(getDirectiveArgument, "getDirectiveArgument");
|
|
242
|
+
function parseFieldArguments(field, variableValues) {
|
|
243
|
+
const args = {};
|
|
244
|
+
for (const arg of field.arguments || []) {
|
|
245
|
+
const argName = arg.name.value;
|
|
246
|
+
const argValue = arg.value;
|
|
247
|
+
if (argValue.kind === "Variable") {
|
|
248
|
+
args[argName] = variableValues[argValue.name.value];
|
|
249
|
+
} else if (argValue.kind === "ObjectValue") {
|
|
250
|
+
args[argName] = parseObjectValue(argValue, variableValues);
|
|
251
|
+
} else if ("value" in argValue) {
|
|
252
|
+
args[argName] = argValue.value;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return args;
|
|
256
|
+
}
|
|
257
|
+
__name(parseFieldArguments, "parseFieldArguments");
|
|
258
|
+
function parseObjectValue(obj, variableValues) {
|
|
259
|
+
const result = {};
|
|
260
|
+
for (const field of obj.fields || []) {
|
|
261
|
+
const fieldName = field.name.value;
|
|
262
|
+
const fieldValue = field.value;
|
|
263
|
+
if (fieldValue.kind === "Variable") {
|
|
264
|
+
result[fieldName] = variableValues[fieldValue.name.value];
|
|
265
|
+
} else if (fieldValue.kind === "ObjectValue") {
|
|
266
|
+
result[fieldName] = parseObjectValue(fieldValue, variableValues);
|
|
267
|
+
} else if ("value" in fieldValue) {
|
|
268
|
+
result[fieldName] = fieldValue.value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
__name(parseObjectValue, "parseObjectValue");
|
|
274
|
+
function toActorId(userId) {
|
|
275
|
+
return userId;
|
|
276
|
+
}
|
|
277
|
+
__name(toActorId, "toActorId");
|
|
278
|
+
function sanitizeHeadersForLogging(headers) {
|
|
279
|
+
const sanitized = {};
|
|
280
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
281
|
+
const lowerKey = key.toLowerCase();
|
|
282
|
+
if (lowerKey.includes("auth") || lowerKey.includes("token") || lowerKey.includes("password") || lowerKey.includes("secret") || lowerKey.includes("key") || lowerKey.includes("credential")) {
|
|
283
|
+
sanitized[key] = "***REDACTED***";
|
|
284
|
+
} else {
|
|
285
|
+
sanitized[key] = value;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return sanitized;
|
|
289
|
+
}
|
|
290
|
+
__name(sanitizeHeadersForLogging, "sanitizeHeadersForLogging");
|
|
291
|
+
function getHeader(context, key) {
|
|
292
|
+
if (context.request?.headers) {
|
|
293
|
+
if (typeof context.request.headers.get === "function") {
|
|
294
|
+
const value2 = context.request.headers.get(key);
|
|
295
|
+
if (value2) return value2;
|
|
296
|
+
}
|
|
297
|
+
const requestHeaders = context.request.headers;
|
|
298
|
+
const value = requestHeaders[key];
|
|
299
|
+
if (value) return value;
|
|
300
|
+
}
|
|
301
|
+
if (context.headers) {
|
|
302
|
+
const contextHeaders = context.headers;
|
|
303
|
+
const value = contextHeaders[key];
|
|
304
|
+
if (value) return value;
|
|
305
|
+
}
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
__name(getHeader, "getHeader");
|
|
309
|
+
function validateBypassTimestamp(timestamp) {
|
|
310
|
+
try {
|
|
311
|
+
const timestampMs = parseInt(timestamp, 10);
|
|
312
|
+
if (isNaN(timestampMs)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const age = now - timestampMs;
|
|
317
|
+
const MAX_AGE = 60 * 1e3;
|
|
318
|
+
if (age > MAX_AGE || age < 0) {
|
|
319
|
+
logger.warn("[RBAC Directive] Stale bypass timestamp", {
|
|
320
|
+
age,
|
|
321
|
+
timestamp: timestampMs,
|
|
322
|
+
now
|
|
323
|
+
});
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
329
|
+
logger.warn("[RBAC Directive] Invalid bypass timestamp format", {
|
|
330
|
+
timestamp,
|
|
331
|
+
error: errorMessage
|
|
332
|
+
});
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
__name(validateBypassTimestamp, "validateBypassTimestamp");
|
|
337
|
+
function checkBypassHeaders(context) {
|
|
338
|
+
const actorType = getHeader(context, "x-actor-type");
|
|
339
|
+
const actorId = getHeader(context, "x-actor-id");
|
|
340
|
+
if (actorType === "service" || actorType === "super_admin") {
|
|
341
|
+
logger.debug("[RBAC Directive] Actor type bypass - service/super_admin", {
|
|
342
|
+
actorType,
|
|
343
|
+
actorId
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
shouldBypass: true
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
logger.debug("[RBAC Directive] Checking bypass headers - context structure", {
|
|
350
|
+
hasRequest: !!context.request,
|
|
351
|
+
hasRequestHeaders: !!context.request?.headers,
|
|
352
|
+
hasContextHeaders: !!context.headers,
|
|
353
|
+
requestHeadersType: context.request?.headers ? typeof context.request.headers : "undefined",
|
|
354
|
+
contextHeadersType: context.headers ? typeof context.headers : "undefined",
|
|
355
|
+
actorType,
|
|
356
|
+
actorId
|
|
357
|
+
});
|
|
358
|
+
const allHeaders = {};
|
|
359
|
+
if (context.request?.headers) {
|
|
360
|
+
if (typeof context.request.headers.get === "function") {
|
|
361
|
+
logger.debug("[RBAC Directive] Headers object has get method");
|
|
362
|
+
} else {
|
|
363
|
+
const headers = context.request.headers;
|
|
364
|
+
Object.keys(headers).forEach((key) => {
|
|
365
|
+
const value = headers[key];
|
|
366
|
+
if (value) allHeaders[key] = value;
|
|
367
|
+
});
|
|
368
|
+
const sanitizedHeaders = sanitizeHeadersForLogging(allHeaders);
|
|
369
|
+
logger.debug("[RBAC Directive] Request headers (plain object)", {
|
|
370
|
+
headers: sanitizedHeaders
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (context.headers) {
|
|
375
|
+
const headers = context.headers;
|
|
376
|
+
const contextHeadersObj = {};
|
|
377
|
+
Object.keys(headers).forEach((key) => {
|
|
378
|
+
const value = headers[key];
|
|
379
|
+
if (value) contextHeadersObj[key] = value;
|
|
380
|
+
});
|
|
381
|
+
const sanitizedHeaders = sanitizeHeadersForLogging(contextHeadersObj);
|
|
382
|
+
logger.debug("[RBAC Directive] Context headers", {
|
|
383
|
+
headers: sanitizedHeaders
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const bypassHeader = getHeader(context, "x-rbac-bypass");
|
|
387
|
+
const bypassSource = getHeader(context, "x-bypass-source");
|
|
388
|
+
const bypassTimestamp = getHeader(context, "x-bypass-timestamp");
|
|
389
|
+
logger.debug("[RBAC Directive] Extracted bypass headers", {
|
|
390
|
+
bypassHeader,
|
|
391
|
+
bypassSource,
|
|
392
|
+
bypassTimestamp
|
|
393
|
+
});
|
|
394
|
+
const bypassAttempted = !!(bypassHeader || bypassSource || bypassTimestamp);
|
|
395
|
+
if (!bypassHeader || !bypassSource || !bypassTimestamp) {
|
|
396
|
+
logger.debug("[RBAC Directive] Missing bypass headers - bypass NOT granted", {
|
|
397
|
+
hasRbacBypass: !!bypassHeader,
|
|
398
|
+
hasBypassSource: !!bypassSource,
|
|
399
|
+
hasBypassTimestamp: !!bypassTimestamp
|
|
400
|
+
});
|
|
401
|
+
if (bypassAttempted) {
|
|
402
|
+
return {
|
|
403
|
+
shouldBypass: false,
|
|
404
|
+
bypassContext: {
|
|
405
|
+
bypassAttempted: true,
|
|
406
|
+
bypassSource,
|
|
407
|
+
bypassTimestamp,
|
|
408
|
+
bypassFailureReason: "Missing required bypass headers"
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
shouldBypass: false
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (bypassHeader !== "true") {
|
|
417
|
+
logger.warn("[RBAC Directive] Invalid bypass header value", {
|
|
418
|
+
value: bypassHeader
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
shouldBypass: false,
|
|
422
|
+
bypassContext: {
|
|
423
|
+
bypassAttempted: true,
|
|
424
|
+
bypassSource,
|
|
425
|
+
bypassTimestamp,
|
|
426
|
+
bypassFailureReason: `Invalid bypass header value: ${bypassHeader}`
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (!validateBypassTimestamp(bypassTimestamp)) {
|
|
431
|
+
logger.warn("[RBAC Directive] Bypass rejected: stale timestamp", {
|
|
432
|
+
source: bypassSource,
|
|
433
|
+
timestamp: bypassTimestamp
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
shouldBypass: false,
|
|
437
|
+
bypassContext: {
|
|
438
|
+
bypassAttempted: true,
|
|
439
|
+
bypassSource,
|
|
440
|
+
bypassTimestamp,
|
|
441
|
+
bypassFailureReason: "Stale or invalid timestamp (replay attack prevention)"
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
logger.debug("[RBAC Directive] Valid bypass headers detected", {
|
|
446
|
+
source: bypassSource,
|
|
447
|
+
timestamp: bypassTimestamp
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
shouldBypass: true
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
__name(checkBypassHeaders, "checkBypassHeaders");
|
|
454
|
+
function verifyAuthentication(contextValue, fieldName, permission) {
|
|
455
|
+
const ctx = contextValue;
|
|
456
|
+
const user = ctx.user ?? (ctx.actor?.id ? {
|
|
457
|
+
id: ctx.actor.id,
|
|
458
|
+
email: ctx.actor.email,
|
|
459
|
+
workspace_id: ctx.workspace?.id
|
|
460
|
+
} : ctx.actorId ? {
|
|
461
|
+
id: ctx.actorId,
|
|
462
|
+
workspace_id: ctx.workspace?.id
|
|
463
|
+
} : void 0);
|
|
464
|
+
logger.debug("[RBAC Directive] Checking authentication", {
|
|
465
|
+
hasUser: !!user,
|
|
466
|
+
userId: user?.id,
|
|
467
|
+
fieldName
|
|
468
|
+
});
|
|
469
|
+
if (!user?.id) {
|
|
470
|
+
logger.warn("[RBAC Directive] Authentication required but user not found", {
|
|
471
|
+
fieldName,
|
|
472
|
+
permission
|
|
473
|
+
});
|
|
474
|
+
throw new GraphQLError("Authentication required", {
|
|
475
|
+
extensions: {
|
|
476
|
+
code: "UNAUTHENTICATED",
|
|
477
|
+
field: fieldName,
|
|
478
|
+
permission
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return user;
|
|
483
|
+
}
|
|
484
|
+
__name(verifyAuthentication, "verifyAuthentication");
|
|
485
|
+
async function resolveResourceId(resourceIdField, selection, variableValues, config, _rbacClient, _ctx, permission, fieldName) {
|
|
486
|
+
let resourceId;
|
|
487
|
+
const fieldArgs = parseFieldArguments(selection, variableValues || {});
|
|
488
|
+
if (resourceIdField) {
|
|
489
|
+
resourceId = getValueAtPath(fieldArgs, resourceIdField);
|
|
490
|
+
if (!resourceId) {
|
|
491
|
+
logger.error("[RBAC Directive] Resource ID not found at specified path", {
|
|
492
|
+
resourceIdField,
|
|
493
|
+
fieldName,
|
|
494
|
+
permission,
|
|
495
|
+
availableArgs: Object.keys(fieldArgs)
|
|
496
|
+
});
|
|
497
|
+
throw new GraphQLError(`Resource ID not found at specified path "${resourceIdField}" for permission "${permission}"`, {
|
|
498
|
+
extensions: {
|
|
499
|
+
code: "INVALID_RESOURCE_CONFIGURATION",
|
|
500
|
+
field: fieldName,
|
|
501
|
+
permission,
|
|
502
|
+
resourceIdField,
|
|
503
|
+
hint: `Check that the field path "${resourceIdField}" exists in your arguments`
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
logger.debug("[RBAC Directive] Using explicit resourceIdField", {
|
|
508
|
+
resourceIdField,
|
|
509
|
+
resourceId,
|
|
510
|
+
fieldName
|
|
511
|
+
});
|
|
512
|
+
} else {
|
|
513
|
+
const defaultPaths = [
|
|
514
|
+
"id",
|
|
515
|
+
"input.id"
|
|
516
|
+
];
|
|
517
|
+
for (const path of defaultPaths) {
|
|
518
|
+
resourceId = getValueAtPath(fieldArgs, path);
|
|
519
|
+
if (resourceId) {
|
|
520
|
+
logger.debug("[RBAC Directive] Auto-resolved resource ID from convention", {
|
|
521
|
+
path,
|
|
522
|
+
resourceId,
|
|
523
|
+
fieldName,
|
|
524
|
+
permission
|
|
525
|
+
});
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!resourceId) {
|
|
530
|
+
const contextWorkspaceId = _ctx.workspace?.id;
|
|
531
|
+
if (contextWorkspaceId) {
|
|
532
|
+
resourceId = contextWorkspaceId;
|
|
533
|
+
logger.debug("[RBAC Directive] Using workspace ID from context headers", {
|
|
534
|
+
resourceId,
|
|
535
|
+
fieldName,
|
|
536
|
+
permission
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
logger.error("[RBAC Directive] No resource ID available for permission check", {
|
|
540
|
+
permission,
|
|
541
|
+
fieldName,
|
|
542
|
+
triedPaths: defaultPaths,
|
|
543
|
+
availableArgs: Object.keys(fieldArgs)
|
|
544
|
+
});
|
|
545
|
+
throw new GraphQLError(`Cannot check permission "${permission}": No resource ID available. Tried default paths: ${defaultPaths.join(", ")}. Either provide one of these fields in your arguments, specify resourceIdField in the directive, or pass x-workspace-id header.`, {
|
|
546
|
+
extensions: {
|
|
547
|
+
code: "INVALID_RESOURCE_CONFIGURATION",
|
|
548
|
+
field: fieldName,
|
|
549
|
+
permission,
|
|
550
|
+
triedPaths: defaultPaths,
|
|
551
|
+
hint: 'Add resourceIdField parameter to @requiresPermission directive (e.g., resourceIdField: "input.workspaceId") or pass x-workspace-id header'
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (resourceId && config.resourceResolver) {
|
|
558
|
+
const resolved = await config.resourceResolver(resourceId, permission.split(":")[0]);
|
|
559
|
+
if (!resolved) {
|
|
560
|
+
logger.warn("[RBAC Directive] Custom resource resolver returned null", {
|
|
561
|
+
originalResourceId: resourceId,
|
|
562
|
+
permission,
|
|
563
|
+
fieldName
|
|
564
|
+
});
|
|
565
|
+
throw new GraphQLError("Resource not found or invalid", {
|
|
566
|
+
extensions: {
|
|
567
|
+
code: "FORBIDDEN",
|
|
568
|
+
field: fieldName,
|
|
569
|
+
permission,
|
|
570
|
+
resourceId,
|
|
571
|
+
hint: "Resource resolver could not resolve the resource ID"
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
logger.debug("[RBAC Directive] Custom resource resolver applied", {
|
|
576
|
+
originalResourceId: resourceId,
|
|
577
|
+
resolvedResourceId: resolved,
|
|
578
|
+
fieldName
|
|
579
|
+
});
|
|
580
|
+
return resolved;
|
|
581
|
+
}
|
|
582
|
+
return resourceId;
|
|
583
|
+
}
|
|
584
|
+
__name(resolveResourceId, "resolveResourceId");
|
|
585
|
+
function extractScopeId(ctx, layer) {
|
|
586
|
+
if (layer === "wspace") {
|
|
587
|
+
const workspaceId = getHeader(ctx, "x-workspace-id") || ctx.workspace?.id;
|
|
588
|
+
if (!workspaceId) {
|
|
589
|
+
throw new GraphQLError("Workspace ID required for workspace-layer RBAC", {
|
|
590
|
+
extensions: {
|
|
591
|
+
code: "BAD_REQUEST",
|
|
592
|
+
hint: "Pass x-workspace-id header or ensure workspace context is set"
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return workspaceId;
|
|
597
|
+
} else {
|
|
598
|
+
const tenantId = getHeader(ctx, "x-tenant-id") || ctx.tenant?.id || "global";
|
|
599
|
+
return tenantId;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
__name(extractScopeId, "extractScopeId");
|
|
603
|
+
function extractActorType(ctx) {
|
|
604
|
+
const actorTypeHeader = getHeader(ctx, "x-actor-type");
|
|
605
|
+
if (actorTypeHeader) {
|
|
606
|
+
const upperType = actorTypeHeader.toUpperCase();
|
|
607
|
+
if ([
|
|
608
|
+
"USER",
|
|
609
|
+
"BOT",
|
|
610
|
+
"INTEGRATION",
|
|
611
|
+
"DELEGATED_AGENT"
|
|
612
|
+
].includes(upperType)) {
|
|
613
|
+
return upperType;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return "USER";
|
|
617
|
+
}
|
|
618
|
+
__name(extractActorType, "extractActorType");
|
|
619
|
+
async function checkAndEnforcePermission(rbacClient, ctx, actorId, permission, resolvedResourceId, userId, fieldName, bypassContext) {
|
|
620
|
+
const layer = rbacClient.getLayer();
|
|
621
|
+
const actorType = extractActorType(ctx);
|
|
622
|
+
const scopeId = extractScopeId(ctx, layer);
|
|
623
|
+
logger.info("[RBAC Directive] Checking permission", {
|
|
624
|
+
userId,
|
|
625
|
+
actorId,
|
|
626
|
+
actorType,
|
|
627
|
+
scopeId,
|
|
628
|
+
permission,
|
|
629
|
+
resourceId: resolvedResourceId,
|
|
630
|
+
fieldName,
|
|
631
|
+
layer,
|
|
632
|
+
bypassAttempted: bypassContext?.bypassAttempted ?? false
|
|
633
|
+
});
|
|
634
|
+
const { hasPermission, reason } = await rbacClient.checkPermission(actorId, actorType, scopeId, permission, resolvedResourceId);
|
|
635
|
+
if (!hasPermission) {
|
|
636
|
+
logger.warn("[RBAC Directive] Permission denied", {
|
|
637
|
+
userId,
|
|
638
|
+
actorType,
|
|
639
|
+
scopeId,
|
|
640
|
+
permission,
|
|
641
|
+
reason,
|
|
642
|
+
fieldName,
|
|
643
|
+
bypassContext
|
|
644
|
+
});
|
|
645
|
+
const reasonSuffix = reason ? ` - ${reason}` : "";
|
|
646
|
+
const errorMessage = `Forbidden: Missing permission "${permission}"${reasonSuffix}`;
|
|
647
|
+
const extensions = {
|
|
648
|
+
code: "FORBIDDEN",
|
|
649
|
+
permission,
|
|
650
|
+
reason,
|
|
651
|
+
field: fieldName,
|
|
652
|
+
resourceId: resolvedResourceId,
|
|
653
|
+
scopeId,
|
|
654
|
+
layer
|
|
655
|
+
};
|
|
656
|
+
if (bypassContext?.bypassAttempted) {
|
|
657
|
+
extensions["bypassAttempted"] = true;
|
|
658
|
+
extensions["bypassSource"] = bypassContext.bypassSource;
|
|
659
|
+
extensions["bypassTimestamp"] = bypassContext.bypassTimestamp;
|
|
660
|
+
extensions["bypassFailureReason"] = bypassContext.bypassFailureReason;
|
|
661
|
+
}
|
|
662
|
+
throw new GraphQLError(errorMessage, {
|
|
663
|
+
extensions
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
logger.info("[RBAC Directive] Permission granted", {
|
|
667
|
+
userId,
|
|
668
|
+
permission,
|
|
669
|
+
fieldName,
|
|
670
|
+
reason
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
__name(checkAndEnforcePermission, "checkAndEnforcePermission");
|
|
674
|
+
function createRBACDirective(config) {
|
|
675
|
+
const skipRbac = process.env["SKIP_RBAC"] === "true" || config.skipRbac === true;
|
|
676
|
+
if (skipRbac) {
|
|
677
|
+
const isProduction = process.env["NODE_ENV"] === "production";
|
|
678
|
+
if (isProduction) {
|
|
679
|
+
throw new Error("[RBAC Directive] FATAL: SKIP_RBAC cannot be enabled in production environments. This would bypass all authorization checks and is a critical security vulnerability. Remove SKIP_RBAC=true from your environment configuration.");
|
|
680
|
+
}
|
|
681
|
+
logger.warn("[RBAC Directive] \u26A0\uFE0F RBAC CHECKS ARE DISABLED (SKIP_RBAC=true) - DO NOT USE IN PRODUCTION!");
|
|
682
|
+
return {
|
|
683
|
+
async onExecute({ args }) {
|
|
684
|
+
const { document } = args;
|
|
685
|
+
const operation = document.definitions.find((def) => def.kind === "OperationDefinition");
|
|
686
|
+
const operationName = operation?.name?.value || "anonymous";
|
|
687
|
+
logger.warn("[RBAC Directive] RBAC check skipped for operation", {
|
|
688
|
+
operationName
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const rbacClient = new RBACClient(config);
|
|
694
|
+
return {
|
|
695
|
+
// 🔒 CRITICAL: Use onExecute (not onExecuteDone) to block BEFORE execution
|
|
696
|
+
async onExecute({ args }) {
|
|
697
|
+
logger.debug("[RBAC Directive] onExecute called");
|
|
698
|
+
const { schema, document, contextValue, variableValues } = args;
|
|
699
|
+
const operation = document.definitions.find((def) => def.kind === "OperationDefinition");
|
|
700
|
+
if (!operation) {
|
|
701
|
+
logger.debug("[RBAC Directive] No operation definition found, skipping");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const typeName = operation.operation === "mutation" ? "Mutation" : "Query";
|
|
705
|
+
const type = schema.getType(typeName);
|
|
706
|
+
if (!type || !("getFields" in type)) {
|
|
707
|
+
logger.debug(`[RBAC Directive] Type ${typeName} not found, skipping`);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const fields = type.getFields();
|
|
711
|
+
for (const selection of operation.selectionSet.selections) {
|
|
712
|
+
if (selection.kind !== "Field") {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const fieldName = selection.name.value;
|
|
716
|
+
const fieldDef = fields[fieldName];
|
|
717
|
+
if (!fieldDef?.astNode) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const directive = fieldDef.astNode.directives?.find((d) => d.name.value === "requiresPermission");
|
|
721
|
+
if (!directive) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const permission = getDirectiveArgument(directive, "permission");
|
|
725
|
+
const resourceIdField = getDirectiveArgument(directive, "resourceIdField");
|
|
726
|
+
if (!permission) {
|
|
727
|
+
throw new GraphQLError("Invalid @requiresPermission directive: permission argument is required and must be a string", {
|
|
728
|
+
extensions: {
|
|
729
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
730
|
+
field: fieldName
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
const ctx = contextValue;
|
|
735
|
+
const bypassCheckResult = checkBypassHeaders(ctx);
|
|
736
|
+
if (bypassCheckResult.shouldBypass) {
|
|
737
|
+
const bypassSource = getHeader(ctx, "x-bypass-source");
|
|
738
|
+
const bypassTimestamp = getHeader(ctx, "x-bypass-timestamp");
|
|
739
|
+
logger.info("[RBAC Directive] RBAC bypassed for internal service call", {
|
|
740
|
+
field: fieldName,
|
|
741
|
+
permission,
|
|
742
|
+
resource: resourceIdField,
|
|
743
|
+
bypassSource,
|
|
744
|
+
bypassTimestamp,
|
|
745
|
+
operationType: operation.operation
|
|
746
|
+
});
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const user = verifyAuthentication(contextValue, fieldName, permission);
|
|
750
|
+
let resolvedResourceId;
|
|
751
|
+
try {
|
|
752
|
+
resolvedResourceId = await resolveResourceId(resourceIdField, selection, variableValues, config, rbacClient, ctx, permission, fieldName);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (!resourceIdField) {
|
|
755
|
+
logger.debug("[RBAC Directive] No resource ID resolved, checking permission at scope level", {
|
|
756
|
+
fieldName,
|
|
757
|
+
permission
|
|
758
|
+
});
|
|
759
|
+
} else {
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const actorId = toActorId(user.id);
|
|
764
|
+
await checkAndEnforcePermission(rbacClient, ctx, actorId, permission, resolvedResourceId, user.id, fieldName, bypassCheckResult.bypassContext);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
__name(createRBACDirective, "createRBACDirective");
|
|
770
|
+
function buildRBACServiceUrl(layer) {
|
|
771
|
+
const routing = DEFAULT_GATEWAY_ROUTING[layer];
|
|
772
|
+
const gateway = layer === "wspace" ? routing.internalGateway : routing.crossLayerGateway;
|
|
773
|
+
return `http://${gateway.host}:${gateway.port}${gateway.graphqlPath}`;
|
|
774
|
+
}
|
|
775
|
+
__name(buildRBACServiceUrl, "buildRBACServiceUrl");
|
|
776
|
+
function createRBACDirectiveForLayer(layer, config) {
|
|
777
|
+
const envPrefix = layer.toUpperCase();
|
|
778
|
+
const rbacServiceUrl = config?.rbacServiceUrl || process.env[`${envPrefix}_RBAC_GATEWAY_URL`] || process.env["RBAC_GATEWAY_URL"] || buildRBACServiceUrl(layer);
|
|
779
|
+
const rbacLayer = layer;
|
|
780
|
+
logger.info("[RBAC Directive] Creating layer-aware RBAC directive", {
|
|
781
|
+
layer: rbacLayer,
|
|
782
|
+
rbacServiceUrl,
|
|
783
|
+
enableCaching: config?.enableCaching ?? true
|
|
784
|
+
});
|
|
785
|
+
return createRBACDirective({
|
|
786
|
+
rbacServiceUrl,
|
|
787
|
+
layer: rbacLayer,
|
|
788
|
+
enableCaching: config?.enableCaching ?? true,
|
|
789
|
+
cacheTTL: config?.cacheTTL,
|
|
790
|
+
cacheMaxSize: config?.cacheMaxSize,
|
|
791
|
+
timeout: config?.timeout,
|
|
792
|
+
failClosed: config?.failClosed,
|
|
793
|
+
resourceResolver: config?.resourceResolver,
|
|
794
|
+
skipRbac: config?.skipRbac
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
__name(createRBACDirectiveForLayer, "createRBACDirectiveForLayer");
|
|
798
|
+
function createRBACDirectiveFromEnv(serviceName, config) {
|
|
799
|
+
const layer = process.env["SERVICE_LAYER"] || (serviceName.startsWith("global-") ? "global" : "wspace");
|
|
800
|
+
return createRBACDirectiveForLayer(layer, config);
|
|
801
|
+
}
|
|
802
|
+
__name(createRBACDirectiveFromEnv, "createRBACDirectiveFromEnv");
|
|
803
|
+
|
|
804
|
+
// src/rbac/index.ts
|
|
805
|
+
var RBACModule = class {
|
|
806
|
+
static {
|
|
807
|
+
__name(this, "RBACModule");
|
|
808
|
+
}
|
|
809
|
+
client;
|
|
810
|
+
context;
|
|
811
|
+
constructor(client, context) {
|
|
812
|
+
this.client = client;
|
|
813
|
+
this.context = context;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Check if user has permission (returns boolean)
|
|
817
|
+
*/
|
|
818
|
+
async checkPermission(options) {
|
|
819
|
+
try {
|
|
820
|
+
await this.requirePermission(options);
|
|
821
|
+
return true;
|
|
822
|
+
} catch {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Require permission (throws if denied)
|
|
828
|
+
*/
|
|
829
|
+
async requirePermission(options) {
|
|
830
|
+
if (!options.userId) {
|
|
831
|
+
throw new ValidationError("User ID is required");
|
|
832
|
+
}
|
|
833
|
+
if (!options.action) {
|
|
834
|
+
throw new ValidationError("Action is required");
|
|
835
|
+
}
|
|
836
|
+
if (!options.resourceType) {
|
|
837
|
+
throw new ValidationError("Resource type is required");
|
|
838
|
+
}
|
|
839
|
+
logger.debug("[RBAC] Checking permission", options);
|
|
840
|
+
const query = `
|
|
841
|
+
query CheckPermission($input: PermissionCheckInput!) {
|
|
842
|
+
checkPermission(input: $input) {
|
|
843
|
+
allowed
|
|
844
|
+
decisionPath
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
`;
|
|
848
|
+
const variables = {
|
|
849
|
+
input: {
|
|
850
|
+
actorId: options.userId,
|
|
851
|
+
actorType: options.actorType || "USER",
|
|
852
|
+
scopeId: options.workspaceId || this.context.workspaceId || this.context.tenantId || "",
|
|
853
|
+
resourceType: options.resourceType,
|
|
854
|
+
action: options.action,
|
|
855
|
+
resourceId: options.resourceId
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
const response = options.workspaceId || this.context.workspaceId ? await this.client.query(query, variables, this.context) : await this.client.tenantQuery(query, variables, this.context);
|
|
859
|
+
if (!response.checkPermission.allowed) {
|
|
860
|
+
throw new PermissionDeniedError(`Permission denied: ${response.checkPermission.decisionPath}`, {
|
|
861
|
+
action: options.action,
|
|
862
|
+
resourceType: options.resourceType,
|
|
863
|
+
resourceId: options.resourceId,
|
|
864
|
+
userId: options.userId
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
logger.debug("[RBAC] Permission granted", {
|
|
868
|
+
...options,
|
|
869
|
+
decisionPath: response.checkPermission.decisionPath
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Get user roles in workspace
|
|
874
|
+
* Uses the new PBAC actorRoleAssignments query
|
|
875
|
+
*/
|
|
876
|
+
async getUserRoles(userId, workspaceId) {
|
|
877
|
+
logger.debug("[RBAC] Getting user roles", {
|
|
878
|
+
userId,
|
|
879
|
+
workspaceId
|
|
880
|
+
});
|
|
881
|
+
const query = `
|
|
882
|
+
query GetActorRoleAssignments($actorId: String!, $scopeId: String!) {
|
|
883
|
+
actorRoleAssignments(actorId: $actorId, scopeId: $scopeId) {
|
|
884
|
+
id
|
|
885
|
+
role {
|
|
886
|
+
id
|
|
887
|
+
name
|
|
888
|
+
description
|
|
889
|
+
defaultPermissions {
|
|
890
|
+
resourceType
|
|
891
|
+
level
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
`;
|
|
897
|
+
const variables = {
|
|
898
|
+
actorId: userId,
|
|
899
|
+
scopeId: workspaceId
|
|
900
|
+
};
|
|
901
|
+
const contextWithWorkspace = {
|
|
902
|
+
...this.context,
|
|
903
|
+
workspaceId
|
|
904
|
+
};
|
|
905
|
+
const response = await this.client.query(query, variables, contextWithWorkspace);
|
|
906
|
+
return response.actorRoleAssignments.map((assignment) => ({
|
|
907
|
+
id: assignment.role.id,
|
|
908
|
+
name: assignment.role.name,
|
|
909
|
+
description: assignment.role.description,
|
|
910
|
+
defaultPermissions: assignment.role.defaultPermissions
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Assign role to user
|
|
915
|
+
* Uses the new PBAC assignRoleToActor mutation
|
|
916
|
+
*/
|
|
917
|
+
async assignRoleToUser(userId, roleId, workspaceId, actorType = "USER") {
|
|
918
|
+
logger.debug("[RBAC] Assigning role to user", {
|
|
919
|
+
userId,
|
|
920
|
+
roleId,
|
|
921
|
+
workspaceId,
|
|
922
|
+
actorType
|
|
923
|
+
});
|
|
924
|
+
const mutation = `
|
|
925
|
+
mutation AssignRoleToActor($input: AssignRoleInput!) {
|
|
926
|
+
assignRoleToActor(input: $input) {
|
|
927
|
+
id
|
|
928
|
+
actorId
|
|
929
|
+
roleId
|
|
930
|
+
scopeId
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
`;
|
|
934
|
+
const variables = {
|
|
935
|
+
input: {
|
|
936
|
+
actorId: userId,
|
|
937
|
+
actorType,
|
|
938
|
+
roleId,
|
|
939
|
+
scopeId: workspaceId
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
const contextWithWorkspace = {
|
|
943
|
+
...this.context,
|
|
944
|
+
workspaceId
|
|
945
|
+
};
|
|
946
|
+
await this.client.query(mutation, variables, contextWithWorkspace);
|
|
947
|
+
logger.info("[RBAC] Role assigned to actor", {
|
|
948
|
+
userId,
|
|
949
|
+
roleId,
|
|
950
|
+
workspaceId
|
|
951
|
+
});
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Remove role from user
|
|
956
|
+
* Uses the new PBAC removeRoleFromActor mutation
|
|
957
|
+
* Note: This first finds the assignment ID, then removes it
|
|
958
|
+
*/
|
|
959
|
+
async removeRoleFromUser(userId, roleId, workspaceId) {
|
|
960
|
+
logger.debug("[RBAC] Removing role from user", {
|
|
961
|
+
userId,
|
|
962
|
+
roleId,
|
|
963
|
+
workspaceId
|
|
964
|
+
});
|
|
965
|
+
const contextWithWorkspace = {
|
|
966
|
+
...this.context,
|
|
967
|
+
workspaceId
|
|
968
|
+
};
|
|
969
|
+
const findQuery = `
|
|
970
|
+
query FindAssignment($actorId: String!, $scopeId: String!) {
|
|
971
|
+
actorRoleAssignments(actorId: $actorId, scopeId: $scopeId) {
|
|
972
|
+
id
|
|
973
|
+
roleId
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
`;
|
|
977
|
+
const findResponse = await this.client.query(findQuery, {
|
|
978
|
+
actorId: userId,
|
|
979
|
+
scopeId: workspaceId
|
|
980
|
+
}, contextWithWorkspace);
|
|
981
|
+
const assignment = findResponse.actorRoleAssignments.find((a) => a.roleId === roleId);
|
|
982
|
+
if (!assignment) {
|
|
983
|
+
logger.warn("[RBAC] No role assignment found to remove", {
|
|
984
|
+
userId,
|
|
985
|
+
roleId,
|
|
986
|
+
workspaceId
|
|
987
|
+
});
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
const mutation = `
|
|
991
|
+
mutation RemoveRoleFromActor($assignmentId: ID!) {
|
|
992
|
+
removeRoleFromActor(assignmentId: $assignmentId)
|
|
993
|
+
}
|
|
994
|
+
`;
|
|
995
|
+
await this.client.query(mutation, {
|
|
996
|
+
assignmentId: assignment.id
|
|
997
|
+
}, contextWithWorkspace);
|
|
998
|
+
logger.info("[RBAC] Role removed from actor", {
|
|
999
|
+
userId,
|
|
1000
|
+
roleId,
|
|
1001
|
+
workspaceId,
|
|
1002
|
+
assignmentId: assignment.id
|
|
1003
|
+
});
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Create custom role with default permissions
|
|
1008
|
+
*
|
|
1009
|
+
* Uses the new PBAC createRole mutation and setRoleDefaultPermission for permissions.
|
|
1010
|
+
*
|
|
1011
|
+
* @param name - Role name
|
|
1012
|
+
* @param description - Role description
|
|
1013
|
+
* @param defaultPermissions - Array of default permission configurations
|
|
1014
|
+
* (e.g., [{ resourceType: 'project', level: 'ADMIN' }])
|
|
1015
|
+
* OR legacy permission strings (converted to ADMIN level)
|
|
1016
|
+
* @param workspaceId - Workspace ID (scopeId)
|
|
1017
|
+
*/
|
|
1018
|
+
async createRole(name, description, defaultPermissions, workspaceId) {
|
|
1019
|
+
logger.debug("[RBAC] Creating role", {
|
|
1020
|
+
name,
|
|
1021
|
+
workspaceId,
|
|
1022
|
+
permissionCount: defaultPermissions.length
|
|
1023
|
+
});
|
|
1024
|
+
const contextWithWorkspace = {
|
|
1025
|
+
...this.context,
|
|
1026
|
+
workspaceId
|
|
1027
|
+
};
|
|
1028
|
+
const createRoleMutation = `
|
|
1029
|
+
mutation CreateRole($input: CreateRoleInput!) {
|
|
1030
|
+
createRole(input: $input) {
|
|
1031
|
+
id
|
|
1032
|
+
name
|
|
1033
|
+
description
|
|
1034
|
+
defaultPermissions {
|
|
1035
|
+
resourceType
|
|
1036
|
+
level
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
`;
|
|
1041
|
+
const createRoleVariables = {
|
|
1042
|
+
input: {
|
|
1043
|
+
name,
|
|
1044
|
+
description,
|
|
1045
|
+
scopeId: workspaceId,
|
|
1046
|
+
scopeType: "workspace"
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
const createResponse = await this.client.query(createRoleMutation, createRoleVariables, contextWithWorkspace);
|
|
1050
|
+
const roleId = createResponse.createRole.id;
|
|
1051
|
+
logger.info("[RBAC] Role created", {
|
|
1052
|
+
name,
|
|
1053
|
+
roleId
|
|
1054
|
+
});
|
|
1055
|
+
if (defaultPermissions.length > 0) {
|
|
1056
|
+
logger.debug("[RBAC] Setting default permissions for role", {
|
|
1057
|
+
roleId,
|
|
1058
|
+
count: defaultPermissions.length
|
|
1059
|
+
});
|
|
1060
|
+
const normalizedPermissions = defaultPermissions.map((perm) => {
|
|
1061
|
+
if (typeof perm === "string") {
|
|
1062
|
+
const resourceType = perm.split(":")[0] || perm;
|
|
1063
|
+
return {
|
|
1064
|
+
resourceType,
|
|
1065
|
+
level: "ADMIN"
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
return perm;
|
|
1069
|
+
});
|
|
1070
|
+
const permMap = /* @__PURE__ */ new Map();
|
|
1071
|
+
const levelPriority = {
|
|
1072
|
+
ADMIN: 4,
|
|
1073
|
+
EDITOR: 3,
|
|
1074
|
+
VIEWER: 2,
|
|
1075
|
+
NONE: 1
|
|
1076
|
+
};
|
|
1077
|
+
for (const perm of normalizedPermissions) {
|
|
1078
|
+
const existing = permMap.get(perm.resourceType);
|
|
1079
|
+
if (!existing || levelPriority[perm.level] > levelPriority[existing]) {
|
|
1080
|
+
permMap.set(perm.resourceType, perm.level);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const setPermissionMutation = `
|
|
1084
|
+
mutation SetRoleDefaultPermission($input: SetRoleDefaultPermissionInput!) {
|
|
1085
|
+
setRoleDefaultPermission(input: $input) {
|
|
1086
|
+
id
|
|
1087
|
+
defaultPermissions {
|
|
1088
|
+
resourceType
|
|
1089
|
+
level
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
`;
|
|
1094
|
+
const failedPermissions = [];
|
|
1095
|
+
for (const [resourceType, level] of permMap) {
|
|
1096
|
+
try {
|
|
1097
|
+
await this.client.query(setPermissionMutation, {
|
|
1098
|
+
input: {
|
|
1099
|
+
roleId,
|
|
1100
|
+
resourceType,
|
|
1101
|
+
level
|
|
1102
|
+
}
|
|
1103
|
+
}, contextWithWorkspace);
|
|
1104
|
+
logger.debug("[RBAC] Set default permission", {
|
|
1105
|
+
roleId,
|
|
1106
|
+
resourceType,
|
|
1107
|
+
level
|
|
1108
|
+
});
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1111
|
+
logger.error("[RBAC] Failed to set default permission", {
|
|
1112
|
+
roleId,
|
|
1113
|
+
resourceType,
|
|
1114
|
+
level,
|
|
1115
|
+
error: errorMsg
|
|
1116
|
+
});
|
|
1117
|
+
failedPermissions.push({
|
|
1118
|
+
resourceType,
|
|
1119
|
+
level,
|
|
1120
|
+
error: errorMsg
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (failedPermissions.length > 0) {
|
|
1125
|
+
const failedTypes = failedPermissions.map((p) => p.resourceType).join(", ");
|
|
1126
|
+
throw new ValidationError(`Role created but failed to set ${failedPermissions.length} default permission(s): ${failedTypes}. Role ID: ${roleId}. Check logs for details.`);
|
|
1127
|
+
}
|
|
1128
|
+
logger.info("[RBAC] Default permissions set for role", {
|
|
1129
|
+
roleId,
|
|
1130
|
+
count: permMap.size
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const roleQuery = `
|
|
1134
|
+
query GetRole($id: ID!) {
|
|
1135
|
+
role(id: $id) {
|
|
1136
|
+
id
|
|
1137
|
+
name
|
|
1138
|
+
description
|
|
1139
|
+
defaultPermissions {
|
|
1140
|
+
resourceType
|
|
1141
|
+
level
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
`;
|
|
1146
|
+
const roleResponse = await this.client.query(roleQuery, {
|
|
1147
|
+
id: roleId
|
|
1148
|
+
}, contextWithWorkspace);
|
|
1149
|
+
const role = roleResponse.role || createResponse.createRole;
|
|
1150
|
+
return {
|
|
1151
|
+
id: role.id,
|
|
1152
|
+
name: role.name,
|
|
1153
|
+
description: role.description,
|
|
1154
|
+
defaultPermissions: role.defaultPermissions
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Get all available action definitions (replaces getAvailablePermissions)
|
|
1159
|
+
* Returns actions organized by resource type
|
|
1160
|
+
*/
|
|
1161
|
+
async getAvailablePermissions() {
|
|
1162
|
+
logger.debug("[RBAC] Getting available action definitions");
|
|
1163
|
+
const resourceTypesQuery = `
|
|
1164
|
+
query GetResourceTypes {
|
|
1165
|
+
resourceTypes
|
|
1166
|
+
}
|
|
1167
|
+
`;
|
|
1168
|
+
const hasWorkspace = !!this.context.workspaceId;
|
|
1169
|
+
const resourceTypesResponse = hasWorkspace ? await this.client.query(resourceTypesQuery, {}, this.context) : await this.client.tenantQuery(resourceTypesQuery, {}, this.context);
|
|
1170
|
+
const permissions = [];
|
|
1171
|
+
const actionsQuery = `
|
|
1172
|
+
query GetActionDefinitions($resourceType: String!) {
|
|
1173
|
+
actionDefinitions(resourceType: $resourceType) {
|
|
1174
|
+
id
|
|
1175
|
+
action
|
|
1176
|
+
resourceType
|
|
1177
|
+
description
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
`;
|
|
1181
|
+
for (const resourceType of resourceTypesResponse.resourceTypes) {
|
|
1182
|
+
const actionsResponse = hasWorkspace ? await this.client.query(actionsQuery, {
|
|
1183
|
+
resourceType
|
|
1184
|
+
}, this.context) : await this.client.tenantQuery(actionsQuery, {
|
|
1185
|
+
resourceType
|
|
1186
|
+
}, this.context);
|
|
1187
|
+
for (const action of actionsResponse.actionDefinitions) {
|
|
1188
|
+
permissions.push({
|
|
1189
|
+
id: action.id,
|
|
1190
|
+
action: action.action,
|
|
1191
|
+
resourceType: action.resourceType,
|
|
1192
|
+
description: action.description
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return permissions;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Check multiple permissions at once
|
|
1200
|
+
* Uses the new PBAC checkPermissions query
|
|
1201
|
+
*/
|
|
1202
|
+
async checkMultiplePermissions(checks) {
|
|
1203
|
+
logger.debug("[RBAC] Checking multiple permissions", {
|
|
1204
|
+
count: checks.length
|
|
1205
|
+
});
|
|
1206
|
+
const query = `
|
|
1207
|
+
query CheckPermissions($inputs: [PermissionCheckInput!]!) {
|
|
1208
|
+
checkPermissions(inputs: $inputs) {
|
|
1209
|
+
allowed
|
|
1210
|
+
decisionPath
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
`;
|
|
1214
|
+
const inputs = checks.map((check) => ({
|
|
1215
|
+
actorId: check.userId,
|
|
1216
|
+
actorType: check.actorType || "USER",
|
|
1217
|
+
scopeId: check.workspaceId || this.context.workspaceId || this.context.tenantId || "",
|
|
1218
|
+
resourceType: check.resourceType,
|
|
1219
|
+
action: check.action,
|
|
1220
|
+
resourceId: check.resourceId
|
|
1221
|
+
}));
|
|
1222
|
+
const hasWorkspace = checks.some((c) => c.workspaceId) || !!this.context.workspaceId;
|
|
1223
|
+
const response = hasWorkspace ? await this.client.query(query, {
|
|
1224
|
+
inputs
|
|
1225
|
+
}, this.context) : await this.client.tenantQuery(query, {
|
|
1226
|
+
inputs
|
|
1227
|
+
}, this.context);
|
|
1228
|
+
return response.checkPermissions.map((result) => ({
|
|
1229
|
+
allowed: result.allowed,
|
|
1230
|
+
reason: result.decisionPath
|
|
1231
|
+
}));
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Get effective permissions for a user in a workspace
|
|
1235
|
+
* New method that uses the PBAC effectivePermissions query
|
|
1236
|
+
*/
|
|
1237
|
+
async getEffectivePermissions(userId, workspaceId, actorType = "USER") {
|
|
1238
|
+
logger.debug("[RBAC] Getting effective permissions", {
|
|
1239
|
+
userId,
|
|
1240
|
+
workspaceId,
|
|
1241
|
+
actorType
|
|
1242
|
+
});
|
|
1243
|
+
const query = `
|
|
1244
|
+
query EffectivePermissions($actorId: String!, $actorType: ActorType!, $scopeId: String!) {
|
|
1245
|
+
effectivePermissions(actorId: $actorId, actorType: $actorType, scopeId: $scopeId) {
|
|
1246
|
+
actorId
|
|
1247
|
+
scopeId
|
|
1248
|
+
permissions {
|
|
1249
|
+
resourceType
|
|
1250
|
+
level
|
|
1251
|
+
allowedActions
|
|
1252
|
+
source
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
`;
|
|
1257
|
+
const variables = {
|
|
1258
|
+
actorId: userId,
|
|
1259
|
+
actorType,
|
|
1260
|
+
scopeId: workspaceId
|
|
1261
|
+
};
|
|
1262
|
+
const contextWithWorkspace = {
|
|
1263
|
+
...this.context,
|
|
1264
|
+
workspaceId
|
|
1265
|
+
};
|
|
1266
|
+
const response = await this.client.query(query, variables, contextWithWorkspace);
|
|
1267
|
+
return response.effectivePermissions;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Initialize workspace roles by cloning system roles
|
|
1271
|
+
* New method for workspace initialization
|
|
1272
|
+
*/
|
|
1273
|
+
async initializeWorkspaceRoles(workspaceId, ownerId) {
|
|
1274
|
+
logger.debug("[RBAC] Initializing workspace roles", {
|
|
1275
|
+
workspaceId,
|
|
1276
|
+
ownerId
|
|
1277
|
+
});
|
|
1278
|
+
const mutation = `
|
|
1279
|
+
mutation InitializeWorkspaceRoles($workspaceId: String!, $ownerId: String!) {
|
|
1280
|
+
initializeWorkspaceRoles(workspaceId: $workspaceId, ownerId: $ownerId) {
|
|
1281
|
+
success
|
|
1282
|
+
rolesCreated
|
|
1283
|
+
roleNames
|
|
1284
|
+
ownerAssigned
|
|
1285
|
+
message
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
`;
|
|
1289
|
+
const contextWithWorkspace = {
|
|
1290
|
+
...this.context,
|
|
1291
|
+
workspaceId
|
|
1292
|
+
};
|
|
1293
|
+
const response = await this.client.query(mutation, {
|
|
1294
|
+
workspaceId,
|
|
1295
|
+
ownerId
|
|
1296
|
+
}, contextWithWorkspace);
|
|
1297
|
+
logger.info("[RBAC] Workspace roles initialized", response.initializeWorkspaceRoles);
|
|
1298
|
+
return response.initializeWorkspaceRoles;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Update an existing role
|
|
1302
|
+
* Uses the new PBAC updateRole mutation
|
|
1303
|
+
*
|
|
1304
|
+
* @param roleId - Role ID to update
|
|
1305
|
+
* @param updates - Fields to update (name, description, priority)
|
|
1306
|
+
* @param workspaceId - Workspace ID (scopeId)
|
|
1307
|
+
*/
|
|
1308
|
+
async updateRole(roleId, updates, workspaceId) {
|
|
1309
|
+
logger.debug("[RBAC] Updating role", {
|
|
1310
|
+
roleId,
|
|
1311
|
+
updates,
|
|
1312
|
+
workspaceId
|
|
1313
|
+
});
|
|
1314
|
+
const contextWithWorkspace = {
|
|
1315
|
+
...this.context,
|
|
1316
|
+
workspaceId
|
|
1317
|
+
};
|
|
1318
|
+
const mutation = `
|
|
1319
|
+
mutation UpdateRole($id: ID!, $input: UpdateRoleInput!) {
|
|
1320
|
+
updateRole(id: $id, input: $input) {
|
|
1321
|
+
id
|
|
1322
|
+
name
|
|
1323
|
+
description
|
|
1324
|
+
priority
|
|
1325
|
+
defaultPermissions {
|
|
1326
|
+
resourceType
|
|
1327
|
+
level
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
`;
|
|
1332
|
+
const response = await this.client.query(mutation, {
|
|
1333
|
+
id: roleId,
|
|
1334
|
+
input: updates
|
|
1335
|
+
}, contextWithWorkspace);
|
|
1336
|
+
logger.info("[RBAC] Role updated", {
|
|
1337
|
+
roleId,
|
|
1338
|
+
name: response.updateRole.name
|
|
1339
|
+
});
|
|
1340
|
+
return {
|
|
1341
|
+
id: response.updateRole.id,
|
|
1342
|
+
name: response.updateRole.name,
|
|
1343
|
+
description: response.updateRole.description,
|
|
1344
|
+
defaultPermissions: response.updateRole.defaultPermissions
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Delete a role
|
|
1349
|
+
* Uses the new PBAC deleteRole mutation
|
|
1350
|
+
*
|
|
1351
|
+
* @param roleId - Role ID to delete
|
|
1352
|
+
* @param workspaceId - Workspace ID (scopeId)
|
|
1353
|
+
* @param reassignToRoleId - Optional role ID to reassign existing actors to
|
|
1354
|
+
*/
|
|
1355
|
+
async deleteRole(roleId, workspaceId, reassignToRoleId) {
|
|
1356
|
+
logger.debug("[RBAC] Deleting role", {
|
|
1357
|
+
roleId,
|
|
1358
|
+
workspaceId,
|
|
1359
|
+
reassignToRoleId
|
|
1360
|
+
});
|
|
1361
|
+
const contextWithWorkspace = {
|
|
1362
|
+
...this.context,
|
|
1363
|
+
workspaceId
|
|
1364
|
+
};
|
|
1365
|
+
const mutation = `
|
|
1366
|
+
mutation DeleteRole($id: ID!, $reassignToRoleId: ID) {
|
|
1367
|
+
deleteRole(id: $id, reassignToRoleId: $reassignToRoleId)
|
|
1368
|
+
}
|
|
1369
|
+
`;
|
|
1370
|
+
const response = await this.client.query(mutation, {
|
|
1371
|
+
id: roleId,
|
|
1372
|
+
reassignToRoleId
|
|
1373
|
+
}, contextWithWorkspace);
|
|
1374
|
+
logger.info("[RBAC] Role deleted", {
|
|
1375
|
+
roleId,
|
|
1376
|
+
success: response.deleteRole
|
|
1377
|
+
});
|
|
1378
|
+
return response.deleteRole;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* List roles in a workspace
|
|
1382
|
+
* Uses the new PBAC roles query
|
|
1383
|
+
*
|
|
1384
|
+
* @param workspaceId - Workspace ID (scopeId)
|
|
1385
|
+
* @param options - Filter and pagination options
|
|
1386
|
+
*/
|
|
1387
|
+
async listRoles(workspaceId, options) {
|
|
1388
|
+
logger.debug("[RBAC] Listing roles", {
|
|
1389
|
+
workspaceId,
|
|
1390
|
+
options
|
|
1391
|
+
});
|
|
1392
|
+
const contextWithWorkspace = {
|
|
1393
|
+
...this.context,
|
|
1394
|
+
workspaceId
|
|
1395
|
+
};
|
|
1396
|
+
const query = `
|
|
1397
|
+
query ListRoles($scopeId: String!, $filter: RoleFilterInput, $pagination: PaginationInput) {
|
|
1398
|
+
roles(scopeId: $scopeId, filter: $filter, pagination: $pagination) {
|
|
1399
|
+
id
|
|
1400
|
+
name
|
|
1401
|
+
description
|
|
1402
|
+
isSystem
|
|
1403
|
+
priority
|
|
1404
|
+
defaultPermissions {
|
|
1405
|
+
resourceType
|
|
1406
|
+
level
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
`;
|
|
1411
|
+
const variables = {
|
|
1412
|
+
scopeId: workspaceId,
|
|
1413
|
+
filter: options?.includeSystem !== void 0 ? {
|
|
1414
|
+
includeSystem: options.includeSystem
|
|
1415
|
+
} : void 0,
|
|
1416
|
+
pagination: {
|
|
1417
|
+
limit: options?.limit ?? 50,
|
|
1418
|
+
offset: options?.offset ?? 0
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
const response = await this.client.query(query, variables, contextWithWorkspace);
|
|
1422
|
+
logger.debug("[RBAC] Roles listed", {
|
|
1423
|
+
workspaceId,
|
|
1424
|
+
count: response.roles.length
|
|
1425
|
+
});
|
|
1426
|
+
return response.roles.map((role) => ({
|
|
1427
|
+
id: role.id,
|
|
1428
|
+
name: role.name,
|
|
1429
|
+
description: role.description,
|
|
1430
|
+
defaultPermissions: role.defaultPermissions
|
|
1431
|
+
}));
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Get a specific role by ID
|
|
1435
|
+
* Uses the new PBAC role query
|
|
1436
|
+
*
|
|
1437
|
+
* @param roleId - Role ID
|
|
1438
|
+
* @param workspaceId - Workspace ID (scopeId)
|
|
1439
|
+
*/
|
|
1440
|
+
async getRole(roleId, workspaceId) {
|
|
1441
|
+
logger.debug("[RBAC] Getting role", {
|
|
1442
|
+
roleId,
|
|
1443
|
+
workspaceId
|
|
1444
|
+
});
|
|
1445
|
+
const contextWithWorkspace = {
|
|
1446
|
+
...this.context,
|
|
1447
|
+
workspaceId
|
|
1448
|
+
};
|
|
1449
|
+
const query = `
|
|
1450
|
+
query GetRole($id: ID!) {
|
|
1451
|
+
role(id: $id) {
|
|
1452
|
+
id
|
|
1453
|
+
name
|
|
1454
|
+
description
|
|
1455
|
+
isSystem
|
|
1456
|
+
priority
|
|
1457
|
+
defaultPermissions {
|
|
1458
|
+
resourceType
|
|
1459
|
+
level
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
`;
|
|
1464
|
+
const response = await this.client.query(query, {
|
|
1465
|
+
id: roleId
|
|
1466
|
+
}, contextWithWorkspace);
|
|
1467
|
+
if (!response.role) {
|
|
1468
|
+
logger.debug("[RBAC] Role not found", {
|
|
1469
|
+
roleId
|
|
1470
|
+
});
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
id: response.role.id,
|
|
1475
|
+
name: response.role.name,
|
|
1476
|
+
description: response.role.description,
|
|
1477
|
+
defaultPermissions: response.role.defaultPermissions
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
export { RBACClient, RBACModule, createRBACDirective, createRBACDirectiveForLayer, createRBACDirectiveFromEnv };
|
|
1483
|
+
//# sourceMappingURL=chunk-J26YGR2U.js.map
|
|
1484
|
+
//# sourceMappingURL=chunk-J26YGR2U.js.map
|