diff --git a/package.json b/package.json index c59357a..fc05a1d 100644 --- a/package.json +++ b/package.json @@ -86,4 +86,4 @@ "*" ] } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9087243..513a4cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,9 +6,6 @@ importers: .: configDependencies: {} packageManagerDependencies: - '@pnpm/exe': - specifier: 12.3.4 - version: 12.3.4 pnpm: specifier: 12.3.4 version: 12.3.4 @@ -59,11 +56,6 @@ packages: cpu: [x64] os: [win32] - '@pnpm/exe@12.3.4': - resolution: {integrity: sha512-Rq7JokWAyYF9IFfn8zofB/q2AgnZlmoMuH5fMHB2+Ta61I9aD7sNm+woZpx6iH+5vb38chdSoYRvOtSqlH72YQ==} - engines: {node: '>=18.*'} - hasBin: true - pnpm@12.3.4: resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} engines: {node: '>=18.*'} @@ -95,17 +87,6 @@ snapshots: '@pnpm/exe.win32-x64@12.3.4': optional: true - '@pnpm/exe@12.3.4': - optionalDependencies: - '@pnpm/exe.darwin-arm64': 12.3.4 - '@pnpm/exe.darwin-x64': 12.3.4 - '@pnpm/exe.linux-arm64': 12.3.4 - '@pnpm/exe.linux-arm64-musl': 12.3.4 - '@pnpm/exe.linux-x64': 12.3.4 - '@pnpm/exe.linux-x64-musl': 12.3.4 - '@pnpm/exe.win32-arm64': 12.3.4 - '@pnpm/exe.win32-x64': 12.3.4 - pnpm@12.3.4: optionalDependencies: '@pnpm/exe.darwin-arm64': 12.3.4 diff --git a/src/components/rbac/catalog-search.tsx b/src/components/rbac/catalog-search.tsx index a5f67a6..2ea49df 100644 --- a/src/components/rbac/catalog-search.tsx +++ b/src/components/rbac/catalog-search.tsx @@ -82,7 +82,7 @@ export function CatalogSearch({ className="flex-1 basis-56 sm:max-w-lg" />

- {query.trim() ? `${count} of ${total}` : total} {kind} + {query.trim() || count !== total ? `${count} of ${total}` : total} {kind}

{children} diff --git a/src/routes/access/permissions/index.tsx b/src/routes/access/permissions/index.tsx index 4760548..9cd0672 100644 --- a/src/routes/access/permissions/index.tsx +++ b/src/routes/access/permissions/index.tsx @@ -16,6 +16,34 @@ export const Route = createFileRoute("/access/permissions/")({ component: PermissionsIndex, }); +function applicationKey(permission: PermissionSummary) { + return permission.key.split(":")[0]; +} + +function applicationLabel(key: string) { + if (key === "idp") return "PoliNetwork Auth"; + if (key === "tg") return "Telegram"; + if (key === "wa") return "WhatsApp"; + if (key === "web") return "Website"; + if (key === "azure") return "Microsoft Entra"; + return key.charAt(0).toUpperCase() + key.slice(1); +} + +/** Labels for each application, suffixed with the key where two would otherwise read the same. */ +function applicationLabels(keys: string[]) { + const counts = new Map(); + for (const key of keys) { + const label = applicationLabel(key); + counts.set(label, (counts.get(label) ?? 0) + 1); + } + return new Map( + keys.map((key) => { + const label = applicationLabel(key); + return [key, (counts.get(label) ?? 0) > 1 ? `${label} (${key})` : label]; + }), + ); +} + function PermissionRow({ permission, implied, @@ -40,6 +68,9 @@ function PermissionRow({

{permission.name}

+ {permission.managed && ( + Built-in + )} {implied.length > 0 && ( Also grants {implied.length} )} @@ -63,16 +94,30 @@ function PermissionsIndex() { const { can } = useAccess(); const catalog = Route.useLoaderData(); const [query, setQuery] = useState(""); + const [application, setApplication] = useState(null); + const applications = [...new Set(catalog.permissions.map(applicationKey))].sort(); + const labels = applicationLabels(applications); const search = query.trim().toLowerCase(); - const filtered = catalog.permissions.filter((permission) => - [permission.name, permission.key, permission.description].some((value) => - value?.toLowerCase().includes(search), - ), + const filtered = catalog.permissions.filter( + (permission) => + (application === null || applicationKey(permission) === application) && + [ + permission.name, + permission.key, + permission.description, + labels.get(applicationKey(permission)), + ].some((value) => value?.toLowerCase().includes(search)), ); const impliedBy = (permission: PermissionSummary) => expandPermissionKeys(catalog, permission.implies).filter((key) => key !== permission.key); - const managed = filtered.filter((permission) => permission.managed); - const custom = filtered.filter((permission) => !permission.managed); + const groups = applications + .map((key) => ({ + key, + permissions: filtered + .filter((permission) => applicationKey(permission) === key) + .sort((a, b) => a.key.localeCompare(b.key)), + })) + .filter((group) => group.permissions.length > 0); return (
@@ -103,85 +148,90 @@ function PermissionsIndex() { )} - {search && filtered.length === 0 && } + {applications.length > 0 && ( +
+ + {applications.map((key) => ( + + ))} +
+ )} - { -
- {(managed.length > 0 || !search) && ( - - - Built into the identity provider + {filtered.length === 0 && + (search ? ( + + ) : ( + + + + + ))} + +
+ {groups.map(({ key, permissions }) => ( + + +
+ {labels.get(key)} + + {permissions.some((permission) => permission.key.includes(":")) + ? `${key}:*` + : key} + + + {permissions.length} {permissions.length === 1 ? "permission" : "permissions"} + +
+ {key === "idp" && ( - These permissions control this service itself. They always exist and cannot be - deleted, because the code checks for these exact keys. Give them to a role to let - someone administer part of PoliNetwork Auth. + Permissions for administering PoliNetwork Auth. Built-in permissions always exist + and cannot be deleted. -
- - {managed.length === 0 ? ( -

- No built-in permissions match your search. -

- ) : ( -
    - {managed.map((permission) => ( - - ))} -
- )} -
-
- )} - - {(custom.length > 0 || !search) && ( -
-

- Permissions you created -

- {custom.length === 0 ? ( - - -
-
-
-

No permissions of your own yet

-

- Name the first thing one of your applications should be able to check for, - such as reading the member list. -

-
- {can("idp:permissions:write") && ( - - )} -
-
- ) : ( - -
    - {custom.map((permission) => ( - - ))} -
-
)} -
- )} -
- } +
+ +
    + {permissions.map((permission) => ( + + ))} +
+
+
+ ))} +
); }