@nsxbet/admin-sdk 0.8.0 → 0.9.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 +16 -0
- package/dist/auth/client/bff.js +19 -14
- package/dist/auth/client/in-memory.js +11 -14
- package/dist/auth/client/interface.d.ts +5 -0
- package/dist/auth/client/permission-match.d.ts +1 -0
- package/dist/auth/client/permission-match.js +13 -0
- package/dist/auth/client/rbac-resolution.d.ts +29 -0
- package/dist/auth/client/rbac-resolution.js +22 -0
- package/dist/auth/client/resolved-role-cache.d.ts +11 -0
- package/dist/auth/client/resolved-role-cache.js +54 -0
- package/dist/components/AuthProvider.d.ts +3 -1
- package/dist/components/AuthProvider.js +95 -23
- package/dist/i18n/locales/en-US.json +66 -1
- package/dist/i18n/locales/es.json +66 -1
- package/dist/i18n/locales/pt-BR.json +66 -1
- package/dist/i18n/locales/ro.json +66 -1
- package/dist/registry/types/manifest.d.ts +3 -1
- package/dist/sdk-version.js +1 -1
- package/dist/shell/AdminShell.js +13 -8
- package/dist/shell/components/HomePage.js +1 -1
- package/dist/shell/components/LeftNav.js +46 -4
- package/dist/shell/components/MainContent.js +25 -0
- package/dist/shell/components/RegistryPage.js +3 -3
- package/dist/shell/components/access-control/AccessControlAuditPage.d.ts +1 -0
- package/dist/shell/components/access-control/AccessControlAuditPage.js +135 -0
- package/dist/shell/components/access-control/AccessControlGroupDetailPage.d.ts +1 -0
- package/dist/shell/components/access-control/AccessControlGroupDetailPage.js +224 -0
- package/dist/shell/components/access-control/AccessControlGroupsPage.d.ts +1 -0
- package/dist/shell/components/access-control/AccessControlGroupsPage.js +183 -0
- package/dist/shell/components/access-control/AccessControlLayout.d.ts +8 -0
- package/dist/shell/components/access-control/AccessControlLayout.js +23 -0
- package/dist/shell/components/access-control/AccessControlMemberPicker.d.ts +10 -0
- package/dist/shell/components/access-control/AccessControlMemberPicker.js +44 -0
- package/dist/shell/components/access-control/AccessControlPermissionPicker.d.ts +8 -0
- package/dist/shell/components/access-control/AccessControlPermissionPicker.js +38 -0
- package/dist/shell/components/access-control/AccessControlUserPage.d.ts +1 -0
- package/dist/shell/components/access-control/AccessControlUserPage.js +42 -0
- package/dist/shell/components/access-control/AccessControlUsersListPage.d.ts +1 -0
- package/dist/shell/components/access-control/AccessControlUsersListPage.js +111 -0
- package/dist/shell/components/access-control/api.d.ts +111 -0
- package/dist/shell/components/access-control/api.js +119 -0
- package/dist/shell/components/access-control/index.d.ts +8 -0
- package/dist/shell/components/access-control/index.js +8 -0
- package/dist/shell/components/index.d.ts +1 -0
- package/dist/shell/components/index.js +1 -0
- package/dist/vite/plugins.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1141,6 +1141,22 @@ When the shell loads modules dynamically from URLs, it validates each URL agains
|
|
|
1141
1141
|
|
|
1142
1142
|
Production auth should go through **admin-bff**: the BFF sets an HttpOnly `access_token` cookie and exposes **`GET /me`** for identity. The shell does not read the JWT in the browser.
|
|
1143
1143
|
|
|
1144
|
+
When `AdminShell` receives an `apiUrl`, the SDK now treats **RBAC-resolved effective roles** from the Admin API as the long-term permission source:
|
|
1145
|
+
|
|
1146
|
+
- `AuthProvider` calls `GET /api/access-control/users/me/resolved-roles` after successful authentication
|
|
1147
|
+
- the current subject's resolved roles are cached in localStorage for up to five minutes
|
|
1148
|
+
- temporary RBAC resolution failures may fall back to that same-subject cached role set
|
|
1149
|
+
- logout clears the cache immediately
|
|
1150
|
+
- once the cache expires, permission checks fail closed until RBAC resolution succeeds again
|
|
1151
|
+
|
|
1152
|
+
This means `hasPermission()` reflects local group membership and hybrid catalog resolution rather than only raw upstream roles from `/me` or JWT payloads.
|
|
1153
|
+
|
|
1154
|
+
The SDK shell also exposes a native `Access Control` route subtree under `/_access-control/*` for groups, user inspection, and audit history. Those pages are gated by exact platform permissions such as:
|
|
1155
|
+
|
|
1156
|
+
- `admin.platform.access-control.groups.manage`
|
|
1157
|
+
- `admin.platform.access-control.users.view`
|
|
1158
|
+
- `admin.platform.access-control.audit.view`
|
|
1159
|
+
|
|
1144
1160
|
### AdminShell
|
|
1145
1161
|
|
|
1146
1162
|
```tsx
|
package/dist/auth/client/bff.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* to build the user, and persists the token in localStorage. Falls back to
|
|
7
7
|
* `GET /me` (cookie-based) when no token is present.
|
|
8
8
|
*/
|
|
9
|
+
import { matchesPermission } from './permission-match';
|
|
9
10
|
const TOKEN_STORAGE_KEY = 'admin-bff-token';
|
|
10
11
|
const EXPIRES_STORAGE_KEY = 'admin-bff-token-expires';
|
|
11
12
|
const LOGGED_OUT_FLAG_KEY = 'admin-bff-logged-out';
|
|
@@ -70,19 +71,6 @@ function jwtToUser(payload) {
|
|
|
70
71
|
roles: [...groups, ...roles],
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
function matchesPermission(userRoles, permission) {
|
|
74
|
-
if (userRoles.includes('*'))
|
|
75
|
-
return true;
|
|
76
|
-
return userRoles.some((role) => {
|
|
77
|
-
if (role === permission)
|
|
78
|
-
return true;
|
|
79
|
-
if (role.endsWith('.*')) {
|
|
80
|
-
const prefix = role.slice(0, -2);
|
|
81
|
-
return permission.startsWith(prefix);
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
74
|
/** Read token + expires from URL query params, then clean the URL. */
|
|
87
75
|
function consumeTokenFromUrl() {
|
|
88
76
|
if (typeof window === 'undefined')
|
|
@@ -148,6 +136,8 @@ export function createBffAuthClient(options = {}) {
|
|
|
148
136
|
const bffBaseUrl = normalizeBaseUrl(options.bffBaseUrl ?? resolveDefaultBffBaseUrl());
|
|
149
137
|
let currentUser = null;
|
|
150
138
|
let accessToken = null;
|
|
139
|
+
let rawRoles = [];
|
|
140
|
+
let resolvedRoles = null;
|
|
151
141
|
let initialized = false;
|
|
152
142
|
const subscribers = new Set();
|
|
153
143
|
function notifySubscribers() {
|
|
@@ -181,6 +171,8 @@ export function createBffAuthClient(options = {}) {
|
|
|
181
171
|
}
|
|
182
172
|
const payload = decodeJwtPayload(token);
|
|
183
173
|
currentUser = jwtToUser(payload);
|
|
174
|
+
rawRoles = currentUser.roles;
|
|
175
|
+
resolvedRoles = null;
|
|
184
176
|
accessToken = token;
|
|
185
177
|
storeToken(token, expires);
|
|
186
178
|
return true;
|
|
@@ -222,6 +214,8 @@ export function createBffAuthClient(options = {}) {
|
|
|
222
214
|
initialized = true;
|
|
223
215
|
if (result.ok) {
|
|
224
216
|
currentUser = result.user;
|
|
217
|
+
rawRoles = result.user.roles;
|
|
218
|
+
resolvedRoles = null;
|
|
225
219
|
notifySubscribers();
|
|
226
220
|
return true;
|
|
227
221
|
}
|
|
@@ -250,11 +244,22 @@ export function createBffAuthClient(options = {}) {
|
|
|
250
244
|
if (!initialized || !currentUser) {
|
|
251
245
|
return false;
|
|
252
246
|
}
|
|
253
|
-
return matchesPermission(currentUser.roles, permission);
|
|
247
|
+
return matchesPermission(resolvedRoles ?? currentUser.roles, permission);
|
|
248
|
+
},
|
|
249
|
+
setResolvedRoles(roles) {
|
|
250
|
+
resolvedRoles = roles ? [...roles] : null;
|
|
251
|
+
if (currentUser) {
|
|
252
|
+
currentUser = {
|
|
253
|
+
...currentUser,
|
|
254
|
+
roles: resolvedRoles ?? rawRoles,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
254
257
|
},
|
|
255
258
|
logout() {
|
|
256
259
|
currentUser = null;
|
|
257
260
|
accessToken = null;
|
|
261
|
+
rawRoles = [];
|
|
262
|
+
resolvedRoles = null;
|
|
258
263
|
clearStoredToken();
|
|
259
264
|
setLoggedOutFlag();
|
|
260
265
|
notifySubscribers();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { fetchGatewayToken } from './gateway-token';
|
|
8
8
|
import { env } from '../../env';
|
|
9
|
+
import { matchesPermission } from './permission-match';
|
|
9
10
|
export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
|
|
10
11
|
/**
|
|
11
12
|
* Create mock users from a role configuration
|
|
@@ -80,6 +81,7 @@ export function createInMemoryAuthClient(options) {
|
|
|
80
81
|
let tokenCache = null;
|
|
81
82
|
let useMockFallback = false;
|
|
82
83
|
let backgroundRefreshInFlight = false;
|
|
84
|
+
let resolvedRoles = null;
|
|
83
85
|
const subscribers = new Set();
|
|
84
86
|
const REFRESH_BUFFER_MS = 60000;
|
|
85
87
|
function loadStorage() {
|
|
@@ -111,7 +113,7 @@ export function createInMemoryAuthClient(options) {
|
|
|
111
113
|
id: selectedUser.id,
|
|
112
114
|
email: selectedUser.email,
|
|
113
115
|
displayName: selectedUser.displayName,
|
|
114
|
-
roles: selectedUser.roles,
|
|
116
|
+
roles: resolvedRoles ?? selectedUser.roles,
|
|
115
117
|
} : null,
|
|
116
118
|
};
|
|
117
119
|
subscribers.forEach((callback) => callback(state));
|
|
@@ -121,7 +123,7 @@ export function createInMemoryAuthClient(options) {
|
|
|
121
123
|
id: mockUser.id,
|
|
122
124
|
email: mockUser.email,
|
|
123
125
|
displayName: mockUser.displayName,
|
|
124
|
-
roles: mockUser.roles,
|
|
126
|
+
roles: resolvedRoles ?? mockUser.roles,
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
function mockToken() {
|
|
@@ -147,6 +149,7 @@ export function createInMemoryAuthClient(options) {
|
|
|
147
149
|
const user = findUser(storage.selectedUserId);
|
|
148
150
|
if (user) {
|
|
149
151
|
selectedUser = user;
|
|
152
|
+
resolvedRoles = null;
|
|
150
153
|
return true;
|
|
151
154
|
}
|
|
152
155
|
}
|
|
@@ -194,23 +197,16 @@ export function createInMemoryAuthClient(options) {
|
|
|
194
197
|
if (!selectedUser) {
|
|
195
198
|
return false;
|
|
196
199
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (role === permission)
|
|
202
|
-
return true;
|
|
203
|
-
if (role.endsWith('.*')) {
|
|
204
|
-
const prefix = role.slice(0, -2);
|
|
205
|
-
return permission.startsWith(prefix);
|
|
206
|
-
}
|
|
207
|
-
return false;
|
|
208
|
-
});
|
|
200
|
+
return matchesPermission(resolvedRoles ?? selectedUser.roles, permission);
|
|
201
|
+
},
|
|
202
|
+
setResolvedRoles(roles) {
|
|
203
|
+
resolvedRoles = roles ? [...roles] : null;
|
|
209
204
|
},
|
|
210
205
|
logout() {
|
|
211
206
|
selectedUser = null;
|
|
212
207
|
tokenCache = null;
|
|
213
208
|
useMockFallback = false;
|
|
209
|
+
resolvedRoles = null;
|
|
214
210
|
const storage = loadStorage();
|
|
215
211
|
storage.selectedUserId = null;
|
|
216
212
|
saveStorage(storage);
|
|
@@ -231,6 +227,7 @@ export function createInMemoryAuthClient(options) {
|
|
|
231
227
|
throw new Error(`User not found: ${userId}`);
|
|
232
228
|
}
|
|
233
229
|
tokenCache = null;
|
|
230
|
+
resolvedRoles = null;
|
|
234
231
|
if (loginOptions?.fallbackToMock || !resolvedGatewayUrl) {
|
|
235
232
|
useMockFallback = true;
|
|
236
233
|
if (resolvedGatewayUrl && loginOptions?.fallbackToMock) {
|
|
@@ -76,6 +76,11 @@ export interface AuthClient {
|
|
|
76
76
|
* Check if user has a specific permission/role
|
|
77
77
|
*/
|
|
78
78
|
hasPermission(permission: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Override raw auth roles with RBAC-resolved roles when available.
|
|
81
|
+
* Passing `null` clears the override and falls back to raw roles.
|
|
82
|
+
*/
|
|
83
|
+
setResolvedRoles?(roles: string[] | null): void;
|
|
79
84
|
/**
|
|
80
85
|
* Log out the current user
|
|
81
86
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function matchesPermission(userRoles: string[], permission: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function matchesPermission(userRoles, permission) {
|
|
2
|
+
if (userRoles.includes('*'))
|
|
3
|
+
return true;
|
|
4
|
+
return userRoles.some((role) => {
|
|
5
|
+
if (role === permission)
|
|
6
|
+
return true;
|
|
7
|
+
if (role.endsWith('.*')) {
|
|
8
|
+
const prefix = role.slice(0, -2);
|
|
9
|
+
return permission.startsWith(prefix);
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ResolvedRoleUser {
|
|
2
|
+
id: number;
|
|
3
|
+
subject: string;
|
|
4
|
+
email: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedRoleGroup {
|
|
8
|
+
id: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
permissions: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvedRoleAttribution {
|
|
13
|
+
permission: string;
|
|
14
|
+
groups: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface ResolvedRolesResponse {
|
|
20
|
+
user: ResolvedRoleUser;
|
|
21
|
+
roles: string[];
|
|
22
|
+
groups: ResolvedRoleGroup[];
|
|
23
|
+
attribution: ResolvedRoleAttribution[];
|
|
24
|
+
}
|
|
25
|
+
export interface FetchResolvedRolesOptions {
|
|
26
|
+
apiUrl: string;
|
|
27
|
+
getAccessToken: () => Promise<string | null>;
|
|
28
|
+
}
|
|
29
|
+
export declare function fetchCurrentUserResolvedRoles(options: FetchResolvedRolesOptions): Promise<ResolvedRolesResponse>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function normalizeApiUrl(apiUrl) {
|
|
2
|
+
const normalized = apiUrl.replace(/\/+$/, '');
|
|
3
|
+
return normalized.endsWith('/api') ? normalized : `${normalized}/api`;
|
|
4
|
+
}
|
|
5
|
+
export async function fetchCurrentUserResolvedRoles(options) {
|
|
6
|
+
const token = await options.getAccessToken();
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
const response = await fetch(`${normalizeApiUrl(options.apiUrl)}/access-control/users/me/resolved-roles`, token === null
|
|
9
|
+
? {
|
|
10
|
+
headers,
|
|
11
|
+
credentials: 'include',
|
|
12
|
+
}
|
|
13
|
+
: {
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${token}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`RBAC resolution failed: ${response.status}`);
|
|
20
|
+
}
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const RESOLVED_ROLE_CACHE_STORAGE_KEY = "adminPlatform.resolvedRoles";
|
|
2
|
+
export declare const RESOLVED_ROLE_CACHE_TTL_MS: number;
|
|
3
|
+
export interface ResolvedRoleCacheEntry {
|
|
4
|
+
subject: string;
|
|
5
|
+
roles: string[];
|
|
6
|
+
cachedAt: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function readResolvedRoleCache(): ResolvedRoleCacheEntry | null;
|
|
9
|
+
export declare function writeResolvedRoleCache(subject: string, roles: string[]): void;
|
|
10
|
+
export declare function clearResolvedRoleCache(): void;
|
|
11
|
+
export declare function readUsableResolvedRolesFromCache(subject: string, now?: number, ttlMs?: number): string[] | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const RESOLVED_ROLE_CACHE_STORAGE_KEY = 'adminPlatform.resolvedRoles';
|
|
2
|
+
export const RESOLVED_ROLE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
3
|
+
export function readResolvedRoleCache() {
|
|
4
|
+
try {
|
|
5
|
+
const raw = localStorage.getItem(RESOLVED_ROLE_CACHE_STORAGE_KEY);
|
|
6
|
+
if (!raw)
|
|
7
|
+
return null;
|
|
8
|
+
const parsed = JSON.parse(raw);
|
|
9
|
+
if (typeof parsed.subject !== 'string' ||
|
|
10
|
+
!Array.isArray(parsed.roles) ||
|
|
11
|
+
typeof parsed.cachedAt !== 'number') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
subject: parsed.subject,
|
|
16
|
+
roles: parsed.roles.filter((role) => typeof role === 'string'),
|
|
17
|
+
cachedAt: parsed.cachedAt,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writeResolvedRoleCache(subject, roles) {
|
|
25
|
+
try {
|
|
26
|
+
const entry = {
|
|
27
|
+
subject,
|
|
28
|
+
roles,
|
|
29
|
+
cachedAt: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
localStorage.setItem(RESOLVED_ROLE_CACHE_STORAGE_KEY, JSON.stringify(entry));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Ignore storage failures and continue without cache.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function clearResolvedRoleCache() {
|
|
38
|
+
try {
|
|
39
|
+
localStorage.removeItem(RESOLVED_ROLE_CACHE_STORAGE_KEY);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore storage failures.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function readUsableResolvedRolesFromCache(subject, now = Date.now(), ttlMs = RESOLVED_ROLE_CACHE_TTL_MS) {
|
|
46
|
+
const entry = readResolvedRoleCache();
|
|
47
|
+
if (!entry)
|
|
48
|
+
return null;
|
|
49
|
+
if (entry.subject !== subject)
|
|
50
|
+
return null;
|
|
51
|
+
if (entry.cachedAt + ttlMs < now)
|
|
52
|
+
return null;
|
|
53
|
+
return entry.roles;
|
|
54
|
+
}
|
|
@@ -31,11 +31,13 @@ interface AuthProviderProps {
|
|
|
31
31
|
children: React.ReactNode;
|
|
32
32
|
/** The auth client to use */
|
|
33
33
|
authClient: AuthClient;
|
|
34
|
+
/** Base URL for the admin API used for RBAC resolution */
|
|
35
|
+
apiUrl?: string;
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* AuthProvider manages authentication state and renders appropriate UI
|
|
37
39
|
*/
|
|
38
|
-
export declare function AuthProvider({ children, authClient }: AuthProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export declare function AuthProvider({ children, authClient, apiUrl }: AuthProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
39
41
|
/**
|
|
40
42
|
* Hook to access auth context
|
|
41
43
|
*/
|
|
@@ -5,8 +5,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
* Works with both in-memory (mock) and BFF cookie auth clients.
|
|
6
6
|
* Shows user selection screen when in-memory client has no selected user.
|
|
7
7
|
*/
|
|
8
|
-
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react';
|
|
8
|
+
import { createContext, useContext, useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
|
9
9
|
import { useCallbackRef } from '../hooks/useCallbackRef';
|
|
10
|
+
import { matchesPermission } from '../auth/client/permission-match';
|
|
11
|
+
import { fetchCurrentUserResolvedRoles } from '../auth/client/rbac-resolution';
|
|
12
|
+
import { clearResolvedRoleCache, readUsableResolvedRolesFromCache, writeResolvedRoleCache, } from '../auth/client/resolved-role-cache';
|
|
10
13
|
import { isLoggedOutFlag } from '../auth/client/bff';
|
|
11
14
|
import { UserSelector } from '../auth/components/UserSelector';
|
|
12
15
|
import { LoginPage } from '../auth/components/LoginPage';
|
|
@@ -20,7 +23,7 @@ function LoadingScreen() {
|
|
|
20
23
|
/**
|
|
21
24
|
* AuthProvider manages authentication state and renders appropriate UI
|
|
22
25
|
*/
|
|
23
|
-
export function AuthProvider({ children, authClient }) {
|
|
26
|
+
export function AuthProvider({ children, authClient, apiUrl }) {
|
|
24
27
|
const [authState, setAuthState] = useState({
|
|
25
28
|
isAuthenticated: false,
|
|
26
29
|
user: null,
|
|
@@ -29,6 +32,53 @@ export function AuthProvider({ children, authClient }) {
|
|
|
29
32
|
const [isInitializing, setIsInitializing] = useState(!bffLoggedOut);
|
|
30
33
|
const [needsUserSelection, setNeedsUserSelection] = useState(false);
|
|
31
34
|
const [needsLogin, setNeedsLogin] = useState(bffLoggedOut);
|
|
35
|
+
const effectiveRolesRef = useRef([]);
|
|
36
|
+
effectiveRolesRef.current = authState.user?.roles ?? [];
|
|
37
|
+
const applyResolvedRoles = useCallback(async (state) => {
|
|
38
|
+
if (!state.isAuthenticated || !state.user) {
|
|
39
|
+
authClient.setResolvedRoles?.(null);
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
if (!apiUrl) {
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const resolved = await fetchCurrentUserResolvedRoles({
|
|
47
|
+
apiUrl,
|
|
48
|
+
getAccessToken: () => authClient.getAccessToken(),
|
|
49
|
+
});
|
|
50
|
+
writeResolvedRoleCache(state.user.id, resolved.roles);
|
|
51
|
+
authClient.setResolvedRoles?.(resolved.roles);
|
|
52
|
+
return {
|
|
53
|
+
...state,
|
|
54
|
+
user: {
|
|
55
|
+
...state.user,
|
|
56
|
+
roles: resolved.roles,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
const cachedRoles = readUsableResolvedRolesFromCache(state.user.id);
|
|
62
|
+
if (cachedRoles) {
|
|
63
|
+
authClient.setResolvedRoles?.(cachedRoles);
|
|
64
|
+
return {
|
|
65
|
+
...state,
|
|
66
|
+
user: {
|
|
67
|
+
...state.user,
|
|
68
|
+
roles: cachedRoles,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
authClient.setResolvedRoles?.([]);
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
user: {
|
|
76
|
+
...state.user,
|
|
77
|
+
roles: [],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}, [apiUrl, authClient]);
|
|
32
82
|
// Initialize auth client
|
|
33
83
|
useEffect(() => {
|
|
34
84
|
let mounted = true;
|
|
@@ -38,18 +88,25 @@ export function AuthProvider({ children, authClient }) {
|
|
|
38
88
|
if (!mounted)
|
|
39
89
|
return;
|
|
40
90
|
if (isAuthenticated) {
|
|
41
|
-
|
|
91
|
+
const resolvedState = await applyResolvedRoles({
|
|
42
92
|
isAuthenticated: true,
|
|
43
93
|
user: authClient.getUser(),
|
|
44
94
|
});
|
|
95
|
+
if (!mounted)
|
|
96
|
+
return;
|
|
97
|
+
setAuthState(resolvedState);
|
|
45
98
|
setNeedsUserSelection(false);
|
|
46
99
|
setNeedsLogin(false);
|
|
47
100
|
}
|
|
48
101
|
else if (authClient.type === 'in-memory') {
|
|
102
|
+
clearResolvedRoleCache();
|
|
103
|
+
authClient.setResolvedRoles?.(null);
|
|
49
104
|
setNeedsUserSelection(true);
|
|
50
105
|
setNeedsLogin(false);
|
|
51
106
|
}
|
|
52
107
|
else if (authClient.type === 'bff') {
|
|
108
|
+
clearResolvedRoleCache();
|
|
109
|
+
authClient.setResolvedRoles?.(null);
|
|
53
110
|
setNeedsLogin(true);
|
|
54
111
|
setNeedsUserSelection(false);
|
|
55
112
|
}
|
|
@@ -69,38 +126,53 @@ export function AuthProvider({ children, authClient }) {
|
|
|
69
126
|
initialize();
|
|
70
127
|
// Subscribe to auth state changes
|
|
71
128
|
const unsubscribe = authClient.subscribe((state) => {
|
|
72
|
-
if (mounted)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
if (!mounted)
|
|
130
|
+
return;
|
|
131
|
+
const syncState = async () => {
|
|
132
|
+
if (!state.isAuthenticated) {
|
|
133
|
+
clearResolvedRoleCache();
|
|
134
|
+
authClient.setResolvedRoles?.(null);
|
|
135
|
+
setAuthState(state);
|
|
136
|
+
if (authClient.type === 'in-memory') {
|
|
137
|
+
setNeedsUserSelection(true);
|
|
138
|
+
setNeedsLogin(false);
|
|
139
|
+
}
|
|
140
|
+
else if (authClient.type === 'bff') {
|
|
141
|
+
setNeedsLogin(true);
|
|
142
|
+
setNeedsUserSelection(false);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
85
145
|
}
|
|
86
|
-
|
|
146
|
+
const resolvedState = await applyResolvedRoles(state);
|
|
147
|
+
if (!mounted)
|
|
148
|
+
return;
|
|
149
|
+
setAuthState(resolvedState);
|
|
150
|
+
setNeedsUserSelection(false);
|
|
151
|
+
setNeedsLogin(false);
|
|
152
|
+
};
|
|
153
|
+
syncState();
|
|
87
154
|
});
|
|
88
155
|
return () => {
|
|
89
156
|
mounted = false;
|
|
90
157
|
unsubscribe();
|
|
91
158
|
};
|
|
92
|
-
}, [authClient]);
|
|
159
|
+
}, [authClient, applyResolvedRoles]);
|
|
93
160
|
// Handle user selection
|
|
94
|
-
const handleUserSelected = useCallback(() => {
|
|
95
|
-
|
|
161
|
+
const handleUserSelected = useCallback(async () => {
|
|
162
|
+
const resolvedState = await applyResolvedRoles({
|
|
96
163
|
isAuthenticated: authClient.isAuthenticated(),
|
|
97
164
|
user: authClient.getUser(),
|
|
98
165
|
});
|
|
166
|
+
setAuthState(resolvedState);
|
|
99
167
|
setNeedsUserSelection(false);
|
|
100
|
-
}, [authClient]);
|
|
168
|
+
}, [authClient, applyResolvedRoles]);
|
|
101
169
|
const getAccessToken = useCallbackRef(() => authClient.getAccessToken());
|
|
102
|
-
const hasPermission = useCallbackRef((permission) =>
|
|
103
|
-
const logout = useCallbackRef(() =>
|
|
170
|
+
const hasPermission = useCallbackRef((permission) => matchesPermission(effectiveRolesRef.current, permission));
|
|
171
|
+
const logout = useCallbackRef(() => {
|
|
172
|
+
clearResolvedRoleCache();
|
|
173
|
+
authClient.setResolvedRoles?.(null);
|
|
174
|
+
authClient.logout();
|
|
175
|
+
});
|
|
104
176
|
const contextValue = useMemo(() => ({
|
|
105
177
|
isAuthenticated: authState.isAuthenticated,
|
|
106
178
|
user: authState.user,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"edit": "Edit",
|
|
11
11
|
"create": "Create",
|
|
12
12
|
"search": "Search",
|
|
13
|
+
"retry": "Retry",
|
|
13
14
|
"noResults": "No results found",
|
|
14
15
|
"close": "Close",
|
|
15
16
|
"back": "Back",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"settings": "Settings",
|
|
43
44
|
"profile": "Profile",
|
|
44
45
|
"registry": "Registry",
|
|
46
|
+
"accessControl": "Access Control",
|
|
45
47
|
"logout": "Logout",
|
|
46
48
|
"pinned": "Pinned",
|
|
47
49
|
"platform": "Platform",
|
|
@@ -219,10 +221,73 @@
|
|
|
219
221
|
"loginButton": "Login with Corporate SSO",
|
|
220
222
|
"secureAuth": "Secure Authentication"
|
|
221
223
|
},
|
|
224
|
+
"accessControlPage": {
|
|
225
|
+
"title": "Access Control",
|
|
226
|
+
"description": "Manage local authorization, inspection, and audit workflows.",
|
|
227
|
+
"groups": "Groups",
|
|
228
|
+
"users": "Users",
|
|
229
|
+
"audit": "Audit",
|
|
230
|
+
"statusAll": "All",
|
|
231
|
+
"statusActive": "Active",
|
|
232
|
+
"statusInactive": "Inactive",
|
|
233
|
+
"groupsTitle": "Groups",
|
|
234
|
+
"groupsDescription": "Browse and manage RBAC groups.",
|
|
235
|
+
"noGroupsTitle": "No groups found",
|
|
236
|
+
"noGroupsDescription": "No groups matched the current filter.",
|
|
237
|
+
"protectedGroup": "Protected",
|
|
238
|
+
"viewGroup": "View Group",
|
|
239
|
+
"deactivate": "Deactivate",
|
|
240
|
+
"reactivate": "Reactivate",
|
|
241
|
+
"confirmDeactivate": "Are you sure you want to deactivate \"{{group}}\"? Members of this group will lose all permissions granted through it.",
|
|
242
|
+
"confirmReactivate": "Are you sure you want to reactivate \"{{group}}\"? Members will regain all permissions assigned to this group.",
|
|
243
|
+
"promptCreateGroup": "Enter a display name for the new group",
|
|
244
|
+
"promptEditGroup": "Update the group display name",
|
|
245
|
+
"groupDetailTitle": "Group Detail",
|
|
246
|
+
"groupDetailDescription": "Review memberships and permissions for group {{groupId}}.",
|
|
247
|
+
"membersTab": "Members",
|
|
248
|
+
"permissionsTab": "Permissions",
|
|
249
|
+
"noMembers": "This group has no members yet.",
|
|
250
|
+
"noPermissions": "This group has no assigned permissions yet.",
|
|
251
|
+
"selfRemoveTitle": "Remove yourself from this group?",
|
|
252
|
+
"selfRemoveDescription": "You are about to remove yourself from the \"{{group}}\" group. This may revoke your access to manage this group and other platform areas.",
|
|
253
|
+
"selfRemovePrompt": "Type CONFIRM to proceed.",
|
|
254
|
+
"selfRemoveConfirmButton": "Remove myself",
|
|
255
|
+
"userTitle": "User Access",
|
|
256
|
+
"userDescription": "Inspect resolved access for user {{userId}}.",
|
|
257
|
+
"userRolesTitle": "Effective Roles",
|
|
258
|
+
"userGroupsTitle": "Contributing Groups",
|
|
259
|
+
"noRoles": "This user has no effective roles.",
|
|
260
|
+
"auditTitle": "Audit History",
|
|
261
|
+
"auditDescription": "Browse read-only Access Control audit events.",
|
|
262
|
+
"filterEventType": "Event type",
|
|
263
|
+
"filterActor": "Actor user id",
|
|
264
|
+
"filterTargetType": "Target type",
|
|
265
|
+
"filterTargetId": "Target id",
|
|
266
|
+
"noAuditTitle": "No audit events found",
|
|
267
|
+
"noAuditDescription": "No audit events matched the current filters.",
|
|
268
|
+
"noAccess": "You do not have permission to access the Access Control area.",
|
|
269
|
+
"columnName": "Name",
|
|
270
|
+
"columnProtected": "Protected",
|
|
271
|
+
"columnEmail": "Email",
|
|
272
|
+
"columnSubject": "Subject",
|
|
273
|
+
"columnTimestamp": "Timestamp",
|
|
274
|
+
"columnEvent": "Event",
|
|
275
|
+
"columnActor": "Actor",
|
|
276
|
+
"columnTarget": "Target",
|
|
277
|
+
"columnSummary": "Summary",
|
|
278
|
+
"inspectUser": "Inspect",
|
|
279
|
+
"usersTitle": "Users",
|
|
280
|
+
"noUsersTitle": "No users found",
|
|
281
|
+
"noUsersDescription": "No known local users matched the search."
|
|
282
|
+
},
|
|
222
283
|
"breadcrumbs": {
|
|
223
284
|
"home": "Home",
|
|
224
285
|
"registry": "Registry",
|
|
225
286
|
"settings": "Settings",
|
|
226
|
-
"profile": "Profile"
|
|
287
|
+
"profile": "Profile",
|
|
288
|
+
"accessControl": "Access Control",
|
|
289
|
+
"accessControlGroups": "Groups",
|
|
290
|
+
"accessControlUsers": "Users",
|
|
291
|
+
"accessControlAudit": "Audit"
|
|
227
292
|
}
|
|
228
293
|
}
|