@checkstack/auth-frontend 0.0.4 → 0.2.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/CHANGELOG.md +172 -0
- package/package.json +1 -1
- package/src/api.ts +2 -2
- package/src/components/ApplicationsTab.tsx +10 -3
- package/src/components/AuthSettingsPage.tsx +66 -64
- package/src/components/LoginPage.tsx +8 -7
- package/src/components/RegisterPage.tsx +5 -8
- package/src/components/RoleDialog.tsx +43 -37
- package/src/components/RolesTab.tsx +10 -10
- package/src/components/StrategiesTab.tsx +3 -3
- package/src/components/TeamAccessEditor.tsx +557 -0
- package/src/components/TeamsTab.tsx +569 -0
- package/src/components/UsersTab.tsx +2 -2
- package/src/hooks/{usePermissions.ts → useAccessRules.ts} +10 -10
- package/src/index.test.tsx +83 -37
- package/src/index.tsx +33 -51
package/src/index.test.tsx
CHANGED
|
@@ -1,95 +1,141 @@
|
|
|
1
1
|
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
2
|
import { authPlugin } from "./index";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { accessApiRef } from "@checkstack/frontend-api";
|
|
4
|
+
import type { AccessRule } from "@checkstack/common";
|
|
5
|
+
import { useAccessRules } from "./hooks/useAccessRules";
|
|
5
6
|
|
|
6
|
-
// Mock the
|
|
7
|
-
mock.module("./hooks/
|
|
8
|
-
|
|
7
|
+
// Mock the useAccessRules hook
|
|
8
|
+
mock.module("./hooks/useAccessRules", () => ({
|
|
9
|
+
useAccessRules: mock(),
|
|
9
10
|
}));
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Test access rule objects
|
|
13
|
+
const testReadAccess: AccessRule = {
|
|
14
|
+
id: "test.read",
|
|
15
|
+
resource: "test",
|
|
16
|
+
level: "read",
|
|
17
|
+
description: "Test read access",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const testManageAccess: AccessRule = {
|
|
21
|
+
id: "test.manage",
|
|
22
|
+
resource: "test",
|
|
23
|
+
level: "manage",
|
|
24
|
+
description: "Test manage access",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const otherAccess: AccessRule = {
|
|
28
|
+
id: "other.read",
|
|
29
|
+
resource: "other",
|
|
30
|
+
level: "read",
|
|
31
|
+
description: "Other read access",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("AuthAccessApi", () => {
|
|
35
|
+
let accessApi: {
|
|
36
|
+
useAccess: (p: AccessRule) => { loading: boolean; allowed: boolean };
|
|
14
37
|
};
|
|
15
38
|
|
|
16
39
|
beforeEach(() => {
|
|
17
|
-
const apiDef = authPlugin.apis?.find(
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
if (!apiDef) throw new Error("Permission API not found in plugin");
|
|
21
|
-
permissionApi = apiDef.factory({ get: () => ({}) } as any) as any;
|
|
40
|
+
const apiDef = authPlugin.apis?.find((a) => a.ref.id === accessApiRef.id);
|
|
41
|
+
if (!apiDef) throw new Error("Access API not found in plugin");
|
|
42
|
+
accessApi = apiDef.factory({ get: () => ({}) } as any) as any;
|
|
22
43
|
});
|
|
23
44
|
|
|
24
|
-
it("should return true if user has the
|
|
25
|
-
(
|
|
26
|
-
|
|
45
|
+
it("should return true if user has the access rule", () => {
|
|
46
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
47
|
+
accessRules: ["test.read"],
|
|
27
48
|
loading: false,
|
|
28
49
|
});
|
|
29
50
|
|
|
30
|
-
expect(
|
|
51
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
31
52
|
loading: false,
|
|
32
53
|
allowed: true,
|
|
33
54
|
});
|
|
34
55
|
});
|
|
35
56
|
|
|
36
|
-
it("should return false if user is missing the
|
|
37
|
-
(
|
|
38
|
-
|
|
57
|
+
it("should return false if user is missing the access rule", () => {
|
|
58
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
59
|
+
accessRules: ["other.read"],
|
|
39
60
|
loading: false,
|
|
40
61
|
});
|
|
41
62
|
|
|
42
|
-
expect(
|
|
63
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
43
64
|
loading: false,
|
|
44
65
|
allowed: false,
|
|
45
66
|
});
|
|
46
67
|
});
|
|
47
68
|
|
|
48
|
-
it("should return false if no session data (no
|
|
49
|
-
(
|
|
50
|
-
|
|
69
|
+
it("should return false if no session data (no access rules)", () => {
|
|
70
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
71
|
+
accessRules: [],
|
|
51
72
|
loading: false,
|
|
52
73
|
});
|
|
53
74
|
|
|
54
|
-
expect(
|
|
75
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
55
76
|
loading: false,
|
|
56
77
|
allowed: false,
|
|
57
78
|
});
|
|
58
79
|
});
|
|
59
80
|
|
|
60
|
-
it("should return false if no user
|
|
61
|
-
(
|
|
62
|
-
|
|
81
|
+
it("should return false if no user access rules (empty array)", () => {
|
|
82
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
83
|
+
accessRules: [],
|
|
63
84
|
loading: false,
|
|
64
85
|
});
|
|
65
86
|
|
|
66
|
-
expect(
|
|
87
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
67
88
|
loading: false,
|
|
68
89
|
allowed: false,
|
|
69
90
|
});
|
|
70
91
|
});
|
|
71
92
|
|
|
72
|
-
it("should return true if user has the wildcard
|
|
73
|
-
(
|
|
74
|
-
|
|
93
|
+
it("should return true if user has the wildcard access rule", () => {
|
|
94
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
95
|
+
accessRules: ["*"],
|
|
75
96
|
loading: false,
|
|
76
97
|
});
|
|
77
98
|
|
|
78
|
-
expect(
|
|
99
|
+
expect(accessApi.useAccess(otherAccess)).toEqual({
|
|
79
100
|
loading: false,
|
|
80
101
|
allowed: true,
|
|
81
102
|
});
|
|
82
103
|
});
|
|
83
104
|
|
|
84
|
-
it("should return
|
|
85
|
-
(
|
|
86
|
-
|
|
105
|
+
it("should return true if user has manage access for a manage check", () => {
|
|
106
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
107
|
+
accessRules: ["test.manage"],
|
|
108
|
+
loading: false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(accessApi.useAccess(testManageAccess)).toEqual({
|
|
112
|
+
loading: false,
|
|
113
|
+
allowed: true,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return loading state if access rules are loading", () => {
|
|
118
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
119
|
+
accessRules: [],
|
|
87
120
|
loading: true,
|
|
88
121
|
});
|
|
89
122
|
|
|
90
|
-
expect(
|
|
123
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
91
124
|
loading: true,
|
|
92
125
|
allowed: false,
|
|
93
126
|
});
|
|
94
127
|
});
|
|
128
|
+
|
|
129
|
+
it("should return true if user has manage access for a read check", () => {
|
|
130
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
131
|
+
accessRules: ["test.manage"],
|
|
132
|
+
loading: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// User has test.manage, which implies test.read
|
|
136
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
137
|
+
loading: false,
|
|
138
|
+
allowed: true,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
95
141
|
});
|
package/src/index.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {
|
|
3
3
|
ApiRef,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
accessApiRef,
|
|
5
|
+
AccessApi,
|
|
6
6
|
createFrontendPlugin,
|
|
7
7
|
createSlotExtension,
|
|
8
8
|
NavbarRightSlot,
|
|
@@ -22,71 +22,52 @@ import { ChangePasswordPage } from "./components/ChangePasswordPage";
|
|
|
22
22
|
import { authApiRef, AuthApi, AuthSession } from "./api";
|
|
23
23
|
import { getAuthClientLazy } from "./lib/auth-client";
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { useAccessRules } from "./hooks/useAccessRules";
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import type { AccessRule } from "@checkstack/common";
|
|
28
28
|
import { useNavigate } from "react-router-dom";
|
|
29
29
|
import { Settings2, Key } from "lucide-react";
|
|
30
30
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
31
31
|
import { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
32
32
|
import { AuthSettingsPage } from "./components/AuthSettingsPage";
|
|
33
33
|
import {
|
|
34
|
-
|
|
34
|
+
authAccess,
|
|
35
35
|
authRoutes,
|
|
36
36
|
pluginMetadata,
|
|
37
37
|
} from "@checkstack/auth-common";
|
|
38
38
|
import { resolveRoute } from "@checkstack/common";
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// If no user, or user has no permissions, return false
|
|
49
|
-
if (!permissions || permissions.length === 0) {
|
|
50
|
-
return { loading: false, allowed: false };
|
|
51
|
-
}
|
|
52
|
-
const allowed =
|
|
53
|
-
permissions.includes("*") || permissions.includes(permission);
|
|
54
|
-
return { loading: false, allowed };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
useResourcePermission(
|
|
58
|
-
resource: string,
|
|
59
|
-
action: PermissionAction
|
|
60
|
-
): { loading: boolean; allowed: boolean } {
|
|
61
|
-
const { permissions, loading } = usePermissions();
|
|
40
|
+
/**
|
|
41
|
+
* Unified access API implementation.
|
|
42
|
+
* Uses AccessRule objects for access checks.
|
|
43
|
+
*/
|
|
44
|
+
class AuthAccessApi implements AccessApi {
|
|
45
|
+
useAccess(accessRule: AccessRule): { loading: boolean; allowed: boolean } {
|
|
46
|
+
const { accessRules, loading } = useAccessRules();
|
|
62
47
|
|
|
63
48
|
if (loading) {
|
|
64
49
|
return { loading: true, allowed: false };
|
|
65
50
|
}
|
|
66
51
|
|
|
67
|
-
|
|
52
|
+
// If no user, or user has no access rules, return false
|
|
53
|
+
if (!accessRules || accessRules.length === 0) {
|
|
68
54
|
return { loading: false, allowed: false };
|
|
69
55
|
}
|
|
70
56
|
|
|
71
|
-
const
|
|
72
|
-
const hasResourceManage = permissions.includes(`${resource}.manage`);
|
|
73
|
-
const hasSpecificPermission = permissions.includes(`${resource}.${action}`);
|
|
57
|
+
const accessRuleId = accessRule.id;
|
|
74
58
|
|
|
75
|
-
// manage implies read
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
hasResourceManage ||
|
|
79
|
-
(action === "read" && hasResourceManage) ||
|
|
80
|
-
hasSpecificPermission;
|
|
59
|
+
// Check wildcard, exact match, or manage implies read
|
|
60
|
+
const isWildcard = accessRules.includes("*");
|
|
61
|
+
const hasExact = accessRules.includes(accessRuleId);
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
|
|
63
|
+
// For read actions, also check if user has manage access for the same resource
|
|
64
|
+
const hasManage =
|
|
65
|
+
accessRule.level === "read"
|
|
66
|
+
? accessRules.includes(`${accessRule.resource}.manage`)
|
|
67
|
+
: false;
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
loading:
|
|
87
|
-
allowed: boolean;
|
|
88
|
-
} {
|
|
89
|
-
return this.useResourcePermission(resource, "manage");
|
|
69
|
+
const allowed = isWildcard || hasExact || hasManage;
|
|
70
|
+
return { loading: false, allowed };
|
|
90
71
|
}
|
|
91
72
|
}
|
|
92
73
|
|
|
@@ -168,6 +149,10 @@ class BetterAuthApi implements AuthApi {
|
|
|
168
149
|
}
|
|
169
150
|
}
|
|
170
151
|
|
|
152
|
+
// Re-export TeamAccessEditor for use in other plugins
|
|
153
|
+
export { TeamAccessEditor } from "./components/TeamAccessEditor";
|
|
154
|
+
export type { TeamAccessEditorProps } from "./components/TeamAccessEditor";
|
|
155
|
+
|
|
171
156
|
export const authPlugin = createFrontendPlugin({
|
|
172
157
|
metadata: pluginMetadata,
|
|
173
158
|
apis: [
|
|
@@ -176,8 +161,8 @@ export const authPlugin = createFrontendPlugin({
|
|
|
176
161
|
factory: () => new BetterAuthApi(),
|
|
177
162
|
},
|
|
178
163
|
{
|
|
179
|
-
ref:
|
|
180
|
-
factory: () => new
|
|
164
|
+
ref: accessApiRef as ApiRef<unknown>,
|
|
165
|
+
factory: () => new AuthAccessApi(),
|
|
181
166
|
},
|
|
182
167
|
],
|
|
183
168
|
routes: [
|
|
@@ -218,12 +203,9 @@ export const authPlugin = createFrontendPlugin({
|
|
|
218
203
|
},
|
|
219
204
|
createSlotExtension(UserMenuItemsSlot, {
|
|
220
205
|
id: "auth.user-menu.settings",
|
|
221
|
-
component: ({
|
|
206
|
+
component: ({ accessRules: userPerms }: UserMenuItemsContext) => {
|
|
222
207
|
const navigate = useNavigate();
|
|
223
|
-
const qualifiedId =
|
|
224
|
-
pluginMetadata,
|
|
225
|
-
authPermissions.strategiesManage
|
|
226
|
-
);
|
|
208
|
+
const qualifiedId = `${pluginMetadata.pluginId}.${authAccess.strategies.id}`;
|
|
227
209
|
const canManage =
|
|
228
210
|
userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
229
211
|
|