From ba71dcfcaddb2ccf311177de8d5037c5ca725dbc Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Tue, 9 Jun 2026 15:53:14 +0500 Subject: [PATCH 01/24] refactor(admin-ui): migrate permission gating to Cedarling usePermission + single-source action catalog (#2872) Signed-off-by: faisalsiddique4400 --- .../__mocks__/cedarlingPermissionBridge.ts | 15 ++ .../constants/cedarlingConstants.test.ts | 30 +++ .../constants/cedarlingLogType.test.ts | 15 ++ .../constants/resourceScopes.test.ts | 51 ++--- .../__tests__/enums/CedarlingLogType.test.ts | 16 -- .../__tests__/hooks/useCedarling.test.ts | 42 ++-- .../__tests__/hooks/usePermission.test.ts | 130 +++++++++++ .../__tests__/utility/resources.test.ts | 33 +-- .../app/cedarling/components/Protected.tsx | 22 ++ admin-ui/app/cedarling/components/index.ts | 1 + .../cedarling/constants/cedarlingConstants.ts | 10 + admin-ui/app/cedarling/constants/index.ts | 3 +- .../cedarling/constants/resourceCatalog.ts | 37 ++++ .../app/cedarling/constants/resourceScopes.ts | 206 ------------------ .../app/cedarling/enums/CedarlingLogType.ts | 4 - admin-ui/app/cedarling/hooks/index.ts | 1 + admin-ui/app/cedarling/hooks/useCedarling.ts | 111 ++++------ admin-ui/app/cedarling/hooks/usePermission.ts | 26 +++ admin-ui/app/cedarling/index.ts | 3 +- admin-ui/app/cedarling/types/cedarTypes.ts | 87 ++++++++ admin-ui/app/cedarling/types/index.ts | 107 +-------- admin-ui/app/cedarling/utility/index.ts | 2 +- admin-ui/app/cedarling/utility/resources.ts | 54 ++--- .../App/PermissionsPolicyInitializer.tsx | 4 +- admin-ui/app/components/Sidebar/types.ts | 3 +- .../redux/features/cedarPermissionsSlice.ts | 4 - .../app/routes/Apps/Gluu/GluuAppSidebar.tsx | 6 +- .../app/routes/Apps/Gluu/GluuCommitDialog.tsx | 21 +- admin-ui/app/routes/Apps/Gluu/GluuDialog.tsx | 21 +- .../Apps/Gluu/GluuWebhookCommitDialog.tsx | 5 +- .../Apps/Gluu/GluuWebhookExecutionDialog.tsx | 20 +- .../Apps/Gluu/Tests/GluuAppSiderbar.test.tsx | 1 - .../app/routes/Apps/Profile/ProfilePage.tsx | 22 +- .../app/routes/Dashboards/DashboardPage.tsx | 23 +- .../__tests__/DashboardPage.test.tsx | 3 - .../License/LicenseDetailsPage.test.tsx | 20 +- .../app/routes/License/LicenseDetailsPage.tsx | 17 +- admin-ui/app/utils/AppAuthProvider.tsx | 21 +- admin-ui/app/utils/PermChecker.ts | 121 ---------- admin-ui/app/utils/auditAction.ts | 10 + .../utils/hooks/useWebhookDialogAction.tsx | 2 +- ...ermCheckerTypes.ts => AuditActionTypes.ts} | 0 admin-ui/app/utils/types/index.ts | 2 +- admin-ui/jest.config.ts | 1 + .../Asset/JansAssetListPage.test.tsx | 12 +- .../components/Asset/assetTestUtils.tsx | 3 - .../components/Audit/AuditListPage.test.tsx | 3 - .../Cedarling/CedarlingConfigPage.test.tsx | 3 - .../__tests__/components/MAU/MauPage.test.tsx | 3 - .../RolePermissionMappingPage.test.tsx | 3 - .../components/Settings/SettingsPage.test.tsx | 4 - .../Webhook/WebhookListPage.test.tsx | 3 - .../components/Assets/JansAssetAddPage.tsx | 8 +- .../components/Assets/JansAssetEditPage.tsx | 8 +- .../components/Assets/JansAssetListPage.tsx | 32 +-- .../admin/components/Audit/AuditListPage.tsx | 19 +- .../Cedarling/CedarlingConfigPage.tsx | 22 +- .../plugins/admin/components/MAU/MauPage.tsx | 13 +- .../Mapping/RolePermissionMappingPage.tsx | 15 +- .../components/Settings/SettingsPage.tsx | 29 +-- .../components/Webhook/WebhookAddPage.tsx | 8 +- .../components/Webhook/WebhookEditPage.tsx | 8 +- .../components/Webhook/WebhookListPage.tsx | 32 +-- admin-ui/plugins/admin/helper/settings.ts | 5 +- .../admin/helper/types/SettingsTypes.ts | 2 +- admin-ui/plugins/admin/plugin-metadata.ts | 61 +++--- .../AuthServerPropertiesPage.test.tsx | 3 - .../components/AuthServerPropertiesPage.tsx | 22 +- .../components/Authentication/Acrs/Acrs.tsx | 27 +-- .../Authentication/AgamaFlows/AgamaFlows.tsx | 33 +-- .../__tests__/helpers/agamaTestUtils.tsx | 3 - .../Authentication/AgamaFlows/helper/utils.ts | 2 +- .../Authentication/Aliases/Aliases.tsx | 24 +- .../Authentication/DefaultAcr/DefaultAcr.tsx | 21 +- .../helpers/authenticationTestUtils.tsx | 3 - .../components/Authentication/constants.ts | 2 - .../components/ConfigApiPropertiesForm.tsx | 17 +- .../components/ConfigApiPropertiesPage.tsx | 17 +- .../__tests__/components/JwkItem.test.tsx | 3 - .../__tests__/components/JwkListPage.test.tsx | 3 - .../__tests__/components/KeysPage.test.tsx | 3 - .../components/Keys/components/KeysPage.tsx | 19 +- .../__tests__/components/LoggingPage.test.tsx | 3 - .../Logging/components/LoggingPage.tsx | 22 +- .../components/ClientListPage.test.tsx | 5 +- .../OidcClients/components/ClientListPage.tsx | 27 +-- .../components/ClientWizardForm.tsx | 20 +- .../components/ScopeListPage.test.tsx | 5 +- .../Scopes/components/ScopeListPage.tsx | 33 +-- .../components/SessionDetailPage.test.tsx | 3 - .../components/SessionListPage.test.tsx | 3 - .../Sessions/components/SessionListPage.tsx | 19 +- .../Ssa/__tests__/helpers/ssaTestUtils.tsx | 3 - .../components/Ssa/components/SsaListPage.tsx | 30 +-- .../hooks/useAuthServerJsonProperties.ts | 2 +- .../plugins/auth-server/plugin-metadata.tsx | 65 +++--- .../auth-server/redux/utils/auditHelpers.ts | 2 +- .../services/jsonPropertiesService.ts | 2 +- .../components/DynamicConfiguration.test.tsx | 3 - .../fido/__tests__/components/Fido.test.tsx | 3 - .../components/Metrics/MetricsPage.test.tsx | 14 +- .../components/StaticConfiguration.test.tsx | 3 - .../fido/components/Configuration/Fido.tsx | 20 +- .../fido/components/Metrics/MetricsPage.tsx | 16 +- admin-ui/plugins/fido/plugin-metadata.ts | 10 +- .../components/JansLockConfiguration.test.tsx | 3 - .../components/JansLockFieldRenderer.test.tsx | 3 - .../plugins/jans-lock/components/JansLock.tsx | 21 +- admin-ui/plugins/jans-lock/plugin-metadata.ts | 6 +- .../saml/components/SamlConfigurationForm.tsx | 17 +- .../WebsiteSsoIdentityBrokeringList.tsx | 34 +-- .../WebsiteSsoServiceProviderList.tsx | 27 +-- admin-ui/plugins/saml/plugin-metadata.ts | 20 +- .../__tests__/components/ScimPage.test.tsx | 3 - admin-ui/plugins/scim/components/ScimPage.tsx | 22 +- admin-ui/plugins/scim/plugin-metadata.ts | 6 +- .../components/CustomScriptAddPage.test.tsx | 3 - .../components/CustomScriptEditPage.test.tsx | 3 - .../components/CustomScriptForm.test.tsx | 3 - .../components/CustomScriptAddPage.tsx | 8 +- .../components/CustomScriptEditPage.tsx | 12 +- .../components/CustomScriptListPage.tsx | 25 +-- admin-ui/plugins/scripts/plugin-metadata.ts | 12 +- .../plugins/services/Components/CachePage.tsx | 22 +- .../services/Components/PersistenceDetail.tsx | 21 +- .../components/PersistenceDetail.test.tsx | 3 - admin-ui/plugins/services/plugin-metadata.ts | 10 +- .../components/SmtpEditPage.test.tsx | 3 - .../__tests__/components/SmtpForm.test.tsx | 3 - .../plugins/smtp/components/SmtpEditPage.tsx | 21 +- admin-ui/plugins/smtp/plugin-metadata.ts | 6 +- .../user-claims/__tests__/cedarTestHelpers.ts | 17 -- .../components/UserClaimsAddPage.test.tsx | 9 +- .../components/UserClaimsEditPage.test.tsx | 9 +- .../components/UserClaimsForm.test.tsx | 9 +- .../components/UserClaimsListPage.test.tsx | 9 +- .../components/UserClaimsAddPage.tsx | 8 +- .../components/UserClaimsEditPage.tsx | 8 +- .../components/UserClaimsListPage.tsx | 32 +-- .../components/UserClaimsViewPage.tsx | 19 +- .../plugins/user-claims/plugin-metadata.ts | 12 +- .../helpers/userManagementTestUtils.tsx | 3 - .../user-management/components/UserList.tsx | 31 +-- .../user-management/plugin-metadata.ts | 10 +- 144 files changed, 801 insertions(+), 1836 deletions(-) create mode 100644 admin-ui/__mocks__/cedarlingPermissionBridge.ts create mode 100644 admin-ui/app/cedarling/__tests__/constants/cedarlingConstants.test.ts create mode 100644 admin-ui/app/cedarling/__tests__/constants/cedarlingLogType.test.ts delete mode 100644 admin-ui/app/cedarling/__tests__/enums/CedarlingLogType.test.ts create mode 100644 admin-ui/app/cedarling/__tests__/hooks/usePermission.test.ts create mode 100644 admin-ui/app/cedarling/components/Protected.tsx create mode 100644 admin-ui/app/cedarling/components/index.ts create mode 100644 admin-ui/app/cedarling/constants/resourceCatalog.ts delete mode 100644 admin-ui/app/cedarling/constants/resourceScopes.ts delete mode 100644 admin-ui/app/cedarling/enums/CedarlingLogType.ts create mode 100644 admin-ui/app/cedarling/hooks/usePermission.ts create mode 100644 admin-ui/app/cedarling/types/cedarTypes.ts delete mode 100644 admin-ui/app/utils/PermChecker.ts create mode 100644 admin-ui/app/utils/auditAction.ts rename admin-ui/app/utils/types/{PermCheckerTypes.ts => AuditActionTypes.ts} (100%) diff --git a/admin-ui/__mocks__/cedarlingPermissionBridge.ts b/admin-ui/__mocks__/cedarlingPermissionBridge.ts new file mode 100644 index 0000000000..c6ede4c38c --- /dev/null +++ b/admin-ui/__mocks__/cedarlingPermissionBridge.ts @@ -0,0 +1,15 @@ +import { useCedarling } from '@/cedarling/hooks/useCedarling' +import type { + AdminUiFeatureResource, + ResourcePermission, + UseCedarlingReturn, +} from '@/cedarling/types' + +export const usePermission = (resource: AdminUiFeatureResource): ResourcePermission => { + const cedar: Partial = useCedarling() + return { + canRead: cedar.hasCedarReadPermission?.(resource) ?? false, + canWrite: cedar.hasCedarWritePermission?.(resource) ?? false, + canDelete: cedar.hasCedarDeletePermission?.(resource) ?? false, + } +} diff --git a/admin-ui/app/cedarling/__tests__/constants/cedarlingConstants.test.ts b/admin-ui/app/cedarling/__tests__/constants/cedarlingConstants.test.ts new file mode 100644 index 0000000000..516d3bd27e --- /dev/null +++ b/admin-ui/app/cedarling/__tests__/constants/cedarlingConstants.test.ts @@ -0,0 +1,30 @@ +import { CEDARLING_CONSTANTS } from '@/cedarling/constants' +import { CEDAR_ACTIONS } from '@/cedarling/constants' +import type { CedarAction } from '@/cedarling' + +const EXPECTED_ACTION_STRINGS: Record = { + read: 'GluuFlexAdminUI::Action::"read"', + write: 'GluuFlexAdminUI::Action::"write"', + delete: 'GluuFlexAdminUI::Action::"delete"', +} + +describe('CEDARLING_CONSTANTS wire contract', () => { + it('pins the Cedar action namespace prefix', () => { + expect(CEDARLING_CONSTANTS.ACTION_TYPE).toBe('GluuFlexAdminUI::Action::') + }) + + it('pins the Cedar resource entity type', () => { + expect(CEDARLING_CONSTANTS.RESOURCE_TYPE).toBe('GluuFlexAdminUIResources::Features') + }) + + it('builds the exact action string the policy store expects for every action', () => { + ;(Object.values(CEDAR_ACTIONS) as CedarAction[]).forEach((action) => { + expect(`${CEDARLING_CONSTANTS.ACTION_TYPE}"${action}"`).toBe(EXPECTED_ACTION_STRINGS[action]) + }) + }) + + it('keeps action and resource namespaces under the same application root', () => { + expect(CEDARLING_CONSTANTS.ACTION_TYPE.startsWith('GluuFlexAdminUI')).toBe(true) + expect(CEDARLING_CONSTANTS.RESOURCE_TYPE.startsWith('GluuFlexAdminUI')).toBe(true) + }) +}) diff --git a/admin-ui/app/cedarling/__tests__/constants/cedarlingLogType.test.ts b/admin-ui/app/cedarling/__tests__/constants/cedarlingLogType.test.ts new file mode 100644 index 0000000000..6bb1922747 --- /dev/null +++ b/admin-ui/app/cedarling/__tests__/constants/cedarlingLogType.test.ts @@ -0,0 +1,15 @@ +import { CEDARLING_LOG_TYPE } from '@/cedarling/constants' + +describe('CEDARLING_LOG_TYPE', () => { + it('has OFF value', () => { + expect(CEDARLING_LOG_TYPE.OFF).toBe('off') + }) + + it('has STD_OUT value', () => { + expect(CEDARLING_LOG_TYPE.STD_OUT).toBe('std_out') + }) + + it('has exactly 2 values', () => { + expect(Object.values(CEDARLING_LOG_TYPE)).toHaveLength(2) + }) +}) diff --git a/admin-ui/app/cedarling/__tests__/constants/resourceScopes.test.ts b/admin-ui/app/cedarling/__tests__/constants/resourceScopes.test.ts index c4bb1b539b..d36344da7b 100644 --- a/admin-ui/app/cedarling/__tests__/constants/resourceScopes.test.ts +++ b/admin-ui/app/cedarling/__tests__/constants/resourceScopes.test.ts @@ -1,13 +1,9 @@ -import { CEDAR_RESOURCE_SCOPES, CEDARLING_CONSTANTS } from '@/cedarling/constants/resourceScopes' +import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import type { AdminUiFeatureResource } from '@/cedarling' -import { - JANS_LOCK_READ, - JANS_LOCK_WRITE, - SMTP_READ, - SMTP_WRITE, - SMTP_DELETE, -} from '@/utils/PermChecker' +import type { AdminUiFeatureResource, CedarAction } from '@/cedarling' + +const VALID_ACTIONS: CedarAction[] = Object.values(CEDAR_ACTIONS) describe('CEDAR_RESOURCE_SCOPES', () => { const allResources = Object.keys(ADMIN_UI_RESOURCES) as AdminUiFeatureResource[] @@ -28,44 +24,23 @@ describe('CEDAR_RESOURCE_SCOPES', () => { expect(scopes.length).toBeGreaterThan(0) }) - it.each(allResources)('%s scope entries have correct resourceId', (resource) => { + it.each(allResources)('%s entries carry the resourceId and a valid action', (resource) => { const scopes = CEDAR_RESOURCE_SCOPES[resource] scopes.forEach((scope) => { expect(scope.resourceId).toBe(resource) - expect(typeof scope.permission).toBe('string') - expect(scope.permission.length).toBeGreaterThan(0) + expect(VALID_ACTIONS).toContain(scope.action) }) }) - it('Lock has read and write scopes', () => { - const lockScopes = CEDAR_RESOURCE_SCOPES.Lock - expect(lockScopes).toHaveLength(2) - const permissions = lockScopes.map((s) => s.permission) - expect(permissions).toContain(JANS_LOCK_READ) - expect(permissions).toContain(JANS_LOCK_WRITE) - }) - - it('SMTP has read, write, and delete scopes', () => { - const smtpScopes = CEDAR_RESOURCE_SCOPES.SMTP - expect(smtpScopes).toHaveLength(3) - const permissions = smtpScopes.map((s) => s.permission) - expect(permissions).toContain(SMTP_READ) - expect(permissions).toContain(SMTP_WRITE) - expect(permissions).toContain(SMTP_DELETE) + it('Lock has read and write actions', () => { + expect(CEDAR_RESOURCE_SCOPES.Lock.map((s) => s.action)).toEqual(['read', 'write']) }) - it('Dashboard has 2 stat scopes', () => { - const dashScopes = CEDAR_RESOURCE_SCOPES.Dashboard - expect(dashScopes).toHaveLength(2) - }) -}) - -describe('CEDARLING_CONSTANTS', () => { - it('has ACTION_TYPE with correct prefix', () => { - expect(CEDARLING_CONSTANTS.ACTION_TYPE).toBe('GluuFlexAdminUI::Action::') + it('SMTP has read, write, and delete actions', () => { + expect(CEDAR_RESOURCE_SCOPES.SMTP.map((s) => s.action)).toEqual(['read', 'write', 'delete']) }) - it('has RESOURCE_TYPE with correct value', () => { - expect(CEDARLING_CONSTANTS.RESOURCE_TYPE).toBe('GluuFlexAdminUIResources::Features') + it('Dashboard has a single read action', () => { + expect(CEDAR_RESOURCE_SCOPES.Dashboard.map((s) => s.action)).toEqual(['read']) }) }) diff --git a/admin-ui/app/cedarling/__tests__/enums/CedarlingLogType.test.ts b/admin-ui/app/cedarling/__tests__/enums/CedarlingLogType.test.ts deleted file mode 100644 index cd71a9d97d..0000000000 --- a/admin-ui/app/cedarling/__tests__/enums/CedarlingLogType.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CedarlingLogType } from '@/cedarling/enums/CedarlingLogType' - -describe('CedarlingLogType', () => { - it('has OFF value', () => { - expect(CedarlingLogType.OFF).toBe('off') - }) - - it('has STD_OUT value', () => { - expect(CedarlingLogType.STD_OUT).toBe('std_out') - }) - - it('has exactly 2 values', () => { - const values = Object.values(CedarlingLogType) - expect(values).toHaveLength(2) - }) -}) diff --git a/admin-ui/app/cedarling/__tests__/hooks/useCedarling.test.ts b/admin-ui/app/cedarling/__tests__/hooks/useCedarling.test.ts index bd98a9fb31..e7d67a860a 100644 --- a/admin-ui/app/cedarling/__tests__/hooks/useCedarling.test.ts +++ b/admin-ui/app/cedarling/__tests__/hooks/useCedarling.test.ts @@ -3,6 +3,8 @@ import React from 'react' import { Provider } from 'react-redux' import { combineReducers, configureStore } from '@reduxjs/toolkit' import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import type { CedarPermissionsState } from '@/cedarling' jest.mock('@/cedarling/client', () => ({ @@ -28,8 +30,6 @@ const createStore = ( const defaultCedar: CedarPermissionsState = { permissions: {}, - loading: false, - error: null, initialized: true, isInitializing: false, cedarFailedStatusAfterMaxTries: null, @@ -65,7 +65,7 @@ describe('useCedarling', () => { it('returns undefined when no cached permission', () => { const store = createStore() const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarReadPermission('Dashboard')).toBeUndefined() + expect(result.current.hasCedarReadPermission(ADMIN_UI_RESOURCES.Dashboard)).toBeUndefined() }) it('returns cached read permission', () => { @@ -73,7 +73,7 @@ describe('useCedarling', () => { cedarState: { permissions: { 'Dashboard::read': true } }, }) const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarReadPermission('Dashboard')).toBe(true) + expect(result.current.hasCedarReadPermission(ADMIN_UI_RESOURCES.Dashboard)).toBe(true) }) it('returns false for denied read permission', () => { @@ -81,7 +81,7 @@ describe('useCedarling', () => { cedarState: { permissions: { 'Users::read': false } }, }) const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarReadPermission('Users')).toBe(false) + expect(result.current.hasCedarReadPermission(ADMIN_UI_RESOURCES.Users)).toBe(false) }) }) @@ -89,7 +89,7 @@ describe('useCedarling', () => { it('returns undefined when no cached permission', () => { const store = createStore() const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarWritePermission('Dashboard')).toBeUndefined() + expect(result.current.hasCedarWritePermission(ADMIN_UI_RESOURCES.Dashboard)).toBeUndefined() }) it('returns cached write permission', () => { @@ -97,7 +97,7 @@ describe('useCedarling', () => { cedarState: { permissions: { 'SMTP::write': true } }, }) const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarWritePermission('SMTP')).toBe(true) + expect(result.current.hasCedarWritePermission(ADMIN_UI_RESOURCES.SMTP)).toBe(true) }) }) @@ -105,7 +105,7 @@ describe('useCedarling', () => { it('returns undefined when no cached permission', () => { const store = createStore() const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarDeletePermission('Scripts')).toBeUndefined() + expect(result.current.hasCedarDeletePermission(ADMIN_UI_RESOURCES.Scripts)).toBeUndefined() }) it('returns cached delete permission', () => { @@ -113,7 +113,7 @@ describe('useCedarling', () => { cedarState: { permissions: { 'Scripts::delete': true } }, }) const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.hasCedarDeletePermission('Scripts')).toBe(true) + expect(result.current.hasCedarDeletePermission(ADMIN_UI_RESOURCES.Scripts)).toBe(true) }) }) @@ -127,7 +127,7 @@ describe('useCedarling', () => { let results: { isAuthorized: boolean; error?: string }[] = [] await act(async () => { results = await result.current.authorizeHelper([ - { permission: 'https://example.com/read', resourceId: 'Dashboard' }, + { action: CEDAR_ACTIONS.READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, ]) }) expect(results[0]?.isAuthorized).toBe(false) @@ -143,7 +143,7 @@ describe('useCedarling', () => { let results: { isAuthorized: boolean; error?: string }[] = [] await act(async () => { results = await result.current.authorizeHelper([ - { permission: 'https://example.com/read', resourceId: 'Dashboard' }, + { action: CEDAR_ACTIONS.READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, ]) }) expect(results[0]?.isAuthorized).toBe(false) @@ -170,7 +170,7 @@ describe('useCedarling', () => { let results: { isAuthorized: boolean }[] = [] await act(async () => { results = await result.current.authorizeHelper([ - { permission: 'https://example.com/read', resourceId: 'Dashboard' }, + { action: CEDAR_ACTIONS.READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, ]) }) expect(results[0]?.isAuthorized).toBe(true) @@ -184,8 +184,8 @@ describe('useCedarling', () => { let results: { isAuthorized: boolean }[] = [] await act(async () => { results = await result.current.authorizeHelper([ - { permission: 'https://example.com/stat-read', resourceId: 'Dashboard' }, - { permission: 'https://example.com/stat-jans-read', resourceId: 'Dashboard' }, + { action: CEDAR_ACTIONS.READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, + { action: CEDAR_ACTIONS.READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, ]) }) expect(results).toHaveLength(2) @@ -205,20 +205,6 @@ describe('useCedarling', () => { expect(typeof result.current.hasCedarReadPermission).toBe('function') expect(typeof result.current.hasCedarWritePermission).toBe('function') expect(typeof result.current.hasCedarDeletePermission).toBe('function') - expect(typeof result.current.isLoading).toBe('boolean') - expect(result.current.error).toBeNull() - }) - - it('reflects loading state from store', () => { - const store = createStore({ cedarState: { loading: true } }) - const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.isLoading).toBe(true) - }) - - it('reflects error state from store', () => { - const store = createStore({ cedarState: { error: 'test error' } }) - const { result } = renderHook(() => useCedarling(), { wrapper: createWrapper(store) }) - expect(result.current.error).toBe('test error') }) }) }) diff --git a/admin-ui/app/cedarling/__tests__/hooks/usePermission.test.ts b/admin-ui/app/cedarling/__tests__/hooks/usePermission.test.ts new file mode 100644 index 0000000000..d8a270f2dd --- /dev/null +++ b/admin-ui/app/cedarling/__tests__/hooks/usePermission.test.ts @@ -0,0 +1,130 @@ +import { renderHook, act, waitFor } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { combineReducers, configureStore } from '@reduxjs/toolkit' +import { usePermission } from '../../hooks/usePermission' +import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import cedarPermissionsReducer from '@/redux/features/cedarPermissionsSlice' +import type { CedarPermissionsState } from '@/cedarling' + +jest.mock('@/cedarling/client', () => ({ + cedarlingClient: { + initialize: jest.fn().mockResolvedValue(undefined), + token_authorize: jest.fn().mockResolvedValue({ decision: true }), + }, +})) + +const createStore = ( + overrides: { + authState?: Record + cedarState?: Partial + } = {}, +) => { + const defaultAuth = { + userinfo_jwt: 'test-userinfo-jwt', + idToken: 'test-id-token', + jwtToken: 'test-access-token', + permissions: [], + ...overrides.authState, + } + + const defaultCedar: CedarPermissionsState = { + permissions: {}, + initialized: true, + isInitializing: false, + cedarFailedStatusAfterMaxTries: null, + policyStoreBytes: '', + ...overrides.cedarState, + } + + return configureStore({ + reducer: combineReducers({ + authReducer: (state = defaultAuth) => state, + cedarPermissions: cedarPermissionsReducer, + noReducer: (state = {}) => state, + }), + preloadedState: { cedarPermissions: defaultCedar }, + }) +} + +const createWrapper = (store: ReturnType) => { + const Wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(Provider, { store } as React.ComponentProps, children) + return Wrapper +} + +describe('usePermission', () => { + let tokenAuthorizeMock: jest.Mock + + beforeEach(() => { + tokenAuthorizeMock = jest.requireMock('@/cedarling/client').cedarlingClient + .token_authorize as jest.Mock + tokenAuthorizeMock.mockClear() + tokenAuthorizeMock.mockResolvedValue({ decision: true }) + }) + + it('returns all permissions false when nothing is cached and cedarling is not initialized', () => { + const store = createStore({ cedarState: { initialized: false } }) + const { result } = renderHook(() => usePermission(ADMIN_UI_RESOURCES.Settings), { + wrapper: createWrapper(store), + }) + + expect(result.current).toEqual({ canRead: false, canWrite: false, canDelete: false }) + expect(typeof result.current.canRead).toBe('boolean') + expect(tokenAuthorizeMock).not.toHaveBeenCalled() + }) + + it('reads cached decisions through to the matching can flags', () => { + const store = createStore({ + cedarState: { permissions: { 'Settings::read': true, 'Settings::write': false } }, + }) + const { result } = renderHook(() => usePermission(ADMIN_UI_RESOURCES.Settings), { + wrapper: createWrapper(store), + }) + + expect(result.current.canRead).toBe(true) + expect(result.current.canWrite).toBe(false) + expect(result.current.canDelete).toBe(false) + }) + + it('authorizes every declared action for the resource on mount when uncached', async () => { + const store = createStore() + const { result } = renderHook(() => usePermission(ADMIN_UI_RESOURCES.Settings), { + wrapper: createWrapper(store), + }) + + await waitFor(() => expect(tokenAuthorizeMock).toHaveBeenCalledTimes(2)) + await waitFor(() => expect(result.current.canRead).toBe(true)) + expect(result.current.canWrite).toBe(true) + expect(result.current.canDelete).toBe(false) + }) + + it('does not call the authorize API when every decision is already cached', async () => { + const store = createStore({ + cedarState: { permissions: { 'Settings::read': true, 'Settings::write': true } }, + }) + const { result } = renderHook(() => usePermission(ADMIN_UI_RESOURCES.Settings), { + wrapper: createWrapper(store), + }) + + await act(async () => { + await Promise.resolve() + }) + + expect(tokenAuthorizeMock).not.toHaveBeenCalled() + expect(result.current.canRead).toBe(true) + expect(result.current.canWrite).toBe(true) + }) + + it('only authorizes the single declared action of a read-only resource', async () => { + const store = createStore() + const { result } = renderHook(() => usePermission(ADMIN_UI_RESOURCES.Dashboard), { + wrapper: createWrapper(store), + }) + + await waitFor(() => expect(tokenAuthorizeMock).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(result.current.canRead).toBe(true)) + expect(result.current.canWrite).toBe(false) + expect(result.current.canDelete).toBe(false) + }) +}) diff --git a/admin-ui/app/cedarling/__tests__/utility/resources.test.ts b/admin-ui/app/cedarling/__tests__/utility/resources.test.ts index 951aa2ed7e..2b239c8f78 100644 --- a/admin-ui/app/cedarling/__tests__/utility/resources.test.ts +++ b/admin-ui/app/cedarling/__tests__/utility/resources.test.ts @@ -1,10 +1,6 @@ -import { - ADMIN_UI_RESOURCES, - findPermissionByUrl, - buildCedarPermissionKey, - CEDARLING_BYPASS, -} from '@/cedarling/utility/resources' -import type { AdminUiFeatureResource, ApiPermissionType } from '@/cedarling' +import { ADMIN_UI_RESOURCES, buildCedarPermissionKey } from '@/cedarling/utility/resources' +import { CEDARLING_BYPASS } from '@/cedarling/constants' +import type { AdminUiFeatureResource } from '@/cedarling' describe('CEDARLING_BYPASS', () => { it('equals "CEDARLING_BYPASS"', () => { @@ -52,29 +48,6 @@ describe('ADMIN_UI_RESOURCES', () => { }) }) -describe('findPermissionByUrl', () => { - const permissions: ApiPermissionType[] = [ - { permission: 'https://example.com/read', tag: 'read-tag' }, - { permission: 'https://example.com/write', tag: 'write-tag' }, - { permission: 'https://example.com/delete', tag: 'delete-tag' }, - ] - - it('finds matching permission by url', () => { - const result = findPermissionByUrl(permissions, 'https://example.com/read') - expect(result).toEqual({ permission: 'https://example.com/read', tag: 'read-tag' }) - }) - - it('returns undefined for non-matching url', () => { - const result = findPermissionByUrl(permissions, 'https://example.com/unknown') - expect(result).toBeUndefined() - }) - - it('returns undefined for empty permissions array', () => { - const result = findPermissionByUrl([], 'https://example.com/read') - expect(result).toBeUndefined() - }) -}) - describe('buildCedarPermissionKey', () => { it('builds key with resourceId and action', () => { expect(buildCedarPermissionKey('Dashboard', 'read')).toBe('Dashboard::read') diff --git a/admin-ui/app/cedarling/components/Protected.tsx b/admin-ui/app/cedarling/components/Protected.tsx new file mode 100644 index 0000000000..117f475e22 --- /dev/null +++ b/admin-ui/app/cedarling/components/Protected.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { usePermission } from '@/cedarling/hooks/usePermission' +import { CEDAR_ACTIONS } from '@/cedarling/constants' +import type { ProtectedProps } from '@/cedarling/types' + +const Protected: React.FC = ({ + resource, + action = CEDAR_ACTIONS.READ, + fallback = null, + children, +}) => { + const { canRead, canWrite, canDelete } = usePermission(resource) + const allowed = + action === CEDAR_ACTIONS.DELETE + ? canDelete + : action === CEDAR_ACTIONS.WRITE + ? canWrite + : canRead + return <>{allowed ? children : fallback} +} + +export default Protected diff --git a/admin-ui/app/cedarling/components/index.ts b/admin-ui/app/cedarling/components/index.ts new file mode 100644 index 0000000000..c325d1f40f --- /dev/null +++ b/admin-ui/app/cedarling/components/index.ts @@ -0,0 +1 @@ +export { default as Protected } from './Protected' diff --git a/admin-ui/app/cedarling/constants/cedarlingConstants.ts b/admin-ui/app/cedarling/constants/cedarlingConstants.ts index a2b62f406b..fc3d88a64c 100644 --- a/admin-ui/app/cedarling/constants/cedarlingConstants.ts +++ b/admin-ui/app/cedarling/constants/cedarlingConstants.ts @@ -1,4 +1,14 @@ export const CEDARLING_CONSTANTS = { ACTION_TYPE: 'GluuFlexAdminUI::Action::', RESOURCE_TYPE: 'GluuFlexAdminUIResources::Features', + TOKEN_MAPPINGS: { + ACCESS_TOKEN: 'GluuFlexAdminUI::Access_token', + ID_TOKEN: 'GluuFlexAdminUI::id_token', + USERINFO_TOKEN: 'GluuFlexAdminUI::Userinfo_token', + }, +} as const + +export const CEDARLING_LOG_TYPE = { + OFF: 'off', + STD_OUT: 'std_out', } as const diff --git a/admin-ui/app/cedarling/constants/index.ts b/admin-ui/app/cedarling/constants/index.ts index 3a93348436..62c6dd0f21 100644 --- a/admin-ui/app/cedarling/constants/index.ts +++ b/admin-ui/app/cedarling/constants/index.ts @@ -1 +1,2 @@ -export { CEDARLING_CONSTANTS } from './cedarlingConstants' +export { CEDARLING_CONSTANTS, CEDARLING_LOG_TYPE } from './cedarlingConstants' +export { CEDAR_ACTIONS, CEDARLING_BYPASS, RESOURCE_ACTIONS } from './resourceCatalog' diff --git a/admin-ui/app/cedarling/constants/resourceCatalog.ts b/admin-ui/app/cedarling/constants/resourceCatalog.ts new file mode 100644 index 0000000000..a8034af53f --- /dev/null +++ b/admin-ui/app/cedarling/constants/resourceCatalog.ts @@ -0,0 +1,37 @@ +export const CEDAR_ACTIONS = { + READ: 'read', + WRITE: 'write', + DELETE: 'delete', +} as const + +export const CEDARLING_BYPASS = 'CEDARLING_BYPASS' as const + +export const RESOURCE_ACTIONS = { + Dashboard: ['read'], + License: ['read', 'write'], + MAU: ['read'], + Security: ['read', 'write'], + Settings: ['read', 'write'], + Webhooks: ['read', 'write', 'delete'], + Assets: ['read', 'write', 'delete'], + AuditLogs: ['read'], + Clients: ['read', 'write', 'delete'], + Scopes: ['read', 'write', 'delete'], + Keys: ['read', 'write'], + AuthenticationServerConfiguration: ['read', 'write'], + Logging: ['read', 'write'], + SSA: ['read', 'write', 'delete'], + Authentication: ['read', 'write', 'delete'], + ConfigApiConfiguration: ['read', 'write'], + Session: ['read', 'delete'], + Users: ['read', 'write', 'delete'], + Scripts: ['read', 'write', 'delete'], + Attributes: ['read', 'write', 'delete'], + Cache: ['read', 'write', 'delete'], + Persistence: ['read'], + SMTP: ['read', 'write', 'delete'], + SCIM: ['read', 'write'], + FIDO: ['read', 'write', 'delete'], + SAML: ['read', 'write', 'delete'], + Lock: ['read', 'write'], +} as const satisfies Record diff --git a/admin-ui/app/cedarling/constants/resourceScopes.ts b/admin-ui/app/cedarling/constants/resourceScopes.ts deleted file mode 100644 index abd5a22019..0000000000 --- a/admin-ui/app/cedarling/constants/resourceScopes.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { AdminUiFeatureResource, ResourceScopeEntry } from '@/cedarling/types' -import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { - ASSETS_DELETE, - ASSETS_READ, - ASSETS_WRITE, - CACHE_DELETE, - CACHE_READ, - CACHE_WRITE, - FIDO_DELETE, - FIDO_READ, - FIDO_WRITE, - JANS_LOCK_READ, - JANS_LOCK_WRITE, - LICENSE_DETAILS_READ, - LICENSE_DETAILS_WRITE, - PERSISTENCE_DETAIL, - SCIM_CONFIG_READ, - SCIM_CONFIG_WRITE, - SCRIPT_DELETE, - SCRIPT_READ, - SCRIPT_WRITE, - SMTP_DELETE, - SMTP_READ, - SMTP_WRITE, - STAT_JANS_READ, - STAT_READ, - SSA_PORTAL, - SSA_ADMIN, - SSA_DELETE, - SCOPE_READ, - SCOPE_WRITE, - SCOPE_DELETE, - SESSION_READ, - SESSION_DELETE, - ACR_READ, - ACR_WRITE, - CLIENT_READ, - CLIENT_WRITE, - CLIENT_DELETE, - PROPERTIES_WRITE, - API_CONFIG_READ, - API_CONFIG_WRITE, - LOGGING_READ, - LOGGING_WRITE, - JWKS_READ, - JWKS_WRITE, - SAML_READ, - SAML_WRITE, - SAML_DELETE, - SAML_TR_READ, - SAML_TR_WRITE, - SAML_CONFIG_READ, - SAML_CONFIG_WRITE, - ATTRIBUTE_READ, - ATTRIBUTE_WRITE, - ATTRIBUTE_DELETE, - USER_READ, - USER_WRITE, - USER_DELETE, - WEBHOOK_DELETE, - WEBHOOK_READ, - WEBHOOK_WRITE, - AGAMA_READ, - AGAMA_WRITE, - AGAMA_DELETE, - ROLE_READ, - PERMISSION_READ, - MAPPING_READ, - MAPPING_WRITE, -} from '@/utils/PermChecker' - -// Type ensures all AdminUiFeatureResource keys are present at compile time -export const CEDAR_RESOURCE_SCOPES: Record = { - [ADMIN_UI_RESOURCES.Dashboard]: [ - { permission: STAT_READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, - { permission: STAT_JANS_READ, resourceId: ADMIN_UI_RESOURCES.Dashboard }, - ], - [ADMIN_UI_RESOURCES.MAU]: [ - { permission: STAT_READ, resourceId: ADMIN_UI_RESOURCES.MAU }, - { permission: STAT_JANS_READ, resourceId: ADMIN_UI_RESOURCES.MAU }, - ], - [ADMIN_UI_RESOURCES.License]: [ - { permission: LICENSE_DETAILS_READ, resourceId: ADMIN_UI_RESOURCES.License }, - { permission: LICENSE_DETAILS_WRITE, resourceId: ADMIN_UI_RESOURCES.License }, - ], - [ADMIN_UI_RESOURCES.Assets]: [ - { permission: ASSETS_READ, resourceId: ADMIN_UI_RESOURCES.Assets }, - { permission: ASSETS_WRITE, resourceId: ADMIN_UI_RESOURCES.Assets }, - { permission: ASSETS_DELETE, resourceId: ADMIN_UI_RESOURCES.Assets }, - ], - [ADMIN_UI_RESOURCES.Webhooks]: [ - { permission: WEBHOOK_READ, resourceId: ADMIN_UI_RESOURCES.Webhooks }, - { permission: WEBHOOK_WRITE, resourceId: ADMIN_UI_RESOURCES.Webhooks }, - { permission: WEBHOOK_DELETE, resourceId: ADMIN_UI_RESOURCES.Webhooks }, - ], - [ADMIN_UI_RESOURCES.Scripts]: [ - { permission: SCRIPT_READ, resourceId: ADMIN_UI_RESOURCES.Scripts }, - { permission: SCRIPT_WRITE, resourceId: ADMIN_UI_RESOURCES.Scripts }, - { permission: SCRIPT_DELETE, resourceId: ADMIN_UI_RESOURCES.Scripts }, - ], - [ADMIN_UI_RESOURCES.Cache]: [ - { permission: CACHE_READ, resourceId: ADMIN_UI_RESOURCES.Cache }, - { permission: CACHE_WRITE, resourceId: ADMIN_UI_RESOURCES.Cache }, - { permission: CACHE_DELETE, resourceId: ADMIN_UI_RESOURCES.Cache }, - ], - [ADMIN_UI_RESOURCES.Persistence]: [ - { permission: PERSISTENCE_DETAIL, resourceId: ADMIN_UI_RESOURCES.Persistence }, - ], - [ADMIN_UI_RESOURCES.Lock]: [ - { permission: JANS_LOCK_READ, resourceId: ADMIN_UI_RESOURCES.Lock }, - { permission: JANS_LOCK_WRITE, resourceId: ADMIN_UI_RESOURCES.Lock }, - ], - [ADMIN_UI_RESOURCES.FIDO]: [ - { permission: FIDO_READ, resourceId: ADMIN_UI_RESOURCES.FIDO }, - { permission: FIDO_WRITE, resourceId: ADMIN_UI_RESOURCES.FIDO }, - { permission: FIDO_DELETE, resourceId: ADMIN_UI_RESOURCES.FIDO }, - ], - [ADMIN_UI_RESOURCES.SMTP]: [ - { permission: SMTP_READ, resourceId: ADMIN_UI_RESOURCES.SMTP }, - { permission: SMTP_WRITE, resourceId: ADMIN_UI_RESOURCES.SMTP }, - { permission: SMTP_DELETE, resourceId: ADMIN_UI_RESOURCES.SMTP }, - ], - [ADMIN_UI_RESOURCES.SCIM]: [ - { permission: SCIM_CONFIG_READ, resourceId: ADMIN_UI_RESOURCES.SCIM }, - { permission: SCIM_CONFIG_WRITE, resourceId: ADMIN_UI_RESOURCES.SCIM }, - ], - [ADMIN_UI_RESOURCES.Users]: [ - { permission: USER_READ, resourceId: ADMIN_UI_RESOURCES.Users }, - { permission: USER_WRITE, resourceId: ADMIN_UI_RESOURCES.Users }, - { permission: USER_DELETE, resourceId: ADMIN_UI_RESOURCES.Users }, - ], - [ADMIN_UI_RESOURCES.SAML]: [ - { permission: SAML_READ, resourceId: ADMIN_UI_RESOURCES.SAML }, - { permission: SAML_WRITE, resourceId: ADMIN_UI_RESOURCES.SAML }, - { permission: SAML_DELETE, resourceId: ADMIN_UI_RESOURCES.SAML }, - { permission: SAML_TR_READ, resourceId: ADMIN_UI_RESOURCES.SAML }, - { permission: SAML_TR_WRITE, resourceId: ADMIN_UI_RESOURCES.SAML }, - { permission: SAML_CONFIG_READ, resourceId: ADMIN_UI_RESOURCES.SAML }, - { permission: SAML_CONFIG_WRITE, resourceId: ADMIN_UI_RESOURCES.SAML }, - ], - [ADMIN_UI_RESOURCES.Attributes]: [ - { permission: ATTRIBUTE_READ, resourceId: ADMIN_UI_RESOURCES.Attributes }, - { permission: ATTRIBUTE_WRITE, resourceId: ADMIN_UI_RESOURCES.Attributes }, - { permission: ATTRIBUTE_DELETE, resourceId: ADMIN_UI_RESOURCES.Attributes }, - ], - [ADMIN_UI_RESOURCES.SSA]: [ - { permission: SSA_PORTAL, resourceId: ADMIN_UI_RESOURCES.SSA }, - { permission: SSA_ADMIN, resourceId: ADMIN_UI_RESOURCES.SSA }, - { permission: SSA_DELETE, resourceId: ADMIN_UI_RESOURCES.SSA }, - ], - [ADMIN_UI_RESOURCES.Scopes]: [ - { permission: SCOPE_READ, resourceId: ADMIN_UI_RESOURCES.Scopes }, - { permission: SCOPE_WRITE, resourceId: ADMIN_UI_RESOURCES.Scopes }, - { permission: SCOPE_DELETE, resourceId: ADMIN_UI_RESOURCES.Scopes }, - ], - [ADMIN_UI_RESOURCES.Session]: [ - { permission: SESSION_READ, resourceId: ADMIN_UI_RESOURCES.Session }, - { permission: SESSION_DELETE, resourceId: ADMIN_UI_RESOURCES.Session }, - ], - [ADMIN_UI_RESOURCES.Authentication]: [ - { permission: ACR_READ, resourceId: ADMIN_UI_RESOURCES.Authentication }, - { permission: ACR_WRITE, resourceId: ADMIN_UI_RESOURCES.Authentication }, - { permission: AGAMA_READ, resourceId: ADMIN_UI_RESOURCES.Authentication }, - { permission: AGAMA_WRITE, resourceId: ADMIN_UI_RESOURCES.Authentication }, - { permission: AGAMA_DELETE, resourceId: ADMIN_UI_RESOURCES.Authentication }, - ], - [ADMIN_UI_RESOURCES.Clients]: [ - { permission: CLIENT_READ, resourceId: ADMIN_UI_RESOURCES.Clients }, - { permission: CLIENT_WRITE, resourceId: ADMIN_UI_RESOURCES.Clients }, - { permission: CLIENT_DELETE, resourceId: ADMIN_UI_RESOURCES.Clients }, - ], - [ADMIN_UI_RESOURCES.AuthenticationServerConfiguration]: [ - { - permission: PROPERTIES_WRITE, - resourceId: ADMIN_UI_RESOURCES.AuthenticationServerConfiguration, - }, - ], - [ADMIN_UI_RESOURCES.ConfigApiConfiguration]: [ - { permission: API_CONFIG_READ, resourceId: ADMIN_UI_RESOURCES.ConfigApiConfiguration }, - { permission: API_CONFIG_WRITE, resourceId: ADMIN_UI_RESOURCES.ConfigApiConfiguration }, - ], - [ADMIN_UI_RESOURCES.Logging]: [ - { permission: LOGGING_READ, resourceId: ADMIN_UI_RESOURCES.Logging }, - { permission: LOGGING_WRITE, resourceId: ADMIN_UI_RESOURCES.Logging }, - ], - [ADMIN_UI_RESOURCES.Keys]: [ - { permission: JWKS_READ, resourceId: ADMIN_UI_RESOURCES.Keys }, - { permission: JWKS_WRITE, resourceId: ADMIN_UI_RESOURCES.Keys }, - ], - [ADMIN_UI_RESOURCES.Security]: [ - { permission: ROLE_READ, resourceId: ADMIN_UI_RESOURCES.Security }, - { permission: PERMISSION_READ, resourceId: ADMIN_UI_RESOURCES.Security }, - { permission: MAPPING_READ, resourceId: ADMIN_UI_RESOURCES.Security }, - { permission: MAPPING_WRITE, resourceId: ADMIN_UI_RESOURCES.Security }, - ], - [ADMIN_UI_RESOURCES.Settings]: [ - { permission: ACR_READ, resourceId: ADMIN_UI_RESOURCES.Settings }, - { permission: ACR_WRITE, resourceId: ADMIN_UI_RESOURCES.Settings }, - ], - [ADMIN_UI_RESOURCES.AuditLogs]: [ - { permission: LOGGING_READ, resourceId: ADMIN_UI_RESOURCES.AuditLogs }, - ], -} as const satisfies Record - -export { CEDARLING_CONSTANTS } from './cedarlingConstants' diff --git a/admin-ui/app/cedarling/enums/CedarlingLogType.ts b/admin-ui/app/cedarling/enums/CedarlingLogType.ts deleted file mode 100644 index 4f5df46645..0000000000 --- a/admin-ui/app/cedarling/enums/CedarlingLogType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum CedarlingLogType { - OFF = 'off', - STD_OUT = 'std_out', -} diff --git a/admin-ui/app/cedarling/hooks/index.ts b/admin-ui/app/cedarling/hooks/index.ts index bb1dc8e5b0..3165d52010 100644 --- a/admin-ui/app/cedarling/hooks/index.ts +++ b/admin-ui/app/cedarling/hooks/index.ts @@ -1 +1,2 @@ export { useCedarling } from './useCedarling' +export { usePermission } from './usePermission' diff --git a/admin-ui/app/cedarling/hooks/useCedarling.ts b/admin-ui/app/cedarling/hooks/useCedarling.ts index 1f7ee88cf8..2ae8517486 100644 --- a/admin-ui/app/cedarling/hooks/useCedarling.ts +++ b/admin-ui/app/cedarling/hooks/useCedarling.ts @@ -1,9 +1,9 @@ -import { useCallback } from 'react' +import { useCallback, useRef } from 'react' import { useAppDispatch, useAppSelector } from '@/redux/hooks' import { setCedarlingPermission } from '@/redux/features/cedarPermissionsSlice' import { cedarlingClient } from '@/cedarling/client' import { buildCedarPermissionKey } from '@/cedarling/utility' -import { CEDARLING_CONSTANTS } from '@/cedarling/constants' +import { CEDAR_ACTIONS, CEDARLING_CONSTANTS } from '@/cedarling/constants' import type { UseCedarlingReturn, AuthorizationResult, @@ -12,14 +12,14 @@ import type { AdminUiFeatureResource, ITokenEntry, } from '@/cedarling/types' -import { OPENID, REVOKE_SESSION, SCIM_BULK, SSA_ADMIN, SSA_DEVELOPER } from '@/utils/PermChecker' import { updateToast } from '@/redux/features/toastSlice' -const executeUrls = new Set([SSA_ADMIN, SSA_DEVELOPER, SCIM_BULK, REVOKE_SESSION, OPENID]) +const inFlightAuthorizations = new Map>() -export const useCedarling = (): UseCedarlingReturn => { - const { ACTION_TYPE, RESOURCE_TYPE } = CEDARLING_CONSTANTS +const MAX_ERROR_MESSAGE_LENGTH = 25 +const { ACTION_TYPE, RESOURCE_TYPE, TOKEN_MAPPINGS } = CEDARLING_CONSTANTS +export const useCedarling = (): UseCedarlingReturn => { const dispatch = useAppDispatch() const { @@ -30,33 +30,17 @@ export const useCedarling = (): UseCedarlingReturn => { const { permissions: permissionsByResourceId, - loading: isLoading, - error, initialized: cedarlingInitialized, isInitializing, } = useAppSelector((state) => state.cedarPermissions) - const getActionLabelFromUrl = useCallback((url: string): CedarAction => { - const lowerUrl = url.toLowerCase() - - if (executeUrls.has(lowerUrl)) { - return `write` - } - - if (lowerUrl.includes('write')) { - return 'write' - } + const permissionsRef = useRef(permissionsByResourceId) + permissionsRef.current = permissionsByResourceId - if (lowerUrl.includes('delete')) { - return 'delete' - } - - return 'read' - }, []) const buildAuthorizationRequest = useCallback( (resourceId: AdminUiFeatureResource, actionLabel: CedarAction) => { const cacheKey = buildCedarPermissionKey(resourceId, actionLabel) - const cachedDecision = permissionsByResourceId[cacheKey] + const cachedDecision = permissionsRef.current[cacheKey] if (cachedDecision !== undefined) { return { cacheKey, cachedDecision } } @@ -67,9 +51,9 @@ export const useCedarling = (): UseCedarlingReturn => { } const tokens: ITokenEntry[] = [ - { mapping: 'GluuFlexAdminUI::Access_token', payload: access_token }, - { mapping: 'GluuFlexAdminUI::id_token', payload: id_token }, - { mapping: 'GluuFlexAdminUI::Userinfo_token', payload: userinfo_token }, + { mapping: TOKEN_MAPPINGS.ACCESS_TOKEN, payload: access_token }, + { mapping: TOKEN_MAPPINGS.ID_TOKEN, payload: id_token }, + { mapping: TOKEN_MAPPINGS.USERINFO_TOKEN, payload: userinfo_token }, ] if (!resourceId) { @@ -92,24 +76,13 @@ export const useCedarling = (): UseCedarlingReturn => { return { request: requestPayload, cacheKey } }, - [access_token, id_token, userinfo_token, RESOURCE_TYPE, permissionsByResourceId, ACTION_TYPE], + [access_token, id_token, userinfo_token], ) const getCachedDecisionByAction = useCallback( (resourceId: AdminUiFeatureResource, action: CedarAction): boolean | undefined => - permissionsByResourceId[buildCedarPermissionKey(resourceId, action)], - [permissionsByResourceId], - ) - - const getCachedPermission = useCallback( - (resourceId: AdminUiFeatureResource): boolean | undefined => { - const readKey = buildCedarPermissionKey(resourceId, 'read') - if (readKey in permissionsByResourceId) { - return permissionsByResourceId[readKey] - } - return permissionsByResourceId[resourceId] - }, - [permissionsByResourceId], + permissionsRef.current[buildCedarPermissionKey(resourceId, action)], + [], ) const authorize = useCallback( @@ -117,7 +90,6 @@ export const useCedarling = (): UseCedarlingReturn => { const scopeEntry = resourceScope[0] if (!scopeEntry) return { isAuthorized: false } - const url = scopeEntry.permission const resolvedResourceId = scopeEntry.resourceId if (!cedarlingInitialized || isInitializing) { @@ -141,7 +113,7 @@ export const useCedarling = (): UseCedarlingReturn => { } } - const actionLabel = getActionLabelFromUrl(url) + const actionLabel = scopeEntry.action try { const { request, cacheKey, cachedDecision } = buildAuthorizationRequest( @@ -151,22 +123,34 @@ export const useCedarling = (): UseCedarlingReturn => { if (cachedDecision !== undefined) { return { isAuthorized: cachedDecision } } - const response = await cedarlingClient.token_authorize(request) - const isAuthorized = response?.decision === true + const pending = inFlightAuthorizations.get(cacheKey) + if (pending) { + return await pending + } - dispatch( - setCedarlingPermission({ - resourceId: cacheKey, - isAuthorized, - }), - ) - return { isAuthorized, response } + const authorizing = cedarlingClient + .token_authorize(request) + .then((response): AuthorizationResult => { + const isAuthorized = response?.decision === true + dispatch(setCedarlingPermission({ resourceId: cacheKey, isAuthorized })) + return { isAuthorized, response } + }) + inFlightAuthorizations.set(cacheKey, authorizing) + + try { + return await authorizing + } finally { + inFlightAuthorizations.delete(cacheKey) + } } catch (error) { const toMessage = (err: Error | string): string => err instanceof Error ? err.message : typeof err === 'string' ? err : 'Unknown error' const rawMessage = toMessage(error as Error | string) - const truncated = rawMessage.length > 25 ? rawMessage.slice(0, 25) + '…' : rawMessage + const truncated = + rawMessage.length > MAX_ERROR_MESSAGE_LENGTH + ? rawMessage.slice(0, MAX_ERROR_MESSAGE_LENGTH) + '…' + : rawMessage dispatch(updateToast(true, 'error', `Authorization error: ${truncated}`)) return { isAuthorized: false, @@ -182,7 +166,6 @@ export const useCedarling = (): UseCedarlingReturn => { userinfo_token, cedarlingInitialized, isInitializing, - getActionLabelFromUrl, ], ) @@ -195,8 +178,8 @@ export const useCedarling = (): UseCedarlingReturn => { const uniqueMap = new Map() resourceScopes.forEach((entry, index) => { - const actionLabel = getActionLabelFromUrl(entry.permission) - const key = `${entry.resourceId}::${actionLabel}` + const actionLabel = entry.action + const key = buildCedarPermissionKey(entry.resourceId, actionLabel) const existing = uniqueMap.get(key) if (existing) { existing.indices.push(index) @@ -238,22 +221,24 @@ export const useCedarling = (): UseCedarlingReturn => { return results }, - [authorize, getActionLabelFromUrl], + [authorize], ) const hasCedarReadPermission = useCallback( (resourceId: AdminUiFeatureResource) => - getCachedDecisionByAction(resourceId, 'read') ?? getCachedPermission(resourceId), - [getCachedDecisionByAction, getCachedPermission], + getCachedDecisionByAction(resourceId, CEDAR_ACTIONS.READ), + [getCachedDecisionByAction], ) const hasCedarWritePermission = useCallback( - (resourceId: AdminUiFeatureResource) => getCachedDecisionByAction(resourceId, 'write'), + (resourceId: AdminUiFeatureResource) => + getCachedDecisionByAction(resourceId, CEDAR_ACTIONS.WRITE), [getCachedDecisionByAction], ) const hasCedarDeletePermission = useCallback( - (resourceId: AdminUiFeatureResource) => getCachedDecisionByAction(resourceId, 'delete'), + (resourceId: AdminUiFeatureResource) => + getCachedDecisionByAction(resourceId, CEDAR_ACTIONS.DELETE), [getCachedDecisionByAction], ) @@ -262,7 +247,5 @@ export const useCedarling = (): UseCedarlingReturn => { hasCedarReadPermission, hasCedarWritePermission, hasCedarDeletePermission, - isLoading, - error, } } diff --git a/admin-ui/app/cedarling/hooks/usePermission.ts b/admin-ui/app/cedarling/hooks/usePermission.ts new file mode 100644 index 0000000000..648a71d27a --- /dev/null +++ b/admin-ui/app/cedarling/hooks/usePermission.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react' +import { useCedarling } from './useCedarling' +import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/utility' +import type { AdminUiFeatureResource, ResourcePermission } from '@/cedarling/types' + +export const usePermission = (resource: AdminUiFeatureResource): ResourcePermission => { + const { + authorizeHelper, + hasCedarReadPermission, + hasCedarWritePermission, + hasCedarDeletePermission, + } = useCedarling() + const scopes = CEDAR_RESOURCE_SCOPES[resource] + + useEffect(() => { + if (scopes && scopes.length > 0) { + authorizeHelper(scopes) + } + }, [authorizeHelper, scopes]) + + return { + canRead: hasCedarReadPermission(resource) ?? false, + canWrite: hasCedarWritePermission(resource) ?? false, + canDelete: hasCedarDeletePermission(resource) ?? false, + } +} diff --git a/admin-ui/app/cedarling/index.ts b/admin-ui/app/cedarling/index.ts index c495a83e92..793b6e450e 100644 --- a/admin-ui/app/cedarling/index.ts +++ b/admin-ui/app/cedarling/index.ts @@ -1,2 +1,3 @@ -export { useCedarling } from './hooks' +export { useCedarling, usePermission } from './hooks' +export { Protected } from './components' export type * from './types' diff --git a/admin-ui/app/cedarling/types/cedarTypes.ts b/admin-ui/app/cedarling/types/cedarTypes.ts new file mode 100644 index 0000000000..45e87b884c --- /dev/null +++ b/admin-ui/app/cedarling/types/cedarTypes.ts @@ -0,0 +1,87 @@ +import type { ReactNode } from 'react' +import type { JsonValue } from 'Routes/Apps/Gluu/types/common' +import { CEDAR_ACTIONS, RESOURCE_ACTIONS, CEDARLING_LOG_TYPE } from '@/cedarling/constants' + +export type CedarAction = (typeof CEDAR_ACTIONS)[keyof typeof CEDAR_ACTIONS] +export type AdminUiFeatureResource = keyof typeof RESOURCE_ACTIONS +export type ResourceScopeEntry = { + action: CedarAction + resourceId: AdminUiFeatureResource +} +export type CedarlingLogType = (typeof CEDARLING_LOG_TYPE)[keyof typeof CEDARLING_LOG_TYPE] + +export type ProtectedProps = { + resource: AdminUiFeatureResource + action?: CedarAction + fallback?: ReactNode + children: ReactNode +} + +export type ITokenEntry = { + mapping: string + payload: string +} + +type ICedarEntityMappingResource = { + cedar_entity_mapping: { + entity_type: string + id: string + } +} + +export type TokenAuthorizationRequest = { + tokens: ITokenEntry[] + action: string + resource: ICedarEntityMappingResource + context: Record +} + +export type AuthorizationResponse = { + decision: boolean + diagnostics?: { + reason?: string[] + errors?: string[] + } + [key: string]: JsonValue | undefined +} + +export type AuthorizationResult = { + isAuthorized: boolean + response?: AuthorizationResponse + error?: string +} + +export type BootStrapConfig = { + [key: string]: JsonValue | undefined +} + +export type ICedarlingClient = { + initialize: (config: BootStrapConfig, policyStoreBytes: Uint8Array) => Promise + token_authorize: (request: TokenAuthorizationRequest) => Promise +} + +export type CedarPermissionsState = { + permissions: Record + initialized: null | boolean + isInitializing: boolean + cedarFailedStatusAfterMaxTries: null | boolean + policyStoreBytes: string +} + +export type SetCedarlingPermissionPayload = { + resourceId: string + isAuthorized: boolean +} + +export type ResourcePermission = { + canRead: boolean + canWrite: boolean + canDelete: boolean +} + +export type UseCedarlingReturn = { + authorizeHelper: (resourceScopes: ResourceScopeEntry[]) => Promise + hasCedarReadPermission: (resourceId: AdminUiFeatureResource) => boolean | undefined + hasCedarWritePermission: (resourceId: AdminUiFeatureResource) => boolean | undefined + hasCedarDeletePermission: (resourceId: AdminUiFeatureResource) => boolean | undefined +} diff --git a/admin-ui/app/cedarling/types/index.ts b/admin-ui/app/cedarling/types/index.ts index 40b7954e2c..f4cb7b55bf 100644 --- a/admin-ui/app/cedarling/types/index.ts +++ b/admin-ui/app/cedarling/types/index.ts @@ -1,106 +1 @@ -import type { JsonValue } from 'Routes/Apps/Gluu/types/common' - -export type ITokenEntry = { - mapping: string - payload: string -} - -type ICedarEntityMappingResource = { - cedar_entity_mapping: { - entity_type: string - id: string - } -} - -export type TokenAuthorizationRequest = { - tokens: ITokenEntry[] - action: string - resource: ICedarEntityMappingResource - context: Record -} - -export type AuthorizationResponse = { - decision: boolean - diagnostics?: { - reason?: string[] - errors?: string[] - } - [key: string]: JsonValue | undefined -} - -export type AuthorizationResult = { - isAuthorized: boolean - response?: AuthorizationResponse - error?: string -} - -export type BootStrapConfig = { - [key: string]: JsonValue | undefined -} - -export type ICedarlingClient = { - initialize: (config: BootStrapConfig, policyStoreBytes: Uint8Array) => Promise - token_authorize: (request: TokenAuthorizationRequest) => Promise -} - -export type CedarPermissionsState = { - permissions: Record - loading: boolean - error: string | null - initialized: null | boolean - isInitializing: boolean - cedarFailedStatusAfterMaxTries: null | boolean - policyStoreBytes: string -} - -export type SetCedarlingPermissionPayload = { - resourceId: string - isAuthorized: boolean -} - -export type ResourceScopeEntry = { - permission: string - resourceId: AdminUiFeatureResource -} - -export type UseCedarlingReturn = { - authorizeHelper: (resourceScopes: ResourceScopeEntry[]) => Promise - hasCedarReadPermission: (resourceId: AdminUiFeatureResource) => boolean | undefined - hasCedarWritePermission: (resourceId: AdminUiFeatureResource) => boolean | undefined - hasCedarDeletePermission: (resourceId: AdminUiFeatureResource) => boolean | undefined - isLoading: boolean - error: string | null -} - -export type AdminUiFeatureResource = - | 'Dashboard' - | 'License' - | 'MAU' - | 'Security' - | 'Settings' - | 'Webhooks' - | 'Assets' - | 'AuditLogs' - | 'Clients' - | 'Scopes' - | 'Keys' - | 'AuthenticationServerConfiguration' - | 'Logging' - | 'SSA' - | 'Authentication' - | 'ConfigApiConfiguration' - | 'Session' - | 'Users' - | 'Scripts' - | 'Attributes' - | 'Cache' - | 'Persistence' - | 'SMTP' - | 'SCIM' - | 'FIDO' - | 'SAML' - | 'Lock' - -export type ApiPermissionType = { permission: string; tag: string } - -export type CedarAction = 'read' | 'write' | 'delete' +export type * from './cedarTypes' diff --git a/admin-ui/app/cedarling/utility/index.ts b/admin-ui/app/cedarling/utility/index.ts index 6bb66e1af5..eac7c03b61 100644 --- a/admin-ui/app/cedarling/utility/index.ts +++ b/admin-ui/app/cedarling/utility/index.ts @@ -1 +1 @@ -export { ADMIN_UI_RESOURCES, CEDARLING_BYPASS, buildCedarPermissionKey } from './resources' +export { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES, buildCedarPermissionKey } from './resources' diff --git a/admin-ui/app/cedarling/utility/resources.ts b/admin-ui/app/cedarling/utility/resources.ts index 315107a710..a40705d889 100644 --- a/admin-ui/app/cedarling/utility/resources.ts +++ b/admin-ui/app/cedarling/utility/resources.ts @@ -1,44 +1,20 @@ -import type { AdminUiFeatureResource, ApiPermissionType, CedarAction } from '@/cedarling/types' +import { RESOURCE_ACTIONS } from '@/cedarling/constants' +import type { AdminUiFeatureResource, CedarAction, ResourceScopeEntry } from '@/cedarling/types' -export const CEDARLING_BYPASS = 'CEDARLING_BYPASS' as const - -export const ADMIN_UI_RESOURCES = { - Dashboard: 'Dashboard', - License: 'License', - MAU: 'MAU', - Security: 'Security', - Settings: 'Settings', - Webhooks: 'Webhooks', - Assets: 'Assets', - AuditLogs: 'AuditLogs', - Clients: 'Clients', - Scopes: 'Scopes', - Keys: 'Keys', - AuthenticationServerConfiguration: 'AuthenticationServerConfiguration', - Logging: 'Logging', - SSA: 'SSA', - Authentication: 'Authentication', - ConfigApiConfiguration: 'ConfigApiConfiguration', - Session: 'Session', - Users: 'Users', - Scripts: 'Scripts', - Attributes: 'Attributes', - Cache: 'Cache', - Persistence: 'Persistence', - SMTP: 'SMTP', - SCIM: 'SCIM', - FIDO: 'FIDO', - SAML: 'SAML', - Lock: 'Lock', -} as const satisfies Record - -export const findPermissionByUrl = (apiPermissions: ApiPermissionType[], url: string) => { - return apiPermissions.find((perm) => perm.permission === url) -} +export const ADMIN_UI_RESOURCES = Object.freeze( + Object.fromEntries( + (Object.keys(RESOURCE_ACTIONS) as AdminUiFeatureResource[]).map((key) => [key, key]), + ), +) as Readonly> export const buildCedarPermissionKey = ( resourceId: AdminUiFeatureResource, action: CedarAction, -): `${AdminUiFeatureResource}::${CedarAction}` => { - return `${resourceId}::${action}` -} +): `${AdminUiFeatureResource}::${CedarAction}` => `${resourceId}::${action}` + +export const CEDAR_RESOURCE_SCOPES = Object.fromEntries( + (Object.keys(RESOURCE_ACTIONS) as AdminUiFeatureResource[]).map((resourceId) => [ + resourceId, + RESOURCE_ACTIONS[resourceId].map((action) => ({ action, resourceId })), + ]), +) as Record diff --git a/admin-ui/app/components/App/PermissionsPolicyInitializer.tsx b/admin-ui/app/components/App/PermissionsPolicyInitializer.tsx index 0c75358764..3cddbfb84c 100644 --- a/admin-ui/app/components/App/PermissionsPolicyInitializer.tsx +++ b/admin-ui/app/components/App/PermissionsPolicyInitializer.tsx @@ -6,7 +6,7 @@ import { setCedarlingInitializing, } from '../../redux/features/cedarPermissionsSlice' import { cedarlingClient } from '@/cedarling/client' -import { CedarlingLogType } from '@/cedarling/enums/CedarlingLogType' +import { CEDARLING_LOG_TYPE } from '@/cedarling/constants' import bootstrap from '@/cedarling/config/cedarling-bootstrap-TBAC.json' const base64ToUint8Array = (base64: string): Uint8Array => { @@ -33,7 +33,7 @@ const PermissionsPolicyInitializer = () => { (state) => state.cedarPermissions, ) const cedarlingLogType = - useAppSelector((state) => state.authReducer?.config?.cedarlingLogType) || CedarlingLogType.OFF + useAppSelector((state) => state.authReducer?.config?.cedarlingLogType) || CEDARLING_LOG_TYPE.OFF useEffect(() => { const isValidPolicyStoreBytes = (bytes: string): boolean => { diff --git a/admin-ui/app/components/Sidebar/types.ts b/admin-ui/app/components/Sidebar/types.ts index fb27ab9c58..dccdd61612 100644 --- a/admin-ui/app/components/Sidebar/types.ts +++ b/admin-ui/app/components/Sidebar/types.ts @@ -1,6 +1,7 @@ import type React from 'react' import type { HealthServiceKey } from 'Redux/features/types' import type { ROUTES } from '@/helpers/navigation' +import type { CedarAction } from '@/cedarling/types' import type { PageConfig } from '../Layout/types' export type SidebarProps = { @@ -15,7 +16,7 @@ export type MenuItem = { icon?: string path?: string title?: string - permission?: string + action?: CedarAction resourceKey?: string children?: MenuItem[] cedarlingPassed?: boolean diff --git a/admin-ui/app/redux/features/cedarPermissionsSlice.ts b/admin-ui/app/redux/features/cedarPermissionsSlice.ts index b398d6d0e1..688fe5a0e7 100644 --- a/admin-ui/app/redux/features/cedarPermissionsSlice.ts +++ b/admin-ui/app/redux/features/cedarPermissionsSlice.ts @@ -4,8 +4,6 @@ import type { CedarPermissionsState, SetCedarlingPermissionPayload } from '../.. const initialState: CedarPermissionsState = { permissions: {}, - loading: false, - error: null, initialized: null, isInitializing: false, cedarFailedStatusAfterMaxTries: null, @@ -19,8 +17,6 @@ const cedarPermissionsSlice = createSlice({ setCedarlingPermission: (state, action: PayloadAction) => { const { resourceId, isAuthorized } = action.payload state.permissions[resourceId] = isAuthorized - state.loading = false - state.error = null }, setCedarlingInitialized: (state, action: PayloadAction) => { state.initialized = action.payload diff --git a/admin-ui/app/routes/Apps/Gluu/GluuAppSidebar.tsx b/admin-ui/app/routes/Apps/Gluu/GluuAppSidebar.tsx index 38b414784c..90933ab05b 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuAppSidebar.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuAppSidebar.tsx @@ -29,7 +29,7 @@ import { import { useCedarling } from '@/cedarling/hooks/useCedarling' import type { AdminUiFeatureResource } from '@/cedarling/types' import { devLogger } from '@/utils/devLogger' -import { CEDARLING_BYPASS } from '@/cedarling/utility' +import { CEDARLING_BYPASS } from '@/cedarling/constants' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { JANS_SERVICES } from '@/constants' import { useHealthStatus, useFido2HealthStatus } from 'Plugins/admin/components/Health/hooks' @@ -118,7 +118,7 @@ const GluuAppSidebar = (): JSX.Element => { } return null } - if (item.permission) { + if (item.action) { if (item.resourceKey === CEDARLING_BYPASS) { return item } @@ -128,7 +128,7 @@ const GluuAppSidebar = (): JSX.Element => { } const [result] = await authorizeHelper([ { - permission: item.permission, + action: item.action, resourceId: item.resourceKey as AdminUiFeatureResource, }, ]) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx index 28c167edf5..2fcc2727a7 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx @@ -9,9 +9,8 @@ import { useAppSelector } from '@/redux/hooks' import Alert from '@mui/material/Alert' import { Close } from '@/components/icons' import { useWebhookDialogAction } from 'Utils/hooks' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import type { GluuCommitDialogProps } from './types/index' import { useStyles } from './styles/GluuCommitDialog.style' import { @@ -40,7 +39,7 @@ const GluuCommitDialog = ({ alertSeverity = 'warning', }: GluuCommitDialogProps) => { const { t } = useTranslation() - const { hasCedarReadPermission, authorizeHelper } = useCedarling() + const { canRead: canReadWebhooks } = usePermission(ADMIN_UI_RESOURCES.Webhooks) const { state: themeState } = useTheme() const isDark = themeState.theme === THEME_DARK @@ -51,22 +50,6 @@ const GluuCommitDialog = ({ const [isSubmitting, setIsSubmitting] = useState(false) const webhookModal = useAppSelector((state) => state.webhookReducer?.webhookModal ?? false) - const webhookResourceId = useMemo(() => ADMIN_UI_RESOURCES.Webhooks, []) - const webhookScopes = useMemo( - () => CEDAR_RESOURCE_SCOPES[webhookResourceId] || [], - [webhookResourceId], - ) - const canReadWebhooks = useMemo( - () => hasCedarReadPermission(webhookResourceId), - [hasCedarReadPermission, webhookResourceId], - ) - - useEffect(() => { - if (webhookScopes && webhookScopes.length > 0) { - authorizeHelper(webhookScopes) - } - }, [authorizeHelper, webhookScopes]) - const { webhookTriggerModal, onCloseModal, webhookCheckComplete } = useWebhookDialogAction({ feature, modal, diff --git a/admin-ui/app/routes/Apps/Gluu/GluuDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuDialog.tsx index 7052f15322..11eb5c3ff0 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuDialog.tsx @@ -5,9 +5,8 @@ import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import getThemeColor from '@/context/theme/config' import { useAppSelector } from '@/redux/hooks' import { useWebhookDialogAction } from 'Utils/hooks' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import customColors from '@/customColors' import { GluuButton } from '@/components' import { GluuModalShell } from '@/components/GluuModalShell' @@ -22,7 +21,7 @@ const ACTION_MESSAGE_MIN_LENGTH = 10 const GluuDialog = ({ row, handler, modal, onAccept, subject, name, feature }: GluuDialogProps) => { const [active, setActive] = useState(false) const { t } = useTranslation() - const { hasCedarReadPermission, authorizeHelper } = useCedarling() + const { canRead: canReadWebhooks } = usePermission(ADMIN_UI_RESOURCES.Webhooks) const [userMessage, setUserMessage] = useState('') const webhookModal = useAppSelector((state) => state.webhookReducer?.webhookModal ?? false) @@ -33,22 +32,6 @@ const GluuDialog = ({ row, handler, modal, onAccept, subject, name, feature }: G const { classes } = useStyles({ isDark, themeColors }) const { classes: commitClasses } = useCommitDialogStyles({ isDark, themeColors }) - const webhookResourceId = useMemo(() => ADMIN_UI_RESOURCES.Webhooks, []) - const webhookScopes = useMemo( - () => CEDAR_RESOURCE_SCOPES[webhookResourceId] || [], - [webhookResourceId], - ) - const canReadWebhooks = useMemo( - () => hasCedarReadPermission(webhookResourceId), - [hasCedarReadPermission, webhookResourceId], - ) - - useEffect(() => { - if (webhookScopes && webhookScopes.length > 0) { - authorizeHelper(webhookScopes) - } - }, [authorizeHelper, webhookScopes]) - const { webhookTriggerModal, onCloseModal } = useWebhookDialogAction({ feature, modal, diff --git a/admin-ui/app/routes/Apps/Gluu/GluuWebhookCommitDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuWebhookCommitDialog.tsx index 037ed6fffb..c8a141d15d 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuWebhookCommitDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuWebhookCommitDialog.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { useAppSelector } from '@/redux/hooks' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' @@ -18,8 +18,7 @@ const GluuWebhookCommitDialog: React.FC = ({ alertMessage, alertSeverity, }) => { - const { hasCedarReadPermission } = useCedarling() - const canReadWebhooks = hasCedarReadPermission(ADMIN_UI_RESOURCES.Webhooks) + const { canRead: canReadWebhooks } = usePermission(ADMIN_UI_RESOURCES.Webhooks) const webhookModal = useAppSelector((state) => state.webhookReducer?.webhookModal ?? false) const { webhookTriggerModal, onCloseModal, webhookCheckComplete } = useWebhookDialogAction({ feature: webhookFeature, diff --git a/admin-ui/app/routes/Apps/Gluu/GluuWebhookExecutionDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuWebhookExecutionDialog.tsx index ccd2a328b3..aaa77c007e 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuWebhookExecutionDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuWebhookExecutionDialog.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect, useCallback } from 'react' +import { useMemo, useCallback } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import Table from '@mui/material/Table' @@ -16,9 +16,8 @@ import { ModalLayer } from '@/components/ModalLayer' import { useTheme } from 'Context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import customColors from '@/customColors' import { useStyles } from './styles/GluuWebhookExecutionDialog.style' import { useStyles as useCommitDialogStyles } from './styles/GluuCommitDialog.style' @@ -40,26 +39,13 @@ const GluuWebhookExecutionDialog = () => { const { t } = useTranslation() const dispatch = useAppDispatch() const webhookState = useAppSelector((state) => state.webhookReducer) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() + const { canRead: canReadWebhooks } = usePermission(ADMIN_UI_RESOURCES.Webhooks) const { state: themeState } = useTheme() const isDark = themeState.theme === THEME_DARK const themeColors = useMemo(() => getThemeColor(themeState.theme), [themeState.theme]) const { classes } = useStyles({ isDark, themeColors }) const { classes: commitClasses } = useCommitDialogStyles({ isDark, themeColors }) - const webhookResourceId = useMemo(() => ADMIN_UI_RESOURCES.Webhooks, []) - const webhookScopes = useMemo(() => CEDAR_RESOURCE_SCOPES[webhookResourceId], [webhookResourceId]) - const canReadWebhooks = useMemo( - () => hasCedarReadPermission(webhookResourceId), - [hasCedarReadPermission, webhookResourceId], - ) - - useEffect(() => { - if (webhookScopes && webhookScopes.length > 0) { - authorizeHelper(webhookScopes) - } - }, [authorizeHelper, webhookScopes]) - const { triggerWebhookInProgress = false, webhookTriggerResults, diff --git a/admin-ui/app/routes/Apps/Gluu/Tests/GluuAppSiderbar.test.tsx b/admin-ui/app/routes/Apps/Gluu/Tests/GluuAppSiderbar.test.tsx index 020f8ccc7b..4549354589 100644 --- a/admin-ui/app/routes/Apps/Gluu/Tests/GluuAppSiderbar.test.tsx +++ b/admin-ui/app/routes/Apps/Gluu/Tests/GluuAppSiderbar.test.tsx @@ -52,7 +52,6 @@ jest.mock('@/cedarling', () => ({ })) jest.mock('@/cedarling/utility', () => ({ - CEDARLING_BYPASS: 'CEDARLING_BYPASS', ADMIN_UI_RESOURCES: { Dashboard: 'Dashboard', License: 'License', diff --git a/admin-ui/app/routes/Apps/Profile/ProfilePage.tsx b/admin-ui/app/routes/Apps/Profile/ProfilePage.tsx index c708813468..822fb722d8 100755 --- a/admin-ui/app/routes/Apps/Profile/ProfilePage.tsx +++ b/admin-ui/app/routes/Apps/Profile/ProfilePage.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useCallback, useMemo, memo } from 'react' +import React, { useContext, useCallback, useMemo, memo } from 'react' import { GluuBadge } from 'Components' import { ErrorBoundary } from 'react-error-boundary' import GluuErrorFallBack from '../Gluu/GluuErrorFallBack' @@ -10,21 +10,19 @@ import { Box, Divider } from '@mui/material' import { EditOutlined } from '@/components/icons' import { randomAvatar } from '../../../utilities' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import type { ThemeContextValue, InfoRowProps } from './types' import { useAppSelector } from '@/redux/hooks' import { useProfileDetails } from './hooks/useProfileDetails' import GluuLoader from '../Gluu/GluuLoader' import GluuViewWrapper from '../Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' import GluuText from 'Routes/Apps/Gluu/GluuText' import { GluuButton } from '@/components/GluuButton' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' const USERS_RESOURCE_ID = ADMIN_UI_RESOURCES.Users -const USERS_SCOPES = CEDAR_RESOURCE_SCOPES[USERS_RESOURCE_ID] const InfoRow = memo(({ label, value, index, classes }: InfoRowProps) => ( { const { profileDetails, loading, surname, roles } = useProfileDetails(userInum, hasSession) - const { authorizeHelper, hasCedarReadPermission, hasCedarWritePermission } = useCedarling() - const canReadProfile = useMemo( - () => hasCedarReadPermission(USERS_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canEditProfile = useMemo( - () => hasCedarWritePermission(USERS_RESOURCE_ID), - [hasCedarWritePermission], - ) + const { canRead: canReadProfile, canWrite: canEditProfile } = usePermission(USERS_RESOURCE_ID) const avatarSrc = useMemo(() => randomAvatar(), []) - useEffect(() => { - if (USERS_SCOPES?.length) { - authorizeHelper(USERS_SCOPES) - } - }, [authorizeHelper]) - const navigateToUserManagement = useCallback((): void => { if (!profileDetails?.inum) return navigateToRoute(ROUTES.USER_EDIT(profileDetails.inum), { diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.tsx b/admin-ui/app/routes/Dashboards/DashboardPage.tsx index 6208fbb0ea..668954ab14 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.tsx +++ b/admin-ui/app/routes/Dashboards/DashboardPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback, useContext } from 'react' +import { useState, useMemo, useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' import { useAppSelector, useAppDispatch } from '@/redux/hooks' import { useMediaQuery } from 'react-responsive' @@ -6,8 +6,7 @@ import type { Dayjs } from 'dayjs' import Grid from '@mui/material/Grid' import Paper from '@mui/material/Paper' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import customColors, { hexToRgb } from '@/customColors' import getThemeColor from '@/context/theme/config' import { useAppNavigation, ROUTES } from '@/helpers/navigation' @@ -43,7 +42,6 @@ import { } from '@/utils/dayjsUtils' const DASHBOARD_RESOURCE_ID = ADMIN_UI_RESOURCES.Dashboard -const DASHBOARD_SCOPES = CEDAR_RESOURCE_SCOPES[DASHBOARD_RESOURCE_ID] const MOBILE_MEDIA_QUERY = { maxWidth: 767 } const DashboardPage = () => { @@ -96,7 +94,7 @@ const DashboardPage = () => { (state) => state.authReducer, ) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() + const { canRead: canViewDashboard } = usePermission(DASHBOARD_RESOURCE_ID) const { navigateToRoute } = useAppNavigation() const cedarInitialized = useAppSelector((state) => state.cedarPermissions?.initialized) const cedarIsInitializing = useAppSelector((state) => state.cedarPermissions?.isInitializing) @@ -105,22 +103,11 @@ const DashboardPage = () => { if (!cedarInitialized || cedarIsInitializing) { return false } - return Boolean(hasCedarReadPermission(DASHBOARD_RESOURCE_ID)) - }, [cedarInitialized, cedarIsInitializing, hasCedarReadPermission]) + return canViewDashboard + }, [cedarInitialized, cedarIsInitializing, canViewDashboard]) SetTitle(t('menus.dashboard')) - const initPermissions = useCallback(async () => { - if (!hasSession || !cedarInitialized) return - await authorizeHelper(DASHBOARD_SCOPES) - }, [hasSession, cedarInitialized, authorizeHelper]) - - useEffect(() => { - if (hasSession && cedarInitialized && !cedarIsInitializing) { - initPermissions() - } - }, [hasSession, cedarInitialized, cedarIsInitializing, initPermissions]) - const { item: license, loading: licenseLoading } = useLicenseDetails() const { totalCount: totalClientsEntries, isLoading: clientsLoading } = useClients() const { allServices, isLoading: healthLoading } = useHealthStatus() diff --git a/admin-ui/app/routes/Dashboards/__tests__/DashboardPage.test.tsx b/admin-ui/app/routes/Dashboards/__tests__/DashboardPage.test.tsx index 1d6118a562..20914a0167 100644 --- a/admin-ui/app/routes/Dashboards/__tests__/DashboardPage.test.tsx +++ b/admin-ui/app/routes/Dashboards/__tests__/DashboardPage.test.tsx @@ -17,9 +17,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Dashboard: 'dashboard' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { dashboard: [] }, })) diff --git a/admin-ui/app/routes/License/LicenseDetailsPage.test.tsx b/admin-ui/app/routes/License/LicenseDetailsPage.test.tsx index b56243a925..c1ebc589ea 100644 --- a/admin-ui/app/routes/License/LicenseDetailsPage.test.tsx +++ b/admin-ui/app/routes/License/LicenseDetailsPage.test.tsx @@ -13,16 +13,16 @@ jest.mock('@/cedarling', () => ({ }), })) -jest.mock('@/cedarling/utility', () => ({ - ADMIN_UI_RESOURCES: { License: 'License', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ - CEDAR_RESOURCE_SCOPES: { - License: [{ permission: 'read', resourceId: 'License' }], - Webhooks: [], - }, -})) +jest.mock('@/cedarling/utility', () => { + const { CEDAR_ACTIONS } = jest.requireActual('@/cedarling/constants') + return { + ADMIN_UI_RESOURCES: { License: 'License', Webhooks: 'Webhooks' }, + CEDAR_RESOURCE_SCOPES: { + License: [{ action: CEDAR_ACTIONS.READ, resourceId: 'License' }], + Webhooks: [], + }, + } +}) jest.mock('../Apps/Gluu/GluuCommitDialog', () => ({ __esModule: true, diff --git a/admin-ui/app/routes/License/LicenseDetailsPage.tsx b/admin-ui/app/routes/License/LicenseDetailsPage.tsx index 8fc96aafdf..d15e1b4e8e 100644 --- a/admin-ui/app/routes/License/LicenseDetailsPage.tsx +++ b/admin-ui/app/routes/License/LicenseDetailsPage.tsx @@ -12,9 +12,8 @@ import Alert from '@mui/material/Alert' import SetTitle from 'Utils/SetTitle' import { formatDate } from 'Utils/Util' import { formatLicenseFieldValue } from '@/utils/licenseUtils' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import GluuCommitDialog from '../Apps/Gluu/GluuCommitDialog' import type { LicenseField } from './types' import { useAppNavigation, ROUTES } from '@/helpers/navigation' @@ -22,7 +21,6 @@ import { useStyles } from './LicenseDetailsPage.style' const PLACEHOLDER = '_' const LICENSE_RESOURCE_ID = ADMIN_UI_RESOURCES.License -const LICENSE_SCOPES = CEDAR_RESOURCE_SCOPES[LICENSE_RESOURCE_ID] const LICENSE_FIELD_CONFIG: ReadonlyArray<{ key: string; label: string }> = [ { key: 'productName', label: 'fields.productName' }, @@ -43,20 +41,9 @@ const LicenseDetailsPage = () => { onResetSuccess: () => navigateToRoute(ROUTES.LOGOUT), }) const { t } = useTranslation() - const { hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canWrite: canWriteLicense } = usePermission(LICENSE_RESOURCE_ID) const [modal, setModal] = useState(false) - const canWriteLicense = useMemo( - () => hasCedarWritePermission(LICENSE_RESOURCE_ID), - [hasCedarWritePermission], - ) - - useEffect(() => { - if (LICENSE_SCOPES?.length) { - authorizeHelper(LICENSE_SCOPES) - } - }, [authorizeHelper]) - useEffect(() => { if (item?.licenseExpired) { navigateToRoute(ROUTES.LOGOUT) diff --git a/admin-ui/app/utils/AppAuthProvider.tsx b/admin-ui/app/utils/AppAuthProvider.tsx index 33f79c68b8..46d6e8782b 100755 --- a/admin-ui/app/utils/AppAuthProvider.tsx +++ b/admin-ui/app/utils/AppAuthProvider.tsx @@ -11,7 +11,10 @@ import { getAPIAccessToken, checkLicensePresent } from 'Redux/actions' import GluuTimeoutModal from 'Routes/Apps/Gluu/GluuTimeoutModal' import GluuErrorModal from 'Routes/Apps/Gluu/GluuErrorModal' import { updateToast } from 'Redux/features/toastSlice' -import { setPolicyStoreBytes } from 'Redux/features/cedarPermissionsSlice' +import { + setPolicyStoreBytes, + setCedarFailedStatusAfterMaxTries, +} from 'Redux/features/cedarPermissionsSlice' import { FetchRequestor, AuthorizationServiceConfiguration, @@ -108,17 +111,21 @@ const AppAuthProvider = ({ children }: Readonly) => { if (hasSession) { fetchPolicyStore() .then((policyStoreResponse) => { - if (isMounted && policyStoreResponse.data) { - const policyStoreBytes = - 'responseBytes' in policyStoreResponse.data - ? policyStoreResponse.data.responseBytes - : undefined - dispatch(setPolicyStoreBytes(policyStoreBytes ?? '')) + if (!isMounted) return + const policyStoreBytes = + policyStoreResponse.data && 'responseBytes' in policyStoreResponse.data + ? policyStoreResponse.data.responseBytes + : undefined + if (policyStoreBytes && policyStoreBytes.trim().length > 0) { + dispatch(setPolicyStoreBytes(policyStoreBytes)) + } else { + dispatch(setCedarFailedStatusAfterMaxTries()) } }) .catch((err: Error) => { if (isMounted) { setError(err) + dispatch(setCedarFailedStatusAfterMaxTries()) } }) } diff --git a/admin-ui/app/utils/PermChecker.ts b/admin-ui/app/utils/PermChecker.ts deleted file mode 100644 index c9b58d9ba5..0000000000 --- a/admin-ui/app/utils/PermChecker.ts +++ /dev/null @@ -1,121 +0,0 @@ -const BASE_URL = 'https://jans.io/oauth' - -export const PROPERTIES_READ = BASE_URL + '/jans-auth-server/config/properties.readonly' -export const PROPERTIES_WRITE = BASE_URL + '/jans-auth-server/config/properties.write' - -export const ATTRIBUTE_READ = BASE_URL + '/config/attributes.readonly' -export const ATTRIBUTE_WRITE = BASE_URL + '/config/attributes.write' -export const ATTRIBUTE_DELETE = BASE_URL + '/config/attributes.delete' - -export const CLIENT_READ = BASE_URL + '/config/openid/clients.readonly' -export const CLIENT_WRITE = BASE_URL + '/config/openid/clients.write' -export const CLIENT_DELETE = BASE_URL + '/config/openid/clients.delete' - -export const ROLE_READ = 'https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly' - -export const PERMISSION_READ = - 'https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly' - -export const MAPPING_READ = - 'https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly' -export const MAPPING_WRITE = - 'https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write' - -export const LICENSE_DETAILS_READ = BASE_URL + '/jans-auth-server/config/adminui/license.readonly' - -export const LICENSE_DETAILS_WRITE = BASE_URL + '/jans-auth-server/config/adminui/license.write' - -export const SCOPE_READ = BASE_URL + '/config/scopes.readonly' -export const SCOPE_WRITE = BASE_URL + '/config/scopes.write' -export const SCOPE_DELETE = BASE_URL + '/config/scopes.delete' - -export const SSA_PORTAL = 'https://jans.io/auth/ssa.portal' -export const SSA_ADMIN = 'https://jans.io/auth/ssa.admin' -export const SSA_DELETE = 'https://jans.io/oauth/config/ssa.delete' -export const SSA_DEVELOPER = 'https://jans.io/auth/ssa.developer' - -export const REVOKE_SESSION = 'revoke_session' -export const OPENID = 'openid' - -export const SCIM_BULK = 'https://jans.io/scim/bulk' - -export const SAML_READ = 'https://jans.io/idp/saml.readonly' -export const SAML_WRITE = 'https://jans.io/idp/saml.write' -export const SAML_DELETE = 'https://jans.io/idp/saml.delete' - -export const SAML_TR_READ = BASE_URL + '/config/saml.readonly' -export const SAML_TR_WRITE = BASE_URL + '/config/saml.write' - -export const SAML_CONFIG_READ = BASE_URL + '/config/saml-config.readonly' -export const SAML_CONFIG_WRITE = BASE_URL + '/config/saml-config.write' - -export const SCRIPT_READ = BASE_URL + '/config/scripts.readonly' -export const SCRIPT_WRITE = BASE_URL + '/config/scripts.write' -export const SCRIPT_DELETE = BASE_URL + '/config/scripts.delete' - -export const SMTP_READ = BASE_URL + '/config/smtp.readonly' -export const SMTP_WRITE = BASE_URL + '/config/smtp.write' -export const SMTP_DELETE = BASE_URL + '/config/smtp.delete' - -export const ACR_READ = BASE_URL + '/config/acrs.readonly' -export const ACR_WRITE = BASE_URL + '/config/acrs.write' - -export const LOGGING_READ = BASE_URL + '/config/logging.readonly' -export const LOGGING_WRITE = BASE_URL + '/config/logging.write' - -export const JWKS_READ = BASE_URL + '/config/jwks.readonly' -export const JWKS_WRITE = BASE_URL + '/config/jwks.write' - -export const FIDO_READ = BASE_URL + '/config/fido2.readonly' -export const FIDO_WRITE = BASE_URL + '/config/fido2.write' -export const FIDO_DELETE = BASE_URL + '/config/fido2.delete' -export const FIDO_METRICS_READ = BASE_URL + '/config/fido2-metrics.readonly' - -export const JANS_LOCK_READ = BASE_URL + '/lock-config.readonly' -export const JANS_LOCK_WRITE = BASE_URL + '/lock-config.write' - -export const CACHE_READ = BASE_URL + '/config/cache.readonly' -export const CACHE_WRITE = BASE_URL + '/config/cache.write' -export const CACHE_DELETE = BASE_URL + '/config/cache.delete' - -export const PERSISTENCE_DETAIL = BASE_URL + '/jans-auth-server/config/properties.readonly' - -export const STAT_READ = BASE_URL + '/config/stats.readonly' -export const STAT_JANS_READ = 'jans_stat' - -export const USER_READ = BASE_URL + '/config/user.readonly' -export const USER_WRITE = BASE_URL + '/config/user.write' -export const USER_DELETE = BASE_URL + '/config/user.delete' - -export const AGAMA_READ = BASE_URL + '/config/agama.readonly' -export const AGAMA_WRITE = BASE_URL + '/config/agama.write' -export const AGAMA_DELETE = BASE_URL + '/config/agama.delete' - -export const SESSION_READ = BASE_URL + '/jans-auth-server/session.readonly' -export const SESSION_DELETE = BASE_URL + '/jans-auth-server/session.delete' - -export const SCIM_CONFIG_READ = 'https://jans.io/scim/config.readonly' -export const SCIM_CONFIG_WRITE = 'https://jans.io/scim/config.write' - -export const WEBHOOK_READ = BASE_URL + '/jans-auth-server/config/adminui/webhook.readonly' -export const WEBHOOK_WRITE = BASE_URL + '/jans-auth-server/config/adminui/webhook.write' -export const WEBHOOK_DELETE = BASE_URL + '/jans-auth-server/config/adminui/webhook.delete' -export const ASSETS_READ = BASE_URL + '/config/jans_asset-read' -export const ASSETS_WRITE = BASE_URL + '/config/jans_asset-write' -export const ASSETS_DELETE = BASE_URL + '/config/jans_asset-delete' - -export const API_CONFIG_READ = BASE_URL + '/config/properties.readonly' -export const API_CONFIG_WRITE = BASE_URL + '/config/properties.write' - -import type { ActionData, UserAction } from './types' - -export type { ActionData, UserAction } - -export const buildPayload = ( - userAction: UserAction, - message: string, - payload: ActionData, -): void => { - userAction['action_message'] = message - userAction['action_data'] = payload -} diff --git a/admin-ui/app/utils/auditAction.ts b/admin-ui/app/utils/auditAction.ts new file mode 100644 index 0000000000..67eb430dba --- /dev/null +++ b/admin-ui/app/utils/auditAction.ts @@ -0,0 +1,10 @@ +import type { ActionData, UserAction } from './types' + +export const buildPayload = ( + userAction: UserAction, + message: string, + payload: ActionData, +): void => { + userAction['action_message'] = message + userAction['action_data'] = payload +} diff --git a/admin-ui/app/utils/hooks/useWebhookDialogAction.tsx b/admin-ui/app/utils/hooks/useWebhookDialogAction.tsx index 0bab1feb86..df5954b5fc 100644 --- a/admin-ui/app/utils/hooks/useWebhookDialogAction.tsx +++ b/admin-ui/app/utils/hooks/useWebhookDialogAction.tsx @@ -20,7 +20,7 @@ import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import { useCedarling } from '@/cedarling/hooks/useCedarling' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/utility' import GluuText from 'Routes/Apps/Gluu/GluuText' import { GluuButton } from '@/components' import { useStyles as useCommitDialogStyles } from '@/routes/Apps/Gluu/styles/GluuCommitDialog.style' diff --git a/admin-ui/app/utils/types/PermCheckerTypes.ts b/admin-ui/app/utils/types/AuditActionTypes.ts similarity index 100% rename from admin-ui/app/utils/types/PermCheckerTypes.ts rename to admin-ui/app/utils/types/AuditActionTypes.ts diff --git a/admin-ui/app/utils/types/index.ts b/admin-ui/app/utils/types/index.ts index 6d739d8876..aff5abd300 100644 --- a/admin-ui/app/utils/types/index.ts +++ b/admin-ui/app/utils/types/index.ts @@ -1,7 +1,7 @@ export * from './DevLoggerTypes' export * from './TokenControllerTypes' export * from './AuditLoggerTypes' -export * from './PermCheckerTypes' +export * from './AuditActionTypes' export * from './AppAuthProviderTypes' export * from './ApiTypes' export * from './ErrorHandlerTypes' diff --git a/admin-ui/jest.config.ts b/admin-ui/jest.config.ts index fbf60f8dda..af012a1ce7 100644 --- a/admin-ui/jest.config.ts +++ b/admin-ui/jest.config.ts @@ -17,6 +17,7 @@ const config: Config = { '\\.(css|less|scss)$': '/__mocks__/styleMock.ts', '^@janssenproject/cedarling_wasm$': '/__mocks__/@janssenproject/cedarling_wasm.ts', '^@/cedarling/hooks/useCedarling$': '/__mocks__/cedarlingHookBridge.ts', + '^@/cedarling/hooks/usePermission$': '/__mocks__/cedarlingPermissionBridge.ts', '^@/utils/hmr$': '/__mocks__/hmr.ts', '^@/utilities$': '/__mocks__/utilities.ts', '^@/(.*)$': '/app/$1', diff --git a/admin-ui/plugins/admin/__tests__/components/Asset/JansAssetListPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/Asset/JansAssetListPage.test.tsx index 2b094fc2a3..1cc2bc8fe2 100644 --- a/admin-ui/plugins/admin/__tests__/components/Asset/JansAssetListPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Asset/JansAssetListPage.test.tsx @@ -28,12 +28,10 @@ jest.mock('@/cedarling', () => { jest.mock('@/cedarling/utility', () => { const { SHARED_CEDAR_CONSTANTS } = jest.requireActual('./assetCedarTestConstants') - return { ADMIN_UI_RESOURCES: SHARED_CEDAR_CONSTANTS.ADMIN_UI_RESOURCES } -}) - -jest.mock('@/cedarling/constants/resourceScopes', () => { - const { SHARED_CEDAR_CONSTANTS } = jest.requireActual('./assetCedarTestConstants') - return { CEDAR_RESOURCE_SCOPES: SHARED_CEDAR_CONSTANTS.CEDAR_RESOURCE_SCOPES } + return { + ADMIN_UI_RESOURCES: SHARED_CEDAR_CONSTANTS.ADMIN_UI_RESOURCES, + CEDAR_RESOURCE_SCOPES: SHARED_CEDAR_CONSTANTS.CEDAR_RESOURCE_SCOPES, + } }) jest.mock('JansConfigApi', () => ({ @@ -109,8 +107,6 @@ describe('JansAssetListPage', () => { hasCedarWritePermission: jest.fn(() => false), hasCedarDeletePermission: jest.fn(() => true), authorizeHelper: jest.fn(), - isLoading: false, - error: null, }) render(, { wrapper: Wrapper }) expect(screen.queryByText(/Add Jans Asset/i)).not.toBeInTheDocument() diff --git a/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx b/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx index 44164e0c70..9867698d36 100644 --- a/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx @@ -26,9 +26,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: mockSHARED_CEDAR_CONSTANTS.ADMIN_UI_RESOURCES, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: mockSHARED_CEDAR_CONSTANTS.CEDAR_RESOURCE_SCOPES, })) diff --git a/admin-ui/plugins/admin/__tests__/components/Audit/AuditListPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/Audit/AuditListPage.test.tsx index db9fcd3a22..32c993a021 100644 --- a/admin-ui/plugins/admin/__tests__/components/Audit/AuditListPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Audit/AuditListPage.test.tsx @@ -13,9 +13,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { AuditLogs: 'auditlogs' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { auditlogs: [] }, })) diff --git a/admin-ui/plugins/admin/__tests__/components/Cedarling/CedarlingConfigPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/Cedarling/CedarlingConfigPage.test.tsx index 699e7c0893..59dcb66ab5 100644 --- a/admin-ui/plugins/admin/__tests__/components/Cedarling/CedarlingConfigPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Cedarling/CedarlingConfigPage.test.tsx @@ -33,9 +33,6 @@ jest.mock('@/cedarling/utility', () => ({ Users: 'Users', Attributes: 'Attributes', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Security: [], Webhooks: [], Lock: [], Users: [], Attributes: [] }, })) diff --git a/admin-ui/plugins/admin/__tests__/components/MAU/MauPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/MAU/MauPage.test.tsx index e03ec97674..2fe0560146 100644 --- a/admin-ui/plugins/admin/__tests__/components/MAU/MauPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/MAU/MauPage.test.tsx @@ -33,9 +33,6 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { MAU: 'mau' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { mau: [] }, })) diff --git a/admin-ui/plugins/admin/__tests__/components/Mapping/RolePermissionMappingPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/Mapping/RolePermissionMappingPage.test.tsx index 2040c049d9..a7083b46c0 100644 --- a/admin-ui/plugins/admin/__tests__/components/Mapping/RolePermissionMappingPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Mapping/RolePermissionMappingPage.test.tsx @@ -13,9 +13,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Security: 'security' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { security: [] }, })) diff --git a/admin-ui/plugins/admin/__tests__/components/Settings/SettingsPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/Settings/SettingsPage.test.tsx index 1b5d07d8e5..3d31f2008c 100644 --- a/admin-ui/plugins/admin/__tests__/components/Settings/SettingsPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Settings/SettingsPage.test.tsx @@ -14,14 +14,10 @@ jest.mock('@/cedarling', () => ({ })), ADMIN_UI_RESOURCES: { Settings: 'settings' }, CEDAR_RESOURCE_SCOPES: { settings: [] }, - CedarlingLogType: { OFF: 'off', STD_OUT: 'std_out' }, })) jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Settings: 'settings' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { settings: [] }, })) diff --git a/admin-ui/plugins/admin/__tests__/components/Webhook/WebhookListPage.test.tsx b/admin-ui/plugins/admin/__tests__/components/Webhook/WebhookListPage.test.tsx index 36deebf2e9..14e95a6f4b 100644 --- a/admin-ui/plugins/admin/__tests__/components/Webhook/WebhookListPage.test.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Webhook/WebhookListPage.test.tsx @@ -24,9 +24,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Webhooks: [] }, })) diff --git a/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx b/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx index 9d59ab19e5..668d92021f 100644 --- a/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx +++ b/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx @@ -5,7 +5,7 @@ import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import AssetForm from './AssetForm' @@ -23,11 +23,7 @@ const JansAssetAddPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission } = useCedarling() - const canReadAssets = useMemo( - () => hasCedarReadPermission(assetResourceId), - [hasCedarReadPermission], - ) + const { canRead: canReadAssets } = usePermission(assetResourceId) return ( diff --git a/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx b/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx index d4d83fd2b9..e0413b5426 100644 --- a/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx +++ b/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx @@ -5,7 +5,7 @@ import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import AssetForm from './AssetForm' @@ -23,11 +23,7 @@ const JansAssetEditPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission } = useCedarling() - const canReadAssets = useMemo( - () => hasCedarReadPermission(assetResourceId), - [hasCedarReadPermission], - ) + const { canRead: canReadAssets } = usePermission(assetResourceId) return ( diff --git a/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx b/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx index dedce32054..52fcd61d4e 100644 --- a/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx +++ b/admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback, useMemo, memo } from 'react' import { Add, DeleteOutlined, Edit } from '@/components/icons' import GluuText from 'Routes/Apps/Gluu/GluuText' import { GluuBadge } from '@/components/GluuBadge' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' @@ -17,7 +17,6 @@ import { useGetAllAssets, getGetAllAssetsQueryKey, type Document } from 'JansCon import { useDeleteAssetWithAudit } from './hooks' import { formatDate } from '@/utils/dayjsUtils' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { GluuTable, COLUMN_WIDTHS } from '@/components/GluuTable' import { GluuSearchToolbar } from '@/components/GluuSearchToolbar' import type { ColumnDef, PaginationConfig } from '@/components/GluuTable' @@ -30,17 +29,15 @@ import { T_KEYS } from './constants' const LIMIT_OPTIONS = getRowsPerPageOptions() const ASSET_RESOURCE_ID = ADMIN_UI_RESOURCES.Assets -const ASSET_SCOPES = CEDAR_RESOURCE_SCOPES[ASSET_RESOURCE_ID] ?? [] const JansAssetListPage: React.FC = () => { const queryClient = useQueryClient() const { navigateToRoute } = useAppNavigation() const { - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - authorizeHelper, - } = useCedarling() + canRead: canReadAssets, + canWrite: canWriteAssets, + canDelete: canDeleteAssets, + } = usePermission(ASSET_RESOURCE_ID) const { t } = useTranslation() const { state: themeState } = useTheme() @@ -74,19 +71,6 @@ const JansAssetListPage: React.FC = () => { SetTitle(t(T_KEYS.TITLE_ASSETS)) - const canReadAssets = useMemo( - () => hasCedarReadPermission(ASSET_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteAssets = useMemo( - () => hasCedarWritePermission(ASSET_RESOURCE_ID), - [hasCedarWritePermission], - ) - const canDeleteAssets = useMemo( - () => hasCedarDeletePermission(ASSET_RESOURCE_ID), - [hasCedarDeletePermission], - ) - const { data, isLoading, isFetching, refetch } = useGetAllAssets( { limit, @@ -114,12 +98,6 @@ const JansAssetListPage: React.FC = () => { } }, [totalItems, pageNumber, limit, effectivePage, setPageNumber]) - useEffect(() => { - if (ASSET_SCOPES.length > 0) { - authorizeHelper(ASSET_SCOPES) - } - }, [authorizeHelper]) - const { deleteAsset, isLoading: isDeleting } = useDeleteAssetWithAudit() const toggle = useCallback(() => setModal((prev) => !prev), []) diff --git a/admin-ui/plugins/admin/components/Audit/AuditListPage.tsx b/admin-ui/plugins/admin/components/Audit/AuditListPage.tsx index 3597b00576..ac62442f3e 100644 --- a/admin-ui/plugins/admin/components/Audit/AuditListPage.tsx +++ b/admin-ui/plugins/admin/components/Audit/AuditListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react' +import React, { useState, useMemo, useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createDate, @@ -12,9 +12,8 @@ import { AccessTimeIcon, SearchIcon } from '@/components/icons' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { GluuTable } from '@/components/GluuTable' import { GluuSearchToolbar } from '@/components/GluuSearchToolbar' import { GluuBadge } from '@/components/GluuBadge' @@ -40,7 +39,6 @@ const getDefaultRange = () => ({ }) const AUDIT_LOGS_RESOURCE_ID = ADMIN_UI_RESOURCES.AuditLogs -const AUDIT_LOGS_SCOPES = CEDAR_RESOURCE_SCOPES[AUDIT_LOGS_RESOURCE_ID] ?? [] const T_KEYS = { TITLE_AUDIT_LOGS: 'titles.audit_logs', @@ -66,21 +64,10 @@ const AuditListPage: React.FC = () => { const { state: themeState } = useTheme() const themeColors = useMemo(() => getThemeColor(themeState.theme), [themeState.theme]) const { classes } = useStyles({ isDark: themeState.theme === THEME_DARK, themeColors }) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() + const { canRead: canReadAuditLogs } = usePermission(AUDIT_LOGS_RESOURCE_ID) SetTitle(t(T_KEYS.TITLE_AUDIT_LOGS)) - const canReadAuditLogs = useMemo( - () => hasCedarReadPermission(AUDIT_LOGS_RESOURCE_ID), - [hasCedarReadPermission], - ) - - useEffect(() => { - if (AUDIT_LOGS_SCOPES.length > 0) { - authorizeHelper(AUDIT_LOGS_SCOPES) - } - }, [authorizeHelper]) - const queryClient = useQueryClient() const { limit, setLimit, pageNumber, setPageNumber, onPagingSizeSync } = usePaginationState() const [pattern, setPattern] = useState('') diff --git a/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx b/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx index 8f95dbf449..0ca8872c38 100644 --- a/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx +++ b/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx @@ -3,9 +3,8 @@ import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' import { useAppDispatch, useAppSelector } from '@/redux/hooks' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useSyncRoleToScopesMappings } from 'JansConfigApi' import GluuLoader from '@/routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from '@/routes/Apps/Gluu/GluuViewWrapper' @@ -33,14 +32,14 @@ import { useStyles } from './CedarlingConfigPage.style' import { uploadPolicyStore, fetchPolicyStore } from '@/redux/api/backend-api' const SECURITY_RESOURCE_ID = ADMIN_UI_RESOURCES.Security -const SECURITY_SCOPES = CEDAR_RESOURCE_SCOPES[SECURITY_RESOURCE_ID] ?? [] const CJAR_ACCEPT = { 'application/zip': ['.cjar'], } const CedarlingConfigPage: React.FC = () => { - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadSecurity, canWrite: canWriteSecurity } = + usePermission(SECURITY_RESOURCE_ID) const { t } = useTranslation() const { navigateBack, navigateToRoute } = useAppNavigation() SetTitle(t('titles.cedarling_config')) @@ -65,21 +64,6 @@ const CedarlingConfigPage: React.FC = () => { const dialogRef = useRef(null) - const canReadSecurity = useMemo( - () => hasCedarReadPermission(SECURITY_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteSecurity = useMemo( - () => hasCedarWritePermission(SECURITY_RESOURCE_ID), - [hasCedarWritePermission], - ) - - useEffect(() => { - if (SECURITY_SCOPES.length > 0) { - authorizeHelper(SECURITY_SCOPES) - } - }, [authorizeHelper]) - useEffect(() => { if (showConfirm) { dialogRef.current?.focus() diff --git a/admin-ui/plugins/admin/components/MAU/MauPage.tsx b/admin-ui/plugins/admin/components/MAU/MauPage.tsx index 77a4f155f2..810ef52067 100644 --- a/admin-ui/plugins/admin/components/MAU/MauPage.tsx +++ b/admin-ui/plugins/admin/components/MAU/MauPage.tsx @@ -1,13 +1,12 @@ -import React, { useState, useMemo, useCallback, useEffect } from 'react' +import React, { useState, useMemo, useCallback } from 'react' import { InfoOutlined, WarningAmberOutlined } from '@/components/icons' import { Row, Col, Alert, GluuPageContent } from 'Components' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useTheme } from '@/context/theme/themeContext' import customColors, { hexToRgb } from '@/customColors' import { THEME_DARK, DEFAULT_THEME } from '@/context/theme/constants' @@ -26,7 +25,6 @@ import { } from './components' const mauResourceId = ADMIN_UI_RESOURCES.MAU -const mauScopes = CEDAR_RESOURCE_SCOPES[mauResourceId] const MauPage: React.FC = () => { const { t } = useTranslation() @@ -67,12 +65,7 @@ const MauPage: React.FC = () => { const [endDate, setEndDate] = useState(() => createDate()) const [selectedPreset, setSelectedPreset] = useState(DEFAULT_DATE_RANGE_MONTHS) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() - const canViewMau = useMemo(() => hasCedarReadPermission(mauResourceId), [hasCedarReadPermission]) - - useEffect(() => { - authorizeHelper(mauScopes) - }, [authorizeHelper]) + const { canRead: canViewMau } = usePermission(mauResourceId) const dateRange: MauDateRange = useMemo(() => ({ startDate, endDate }), [startDate, endDate]) diff --git a/admin-ui/plugins/admin/components/Mapping/RolePermissionMappingPage.tsx b/admin-ui/plugins/admin/components/Mapping/RolePermissionMappingPage.tsx index e83bf600ce..df5d162690 100644 --- a/admin-ui/plugins/admin/components/Mapping/RolePermissionMappingPage.tsx +++ b/admin-ui/plugins/admin/components/Mapping/RolePermissionMappingPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react' +import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Box } from '@mui/material' import InfoOutlined from '@mui/icons-material/InfoOutlined' @@ -13,14 +13,12 @@ import { GluuPageContent } from '@/components' import RolePermissionCard from './RolePermissionCard' import SetTitle from 'Utils/SetTitle' import { ROUTES } from '@/helpers/navigation' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useMappingData } from './hooks/useMappingApi' import { useStyles } from './styles/MappingPage.style' const MAPPING_RESOURCE_ID = ADMIN_UI_RESOURCES.Security -const MAPPING_SCOPES = CEDAR_RESOURCE_SCOPES[MAPPING_RESOURCE_ID] || [] const RolePermissionMappingPage: React.FC = React.memo(() => { const { t } = useTranslation() @@ -31,8 +29,7 @@ const RolePermissionMappingPage: React.FC = React.memo(() => { const currentTheme = themeConfig[state.theme] const { classes } = useStyles({ isDark, theme: currentTheme }) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() - const canReadMapping = hasCedarReadPermission(MAPPING_RESOURCE_ID) + const { canRead: canReadMapping } = usePermission(MAPPING_RESOURCE_ID) const { mapping, permissions, isLoading, isError } = useMappingData(canReadMapping) @@ -41,12 +38,6 @@ const RolePermissionMappingPage: React.FC = React.memo(() => { [permissions], ) - useEffect(() => { - if (MAPPING_SCOPES.length > 0) { - authorizeHelper(MAPPING_SCOPES) - } - }, [authorizeHelper]) - return ( diff --git a/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx b/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx index 28f77e4c33..26231a0181 100644 --- a/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx +++ b/admin-ui/plugins/admin/components/Settings/SettingsPage.tsx @@ -16,10 +16,9 @@ import SetTitle from 'Utils/SetTitle' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { SCRIPT_TYPES, SIMPLE_PASSWORD_AUTH } from '@/constants' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CedarlingLogType } from '@/cedarling/enums/CedarlingLogType' +import { usePermission } from '@/cedarling/hooks/usePermission' +import { CEDARLING_LOG_TYPE } from '@/cedarling/constants' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { getDefaultPagingSize, savePagingSize as savePagingSizeToStorage, @@ -53,7 +52,6 @@ import { GluuButton } from '@/components/GluuButton' import { useStyles } from './SettingsPage.style' const settingsResourceId = ADMIN_UI_RESOURCES.Settings -const settingsScopes = CEDAR_RESOURCE_SCOPES[settingsResourceId] || [] const SCRIPTS_FETCH_LIMIT = 200 const SettingsPage: React.FC = () => { @@ -63,7 +61,7 @@ const SettingsPage: React.FC = () => { const isDark = (themeState?.theme ?? DEFAULT_THEME) === THEME_DARK const queryClient = useQueryClient() - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadSettings, canWrite: canWriteSettings } = usePermission(settingsResourceId) const userinfo = useAppSelector((state) => state.authReducer?.userinfo) const clientId = useAppSelector((state) => state.authReducer?.config?.clientId) @@ -86,24 +84,9 @@ const SettingsPage: React.FC = () => { refetch: refetchScripts, } = useGetConfigScriptsByType(SCRIPT_TYPES.PERSON_AUTHENTICATION, { limit: SCRIPTS_FETCH_LIMIT }) - const canReadSettings = useMemo( - () => hasCedarReadPermission(settingsResourceId), - [hasCedarReadPermission], - ) - const canWriteSettings = useMemo( - () => hasCedarWritePermission(settingsResourceId), - [hasCedarWritePermission], - ) - const pageTitle = t('titles.application_settings') SetTitle(pageTitle) - useEffect(() => { - if (settingsScopes.length > 0) { - authorizeHelper(settingsScopes) - } - }, [authorizeHelper]) - const validationSchema = useMemo(() => getSettingsValidationSchema(t), [t]) const savedPagingSize = useMemo(() => getDefaultPagingSize(), []) const selectedTheme = useMemo(() => themeState?.theme || DEFAULT_THEME, [themeState?.theme]) @@ -443,7 +426,7 @@ const SettingsPage: React.FC = () => { label="fields.showCedarLogs?" name="cedarlingLogType" formik={formik} - value={formik.values.cedarlingLogType === CedarlingLogType.STD_OUT} + value={formik.values.cedarlingLogType === CEDARLING_LOG_TYPE.STD_OUT} doc_category={SETTINGS} doc_entry="cedarSwitch" lsize={12} @@ -453,7 +436,9 @@ const SettingsPage: React.FC = () => { handler={(event: React.ChangeEvent) => { formik.setFieldValue( 'cedarlingLogType', - event.target.checked ? CedarlingLogType.STD_OUT : CedarlingLogType.OFF, + event.target.checked + ? CEDARLING_LOG_TYPE.STD_OUT + : CEDARLING_LOG_TYPE.OFF, ) }} /> diff --git a/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx b/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx index e4327e8db8..194b0833c7 100644 --- a/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx +++ b/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx @@ -5,7 +5,7 @@ import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import WebhookForm from './WebhookForm' @@ -20,11 +20,7 @@ const WebhookAddPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission } = useCedarling() - const canReadWebhooks = useMemo( - () => hasCedarReadPermission(webhookResourceId), - [hasCedarReadPermission, webhookResourceId], - ) + const { canRead: canReadWebhooks } = usePermission(webhookResourceId) SetTitle(t('messages.add_webhook', { defaultValue: 'Add Webhook' })) diff --git a/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx b/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx index 774fde81e9..1b22ded79a 100644 --- a/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx +++ b/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx @@ -5,7 +5,7 @@ import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import WebhookForm from './WebhookForm' @@ -20,11 +20,7 @@ const WebhookEditPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission } = useCedarling() - const canReadWebhooks = useMemo( - () => hasCedarReadPermission(webhookResourceId), - [hasCedarReadPermission, webhookResourceId], - ) + const { canRead: canReadWebhooks } = usePermission(webhookResourceId) SetTitle(t('titles.edit_webhook', { defaultValue: 'Edit Webhook' })) diff --git a/admin-ui/plugins/admin/components/Webhook/WebhookListPage.tsx b/admin-ui/plugins/admin/components/Webhook/WebhookListPage.tsx index 1882498c46..aba11108e2 100644 --- a/admin-ui/plugins/admin/components/Webhook/WebhookListPage.tsx +++ b/admin-ui/plugins/admin/components/Webhook/WebhookListPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback, useMemo, memo } from 'react' import { Add, DeleteOutlined, Edit } from '@/components/icons' import GluuText from 'Routes/Apps/Gluu/GluuText' import { GluuBadge } from '@/components/GluuBadge' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' @@ -14,7 +14,6 @@ import { THEME_DARK } from '@/context/theme/constants' import SetTitle from 'Utils/SetTitle' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useGetAllWebhooks } from 'JansConfigApi' import { useDeleteWebhookWithAudit } from './hooks' import { GluuTable, COLUMN_WIDTHS } from '@/components/GluuTable' @@ -38,15 +37,13 @@ const SORT_COLUMN_LABELS: Record = { } const DEFAULT_SERVER_SORT: { column: string; desc: boolean } = { column: 'inum', desc: false } const WEBHOOK_RESOURCE_ID = ADMIN_UI_RESOURCES.Webhooks -const WEBHOOK_SCOPES = CEDAR_RESOURCE_SCOPES[WEBHOOK_RESOURCE_ID] ?? [] const WebhookListPage: React.FC = () => { const { navigateToRoute } = useAppNavigation() const { - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - authorizeHelper, - } = useCedarling() + canRead: canReadWebhooks, + canWrite: canWriteWebhooks, + canDelete: canDeleteWebhooks, + } = usePermission(WEBHOOK_RESOURCE_ID) const { t } = useTranslation() const { state: themeState } = useTheme() @@ -62,19 +59,6 @@ const WebhookListPage: React.FC = () => { SetTitle(t('titles.webhooks')) - const canReadWebhooks = useMemo( - () => hasCedarReadPermission(WEBHOOK_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteWebhooks = useMemo( - () => hasCedarWritePermission(WEBHOOK_RESOURCE_ID), - [hasCedarWritePermission], - ) - const canDeleteWebhooks = useMemo( - () => hasCedarDeletePermission(WEBHOOK_RESOURCE_ID), - [hasCedarDeletePermission], - ) - const { data, isLoading, refetch } = useGetAllWebhooks( { limit, @@ -106,12 +90,6 @@ const WebhookListPage: React.FC = () => { const { deleteWebhook, isLoading: isDeleting } = useDeleteWebhookWithAudit() - useEffect(() => { - if (WEBHOOK_SCOPES.length > 0) { - authorizeHelper(WEBHOOK_SCOPES) - } - }, [authorizeHelper]) - const toggle = useCallback(() => setModal((prev) => !prev), []) const submitForm = useCallback( diff --git a/admin-ui/plugins/admin/helper/settings.ts b/admin-ui/plugins/admin/helper/settings.ts index e4693e3426..85a9c503f9 100644 --- a/admin-ui/plugins/admin/helper/settings.ts +++ b/admin-ui/plugins/admin/helper/settings.ts @@ -1,4 +1,5 @@ -import { CedarlingLogType } from '@/cedarling/enums/CedarlingLogType' +import { CEDARLING_LOG_TYPE } from '@/cedarling/constants' +import type { CedarlingLogType } from '@/cedarling/types' import type { AppConfigResponse, KeyValuePair } from 'JansConfigApi' import type { SettingsFormValues } from './types' @@ -24,7 +25,7 @@ export const buildSettingsInitialValues = ( ): SettingsFormValues => ({ sessionTimeoutInMins: configData?.sessionTimeoutInMins ?? '', acrValues: configData?.acrValues ?? '', - cedarlingLogType: (configData?.cedarlingLogType as CedarlingLogType) ?? CedarlingLogType.OFF, + cedarlingLogType: (configData?.cedarlingLogType as CedarlingLogType) ?? CEDARLING_LOG_TYPE.OFF, additionalParameters: sanitizeAdditionalParameters(configData?.additionalParameters).map( (param) => ({ id: crypto.randomUUID(), diff --git a/admin-ui/plugins/admin/helper/types/SettingsTypes.ts b/admin-ui/plugins/admin/helper/types/SettingsTypes.ts index 8c3e27a82b..b65e89399e 100644 --- a/admin-ui/plugins/admin/helper/types/SettingsTypes.ts +++ b/admin-ui/plugins/admin/helper/types/SettingsTypes.ts @@ -1,4 +1,4 @@ -import type { CedarlingLogType } from '@/cedarling/enums/CedarlingLogType' +import type { CedarlingLogType } from '@/cedarling/types' import type { KeyValuePair } from 'JansConfigApi' export type AdditionalParameterFormItem = KeyValuePair & { diff --git a/admin-ui/plugins/admin/plugin-metadata.ts b/admin-ui/plugins/admin/plugin-metadata.ts index 7ab472a31e..574d7461b0 100644 --- a/admin-ui/plugins/admin/plugin-metadata.ts +++ b/admin-ui/plugins/admin/plugin-metadata.ts @@ -2,19 +2,8 @@ import { setupWebhookListener } from './redux/listeners/webhookListener' import { reducer as webhookReducer } from 'Plugins/admin/redux/features/WebhookSlice' -import { - ACR_READ, - MAPPING_READ, - WEBHOOK_READ, - WEBHOOK_WRITE, - ASSETS_READ, - ASSETS_WRITE, - LICENSE_DETAILS_READ, - LOGGING_READ, - PROPERTIES_READ, - STAT_READ, -} from 'Utils/PermChecker' -import { ADMIN_UI_RESOURCES, CEDARLING_BYPASS } from '@/cedarling/utility' +import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS, CEDARLING_BYPASS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -47,31 +36,31 @@ const pluginMetadata = { { title: 'menus.dashboard', path: ROUTES.ADMIN_DASHBOARD, - permission: STAT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Dashboard, }, { title: 'menus.health', path: ROUTES.ADMIN_HEALTH, - permission: PROPERTIES_READ, + action: CEDAR_ACTIONS.READ, resourceKey: CEDARLING_BYPASS, }, { title: 'menus.licenseDetails', path: ROUTES.ADMIN_LICENSE_DETAILS, - permission: LICENSE_DETAILS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.License, }, { title: 'menus.maugraph', path: ROUTES.ADMIN_MAU_GRAPH, - permission: ACR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.MAU, }, { title: 'menus.settings', path: ROUTES.ADMIN_SETTINGS, - permission: ACR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Settings, }, { @@ -80,13 +69,13 @@ const pluginMetadata = { { title: 'menus.securityDropdown.mapping', path: ROUTES.ADMIN_MAPPING, - permission: MAPPING_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Security, }, { title: 'menus.securityDropdown.cedarlingConfig', path: ROUTES.ADMIN_CEDARLING_CONFIG, - permission: MAPPING_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Security, }, ], @@ -95,19 +84,19 @@ const pluginMetadata = { { title: 'menus.webhooks', path: ROUTES.WEBHOOK_LIST, - permission: WEBHOOK_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Webhooks, }, { title: 'menus.assets', path: ROUTES.ASSETS_LIST, - permission: ASSETS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Assets, }, { title: 'menus.audit_logs', path: ROUTES.ADMIN_AUDIT_LOGS, - permission: LOGGING_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.AuditLogs, }, ], @@ -117,79 +106,79 @@ const pluginMetadata = { { component: HealthPage, path: ROUTES.ADMIN_HEALTH, - permission: PROPERTIES_READ, + action: CEDAR_ACTIONS.READ, resourceKey: CEDARLING_BYPASS, }, { component: LicenseDetailsPage, path: ROUTES.ADMIN_LICENSE_DETAILS, - permission: LICENSE_DETAILS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.License, }, { component: MauPage, path: ROUTES.ADMIN_MAU_GRAPH, - permission: ACR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.MAU, }, { component: SettingsPage, path: ROUTES.ADMIN_SETTINGS, - permission: ACR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Settings, }, { component: RolePermissionMappingPage, path: ROUTES.ADMIN_MAPPING, - permission: MAPPING_READ, + action: CEDAR_ACTIONS.READ, }, { component: CedarlingConfigPage, path: ROUTES.ADMIN_CEDARLING_CONFIG, - permission: PROPERTIES_READ, + action: CEDAR_ACTIONS.READ, }, { component: WebhookListPage, path: ROUTES.WEBHOOK_LIST, - permission: WEBHOOK_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Webhooks, }, { component: WebhookAddPage, path: ROUTES.WEBHOOK_ADD, - permission: WEBHOOK_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Webhooks, }, { component: WebhookEditPage, path: ROUTES.WEBHOOK_EDIT_TEMPLATE, - permission: WEBHOOK_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Webhooks, }, { component: JansAssetListPageLazy, path: ROUTES.ASSETS_LIST, - permission: ASSETS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Assets, }, { component: JansAssetAddPageLazy, path: ROUTES.ASSET_ADD, - permission: ASSETS_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Assets, }, { component: JansAssetEditPageLazy, path: ROUTES.ASSET_EDIT_TEMPLATE, - permission: ASSETS_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Assets, }, { component: AuditListPage, path: ROUTES.ADMIN_AUDIT_LOGS, - permission: LOGGING_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.AuditLogs, }, ], diff --git a/admin-ui/plugins/auth-server/components/AuthServerProperties/__tests__/components/AuthServerPropertiesPage.test.tsx b/admin-ui/plugins/auth-server/components/AuthServerProperties/__tests__/components/AuthServerPropertiesPage.test.tsx index 29d75924c1..2d04720cb4 100644 --- a/admin-ui/plugins/auth-server/components/AuthServerProperties/__tests__/components/AuthServerPropertiesPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/AuthServerProperties/__tests__/components/AuthServerPropertiesPage.test.tsx @@ -41,9 +41,6 @@ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { AuthenticationServerConfiguration: 'AuthenticationServerConfiguration', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { AuthenticationServerConfiguration: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/AuthServerProperties/components/AuthServerPropertiesPage.tsx b/admin-ui/plugins/auth-server/components/AuthServerProperties/components/AuthServerPropertiesPage.tsx index 694531a40c..c8de5a25d1 100644 --- a/admin-ui/plugins/auth-server/components/AuthServerProperties/components/AuthServerPropertiesPage.tsx +++ b/admin-ui/plugins/auth-server/components/AuthServerProperties/components/AuthServerPropertiesPage.tsx @@ -16,7 +16,7 @@ import type { GluuSelectRowFormik } from 'Routes/Apps/Gluu/types/GluuSelectRow.t import PropertyBuilder, { NumberField } from './JsonPropertyBuilder' import DefaultAcrInput from './DefaultAcrInput' import SetTitle from 'Utils/SetTitle' -import { buildPayload } from 'Utils/PermChecker' +import { buildPayload } from 'Utils/auditAction' import { devLogger } from '@/utils/devLogger' import { updateToast } from 'Redux/features/toastSlice' import { SIMPLE_PASSWORD_AUTH } from '@/constants' @@ -26,8 +26,7 @@ import { usePatchAuthServerJsonPropertiesMutation, } from 'Plugins/auth-server/hooks/useAuthServerJsonProperties' import { useAuthServerScripts } from '../hooks/useAuthServerScripts' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { useTheme } from '@/context/theme/themeContext' @@ -74,10 +73,9 @@ import type { SimpleFieldModel, } from '../types' import type { GluuCommitDialogOperation, JsonValue } from 'Routes/Apps/Gluu/types/index' -import type { UserAction } from 'Utils/PermChecker' +import type { UserAction } from 'Utils/types' const propertiesResourceId = ADMIN_UI_RESOURCES.AuthenticationServerConfiguration -const propertiesScopes = CEDAR_RESOURCE_SCOPES[propertiesResourceId] || [] const createUserAction = (): UserAction => ({ action_message: '', action_data: null, @@ -99,7 +97,8 @@ const AuthServerPropertiesPage: React.FC = () => { [themeState?.theme], ) const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadProperties, canWrite: canWriteProperties } = + usePermission(propertiesResourceId) const { logAcrUpdate } = useAcrAudit() const { data: serverConfigurationData, @@ -118,14 +117,6 @@ const AuthServerPropertiesPage: React.FC = () => { query: { staleTime: 30000 }, }) const lSize = DEFAULT_FORM_LABEL_SIZE - const canReadProperties = useMemo( - () => hasCedarReadPermission(propertiesResourceId), - [hasCedarReadPermission], - ) - const canWriteProperties = useMemo( - () => hasCedarWritePermission(propertiesResourceId), - [hasCedarWritePermission], - ) const [modal, setModal] = useState(false) const [patches, setPatches] = useState([]) const [resetKey, setResetKey] = useState(0) @@ -182,9 +173,6 @@ const AuthServerPropertiesPage: React.FC = () => { validateOnMount: false, onSubmit: () => {}, }) - useEffect(() => { - authorizeHelper(propertiesScopes) - }, [authorizeHelper]) useEffect(() => { setFieldValueRef.current = formik.setFieldValue resetFormRef.current = formik.resetForm diff --git a/admin-ui/plugins/auth-server/components/Authentication/Acrs/Acrs.tsx b/admin-ui/plugins/auth-server/components/Authentication/Acrs/Acrs.tsx index cae15fc3d1..d7bcf9a7b0 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/Acrs/Acrs.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/Acrs/Acrs.tsx @@ -1,7 +1,7 @@ -import { useEffect, useCallback, useMemo, useState, type ReactElement } from 'react' +import { useCallback, useMemo, useState, type ReactElement } from 'react' import { Check, Close, Edit } from '@/components/icons' import { useAppNavigation, ROUTES } from '@/helpers/navigation' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import { useTranslation } from 'react-i18next' @@ -15,13 +15,7 @@ import { AUTHN } from 'Utils/ApiResources' import { DEFAULT_SCRIPT_TYPE, useCustomScriptsByType } from 'Plugins/scripts/components' import { useGetAcrs, useGetConfigDatabaseLdap, type GluuLdapConfiguration } from 'JansConfigApi' import type { AuthNItem } from '../types' -import { - BUILT_IN_ACRS, - AUTH_RESOURCE_ID, - AUTH_SCOPES, - PAGE_SIZE, - AUTH_METHOD_NAMES, -} from '../constants' +import { BUILT_IN_ACRS, AUTH_RESOURCE_ID, PAGE_SIZE, AUTH_METHOD_NAMES } from '../constants' import { useStyles } from './Acrs.style' import { displayOrDash } from './helper/acrUtils' @@ -30,7 +24,7 @@ type AcrsProps = { } const Acrs = ({ isBuiltIn = false }: AcrsProps): ReactElement => { - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadAuthN, canWrite: canWriteAuthN } = usePermission(AUTH_RESOURCE_ID) const { t } = useTranslation() const { navigateToRoute } = useAppNavigation() @@ -44,15 +38,6 @@ const Acrs = ({ isBuiltIn = false }: AcrsProps): ReactElement => { const [page, setPage] = useState(0) const [rowsPerPage, setRowsPerPage] = useState(PAGE_SIZE) - const canReadAuthN = useMemo( - () => hasCedarReadPermission(AUTH_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteAuthN = useMemo( - () => hasCedarWritePermission(AUTH_RESOURCE_ID), - [hasCedarWritePermission], - ) - const { data: ldapConfigurations = [], isLoading: ldapLoading } = useGetConfigDatabaseLdap({ query: { staleTime: 30000, enabled: canReadAuthN }, }) @@ -67,10 +52,6 @@ const Acrs = ({ isBuiltIn = false }: AcrsProps): ReactElement => { SetTitle(t('titles.authentication')) - useEffect(() => { - authorizeHelper(AUTH_SCOPES) - }, [authorizeHelper]) - const handleGoToAuthNEditPage = useCallback( (row: AuthNItem) => { const id = row.inum || row.acrName || row.name || 'built-in' diff --git a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx index 0bee5abe53..65ef9849ba 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx @@ -6,7 +6,7 @@ import useSetTitle from 'Utils/SetTitle' import { useAgamaActions } from './hooks/useAgamaActions' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from 'Context/theme/config' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' @@ -53,7 +53,7 @@ import type { AgamaTableRow, } from './types' import { DATE_TIME_FORMAT_OPTIONS, ACCEPTED_PROJECT_TYPES, ACCEPTED_SHA_TYPES } from './constants' -import { AUTH_RESOURCE_ID, AUTH_SCOPES } from '../constants' +import { AUTH_RESOURCE_ID } from '../constants' const agamaButtonStyle = { minHeight: BUTTON_STYLES.height, @@ -66,11 +66,10 @@ const agamaButtonStyle = { const AgamaFlows: React.FC = () => { const { - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - authorizeHelper, - } = useCedarling() + canRead: canReadAuth, + canWrite: canWriteAuth, + canDelete: canDeleteAuth, + } = usePermission(AUTH_RESOURCE_ID) const { t } = useTranslation() const dispatch = useAppDispatch() const queryClient = useQueryClient() @@ -109,11 +108,6 @@ const AgamaFlows: React.FC = () => { const { classes } = useStyles({ isDark, themeColors }) const { classes: commitClasses } = useCommitDialogStyles({ isDark, themeColors }) - const canReadAuth = useMemo( - () => hasCedarReadPermission(AUTH_RESOURCE_ID), - [hasCedarReadPermission], - ) - const { data: configuration = {}, isLoading: isConfigLoading, @@ -174,21 +168,6 @@ const AgamaFlows: React.FC = () => { [agamaRepositoriesData], ) - const canWriteAuth = useMemo( - () => hasCedarWritePermission(AUTH_RESOURCE_ID), - [hasCedarWritePermission], - ) - const canDeleteAuth = useMemo( - () => hasCedarDeletePermission(AUTH_RESOURCE_ID), - [hasCedarDeletePermission], - ) - - useEffect(() => { - if (AUTH_SCOPES.length > 0) { - authorizeHelper(AUTH_SCOPES) - } - }, [authorizeHelper]) - useEffect(() => { if (repositoriesError) { const errorMessage = diff --git a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx index e9ba3e3be2..e7e653a5cc 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx @@ -25,9 +25,6 @@ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Authentication: 'Authentication', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Authentication: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/helper/utils.ts b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/helper/utils.ts index 299c9811ce..7849c98d87 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/helper/utils.ts +++ b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/helper/utils.ts @@ -1,7 +1,7 @@ import type { AcrMapping, ApiError } from '../types' import type { AgamaJsonPatchRequestBody } from '../types/agamaTypes' import type { AcrMappingFormValues } from '../types/formTypes' -import type { ActionData } from 'Utils/PermChecker' +import type { ActionData } from 'Utils/types' import { JSON_PATCH_PATHS, JSON_PATCH_OPS } from '../../constants' export const getErrorMessage = (error: ApiError, fallback = 'An error occurred'): string => diff --git a/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx b/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx index 57e8160f96..15e4183395 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx @@ -12,16 +12,17 @@ import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import { GluuButton } from '@/components/GluuButton' import { GluuTable, type ColumnDef, type ActionDef } from '@/components/GluuTable' import SetTitle from 'Utils/SetTitle' -import { buildPayload, type UserAction } from 'Utils/PermChecker' +import { buildPayload } from 'Utils/auditAction' +import type { UserAction } from 'Utils/types' import type { JsonValue } from 'Routes/Apps/Gluu/types/common' import { devLogger } from '@/utils/devLogger' import { getFieldPlaceholder } from '@/utils/placeholderUtils' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import { useStyles } from './Aliases.style' -import { AUTH_RESOURCE_ID, AUTH_SCOPES, PAGE_SIZE } from '../constants' +import { AUTH_RESOURCE_ID, PAGE_SIZE } from '../constants' import { useStyles as useCommitDialogStyles } from 'Routes/Apps/Gluu/styles/GluuCommitDialog.style' import { BUTTON_STYLES } from 'Routes/Apps/Gluu/styles/GluuThemeFormFooter.style' import { @@ -48,7 +49,7 @@ const Aliases = ({ onWritePermissionChange, }: AliasesProps): React.ReactElement => { const { t } = useTranslation() - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadAuth, canWrite: canWriteAuth } = usePermission(AUTH_RESOURCE_ID) const { state: themeState } = useTheme() const themeColors = useMemo( @@ -69,15 +70,6 @@ const Aliases = ({ SetTitle(t('titles.authentication')) - const canReadAuth = useMemo( - () => hasCedarReadPermission(AUTH_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteAuth = useMemo( - () => hasCedarWritePermission(AUTH_RESOURCE_ID), - [hasCedarWritePermission], - ) - const { data: configuration = {}, isLoading: configLoading, @@ -133,12 +125,6 @@ const Aliases = ({ formikRef.current = formik initialFormValuesRef.current = initialFormValues - useEffect(() => { - if (AUTH_SCOPES && AUTH_SCOPES.length > 0) { - authorizeHelper(AUTH_SCOPES) - } - }, [authorizeHelper]) - useEffect(() => { if (!showAddModal) { formikRef.current.resetForm({ values: initialFormValuesRef.current }) diff --git a/admin-ui/plugins/auth-server/components/Authentication/DefaultAcr/DefaultAcr.tsx b/admin-ui/plugins/auth-server/components/Authentication/DefaultAcr/DefaultAcr.tsx index 6f830509b3..07b62a61e8 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/DefaultAcr/DefaultAcr.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/DefaultAcr/DefaultAcr.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react' import { useAppDispatch } from '@/redux/hooks' import { useFormik } from 'formik' import { useQueryClient } from '@tanstack/react-query' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' @@ -33,14 +33,14 @@ import { DEFAULT_THEME } from '@/context/theme/constants' import { devLogger } from '@/utils/devLogger' import { useStyles } from './DefaultAcr.style' import { MAX_AGAMA_PROJECTS_FOR_ACR } from './constants' -import { AUTH_RESOURCE_ID, AUTH_SCOPES } from '../constants' +import { AUTH_RESOURCE_ID } from '../constants' type DefaultAcrFormValues = { defaultAcr: string } const DefaultAcr = (): React.ReactElement => { - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadAuth, canWrite: canWriteAuth } = usePermission(AUTH_RESOURCE_ID) const dispatch = useAppDispatch() const queryClient = useQueryClient() const { logAcrUpdate } = useAcrAudit() @@ -53,15 +53,6 @@ const DefaultAcr = (): React.ReactElement => { const [modal, setModal] = useState(false) - const canReadAuth = useMemo( - () => hasCedarReadPermission(AUTH_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteAuth = useMemo( - () => hasCedarWritePermission(AUTH_RESOURCE_ID), - [hasCedarWritePermission], - ) - const { data: scriptsResponse, isLoading: loadingScripts } = useCustomScriptsByType( DEFAULT_SCRIPT_TYPE, undefined, @@ -108,12 +99,6 @@ const DefaultAcr = (): React.ReactElement => { SetTitle(t('titles.authentication')) - useEffect(() => { - if (AUTH_SCOPES.length > 0) { - authorizeHelper(AUTH_SCOPES) - } - }, [authorizeHelper]) - useEffect(() => { if (acrError) { const errorMessage = (acrError as Error)?.message || 'Failed to fetch ACR configuration' diff --git a/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx b/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx index 8651fbb5f6..bc73ea39ce 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx @@ -25,9 +25,6 @@ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Authentication: 'Authentication', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Authentication: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Authentication/constants.ts b/admin-ui/plugins/auth-server/components/Authentication/constants.ts index 03df453f44..b5daca450a 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/constants.ts +++ b/admin-ui/plugins/auth-server/components/Authentication/constants.ts @@ -1,9 +1,7 @@ import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import type { BuiltInAcr } from './types' export const AUTH_RESOURCE_ID = ADMIN_UI_RESOURCES.Authentication -export const AUTH_SCOPES = CEDAR_RESOURCE_SCOPES[AUTH_RESOURCE_ID] || [] export const PAGE_SIZE = 10 diff --git a/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesForm.tsx b/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesForm.tsx index 890396f10d..5434942ca3 100644 --- a/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesForm.tsx +++ b/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesForm.tsx @@ -5,8 +5,7 @@ import { devLogger } from '@/utils/devLogger' import { toast } from 'react-toastify' import { useTranslation } from 'react-i18next' import { Form } from 'Components' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { useTheme } from '@/context/theme/themeContext' @@ -37,7 +36,6 @@ import type { AppConfiguration, PropertyValue } from '../../AuthServerProperties import { useStyles } from './styles/ConfigApiPropertiesForm.style' const CONFIG_API_RESOURCE_ID = ADMIN_UI_RESOURCES.ConfigApiConfiguration -const configApiScopes = CEDAR_RESOURCE_SCOPES[CONFIG_API_RESOURCE_ID] || [] const ConfigApiPropertiesForm: React.FC = ({ configuration, @@ -46,7 +44,7 @@ const ConfigApiPropertiesForm: React.FC = ({ }) => { const { t } = useTranslation() const deferredSearch = useDeferredValue(search.toLowerCase()) - const { authorizeHelper, hasCedarWritePermission } = useCedarling() + const { canWrite: canWriteConfigApi } = usePermission(CONFIG_API_RESOURCE_ID) const { navigateToRoute } = useAppNavigation() const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( @@ -67,11 +65,6 @@ const ConfigApiPropertiesForm: React.FC = ({ const previousConfigurationRef = useRef(null) const processingRemovalsRef = useRef>(new Set()) - const canWriteConfigApi = useMemo( - () => hasCedarWritePermission(CONFIG_API_RESOURCE_ID), - [hasCedarWritePermission], - ) - const formik = useFormik({ initialValues: configuration, validationSchema: configApiPropertiesSchema, @@ -116,12 +109,6 @@ const ConfigApiPropertiesForm: React.FC = ({ } }, [configuration, patches.length, formik.resetForm]) - useEffect(() => { - if (configApiScopes && configApiScopes.length > 0) { - authorizeHelper(configApiScopes) - } - }, [authorizeHelper]) - const operations: GluuCommitDialogOperation[] = useMemo(() => { return patches.map((patch) => ({ path: formatPatchPath(patch.path as string), diff --git a/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesPage.tsx b/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesPage.tsx index f0add8adbb..a8334ba631 100644 --- a/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesPage.tsx +++ b/admin-ui/plugins/auth-server/components/ConfigApiProperties/components/ConfigApiPropertiesPage.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo, useEffect } from 'react' +import { useState, useCallback, useMemo } from 'react' import ConfigApiPropertiesForm from './ConfigApiPropertiesForm' import { Card, CardBody } from 'Components' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' @@ -13,9 +13,8 @@ import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { GluuSearchToolbar } from '@/components/GluuSearchToolbar' import { devLogger } from '@/utils/devLogger' import { useStyles } from './styles/ConfigApiPropertiesForm.style' @@ -28,22 +27,12 @@ import type { } from '../types' const configApiResourceId = ADMIN_UI_RESOURCES.ConfigApiConfiguration -const configApiScopes = CEDAR_RESOURCE_SCOPES[configApiResourceId] || [] const ConfigApiPropertiesPage = (): JSX.Element => { const { t } = useTranslation() const { logConfigApiUpdate } = useConfigApiActions() const [errorMessage, setErrorMessage] = useState(null) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() - const canReadConfigApi = useMemo( - () => hasCedarReadPermission(configApiResourceId), - [hasCedarReadPermission], - ) - useEffect(() => { - if (configApiScopes.length > 0) { - authorizeHelper(configApiScopes) - } - }, [authorizeHelper, configApiScopes]) + const { canRead: canReadConfigApi } = usePermission(configApiResourceId) const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( diff --git a/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkItem.test.tsx b/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkItem.test.tsx index 347f384e35..804ff2f495 100644 --- a/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkItem.test.tsx +++ b/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkItem.test.tsx @@ -16,9 +16,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Keys: 'Keys', Lock: 'Lock', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Keys: [], Lock: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkListPage.test.tsx b/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkListPage.test.tsx index 3759ae584c..bfef7228a6 100644 --- a/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkListPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Keys/__tests__/components/JwkListPage.test.tsx @@ -17,9 +17,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Keys: 'Keys', Lock: 'Lock', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Keys: [], Lock: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Keys/__tests__/components/KeysPage.test.tsx b/admin-ui/plugins/auth-server/components/Keys/__tests__/components/KeysPage.test.tsx index 0644bc4ccb..3b4484f00a 100644 --- a/admin-ui/plugins/auth-server/components/Keys/__tests__/components/KeysPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Keys/__tests__/components/KeysPage.test.tsx @@ -21,9 +21,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Keys: 'Keys', Lock: 'Lock', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Keys: [], Lock: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Keys/components/KeysPage.tsx b/admin-ui/plugins/auth-server/components/Keys/components/KeysPage.tsx index 4cb20631e0..1511886dbc 100644 --- a/admin-ui/plugins/auth-server/components/Keys/components/KeysPage.tsx +++ b/admin-ui/plugins/auth-server/components/Keys/components/KeysPage.tsx @@ -1,9 +1,8 @@ -import React, { useMemo, useEffect, useCallback } from 'react' +import React, { useMemo, useCallback } from 'react' import JwkListPage from './JwkListPage' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuThemeFormFooter from 'Routes/Apps/Gluu/GluuThemeFormFooter' @@ -15,11 +14,10 @@ import { useStyles } from './styles/KeysPage.style' import { useAppNavigation, ROUTES } from '@/helpers/navigation' const keysResourceId = ADMIN_UI_RESOURCES.Keys -const keysScopes = CEDAR_RESOURCE_SCOPES[keysResourceId] || [] const KeysPage: React.FC = () => { const { t } = useTranslation() - const { hasCedarReadPermission, authorizeHelper } = useCedarling() + const { canRead: canReadKeys } = usePermission(keysResourceId) const { navigateBack } = useAppNavigation() const { state: themeState } = useTheme() @@ -32,17 +30,6 @@ const KeysPage: React.FC = () => { ) const { classes } = useStyles({ isDark, themeColors }) - const canReadKeys = useMemo( - () => hasCedarReadPermission(keysResourceId), - [hasCedarReadPermission], - ) - - useEffect(() => { - if (keysScopes.length > 0) { - authorizeHelper(keysScopes) - } - }, [authorizeHelper]) - const handleBack = useCallback(() => { navigateBack(ROUTES.AUTH_SERVER_CONFIG_KEYS) }, [navigateBack]) diff --git a/admin-ui/plugins/auth-server/components/Logging/__tests__/components/LoggingPage.test.tsx b/admin-ui/plugins/auth-server/components/Logging/__tests__/components/LoggingPage.test.tsx index 4882e63e10..63bba5ee21 100644 --- a/admin-ui/plugins/auth-server/components/Logging/__tests__/components/LoggingPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Logging/__tests__/components/LoggingPage.test.tsx @@ -69,9 +69,6 @@ jest.mock('@/cedarling/utility', () => ({ Lock: 'Lock', Webhooks: 'Webhooks', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Logging: [], Keys: [], Lock: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Logging/components/LoggingPage.tsx b/admin-ui/plugins/auth-server/components/Logging/components/LoggingPage.tsx index 08cf955f86..807a69b390 100644 --- a/admin-ui/plugins/auth-server/components/Logging/components/LoggingPage.tsx +++ b/admin-ui/plugins/auth-server/components/Logging/components/LoggingPage.tsx @@ -18,8 +18,7 @@ import { import type { LoggingFormValues } from '../utils' import { Formik } from 'formik' import { useLoggingConfig, useUpdateLoggingConfig } from '../hooks' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' @@ -36,7 +35,6 @@ import { devLogger } from '@/utils/devLogger' import { useAppNavigation, ROUTES } from '@/helpers/navigation' const LOGGING_RESOURCE_ID = ADMIN_UI_RESOURCES.Logging -const LOGGING_SCOPES = CEDAR_RESOURCE_SCOPES[LOGGING_RESOURCE_ID] || [] const FORM_STYLE: React.CSSProperties = { display: 'flex', @@ -47,7 +45,7 @@ const FORM_STYLE: React.CSSProperties = { const LoggingPage = (): React.ReactElement => { const { t } = useTranslation() const dispatch = useAppDispatch() - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadLogging, canWrite: canWriteLogging } = usePermission(LOGGING_RESOURCE_ID) const { navigateBack } = useAppNavigation() const { data: logging, isLoading: queryLoading } = useLoggingConfig() @@ -55,16 +53,6 @@ const LoggingPage = (): React.ReactElement => { const loading = queryLoading || updateLoggingMutation.isPending - const canReadLogging = useMemo( - () => !!hasCedarReadPermission(LOGGING_RESOURCE_ID), - [hasCedarReadPermission], - ) - - const canWriteLogging = useMemo( - () => !!hasCedarWritePermission(LOGGING_RESOURCE_ID), - [hasCedarWritePermission], - ) - const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( () => ({ @@ -90,12 +78,6 @@ const LoggingPage = (): React.ReactElement => { navigateBack(ROUTES.AUTH_SERVER_CONFIG_PROPERTIES) }, [navigateBack]) - useEffect(() => { - if (LOGGING_SCOPES.length > 0) { - authorizeHelper(LOGGING_SCOPES) - } - }, [authorizeHelper]) - const initialValues: LoggingFormValues = useMemo( () => getLoggingInitialValues(logging), [logging], diff --git a/admin-ui/plugins/auth-server/components/OidcClients/__tests__/components/ClientListPage.test.tsx b/admin-ui/plugins/auth-server/components/OidcClients/__tests__/components/ClientListPage.test.tsx index 4a539954e7..ff126d14d8 100644 --- a/admin-ui/plugins/auth-server/components/OidcClients/__tests__/components/ClientListPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/OidcClients/__tests__/components/ClientListPage.test.tsx @@ -50,15 +50,12 @@ jest.mock('@/cedarling', () => ({ })), })) -jest.mock('@/cedarling/constants/resourceScopes', () => ({ - CEDAR_RESOURCE_SCOPES: {}, -})) - jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Clients: 'clients', Webhooks: 'webhooks', }, + CEDAR_RESOURCE_SCOPES: {}, })) jest.mock('@/cedarling/hooks/useCedarling', () => ({ diff --git a/admin-ui/plugins/auth-server/components/OidcClients/components/ClientListPage.tsx b/admin-ui/plugins/auth-server/components/OidcClients/components/ClientListPage.tsx index 04ce8099ce..9517f0442b 100644 --- a/admin-ui/plugins/auth-server/components/OidcClients/components/ClientListPage.tsx +++ b/admin-ui/plugins/auth-server/components/OidcClients/components/ClientListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useCallback, memo } from 'react' +import React, { useState, useMemo, useCallback, memo } from 'react' import { Add, DeleteOutlined, Edit, VisibilityOutlined } from '@/components/icons' import { useLocation } from 'react-router-dom' import { useAppSelector } from '@/redux/hooks' @@ -16,8 +16,7 @@ import { useTheme } from '@/context/theme/themeContext' import getThemeColor from 'Context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import SetTitle from 'Utils/SetTitle' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { adminUiFeatures } from '@/constants' import { devLogger } from '@/utils/devLogger' @@ -50,7 +49,6 @@ import { const LIMIT_OPTIONS = getRowsPerPageOptions() const clientResourceId = ADMIN_UI_RESOURCES.Clients -const clientCedarScopes = CEDAR_RESOURCE_SCOPES[clientResourceId] || [] const addOrg = (client: ClientRow): ClientRow => { if (!client) return client @@ -79,12 +77,7 @@ const ClientListPage: React.FC = () => { const { search } = useLocation() const { state } = useTheme() - const { - authorizeHelper, - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - } = useCedarling() + const { canRead, canWrite, canDelete } = usePermission(clientResourceId) const { themeColors, isDarkTheme } = useMemo(() => { const selected = state?.theme || DEFAULT_THEME @@ -111,16 +104,6 @@ const ClientListPage: React.FC = () => { }, [search]) const haveScopeINUMParam = useMemo(() => !!scopeInumParam, [scopeInumParam]) - const canRead = useMemo(() => hasCedarReadPermission(clientResourceId), [hasCedarReadPermission]) - const canWrite = useMemo( - () => hasCedarWritePermission(clientResourceId), - [hasCedarWritePermission], - ) - const canDelete = useMemo( - () => hasCedarDeletePermission(clientResourceId), - [hasCedarDeletePermission], - ) - const clientQueryParams = useMemo( () => ({ limit, @@ -293,10 +276,6 @@ const ClientListPage: React.FC = () => { [setLimit, setPageNumber], ) - useEffect(() => { - if (clientCedarScopes.length > 0) authorizeHelper(clientCedarScopes) - }, [authorizeHelper, clientCedarScopes]) - const shouldHideOrgColumn = useMemo( () => !clients.some((c) => c.organization !== EM_DASH_PLACEHOLDER), [clients], diff --git a/admin-ui/plugins/auth-server/components/OidcClients/components/ClientWizardForm.tsx b/admin-ui/plugins/auth-server/components/OidcClients/components/ClientWizardForm.tsx index b24db636d1..ceec9917a0 100644 --- a/admin-ui/plugins/auth-server/components/OidcClients/components/ClientWizardForm.tsx +++ b/admin-ui/plugins/auth-server/components/OidcClients/components/ClientWizardForm.tsx @@ -11,8 +11,7 @@ import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' import { Formik, type FormikProps } from 'formik' import { useTranslation } from 'react-i18next' import { useAppNavigation, ROUTES } from '@/helpers/navigation' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' @@ -51,6 +50,7 @@ import type { } from '../types' import type { FilterField } from '@/components/GluuFilterPopover' const INITIAL_TOKEN_PATTERN: TokenSearchPattern = { dateAfter: null, dateBefore: null } +const clientResourceId = ADMIN_UI_RESOURCES.Clients const ClientWizardForm = ({ client_data, @@ -62,7 +62,6 @@ const ClientWizardForm = ({ modifiedFields, setModifiedFields, }: ClientWizardFormProps) => { - const { hasCedarWritePermission, authorizeHelper } = useCedarling() const formRef = useRef>(null) const footerRef = useRef(null) const commitMessageRef = useRef('') @@ -168,20 +167,7 @@ const ClientWizardForm = ({ const [currentStep, setCurrentStep] = useState(CLIENT_WIZARD_SEQUENCE[0]) const dispatch = useAppDispatch() - const clientResourceId = ADMIN_UI_RESOURCES.Clients - const clientScopes = useMemo( - () => CEDAR_RESOURCE_SCOPES[clientResourceId] || [], - [clientResourceId], - ) - - const canWriteClient = useMemo( - () => hasCedarWritePermission(clientResourceId), - [hasCedarWritePermission, clientResourceId], - ) - - useEffect(() => { - authorizeHelper(clientScopes) - }, [authorizeHelper, clientScopes]) + const { canWrite: canWriteClient } = usePermission(clientResourceId) const initialValues = useMemo(() => buildClientInitialValues(client_data), [client_data]) const clientSnapshot = useMemo(() => cloneDeep(initialValues), [initialValues]) diff --git a/admin-ui/plugins/auth-server/components/Scopes/__tests__/components/ScopeListPage.test.tsx b/admin-ui/plugins/auth-server/components/Scopes/__tests__/components/ScopeListPage.test.tsx index 358f8daa70..9542bdd870 100644 --- a/admin-ui/plugins/auth-server/components/Scopes/__tests__/components/ScopeListPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Scopes/__tests__/components/ScopeListPage.test.tsx @@ -43,15 +43,12 @@ jest.mock('@/cedarling', () => ({ })), })) -jest.mock('@/cedarling/constants/resourceScopes', () => ({ - CEDAR_RESOURCE_SCOPES: {}, -})) - jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Scopes: 'scopes', Webhooks: 'webhooks', }, + CEDAR_RESOURCE_SCOPES: {}, })) jest.mock('@/cedarling/hooks/useCedarling', () => ({ diff --git a/admin-ui/plugins/auth-server/components/Scopes/components/ScopeListPage.tsx b/admin-ui/plugins/auth-server/components/Scopes/components/ScopeListPage.tsx index 2bb746a68f..e550ca3d4d 100644 --- a/admin-ui/plugins/auth-server/components/Scopes/components/ScopeListPage.tsx +++ b/admin-ui/plugins/auth-server/components/Scopes/components/ScopeListPage.tsx @@ -14,8 +14,7 @@ import { ThemeContext } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import SetTitle from 'Utils/SetTitle' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { updateToast } from 'Redux/features/toastSlice' import { triggerWebhook } from 'Plugins/admin/redux/features/WebhookSlice' @@ -67,12 +66,7 @@ const ScopeListPage: React.FC = () => { const dispatch = useAppDispatch() const queryClient = useQueryClient() const { navigateToRoute } = useAppNavigation() - const { - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - authorizeHelper, - } = useCedarling() + const { canRead, canWrite, canDelete } = usePermission(ADMIN_UI_RESOURCES.Scopes) const { logScopeDeletion } = useScopeActions() const theme = useContext(ThemeContext) @@ -93,29 +87,6 @@ const ScopeListPage: React.FC = () => { const [modal, setModal] = useState(false) const [itemToDelete, setItemToDelete] = useState(null) - const scopesResourceId = ADMIN_UI_RESOURCES.Scopes - const scopesScopes = useMemo( - () => CEDAR_RESOURCE_SCOPES[scopesResourceId] ?? [], - [scopesResourceId], - ) - - const canRead = useMemo( - () => hasCedarReadPermission(scopesResourceId), - [hasCedarReadPermission, scopesResourceId], - ) - const canWrite = useMemo( - () => hasCedarWritePermission(scopesResourceId), - [hasCedarWritePermission, scopesResourceId], - ) - const canDelete = useMemo( - () => hasCedarDeletePermission(scopesResourceId), - [hasCedarDeletePermission, scopesResourceId], - ) - - useEffect(() => { - authorizeHelper(scopesScopes) - }, [authorizeHelper, scopesScopes]) - const startIndex = useMemo(() => pageNumber * limit, [pageNumber, limit]) const debouncedPattern = useDebounce(pattern, 400) diff --git a/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionDetailPage.test.tsx b/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionDetailPage.test.tsx index 5e247b6cbe..b000f70c15 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionDetailPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionDetailPage.test.tsx @@ -18,9 +18,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Session: 'sessions', Webhooks: 'webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { sessions: [], webhooks: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionListPage.test.tsx b/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionListPage.test.tsx index 91b5d6b304..505ee347fe 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionListPage.test.tsx +++ b/admin-ui/plugins/auth-server/components/Sessions/__tests__/components/SessionListPage.test.tsx @@ -54,9 +54,6 @@ jest.mock('Plugins/auth-server/components/Sessions/hooks/useSessionMutations', ( jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Session: 'sessions', Webhooks: 'webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { sessions: [], webhooks: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Sessions/components/SessionListPage.tsx b/admin-ui/plugins/auth-server/components/Sessions/components/SessionListPage.tsx index 320bb743ab..0e329c6c46 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/components/SessionListPage.tsx +++ b/admin-ui/plugins/auth-server/components/Sessions/components/SessionListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo, memo } from 'react' +import React, { useState, useCallback, useMemo, memo } from 'react' import isEmpty from 'lodash/isEmpty' import { DeleteOutlined, FilterListIcon, GetAppIcon } from '@/components/icons' import { GluuBadge } from '@/components/GluuBadge' @@ -16,8 +16,7 @@ import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' import SetTitle from 'Utils/SetTitle' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { formatDate } from '@/utils/dayjsUtils' import { devLogger } from '@/utils/devLogger' @@ -42,7 +41,6 @@ import type { Dayjs } from 'dayjs' const LIMIT_OPTIONS = getRowsPerPageOptions() const sessionResourceId = ADMIN_UI_RESOURCES.Session -const SESSION_SCOPES = CEDAR_RESOURCE_SCOPES[sessionResourceId] || [] const DATE_FILTER_FIELDS = ['expirationDate', 'authenticationTime'] as const const FILTER_FIELD_OPTIONS = [ { value: '', labelKey: 'options.choose' }, @@ -59,7 +57,7 @@ const isDateFilterField = (field: string): field is (typeof DATE_FILTER_FIELDS)[ DATE_FILTER_FIELDS.includes(field as (typeof DATE_FILTER_FIELDS)[number]) const SessionListPage: React.FC = () => { - const { hasCedarDeletePermission, authorizeHelper } = useCedarling() + const { canDelete } = usePermission(sessionResourceId) const { t } = useTranslation() const { data: sessionsData, isLoading: sessionsLoading } = useGetSessions() @@ -104,17 +102,6 @@ const SessionListPage: React.FC = () => { const [item, setItem] = useState({} as Session) const [revokeUsername, setRevokeUsername] = useState(null) - const canDelete = useMemo( - () => hasCedarDeletePermission(sessionResourceId), - [hasCedarDeletePermission], - ) - - useEffect(() => { - if (SESSION_SCOPES.length > 0) { - authorizeHelper(SESSION_SCOPES) - } - }, [authorizeHelper]) - const adaptSessionIdToSession = useCallback( (sessionId: SessionId): Session => ({ id: sessionId.id, diff --git a/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx b/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx index af8f86105b..d7cbc87e9b 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx +++ b/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx @@ -25,9 +25,6 @@ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { SSA: 'SSA', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { SSA: [] }, })) diff --git a/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx b/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx index 3a635ed070..6a23218353 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx +++ b/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx @@ -9,8 +9,7 @@ import { SSA } from 'Utils/ApiResources' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { useAppDispatch } from '@/redux/hooks' @@ -38,16 +37,14 @@ import type { SsaData } from '../types/SsaApiTypes' import type { SsaTableRowData } from '../types/SsaFormTypes' const SSA_RESOURCE_ID = ADMIN_UI_RESOURCES.SSA -const SSA_SCOPES = CEDAR_RESOURCE_SCOPES[SSA_RESOURCE_ID] ?? [] const SSA_ROWS_PER_PAGE_OPTIONS = getRowsPerPageOptions() const SsaListPage: React.FC = () => { const { - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - authorizeHelper, - } = useCedarling() + canRead: canReadSsa, + canWrite: canWriteSsa, + canDelete: canDeleteSsa, + } = usePermission(SSA_RESOURCE_ID) const { t } = useTranslation() const dispatch = useAppDispatch() const { navigateToRoute } = useAppNavigation() @@ -66,23 +63,6 @@ const SsaListPage: React.FC = () => { const { limit, setLimit, pageNumber, setPageNumber, onPagingSizeSync } = usePaginationState() - const canReadSsa = useMemo( - () => hasCedarReadPermission(SSA_RESOURCE_ID), - [hasCedarReadPermission], - ) - const canWriteSsa = useMemo( - () => hasCedarWritePermission(SSA_RESOURCE_ID), - [hasCedarWritePermission], - ) - const canDeleteSsa = useMemo( - () => hasCedarDeletePermission(SSA_RESOURCE_ID), - [hasCedarDeletePermission], - ) - - useEffect(() => { - authorizeHelper(SSA_SCOPES) - }, [authorizeHelper]) - SetTitle(t('titles.ssa_management')) const { data: items = [], isLoading: loading } = useGetAllSsas() diff --git a/admin-ui/plugins/auth-server/hooks/useAuthServerJsonProperties.ts b/admin-ui/plugins/auth-server/hooks/useAuthServerJsonProperties.ts index 49c83eb059..573274b15b 100644 --- a/admin-ui/plugins/auth-server/hooks/useAuthServerJsonProperties.ts +++ b/admin-ui/plugins/auth-server/hooks/useAuthServerJsonProperties.ts @@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query' import { useAppDispatch } from '@/redux/hooks' import { updateToast } from 'Redux/features/toastSlice' -import type { UserAction } from 'Utils/PermChecker' +import type { UserAction } from 'Utils/types' import type { AppConfiguration } from '../components/AuthServerProperties/types' import { fetchAuthServerJsonProperties, diff --git a/admin-ui/plugins/auth-server/plugin-metadata.tsx b/admin-ui/plugins/auth-server/plugin-metadata.tsx index 0eada5794b..bcc7ab0bb7 100644 --- a/admin-ui/plugins/auth-server/plugin-metadata.tsx +++ b/admin-ui/plugins/auth-server/plugin-metadata.tsx @@ -1,19 +1,6 @@ import { reducer as scopeReducer } from './redux/features/scopeSlice' -import { - ACR_READ, - CLIENT_READ, - SCOPE_READ, - CLIENT_WRITE, - SCOPE_WRITE, - JWKS_READ, - SESSION_READ, - PROPERTIES_READ, - LOGGING_READ, - AGAMA_READ, - SSA_PORTAL, - API_CONFIG_READ, -} from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -60,55 +47,55 @@ const pluginMetadata = { { title: 'menus.clients', path: ROUTES.AUTH_SERVER_CLIENTS_LIST, - permission: CLIENT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Clients, }, { title: 'menus.scopes', path: ROUTES.AUTH_SERVER_SCOPES_LIST, - permission: SCOPE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Scopes, }, { title: 'menus.keys', path: ROUTES.AUTH_SERVER_CONFIG_KEYS, - permission: JWKS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Keys, }, { title: 'menus.properties', path: ROUTES.AUTH_SERVER_CONFIG_PROPERTIES, - permission: PROPERTIES_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.AuthenticationServerConfiguration, }, { title: 'menus.logging', path: ROUTES.AUTH_SERVER_CONFIG_LOGGING, - permission: LOGGING_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Logging, }, { title: 'menus.ssa', path: ROUTES.AUTH_SERVER_SSA_LIST, - permission: SSA_PORTAL, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SSA, }, { title: 'menus.authentication', path: ROUTES.AUTH_SERVER_AUTHN, - permission: SCOPE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Authentication, }, { title: 'menus.api_config', path: ROUTES.AUTH_SERVER_CONFIG_API, - permission: API_CONFIG_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.ConfigApiConfiguration, }, { title: 'menus.sessions', path: ROUTES.AUTH_SERVER_SESSIONS, - permission: SESSION_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Session, }, ], @@ -118,97 +105,97 @@ const pluginMetadata = { { component: SessionListPage, path: ROUTES.AUTH_SERVER_SESSIONS, - permission: SESSION_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Session, }, { component: ClientListPage, path: ROUTES.AUTH_SERVER_CLIENTS_LIST, - permission: CLIENT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Clients, }, { component: ClientAddPage, path: ROUTES.AUTH_SERVER_CLIENT_ADD, - permission: CLIENT_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Clients, }, { component: ClientEditPage, path: ROUTES.AUTH_SERVER_CLIENT_EDIT_TEMPLATE, - permission: CLIENT_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Clients, }, { component: ScopeListPage, path: ROUTES.AUTH_SERVER_SCOPES_LIST, - permission: SCOPE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Scopes, }, { component: AcrsEditPage, path: ROUTES.AUTH_SERVER_AUTHN_EDIT_TEMPLATE, - permission: SCOPE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Authentication, }, { component: AuthNPage, path: ROUTES.AUTH_SERVER_AUTHN, - permission: SCOPE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Authentication, }, { component: ScopeAddPage, path: ROUTES.AUTH_SERVER_SCOPE_ADD, - permission: SCOPE_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Scopes, }, { component: ScopeEditPage, path: ROUTES.AUTH_SERVER_SCOPE_EDIT_TEMPLATE, - permission: SCOPE_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Scopes, }, { component: PropertiesPage, path: ROUTES.AUTH_SERVER_CONFIG_PROPERTIES, - permission: ACR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.AuthenticationServerConfiguration, }, { component: KeysPage, path: ROUTES.AUTH_SERVER_CONFIG_KEYS, - permission: JWKS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Keys, }, { component: LoggingPage, path: ROUTES.AUTH_SERVER_CONFIG_LOGGING, - permission: ACR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Logging, }, { component: AgamaListPageWrapper, path: ROUTES.AUTH_SERVER_AGAMA, - permission: AGAMA_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Authentication, }, { component: SsaListPage, path: ROUTES.AUTH_SERVER_SSA_LIST, - permission: SSA_PORTAL, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SSA, }, { component: SsaAddPage, path: ROUTES.AUTH_SERVER_SSA_ADD, - permission: SSA_PORTAL, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SSA, }, { component: ConfigApiPage, path: ROUTES.AUTH_SERVER_CONFIG_API, - permission: API_CONFIG_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.ConfigApiConfiguration, }, ], diff --git a/admin-ui/plugins/auth-server/redux/utils/auditHelpers.ts b/admin-ui/plugins/auth-server/redux/utils/auditHelpers.ts index 89bebbcf84..7750b06511 100644 --- a/admin-ui/plugins/auth-server/redux/utils/auditHelpers.ts +++ b/admin-ui/plugins/auth-server/redux/utils/auditHelpers.ts @@ -1,5 +1,5 @@ import type { AgamaJsonPatch } from '../../components/Authentication/AgamaFlows/types/agamaTypes' -import type { UserAction } from 'Utils/PermChecker' +import type { UserAction } from 'Utils/types' import { JSON_PATCH_PATHS } from '../../components/Authentication/constants' const ACR_MAPPINGS_PATH = JSON_PATCH_PATHS.ACR_MAPPINGS diff --git a/admin-ui/plugins/auth-server/services/jsonPropertiesService.ts b/admin-ui/plugins/auth-server/services/jsonPropertiesService.ts index 714c1ce1c0..98d345182d 100644 --- a/admin-ui/plugins/auth-server/services/jsonPropertiesService.ts +++ b/admin-ui/plugins/auth-server/services/jsonPropertiesService.ts @@ -4,7 +4,7 @@ import { postUserAction } from 'Redux/api/backend-api' import type { UserActionPayload } from 'Redux/api/types/BackendApi' import { FETCH, PATCH, DELETION, createSuccessAuditInit, getCurrentAuditContext } from '@/audit' import type { AuditLog, AuditRecord } from 'Redux/types/audit' -import type { UserAction } from 'Utils/PermChecker' +import type { UserAction } from 'Utils/types' import type { JsonPatch } from 'JansConfigApi' import type { AppConfiguration } from '../components/AuthServerProperties/types' import { JSON_CONFIG } from '../redux/audit/Resources' diff --git a/admin-ui/plugins/fido/__tests__/components/DynamicConfiguration.test.tsx b/admin-ui/plugins/fido/__tests__/components/DynamicConfiguration.test.tsx index 0eb3522aac..8318dcdd33 100644 --- a/admin-ui/plugins/fido/__tests__/components/DynamicConfiguration.test.tsx +++ b/admin-ui/plugins/fido/__tests__/components/DynamicConfiguration.test.tsx @@ -18,9 +18,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { FIDO: 'FIDO', Lock: 'Lock' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { FIDO: [], Lock: [] }, })) diff --git a/admin-ui/plugins/fido/__tests__/components/Fido.test.tsx b/admin-ui/plugins/fido/__tests__/components/Fido.test.tsx index cc499bd956..dca8c403b9 100644 --- a/admin-ui/plugins/fido/__tests__/components/Fido.test.tsx +++ b/admin-ui/plugins/fido/__tests__/components/Fido.test.tsx @@ -23,9 +23,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { FIDO: 'FIDO', Lock: 'Lock' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { FIDO: [], Lock: [] }, })) diff --git a/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx b/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx index b30c55d065..814458d99e 100644 --- a/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx +++ b/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx @@ -3,11 +3,8 @@ import { render, screen, fireEvent } from '@testing-library/react' import AppTestWrapper from 'Routes/Apps/Gluu/Tests/Components/AppTestWrapper' import MetricsPage from 'Plugins/fido/components/Metrics/MetricsPage' -jest.mock('@/cedarling', () => ({ - useCedarling: jest.fn(() => ({ - hasCedarReadPermission: jest.fn(() => true), - authorizeHelper: jest.fn(), - })), +jest.mock('@/cedarling/hooks/usePermission', () => ({ + usePermission: jest.fn(() => ({ canRead: true, canWrite: false, canDelete: false })), })) jest.mock('@/cedarling/utility', () => ({ @@ -79,11 +76,8 @@ describe('MetricsPage', () => { }) it('shows missing permission message when user lacks read permission', () => { - const { useCedarling } = jest.requireMock('@/cedarling') - useCedarling.mockImplementation(() => ({ - hasCedarReadPermission: jest.fn(() => false), - authorizeHelper: jest.fn(), - })) + const { usePermission } = jest.requireMock('@/cedarling/hooks/usePermission') + usePermission.mockImplementation(() => ({ canRead: false, canWrite: false, canDelete: false })) render(, { wrapper: Wrapper }) expect(screen.queryByTestId('passkey-auth-chart')).not.toBeInTheDocument() expect(screen.getByTestId('MISSING')).toBeInTheDocument() diff --git a/admin-ui/plugins/fido/__tests__/components/StaticConfiguration.test.tsx b/admin-ui/plugins/fido/__tests__/components/StaticConfiguration.test.tsx index 02e3ede2ab..f585c0736f 100644 --- a/admin-ui/plugins/fido/__tests__/components/StaticConfiguration.test.tsx +++ b/admin-ui/plugins/fido/__tests__/components/StaticConfiguration.test.tsx @@ -18,9 +18,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { FIDO: 'FIDO', Lock: 'Lock' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { FIDO: [], Lock: [] }, })) diff --git a/admin-ui/plugins/fido/components/Configuration/Fido.tsx b/admin-ui/plugins/fido/components/Configuration/Fido.tsx index bade9e1145..d192e8fa6d 100644 --- a/admin-ui/plugins/fido/components/Configuration/Fido.tsx +++ b/admin-ui/plugins/fido/components/Configuration/Fido.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import GluuTabs from 'Routes/Apps/Gluu/GluuTabs' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' @@ -10,9 +10,8 @@ import { fidoConstants } from '../../helper' import { useFidoConfig, useUpdateFidoConfig } from '../../hooks' import type { DynamicConfigFormValues, StaticConfigFormValues } from '../../types' import type { UpdateFidoParams } from '../../types' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import GluuViewWrapper from '@/routes/Apps/Gluu/GluuViewWrapper' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' @@ -20,24 +19,11 @@ import { THEME_DARK } from '@/context/theme/constants' import { useStyles } from './styles/FidoFormPage.style' const fidoResourceId = ADMIN_UI_RESOURCES.FIDO -const fidoScopes = CEDAR_RESOURCE_SCOPES[fidoResourceId] const Fido: React.FC = () => { const { t } = useTranslation() - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() - const canReadFido = useMemo( - () => hasCedarReadPermission(fidoResourceId), - [hasCedarReadPermission], - ) - const canWriteFido = useMemo( - () => hasCedarWritePermission(fidoResourceId), - [hasCedarWritePermission], - ) - - useEffect(() => { - authorizeHelper(fidoScopes) - }, [authorizeHelper]) + const { canRead: canReadFido, canWrite: canWriteFido } = usePermission(fidoResourceId) const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( diff --git a/admin-ui/plugins/fido/components/Metrics/MetricsPage.tsx b/admin-ui/plugins/fido/components/Metrics/MetricsPage.tsx index 0cb18ac4d1..2a794bbda0 100644 --- a/admin-ui/plugins/fido/components/Metrics/MetricsPage.tsx +++ b/admin-ui/plugins/fido/components/Metrics/MetricsPage.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback, useEffect, useState } from 'react' +import React, { useMemo, useCallback, useState } from 'react' import { Row, Col, GluuPageContent } from 'Components' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' @@ -7,9 +7,8 @@ import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuTabs from 'Routes/Apps/Gluu/GluuTabs' import { GluuDatePicker } from '@/components/GluuDatePicker' import { GluuButton } from '@/components/GluuButton' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { FIDO_METRICS_READ } from '@/utils/PermChecker' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' @@ -25,7 +24,6 @@ import { useAdoptionMetrics, useErrorsAnalytics, usePerformanceAnalytics } from import type { MetricsDateRange } from './types' const METRICS_RESOURCE_ID = ADMIN_UI_RESOURCES.FIDO -const METRICS_SCOPES = [{ permission: FIDO_METRICS_READ, resourceId: METRICS_RESOURCE_ID }] const MetricsPage: React.FC = () => { const { t } = useTranslation() @@ -49,11 +47,7 @@ const MetricsPage: React.FC = () => { const isApplyEnabled = !!(startDate && endDate) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() - const canView = useMemo( - () => hasCedarReadPermission(METRICS_RESOURCE_ID), - [hasCedarReadPermission], - ) + const { canRead: canView } = usePermission(METRICS_RESOURCE_ID) const { isLoading: adoptionLoading, isFetching: adoptionFetching } = useAdoptionMetrics(appliedRange) @@ -69,10 +63,6 @@ const MetricsPage: React.FC = () => { errorsFetching || performanceFetching - useEffect(() => { - authorizeHelper(METRICS_SCOPES) - }, [authorizeHelper]) - const handleStartDateChange = useCallback((date: Dayjs | null) => { setStartDate(date ? date.millisecond(0) : null) }, []) diff --git a/admin-ui/plugins/fido/plugin-metadata.ts b/admin-ui/plugins/fido/plugin-metadata.ts index 51dac67099..b2c616c6c3 100644 --- a/admin-ui/plugins/fido/plugin-metadata.ts +++ b/admin-ui/plugins/fido/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { FIDO_READ, FIDO_WRITE, FIDO_METRICS_READ } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -16,13 +16,13 @@ const pluginMetadata = { { title: 'menus.configuration', path: ROUTES.FIDO_BASE, - permission: FIDO_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.FIDO, }, { title: 'menus.metrics', path: ROUTES.FIDO_METRICS, - permission: FIDO_METRICS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.FIDO, }, ], @@ -32,13 +32,13 @@ const pluginMetadata = { { component: Fido, path: ROUTES.FIDO_BASE, - permission: FIDO_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.FIDO, }, { component: MetricsPage, path: ROUTES.FIDO_METRICS, - permission: FIDO_METRICS_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.FIDO, }, ], diff --git a/admin-ui/plugins/jans-lock/__tests__/components/JansLockConfiguration.test.tsx b/admin-ui/plugins/jans-lock/__tests__/components/JansLockConfiguration.test.tsx index b010c0f405..c923ce620a 100644 --- a/admin-ui/plugins/jans-lock/__tests__/components/JansLockConfiguration.test.tsx +++ b/admin-ui/plugins/jans-lock/__tests__/components/JansLockConfiguration.test.tsx @@ -19,9 +19,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Lock: 'Lock', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Lock: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/jans-lock/__tests__/components/JansLockFieldRenderer.test.tsx b/admin-ui/plugins/jans-lock/__tests__/components/JansLockFieldRenderer.test.tsx index a4108d8fbf..6eaf1fb964 100644 --- a/admin-ui/plugins/jans-lock/__tests__/components/JansLockFieldRenderer.test.tsx +++ b/admin-ui/plugins/jans-lock/__tests__/components/JansLockFieldRenderer.test.tsx @@ -18,9 +18,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Lock: 'Lock' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Lock: [] }, })) diff --git a/admin-ui/plugins/jans-lock/components/JansLock.tsx b/admin-ui/plugins/jans-lock/components/JansLock.tsx index dd66193653..2e004e53b1 100644 --- a/admin-ui/plugins/jans-lock/components/JansLock.tsx +++ b/admin-ui/plugins/jans-lock/components/JansLock.tsx @@ -16,9 +16,8 @@ import { PatchOperation } from '../types' import SetTitle from 'Utils/SetTitle' import { updateToast } from 'Redux/features/toastSlice' import { getQueryErrorMessage } from '@/utils/errorHandler' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { triggerWebhookForFeature } from '@/utils/triggerWebhookForFeature' import { adminUiFeatures } from '@/constants' import { useTheme } from '@/context/theme/themeContext' @@ -27,28 +26,12 @@ import { THEME_DARK } from '@/context/theme/constants' import { useStyles } from './styles/JansLockFormPage.style' const lockResourceId = ADMIN_UI_RESOURCES.Lock -const lockScopes = CEDAR_RESOURCE_SCOPES[lockResourceId] const JansLock: React.FC = () => { const { t } = useTranslation() const dispatch = useAppDispatch() const queryClient = useQueryClient() - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() - - const canReadLock = useMemo( - () => hasCedarReadPermission(lockResourceId), - [hasCedarReadPermission], - ) - const canWriteLock = useMemo( - () => hasCedarWritePermission(lockResourceId), - [hasCedarWritePermission], - ) - - useEffect(() => { - if (lockScopes && lockScopes.length > 0) { - authorizeHelper(lockScopes) - } - }, [authorizeHelper]) + const { canRead: canReadLock, canWrite: canWriteLock } = usePermission(lockResourceId) const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( diff --git a/admin-ui/plugins/jans-lock/plugin-metadata.ts b/admin-ui/plugins/jans-lock/plugin-metadata.ts index e5434d919a..a14a818f55 100644 --- a/admin-ui/plugins/jans-lock/plugin-metadata.ts +++ b/admin-ui/plugins/jans-lock/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { JANS_LOCK_READ } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -11,7 +11,7 @@ const pluginMetadata = { title: 'titles.jans_lock', icon: 'jans_lock', path: ROUTES.JANS_LOCK_BASE, - permission: JANS_LOCK_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Lock, }, ], @@ -19,7 +19,7 @@ const pluginMetadata = { { component: JansLock, path: ROUTES.JANS_LOCK_BASE, - permission: JANS_LOCK_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Lock, }, ], diff --git a/admin-ui/plugins/saml/components/SamlConfigurationForm.tsx b/admin-ui/plugins/saml/components/SamlConfigurationForm.tsx index bb706190c6..a1d1ad3409 100644 --- a/admin-ui/plugins/saml/components/SamlConfigurationForm.tsx +++ b/admin-ui/plugins/saml/components/SamlConfigurationForm.tsx @@ -1,17 +1,16 @@ import { useFormik } from 'formik' -import React, { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react' +import React, { useState, useMemo, useCallback, memo, useRef } from 'react' import { Row, Col, Form, FormGroup, CustomInput } from 'Components' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' import GluuThemeFormFooter from 'Routes/Apps/Gluu/GluuThemeFormFooter' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' import GluuToggleRow from 'Routes/Apps/Gluu/GluuToggleRow' import { useTranslation } from 'react-i18next' import SetTitle from 'Utils/SetTitle' import { adminUiFeatures } from '@/constants' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useStyles } from './styles/SamlPage.style' import { samlConfigurationValidationSchema, transformToFormValues } from '../helper' import { updateToast } from 'Redux/features/toastSlice' @@ -24,7 +23,6 @@ const DOC_SECTION = 'samlConfiguration' as const const SamlConfigurationForm: React.FC = () => { const { t } = useTranslation() const dispatch = useAppDispatch() - const { authorizeHelper, hasCedarWritePermission } = useCedarling() const [modal, setModal] = useState(false) const { classes } = useStyles() @@ -34,16 +32,7 @@ const SamlConfigurationForm: React.FC = () => { SetTitle(t('titles.saml_management')) - const samlResourceId = useMemo(() => ADMIN_UI_RESOURCES.SAML, []) - const samlScopes = useMemo(() => CEDAR_RESOURCE_SCOPES[samlResourceId], [samlResourceId]) - const canWriteConfig = useMemo( - () => hasCedarWritePermission(samlResourceId), - [hasCedarWritePermission, samlResourceId], - ) - - useEffect(() => { - authorizeHelper(samlScopes) - }, [authorizeHelper, samlScopes]) + const { canWrite: canWriteConfig } = usePermission(ADMIN_UI_RESOURCES.SAML) const toggle = useCallback(() => { setModal((prev) => !prev) diff --git a/admin-ui/plugins/saml/components/WebsiteSsoIdentityBrokeringList.tsx b/admin-ui/plugins/saml/components/WebsiteSsoIdentityBrokeringList.tsx index 0c9ed9d1f6..eacd8f4576 100644 --- a/admin-ui/plugins/saml/components/WebsiteSsoIdentityBrokeringList.tsx +++ b/admin-ui/plugins/saml/components/WebsiteSsoIdentityBrokeringList.tsx @@ -4,8 +4,8 @@ import MaterialTable, { type Action } from '@material-table/core' import { useTranslation } from 'react-i18next' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { buildPayload } from 'Utils/PermChecker' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { buildPayload } from 'Utils/auditAction' +import { usePermission } from '@/cedarling/hooks/usePermission' import applicationStyle from '@/routes/Apps/Gluu/styles/applicationStyle' import { DeleteOutlined } from '@/components/icons' import GluuDialog from 'Routes/Apps/Gluu/GluuDialog' @@ -16,7 +16,6 @@ import customColors from '@/customColors' import getThemeColor from 'Context/theme/config' import { ThemeContext } from 'Context/theme/themeContext' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { PaperContainer, getIdentityProviderTableCols } from '../helper' import { useIdentityProviders, useDeleteIdentityProvider, type IdentityProvider } from './hooks' @@ -31,12 +30,6 @@ interface DeleteItem { const DeleteOutlinedIcon = () => const WebsiteSsoIdentityBrokeringList = React.memo(() => { - const { - authorizeHelper, - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - } = useCedarling() const theme = useContext(ThemeContext) const selectedTheme = theme?.state?.theme ?? DEFAULT_THEME const themeColors = getThemeColor(selectedTheme) @@ -73,24 +66,11 @@ const WebsiteSsoIdentityBrokeringList = React.memo(() => { const items = idpData?.entries ?? [] const totalItems = idpData?.totalEntriesCount ?? 0 - const samlResourceId = useMemo(() => ADMIN_UI_RESOURCES.SAML, []) - const samlScopes = useMemo(() => CEDAR_RESOURCE_SCOPES[samlResourceId], [samlResourceId]) - const canReadIdentities = useMemo( - () => hasCedarReadPermission(samlResourceId), - [hasCedarReadPermission, samlResourceId], - ) - const canWriteIdentities = useMemo( - () => hasCedarWritePermission(samlResourceId), - [hasCedarWritePermission, samlResourceId], - ) - const canDeleteIdentities = useMemo( - () => hasCedarDeletePermission(samlResourceId), - [hasCedarDeletePermission, samlResourceId], - ) - - useEffect(() => { - authorizeHelper(samlScopes) - }, [authorizeHelper, samlScopes]) + const { + canRead: canReadIdentities, + canWrite: canWriteIdentities, + canDelete: canDeleteIdentities, + } = usePermission(ADMIN_UI_RESOURCES.SAML) const debouncedSetPattern = useMemo( () => diff --git a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx index dc2a063831..2a9781f7d7 100644 --- a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx +++ b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useContext, useMemo, useState } from 'react' import MaterialTable, { type Action } from '@material-table/core' import { useTranslation } from 'react-i18next' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' -import { buildPayload, type UserAction } from 'Utils/PermChecker' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { buildPayload } from 'Utils/auditAction' +import type { UserAction } from 'Utils/types' +import { usePermission } from '@/cedarling/hooks/usePermission' import applicationStyle from '@/routes/Apps/Gluu/styles/applicationStyle' import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' @@ -14,7 +15,6 @@ import { adminUiFeatures } from '@/constants' import { PaperContainer, getServiceProviderTableCols } from '../helper' import customColors from '@/customColors' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { useTrustRelationships, @@ -32,7 +32,6 @@ interface DeleteItem { const DeleteOutlinedIcon = () => const WebsiteSsoServiceProviderList = React.memo(() => { - const { authorizeHelper, hasCedarReadPermission, hasCedarWritePermission } = useCedarling() const theme = useContext(ThemeContext) const selectedTheme = useMemo(() => theme?.state?.theme ?? DEFAULT_THEME, [theme?.state?.theme]) const themeColors = useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) @@ -54,20 +53,10 @@ const WebsiteSsoServiceProviderList = React.memo(() => { const isLoading = isInitialLoading || isFetchingData || deleteTrustRelationshipMutation.isPending - const samlResourceId = useMemo(() => ADMIN_UI_RESOURCES.SAML, []) - const samlScopes = useMemo(() => CEDAR_RESOURCE_SCOPES[samlResourceId], [samlResourceId]) - const canReadWebsiteSsoServiceProviders = useMemo( - () => hasCedarReadPermission(samlResourceId), - [hasCedarReadPermission, samlResourceId], - ) - const canWriteWebsiteSsoServiceProviders = useMemo( - () => hasCedarWritePermission(samlResourceId), - [hasCedarWritePermission, samlResourceId], - ) - - useEffect(() => { - authorizeHelper(samlScopes) - }, [authorizeHelper, samlScopes]) + const { + canRead: canReadWebsiteSsoServiceProviders, + canWrite: canWriteWebsiteSsoServiceProviders, + } = usePermission(ADMIN_UI_RESOURCES.SAML) const handleGoToEditPage = useCallback( (rowData: TrustRelationship, viewOnly?: boolean) => { diff --git a/admin-ui/plugins/saml/plugin-metadata.ts b/admin-ui/plugins/saml/plugin-metadata.ts index 8fb3c6c3e3..3c7e66faef 100644 --- a/admin-ui/plugins/saml/plugin-metadata.ts +++ b/admin-ui/plugins/saml/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { SAML_READ, SAML_WRITE, SAML_CONFIG_READ, SAML_TR_READ } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { createLazyRoute } from '@/utils/RouteLoader' const PLUGIN_BASE_APTH = '/saml' @@ -17,7 +17,7 @@ const pluginMetadata = { title: 'menus.saml', icon: 'saml', path: PLUGIN_BASE_APTH, - permission: SAML_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SAML, }, ], @@ -25,49 +25,49 @@ const pluginMetadata = { { component: SamlPage, path: PLUGIN_BASE_APTH, - permission: SAML_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: SamlPage, path: `${PLUGIN_BASE_APTH}/config`, - permission: SAML_CONFIG_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: SamlPage, path: `${PLUGIN_BASE_APTH}/service-providers`, - permission: SAML_TR_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: SamlPage, path: `${PLUGIN_BASE_APTH}/identity-providers`, - permission: SAML_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: WebsiteSsoIdentityProviderForm, path: `${PLUGIN_BASE_APTH}/identity-providers/edit`, - permission: SAML_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: WebsiteSsoIdentityProviderForm, path: `${PLUGIN_BASE_APTH}/identity-providers/add`, - permission: SAML_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: WebsiteSsoServiceProviderForm, path: `${PLUGIN_BASE_APTH}/service-providers/edit`, - permission: SAML_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SAML, }, { component: WebsiteSsoServiceProviderForm, path: `${PLUGIN_BASE_APTH}/service-providers/add`, - permission: SAML_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SAML, }, ], diff --git a/admin-ui/plugins/scim/__tests__/components/ScimPage.test.tsx b/admin-ui/plugins/scim/__tests__/components/ScimPage.test.tsx index 194d245163..a5ef34f065 100644 --- a/admin-ui/plugins/scim/__tests__/components/ScimPage.test.tsx +++ b/admin-ui/plugins/scim/__tests__/components/ScimPage.test.tsx @@ -20,9 +20,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { SCIM: 'SCIM' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { SCIM: [] }, })) diff --git a/admin-ui/plugins/scim/components/ScimPage.tsx b/admin-ui/plugins/scim/components/ScimPage.tsx index 645505703d..bf1e016cdc 100644 --- a/admin-ui/plugins/scim/components/ScimPage.tsx +++ b/admin-ui/plugins/scim/components/ScimPage.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useAppDispatch, useAppSelector } from '@/redux/hooks' import { useQueryClient } from '@tanstack/react-query' @@ -17,9 +17,8 @@ import { logAuditUserAction } from 'Utils/AuditLogger' import { PATCH } from '@/audit' import type { JsonPatch } from 'JansConfigApi' import type { JsonValue } from 'Routes/Apps/Gluu/types/common' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' @@ -27,7 +26,6 @@ import { resolveApiErrorMessage } from '@/utils/apiErrorMessage' import { useStyles } from './styles/ScimFormPage.style' const scimResourceId = ADMIN_UI_RESOURCES.SCIM -const scimScopes = CEDAR_RESOURCE_SCOPES[scimResourceId] const ScimPage: React.FC = () => { const { t } = useTranslation() @@ -36,21 +34,7 @@ const ScimPage: React.FC = () => { const userinfo = useAppSelector((state) => state.authReducer?.userinfo) const client_id = useAppSelector((state) => state.authReducer?.config?.clientId) SetTitle(t('titles.scim_management')) - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() - const canReadScim = useMemo( - () => hasCedarReadPermission(scimResourceId), - [hasCedarReadPermission, scimResourceId], - ) - const canWriteScim = useMemo( - () => hasCedarWritePermission(scimResourceId), - [hasCedarWritePermission, scimResourceId], - ) - - useEffect(() => { - if (scimScopes && scimScopes.length > 0) { - authorizeHelper(scimScopes) - } - }, [authorizeHelper, scimScopes]) + const { canRead: canReadScim, canWrite: canWriteScim } = usePermission(scimResourceId) const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( diff --git a/admin-ui/plugins/scim/plugin-metadata.ts b/admin-ui/plugins/scim/plugin-metadata.ts index 8f5e49ee31..51208bb975 100644 --- a/admin-ui/plugins/scim/plugin-metadata.ts +++ b/admin-ui/plugins/scim/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { SCIM_CONFIG_READ, SCIM_CONFIG_WRITE } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -11,7 +11,7 @@ const pluginMetadata = { title: 'menus.scim', icon: 'scim', path: ROUTES.SCIM_BASE, - permission: SCIM_CONFIG_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SCIM, }, ], @@ -19,7 +19,7 @@ const pluginMetadata = { { component: ScimPage, path: ROUTES.SCIM_BASE, - permission: SCIM_CONFIG_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SCIM, }, ], diff --git a/admin-ui/plugins/scripts/__tests__/components/CustomScriptAddPage.test.tsx b/admin-ui/plugins/scripts/__tests__/components/CustomScriptAddPage.test.tsx index 85053e3719..9684397079 100644 --- a/admin-ui/plugins/scripts/__tests__/components/CustomScriptAddPage.test.tsx +++ b/admin-ui/plugins/scripts/__tests__/components/CustomScriptAddPage.test.tsx @@ -32,9 +32,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Webhooks: 'Webhooks', Scripts: 'Scripts' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Webhooks: [], Scripts: [] }, })) diff --git a/admin-ui/plugins/scripts/__tests__/components/CustomScriptEditPage.test.tsx b/admin-ui/plugins/scripts/__tests__/components/CustomScriptEditPage.test.tsx index 8188ec34d5..874de5b776 100644 --- a/admin-ui/plugins/scripts/__tests__/components/CustomScriptEditPage.test.tsx +++ b/admin-ui/plugins/scripts/__tests__/components/CustomScriptEditPage.test.tsx @@ -36,9 +36,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Webhooks: 'Webhooks', Scripts: 'Scripts' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Webhooks: [], Scripts: [] }, })) diff --git a/admin-ui/plugins/scripts/__tests__/components/CustomScriptForm.test.tsx b/admin-ui/plugins/scripts/__tests__/components/CustomScriptForm.test.tsx index af60fabdfd..adf4c80cee 100644 --- a/admin-ui/plugins/scripts/__tests__/components/CustomScriptForm.test.tsx +++ b/admin-ui/plugins/scripts/__tests__/components/CustomScriptForm.test.tsx @@ -49,9 +49,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Webhooks: 'Webhooks', Scripts: 'Scripts' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Webhooks: [], Scripts: [] }, })) diff --git a/admin-ui/plugins/scripts/components/CustomScriptAddPage.tsx b/admin-ui/plugins/scripts/components/CustomScriptAddPage.tsx index cd20d9511f..f3fb941a64 100644 --- a/admin-ui/plugins/scripts/components/CustomScriptAddPage.tsx +++ b/admin-ui/plugins/scripts/components/CustomScriptAddPage.tsx @@ -8,7 +8,7 @@ import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import { updateToast } from 'Redux/features/toastSlice' @@ -33,11 +33,7 @@ const CustomScriptAddPage: React.FC = () => { ) const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarWritePermission } = useCedarling() - const canWrite = useMemo( - () => hasCedarWritePermission(scriptsResourceId), - [hasCedarWritePermission, scriptsResourceId], - ) + const { canWrite } = usePermission(scriptsResourceId) const createMutation = useCreateCustomScript() diff --git a/admin-ui/plugins/scripts/components/CustomScriptEditPage.tsx b/admin-ui/plugins/scripts/components/CustomScriptEditPage.tsx index b91b5f6a8e..4321d4340a 100644 --- a/admin-ui/plugins/scripts/components/CustomScriptEditPage.tsx +++ b/admin-ui/plugins/scripts/components/CustomScriptEditPage.tsx @@ -14,7 +14,7 @@ import { useCustomScript, useUpdateCustomScript, useMutationEffects } from './ho import CustomScriptForm from './CustomScriptForm' import { useStyles } from './styles/CustomScriptFormPage.style' import { updateToast } from 'Redux/features/toastSlice' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import { ROUTES } from '@/helpers/navigation' @@ -39,15 +39,7 @@ const CustomScriptEditPage: React.FC = () => { SetTitle(t('titles.edit_script', { defaultValue: 'Edit Custom Script' })) - const { hasCedarReadPermission, hasCedarWritePermission } = useCedarling() - const canRead = useMemo( - () => hasCedarReadPermission(scriptsResourceId), - [hasCedarReadPermission, scriptsResourceId], - ) - const canWrite = useMemo( - () => hasCedarWritePermission(scriptsResourceId), - [hasCedarWritePermission, scriptsResourceId], - ) + const { canRead, canWrite } = usePermission(scriptsResourceId) const viewMatch = useMatch(ROUTES.CUSTOM_SCRIPT_VIEW_TEMPLATE) diff --git a/admin-ui/plugins/scripts/components/CustomScriptListPage.tsx b/admin-ui/plugins/scripts/components/CustomScriptListPage.tsx index e7c916e5b0..33039e0043 100644 --- a/admin-ui/plugins/scripts/components/CustomScriptListPage.tsx +++ b/admin-ui/plugins/scripts/components/CustomScriptListPage.tsx @@ -13,8 +13,7 @@ import { ThemeContext } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import SetTitle from 'Utils/SetTitle' -import { useCedarling } from '@/cedarling/hooks/useCedarling' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import { devLogger } from '@/utils/devLogger' import { updateToast } from 'Redux/features/toastSlice' @@ -40,7 +39,6 @@ import { const DELETE_SUBJECT_SCRIPT = 'script' const EMPTY_DESCRIPTION_PLACEHOLDER = '—' const scriptsResourceId = ADMIN_UI_RESOURCES.Scripts -const scriptScopes = CEDAR_RESOURCE_SCOPES[scriptsResourceId] ?? [] const LIMIT_OPTIONS = getRowsPerPageOptions() @@ -55,12 +53,7 @@ const CustomScriptListPage: React.FC = () => { const { t } = useTranslation() const dispatch = useAppDispatch() const { navigateToRoute } = useAppNavigation() - const { - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - authorizeHelper, - } = useCedarling() + const { canRead, canWrite, canDelete } = usePermission(scriptsResourceId) const theme = useContext(ThemeContext) const { themeColors, isDarkTheme } = useMemo(() => { @@ -80,20 +73,6 @@ const CustomScriptListPage: React.FC = () => { const [modal, setModal] = useState(false) const [itemToDelete, setItemToDelete] = useState(null) - const canRead = useMemo(() => hasCedarReadPermission(scriptsResourceId), [hasCedarReadPermission]) - const canWrite = useMemo( - () => hasCedarWritePermission(scriptsResourceId), - [hasCedarWritePermission], - ) - const canDelete = useMemo( - () => hasCedarDeletePermission(scriptsResourceId), - [hasCedarDeletePermission], - ) - - useEffect(() => { - authorizeHelper(scriptScopes) - }, [authorizeHelper]) - const { data: scriptsResponse, isLoading, diff --git a/admin-ui/plugins/scripts/plugin-metadata.ts b/admin-ui/plugins/scripts/plugin-metadata.ts index 2f7cd4ae9a..6a27f0b894 100644 --- a/admin-ui/plugins/scripts/plugin-metadata.ts +++ b/admin-ui/plugins/scripts/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { SCRIPT_READ, SCRIPT_WRITE } from '../../app/utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -13,7 +13,7 @@ const pluginMetadata = { title: 'menus.scripts', icon: 'scripts', path: ROUTES.CUSTOM_SCRIPT_LIST, - permission: SCRIPT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Scripts, }, ], @@ -21,25 +21,25 @@ const pluginMetadata = { { component: CustomScriptListPage, path: ROUTES.CUSTOM_SCRIPT_LIST, - permission: SCRIPT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Scripts, }, { component: CustomScriptAddPage, path: ROUTES.CUSTOM_SCRIPT_ADD, - permission: SCRIPT_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Scripts, }, { component: CustomScriptEditPage, path: ROUTES.CUSTOM_SCRIPT_VIEW_TEMPLATE, - permission: SCRIPT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Scripts, }, { component: CustomScriptEditPage, path: ROUTES.CUSTOM_SCRIPT_EDIT_TEMPLATE, - permission: SCRIPT_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Scripts, }, ], diff --git a/admin-ui/plugins/services/Components/CachePage.tsx b/admin-ui/plugins/services/Components/CachePage.tsx index 246eca34fd..e8f12ec118 100644 --- a/admin-ui/plugins/services/Components/CachePage.tsx +++ b/admin-ui/plugins/services/Components/CachePage.tsx @@ -18,9 +18,8 @@ import SetTitle from 'Utils/SetTitle' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useQueryClient } from '@tanstack/react-query' import { useAppDispatch } from '@/redux/hooks' import { updateToast } from 'Redux/features/toastSlice' @@ -72,28 +71,11 @@ const CachePage: React.FC = () => { const [modal, setModal] = useState(false) - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() - const cacheResourceId = useMemo(() => ADMIN_UI_RESOURCES.Cache, []) - const cacheScopes = useMemo(() => CEDAR_RESOURCE_SCOPES[cacheResourceId] || [], [cacheResourceId]) - - const canReadCache = useMemo( - () => hasCedarReadPermission(cacheResourceId), - [hasCedarReadPermission, cacheResourceId], - ) - const canWriteCache = useMemo( - () => hasCedarWritePermission(cacheResourceId), - [hasCedarWritePermission, cacheResourceId], - ) + const { canRead: canReadCache, canWrite: canWriteCache } = usePermission(ADMIN_UI_RESOURCES.Cache) const pageTitle = t('fields.cache_configuration') SetTitle(pageTitle) - useEffect(() => { - if (cacheScopes.length > 0) { - authorizeHelper(cacheScopes) - } - }, [authorizeHelper, cacheScopes]) - const { data: cacheData = {} as CacheConfiguration, isLoading: cacheLoading, diff --git a/admin-ui/plugins/services/Components/PersistenceDetail.tsx b/admin-ui/plugins/services/Components/PersistenceDetail.tsx index 5e0e0da3f5..5f3e10ecf7 100644 --- a/admin-ui/plugins/services/Components/PersistenceDetail.tsx +++ b/admin-ui/plugins/services/Components/PersistenceDetail.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' @@ -8,9 +8,8 @@ import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuThemeFormFooter from '@/routes/Apps/Gluu/GluuThemeFormFooter' import { GluuPageContent } from 'Components' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useGetPropertiesPersistence } from 'JansConfigApi' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import { useStyles } from './styles/PersistenceDetail.style' @@ -24,24 +23,10 @@ const PersistenceDetail = () => { const themeColors = useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission, authorizeHelper } = useCedarling() - - const persistenceResourceId = useMemo(() => ADMIN_UI_RESOURCES.Persistence, []) - const persistenceScopes = useMemo( - () => CEDAR_RESOURCE_SCOPES[persistenceResourceId], - [persistenceResourceId], - ) - const canReadPersistence = useMemo( - () => hasCedarReadPermission(persistenceResourceId), - [hasCedarReadPermission, persistenceResourceId], - ) + const { canRead: canReadPersistence } = usePermission(ADMIN_UI_RESOURCES.Persistence) SetTitle(t('menus.persistence')) - useEffect(() => { - authorizeHelper(persistenceScopes) - }, [authorizeHelper, persistenceScopes]) - const { data: persistenceData, isLoading } = useGetPropertiesPersistence({ query: { staleTime: queryDefaults.queryOptions.staleTime, enabled: canReadPersistence }, }) diff --git a/admin-ui/plugins/services/__tests__/components/PersistenceDetail.test.tsx b/admin-ui/plugins/services/__tests__/components/PersistenceDetail.test.tsx index 58e97137b5..fc813f85ac 100644 --- a/admin-ui/plugins/services/__tests__/components/PersistenceDetail.test.tsx +++ b/admin-ui/plugins/services/__tests__/components/PersistenceDetail.test.tsx @@ -16,9 +16,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { Persistence: 'Persistence' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Persistence: [] }, })) diff --git a/admin-ui/plugins/services/plugin-metadata.ts b/admin-ui/plugins/services/plugin-metadata.ts index 16849c972e..1562fa3b3d 100644 --- a/admin-ui/plugins/services/plugin-metadata.ts +++ b/admin-ui/plugins/services/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { CACHE_READ, PERSISTENCE_DETAIL } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -15,13 +15,13 @@ const pluginMetadata = { { title: 'menus.cache', path: ROUTES.SERVICES_CACHE, - permission: CACHE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Cache, }, { title: 'menus.persistence', path: ROUTES.SERVICES_PERSISTENCE, - permission: PERSISTENCE_DETAIL, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Persistence, }, ], @@ -31,13 +31,13 @@ const pluginMetadata = { { component: CachePage, path: ROUTES.SERVICES_CACHE, - permission: CACHE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Cache, }, { component: PersistenceDetail, path: ROUTES.SERVICES_PERSISTENCE, - permission: PERSISTENCE_DETAIL, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Persistence, }, ], diff --git a/admin-ui/plugins/smtp/__tests__/components/SmtpEditPage.test.tsx b/admin-ui/plugins/smtp/__tests__/components/SmtpEditPage.test.tsx index 897b6b91e3..c85ac3c1e8 100644 --- a/admin-ui/plugins/smtp/__tests__/components/SmtpEditPage.test.tsx +++ b/admin-ui/plugins/smtp/__tests__/components/SmtpEditPage.test.tsx @@ -22,9 +22,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { SMTP: 'SMTP', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { SMTP: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/smtp/__tests__/components/SmtpForm.test.tsx b/admin-ui/plugins/smtp/__tests__/components/SmtpForm.test.tsx index feb5352f42..574a252d87 100644 --- a/admin-ui/plugins/smtp/__tests__/components/SmtpForm.test.tsx +++ b/admin-ui/plugins/smtp/__tests__/components/SmtpForm.test.tsx @@ -22,9 +22,6 @@ jest.mock('@/cedarling', () => ({ jest.mock('@/cedarling/utility', () => ({ ADMIN_UI_RESOURCES: { SMTP: 'SMTP', Webhooks: 'Webhooks' }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { SMTP: ['read', 'write', 'delete'], Webhooks: ['read', 'write', 'delete'], diff --git a/admin-ui/plugins/smtp/components/SmtpEditPage.tsx b/admin-ui/plugins/smtp/components/SmtpEditPage.tsx index 6ecb96efcb..676d59ea81 100644 --- a/admin-ui/plugins/smtp/components/SmtpEditPage.tsx +++ b/admin-ui/plugins/smtp/components/SmtpEditPage.tsx @@ -29,14 +29,12 @@ import { triggerWebhookForFeature } from '@/utils/triggerWebhookForFeature' import { adminUiFeatures } from '@/constants' import type { JsonValue } from 'Routes/Apps/Gluu/types/common' import type { SmtpFormValues, ApiError, PatchOp } from 'Plugins/smtp/types' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { useStyles } from './styles/SmtpFormPage.style' const API_SMTP = 'api-smtp-configuration' const smtpResourceId = ADMIN_UI_RESOURCES.SMTP -const smtpScopes = CEDAR_RESOURCE_SCOPES[smtpResourceId] const buildPatches = ( originalConfig: Partial | undefined, @@ -76,7 +74,7 @@ const SmtpEditPage = () => { isError: isSmtpError, error: smtpError, } = useGetConfigSmtp() - const { hasCedarReadPermission, hasCedarWritePermission, authorizeHelper } = useCedarling() + const { canRead: canReadSmtp, canWrite: canWriteSmtp } = usePermission(smtpResourceId) const { state: themeState } = useTheme() const { themeColors, isDark } = useMemo( () => ({ @@ -87,21 +85,6 @@ const SmtpEditPage = () => { ) const { classes } = useStyles({ isDark, themeColors }) - const canReadSmtp = useMemo( - () => hasCedarReadPermission(smtpResourceId), - [hasCedarReadPermission], - ) - const canWriteSmtp = useMemo( - () => hasCedarWritePermission(smtpResourceId), - [hasCedarWritePermission], - ) - - useEffect(() => { - if (smtpScopes.length > 0) { - authorizeHelper(smtpScopes) - } - }, [authorizeHelper]) - useEffect(() => { if (!isSmtpError) return const errorMsg = getQueryErrorMessage(smtpError, t('messages.error_in_loading')) diff --git a/admin-ui/plugins/smtp/plugin-metadata.ts b/admin-ui/plugins/smtp/plugin-metadata.ts index 3b867e972a..c929bb6854 100644 --- a/admin-ui/plugins/smtp/plugin-metadata.ts +++ b/admin-ui/plugins/smtp/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { SMTP_READ, SMTP_WRITE } from '../../app/utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -11,7 +11,7 @@ const pluginMetadata = { title: 'menus.smtp', icon: 'smtpmanagement', path: ROUTES.SMTP_BASE, - permission: SMTP_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.SMTP, }, ], @@ -19,7 +19,7 @@ const pluginMetadata = { { component: SmtpEditPage, path: ROUTES.SMTP_BASE, - permission: SMTP_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SMTP, }, ], diff --git a/admin-ui/plugins/user-claims/__tests__/cedarTestHelpers.ts b/admin-ui/plugins/user-claims/__tests__/cedarTestHelpers.ts index 46096995db..374aa349c2 100644 --- a/admin-ui/plugins/user-claims/__tests__/cedarTestHelpers.ts +++ b/admin-ui/plugins/user-claims/__tests__/cedarTestHelpers.ts @@ -11,20 +11,3 @@ export const CEDAR_RESOURCE_SCOPES = { Webhooks: [] as ResourceScopeEntry[], Lock: [] as ResourceScopeEntry[], } as const - -export const CEDAR_RESOURCE_SCOPES_WITH_PERMISSIONS = { - Attributes: [ - { permission: 'read', resourceId: ADMIN_UI_RESOURCES.Attributes }, - { permission: 'write', resourceId: ADMIN_UI_RESOURCES.Attributes }, - { permission: 'delete', resourceId: ADMIN_UI_RESOURCES.Attributes }, - ] as ResourceScopeEntry[], - Webhooks: [ - { permission: 'read', resourceId: ADMIN_UI_RESOURCES.Webhooks }, - { permission: 'write', resourceId: ADMIN_UI_RESOURCES.Webhooks }, - { permission: 'delete', resourceId: ADMIN_UI_RESOURCES.Webhooks }, - ] as ResourceScopeEntry[], - Lock: [ - { permission: 'read', resourceId: ADMIN_UI_RESOURCES.Lock }, - { permission: 'write', resourceId: ADMIN_UI_RESOURCES.Lock }, - ] as ResourceScopeEntry[], -} as const diff --git a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsAddPage.test.tsx b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsAddPage.test.tsx index d05e69432c..aa85cece96 100644 --- a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsAddPage.test.tsx +++ b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsAddPage.test.tsx @@ -24,13 +24,8 @@ jest.mock('@/cedarling', () => { }) jest.mock('@/cedarling/utility', () => { - const { ADMIN_UI_RESOURCES } = jest.requireActual('../cedarTestHelpers') - return { ADMIN_UI_RESOURCES: ADMIN_UI_RESOURCES } -}) - -jest.mock('@/cedarling/constants/resourceScopes', () => { - const { CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') - return { CEDAR_RESOURCE_SCOPES: CEDAR_RESOURCE_SCOPES } + const { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') + return { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } }) jest.mock('Plugins/user-claims/hooks', () => ({ diff --git a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsEditPage.test.tsx b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsEditPage.test.tsx index c7c43abccf..df34879f8f 100644 --- a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsEditPage.test.tsx +++ b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsEditPage.test.tsx @@ -43,13 +43,8 @@ jest.mock('@/cedarling', () => { }) jest.mock('@/cedarling/utility', () => { - const { ADMIN_UI_RESOURCES } = jest.requireActual('../cedarTestHelpers') - return { ADMIN_UI_RESOURCES: ADMIN_UI_RESOURCES } -}) - -jest.mock('@/cedarling/constants/resourceScopes', () => { - const { CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') - return { CEDAR_RESOURCE_SCOPES: CEDAR_RESOURCE_SCOPES } + const { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') + return { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } }) jest.mock('react-router-dom', () => ({ diff --git a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsForm.test.tsx b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsForm.test.tsx index 898c70fa49..f02baf499a 100644 --- a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsForm.test.tsx +++ b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsForm.test.tsx @@ -63,13 +63,8 @@ jest.mock('@/cedarling', () => { }) jest.mock('@/cedarling/utility', () => { - const { ADMIN_UI_RESOURCES } = jest.requireActual('../cedarTestHelpers') - return { ADMIN_UI_RESOURCES: ADMIN_UI_RESOURCES } -}) - -jest.mock('@/cedarling/constants/resourceScopes', () => { - const { CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') - return { CEDAR_RESOURCE_SCOPES: CEDAR_RESOURCE_SCOPES } + const { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') + return { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } }) jest.mock('react-router-dom', () => ({ diff --git a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsListPage.test.tsx b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsListPage.test.tsx index 108d153ed8..335c0b4126 100644 --- a/admin-ui/plugins/user-claims/__tests__/components/UserClaimsListPage.test.tsx +++ b/admin-ui/plugins/user-claims/__tests__/components/UserClaimsListPage.test.tsx @@ -25,13 +25,8 @@ jest.mock('@/cedarling', () => { }) jest.mock('@/cedarling/utility', () => { - const { ADMIN_UI_RESOURCES } = jest.requireActual('../cedarTestHelpers') - return { ADMIN_UI_RESOURCES: ADMIN_UI_RESOURCES } -}) - -jest.mock('@/cedarling/constants/resourceScopes', () => { - const { CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') - return { CEDAR_RESOURCE_SCOPES: CEDAR_RESOURCE_SCOPES } + const { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } = jest.requireActual('../cedarTestHelpers') + return { ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES } }) jest.mock('JansConfigApi', () => ({ diff --git a/admin-ui/plugins/user-claims/components/UserClaimsAddPage.tsx b/admin-ui/plugins/user-claims/components/UserClaimsAddPage.tsx index 0868723aac..6e919a2921 100644 --- a/admin-ui/plugins/user-claims/components/UserClaimsAddPage.tsx +++ b/admin-ui/plugins/user-claims/components/UserClaimsAddPage.tsx @@ -6,7 +6,7 @@ import { THEME_DARK } from '@/context/theme/constants' import { GluuPageContent } from '@/components' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import UserClaimsForm from 'Plugins/user-claims/components/UserClaimsForm' @@ -30,11 +30,7 @@ const UserClaimsAddPage = (): JSX.Element => { ) const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarWritePermission } = useCedarling() - const canWrite = useMemo( - () => hasCedarWritePermission(attributeResourceId), - [hasCedarWritePermission], - ) + const { canWrite } = usePermission(attributeResourceId) SetTitle(t('fields.add_attribute', { defaultValue: 'Add User Claim' })) diff --git a/admin-ui/plugins/user-claims/components/UserClaimsEditPage.tsx b/admin-ui/plugins/user-claims/components/UserClaimsEditPage.tsx index c8d0cd24c1..85b2316ab1 100644 --- a/admin-ui/plugins/user-claims/components/UserClaimsEditPage.tsx +++ b/admin-ui/plugins/user-claims/components/UserClaimsEditPage.tsx @@ -9,7 +9,7 @@ import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import { Alert } from '@mui/material' import GluuText from 'Routes/Apps/Gluu/GluuText' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' import SetTitle from 'Utils/SetTitle' import UserClaimsForm from 'Plugins/user-claims/components/UserClaimsForm' @@ -38,11 +38,7 @@ const UserClaimsEditPage = (): JSX.Element => { ) const { classes } = useStyles({ isDark, themeColors }) - const { hasCedarReadPermission } = useCedarling() - const canRead = useMemo( - () => hasCedarReadPermission(attributeResourceId), - [hasCedarReadPermission], - ) + const { canRead } = usePermission(attributeResourceId) SetTitle(t('titles.edit_attribute', { defaultValue: 'Edit User Claim' })) diff --git a/admin-ui/plugins/user-claims/components/UserClaimsListPage.tsx b/admin-ui/plugins/user-claims/components/UserClaimsListPage.tsx index 96b85ae3b3..986ecb6491 100644 --- a/admin-ui/plugins/user-claims/components/UserClaimsListPage.tsx +++ b/admin-ui/plugins/user-claims/components/UserClaimsListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext, useCallback, useMemo, memo } from 'react' +import React, { useState, useContext, useCallback, useMemo, memo } from 'react' import { Add, DeleteOutlined, Edit, VisibilityOutlined } from '@/components/icons' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { GluuBadge } from '@/components/GluuBadge' @@ -13,9 +13,8 @@ import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import SetTitle from 'Utils/SetTitle' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { adminUiFeatures } from '@/constants' import { getRowsPerPageOptions, usePaginationState } from '@/utils/pagingUtils' import { useQueryClient } from '@tanstack/react-query' @@ -34,19 +33,13 @@ const displayOrDash = (value: DisplayValue): string => value === null || value === undefined || value === '' ? '—' : String(value) const attributeResourceId = ADMIN_UI_RESOURCES.Attributes -const attributeScopes = CEDAR_RESOURCE_SCOPES[attributeResourceId] ?? [] const UserClaimsListPage: React.FC = () => { const { t } = useTranslation() const queryClient = useQueryClient() const { navigateToRoute } = useAppNavigation() - const { - authorizeHelper, - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - } = useCedarling() + const { canRead, canWrite, canDelete } = usePermission(attributeResourceId) const theme = useContext(ThemeContext) const { themeColors, isDarkTheme } = useMemo(() => { @@ -71,25 +64,6 @@ const UserClaimsListPage: React.FC = () => { const [modal, setModal] = useState(false) const [itemToDelete, setItemToDelete] = useState(null) - const canRead = useMemo( - () => hasCedarReadPermission(attributeResourceId), - [hasCedarReadPermission], - ) - const canWrite = useMemo( - () => hasCedarWritePermission(attributeResourceId), - [hasCedarWritePermission], - ) - const canDelete = useMemo( - () => hasCedarDeletePermission(attributeResourceId), - [hasCedarDeletePermission], - ) - - useEffect(() => { - if (attributeScopes.length > 0) { - authorizeHelper(attributeScopes) - } - }, [authorizeHelper]) - const startIndex = useMemo(() => pageNumber * limit, [pageNumber, limit]) const { diff --git a/admin-ui/plugins/user-claims/components/UserClaimsViewPage.tsx b/admin-ui/plugins/user-claims/components/UserClaimsViewPage.tsx index 1f011b4abf..17e738f14f 100644 --- a/admin-ui/plugins/user-claims/components/UserClaimsViewPage.tsx +++ b/admin-ui/plugins/user-claims/components/UserClaimsViewPage.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect } from 'react' +import { useMemo } from 'react' import { useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useTheme } from '@/context/theme/themeContext' @@ -9,9 +9,8 @@ import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import { Alert } from '@mui/material' import GluuText from 'Routes/Apps/Gluu/GluuText' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import SetTitle from 'Utils/SetTitle' import UserClaimsForm from 'Plugins/user-claims/components/UserClaimsForm' import { useStyles } from './styles/UserClaimsFormPage.style' @@ -24,7 +23,6 @@ import type { AttributeItem } from './types' import type { JansAttribute } from 'JansConfigApi' const attributeResourceId = ADMIN_UI_RESOURCES.Attributes -const attributeScopes = CEDAR_RESOURCE_SCOPES[attributeResourceId] ?? [] const UserClaimsViewPage = (): JSX.Element => { const { gid } = useParams<{ gid: string }>() @@ -40,18 +38,7 @@ const UserClaimsViewPage = (): JSX.Element => { ) const { classes } = useStyles({ isDark, themeColors }) - const { authorizeHelper, hasCedarReadPermission } = useCedarling() - - useEffect(() => { - if (attributeScopes.length > 0) { - authorizeHelper(attributeScopes) - } - }, [authorizeHelper]) - - const canRead = useMemo( - () => hasCedarReadPermission(attributeResourceId), - [hasCedarReadPermission], - ) + const { canRead } = usePermission(attributeResourceId) SetTitle(t('titles.view_attribute', { defaultValue: 'View User Claim' })) diff --git a/admin-ui/plugins/user-claims/plugin-metadata.ts b/admin-ui/plugins/user-claims/plugin-metadata.ts index 50e0da34fd..b53d259dd2 100644 --- a/admin-ui/plugins/user-claims/plugin-metadata.ts +++ b/admin-ui/plugins/user-claims/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { ATTRIBUTE_READ, ATTRIBUTE_WRITE } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -14,7 +14,7 @@ const pluginMetadata = { title: 'menus.user_claims', icon: 'user_claims', path: ROUTES.ATTRIBUTES_LIST, - permission: ATTRIBUTE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Attributes, }, ], @@ -22,25 +22,25 @@ const pluginMetadata = { { component: UserClaimsEditPage, path: ROUTES.ATTRIBUTE_EDIT_TEMPLATE, - permission: ATTRIBUTE_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Attributes, }, { component: UserClaimsViewPage, path: ROUTES.ATTRIBUTE_VIEW_TEMPLATE, - permission: ATTRIBUTE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Attributes, }, { component: UserClaimsAddPage, path: ROUTES.ATTRIBUTE_ADD, - permission: ATTRIBUTE_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Attributes, }, { component: UserClaimsListPage, path: ROUTES.ATTRIBUTES_LIST, - permission: ATTRIBUTE_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Attributes, }, ], diff --git a/admin-ui/plugins/user-management/__tests__/helpers/userManagementTestUtils.tsx b/admin-ui/plugins/user-management/__tests__/helpers/userManagementTestUtils.tsx index 4de8238e00..e066f9cea9 100644 --- a/admin-ui/plugins/user-management/__tests__/helpers/userManagementTestUtils.tsx +++ b/admin-ui/plugins/user-management/__tests__/helpers/userManagementTestUtils.tsx @@ -32,9 +32,6 @@ jest.mock('@/cedarling/utility', () => ({ Attributes: 'Attributes', Webhooks: 'Webhooks', }, -})) - -jest.mock('@/cedarling/constants/resourceScopes', () => ({ CEDAR_RESOURCE_SCOPES: { Users: [], Lock: [], Attributes: [], Webhooks: [] }, })) diff --git a/admin-ui/plugins/user-management/components/UserList.tsx b/admin-ui/plugins/user-management/components/UserList.tsx index 3fe97fcdf2..f3f8046115 100644 --- a/admin-ui/plugins/user-management/components/UserList.tsx +++ b/admin-ui/plugins/user-management/components/UserList.tsx @@ -5,7 +5,7 @@ import User2FADevicesModal from './User2FADevicesModal' import { useTranslation } from 'react-i18next' import GluuViewWrapper from 'Routes/Apps/Gluu/GluuViewWrapper' import { useAppNavigation, ROUTES } from '@/helpers/navigation' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +import { usePermission } from '@/cedarling/hooks/usePermission' import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog' import SetTitle from 'Utils/SetTitle' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' @@ -21,7 +21,6 @@ import { UserTableRowData, CustomUser } from '../types' import { useDeleteUserWithAudit } from '../hooks/useUserMutations' import { adminUiFeatures } from '@/constants' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import { GluuTable } from '@/components/GluuTable' import { GluuSearchToolbar } from '@/components/GluuSearchToolbar' @@ -31,15 +30,13 @@ import { invalidateQueriesByKey } from '@/utils/queryUtils' import { useStyles } from './UserListPage.style' const usersResourceId = ADMIN_UI_RESOURCES.Users -const usersScopes = CEDAR_RESOURCE_SCOPES[usersResourceId] const UserList = (): JSX.Element => { const { - authorizeHelper, - hasCedarReadPermission, - hasCedarWritePermission, - hasCedarDeletePermission, - } = useCedarling() + canRead: canReadUsers, + canWrite: canWriteUsers, + canDelete: canDeleteUsers, + } = usePermission(usersResourceId) const queryClient = useQueryClient() const { t } = useTranslation() @@ -54,24 +51,6 @@ const UserList = (): JSX.Element => { const LIMIT_OPTIONS = useMemo(() => getRowsPerPageOptions(), []) // React Query hooks for data fetching - const canReadUsers = useMemo( - () => hasCedarReadPermission(usersResourceId), - [hasCedarReadPermission], - ) - const canWriteUsers = useMemo( - () => hasCedarWritePermission(usersResourceId), - [hasCedarWritePermission], - ) - const canDeleteUsers = useMemo( - () => hasCedarDeletePermission(usersResourceId), - [hasCedarDeletePermission], - ) - - useEffect(() => { - if (usersScopes.length > 0) { - authorizeHelper(usersScopes) - } - }, [authorizeHelper]) const { data: usersData, diff --git a/admin-ui/plugins/user-management/plugin-metadata.ts b/admin-ui/plugins/user-management/plugin-metadata.ts index b01084eead..c88edbb111 100644 --- a/admin-ui/plugins/user-management/plugin-metadata.ts +++ b/admin-ui/plugins/user-management/plugin-metadata.ts @@ -1,5 +1,5 @@ -import { USER_READ, USER_WRITE } from 'Utils/PermChecker' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' +import { CEDAR_ACTIONS } from '@/cedarling/constants' import { ROUTES } from '@/helpers/navigation' import { createLazyRoute } from '@/utils/RouteLoader' @@ -13,7 +13,7 @@ const pluginMetadata = { title: 'menus.users', icon: 'usersmanagement', path: ROUTES.USER_MANAGEMENT, - permission: USER_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Users, }, ], @@ -21,19 +21,19 @@ const pluginMetadata = { { component: UserList, path: ROUTES.USER_MANAGEMENT, - permission: USER_READ, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.Users, }, { component: UserAddPage, path: ROUTES.USER_ADD, - permission: USER_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Users, }, { component: UserEditPage, path: ROUTES.USER_EDIT_TEMPLATE, - permission: USER_WRITE, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Users, }, ], From 749e9d5c477d0f5c41144e80da86cafeaa660962 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Tue, 9 Jun 2026 16:50:57 +0500 Subject: [PATCH 02/24] coderabbit fixes Signed-off-by: faisalsiddique4400 --- .../components/Assets/JansAssetAddPage.tsx | 4 +-- .../components/Assets/JansAssetEditPage.tsx | 4 +-- .../components/Webhook/WebhookAddPage.tsx | 4 +-- .../components/Webhook/WebhookEditPage.tsx | 4 +-- admin-ui/plugins/admin/helper/settings.ts | 31 +++++++++-------- admin-ui/plugins/admin/plugin-metadata.ts | 2 ++ .../Authentication/Aliases/Aliases.tsx | 33 ++++++++++++++----- .../Aliases/__tests__/Aliases.test.tsx | 9 ++++- .../components/Ssa/components/SsaListPage.tsx | 2 +- .../plugins/auth-server/plugin-metadata.tsx | 4 +-- .../components/Metrics/MetricsPage.test.tsx | 2 ++ admin-ui/plugins/fido/plugin-metadata.ts | 2 +- .../WebsiteSsoServiceProviderList.tsx | 6 ++-- admin-ui/plugins/scim/plugin-metadata.ts | 2 +- admin-ui/plugins/scripts/plugin-metadata.ts | 2 +- admin-ui/plugins/smtp/plugin-metadata.ts | 2 +- 16 files changed, 73 insertions(+), 40 deletions(-) diff --git a/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx b/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx index 668d92021f..953f59e28c 100644 --- a/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx +++ b/admin-ui/plugins/admin/components/Assets/JansAssetAddPage.tsx @@ -23,11 +23,11 @@ const JansAssetAddPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { canRead: canReadAssets } = usePermission(assetResourceId) + const { canWrite: canWriteAssets } = usePermission(assetResourceId) return ( - +
diff --git a/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx b/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx index e0413b5426..58118531dc 100644 --- a/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx +++ b/admin-ui/plugins/admin/components/Assets/JansAssetEditPage.tsx @@ -23,11 +23,11 @@ const JansAssetEditPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { canRead: canReadAssets } = usePermission(assetResourceId) + const { canWrite: canWriteAssets } = usePermission(assetResourceId) return ( - +
diff --git a/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx b/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx index 194b0833c7..464ff65f9f 100644 --- a/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx +++ b/admin-ui/plugins/admin/components/Webhook/WebhookAddPage.tsx @@ -20,13 +20,13 @@ const WebhookAddPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { canRead: canReadWebhooks } = usePermission(webhookResourceId) + const { canWrite: canWriteWebhooks } = usePermission(webhookResourceId) SetTitle(t('messages.add_webhook', { defaultValue: 'Add Webhook' })) return ( - +
diff --git a/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx b/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx index 1b22ded79a..f592952b62 100644 --- a/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx +++ b/admin-ui/plugins/admin/components/Webhook/WebhookEditPage.tsx @@ -20,13 +20,13 @@ const WebhookEditPage: React.FC = () => { const isDark = themeState.theme === THEME_DARK const { classes } = useStyles({ isDark, themeColors }) - const { canRead: canReadWebhooks } = usePermission(webhookResourceId) + const { canWrite: canWriteWebhooks } = usePermission(webhookResourceId) SetTitle(t('titles.edit_webhook', { defaultValue: 'Edit Webhook' })) return ( - +
diff --git a/admin-ui/plugins/admin/helper/settings.ts b/admin-ui/plugins/admin/helper/settings.ts index 85a9c503f9..4ee221d02e 100644 --- a/admin-ui/plugins/admin/helper/settings.ts +++ b/admin-ui/plugins/admin/helper/settings.ts @@ -1,5 +1,4 @@ import { CEDARLING_LOG_TYPE } from '@/cedarling/constants' -import type { CedarlingLogType } from '@/cedarling/types' import type { AppConfigResponse, KeyValuePair } from 'JansConfigApi' import type { SettingsFormValues } from './types' @@ -22,15 +21,21 @@ const sanitizeAdditionalParameters = (params?: KeyValuePair[] | null): KeyValueP export const buildSettingsInitialValues = ( configData?: AppConfigResponse | null, -): SettingsFormValues => ({ - sessionTimeoutInMins: configData?.sessionTimeoutInMins ?? '', - acrValues: configData?.acrValues ?? '', - cedarlingLogType: (configData?.cedarlingLogType as CedarlingLogType) ?? CEDARLING_LOG_TYPE.OFF, - additionalParameters: sanitizeAdditionalParameters(configData?.additionalParameters).map( - (param) => ({ - id: crypto.randomUUID(), - key: param.key || '', - value: param.value || '', - }), - ), -}) +): SettingsFormValues => { + const cedarlingLogType = configData?.cedarlingLogType + return { + sessionTimeoutInMins: configData?.sessionTimeoutInMins ?? '', + acrValues: configData?.acrValues ?? '', + cedarlingLogType: + cedarlingLogType === CEDARLING_LOG_TYPE.OFF || cedarlingLogType === CEDARLING_LOG_TYPE.STD_OUT + ? cedarlingLogType + : CEDARLING_LOG_TYPE.OFF, + additionalParameters: sanitizeAdditionalParameters(configData?.additionalParameters).map( + (param) => ({ + id: crypto.randomUUID(), + key: param.key || '', + value: param.value || '', + }), + ), + } +} diff --git a/admin-ui/plugins/admin/plugin-metadata.ts b/admin-ui/plugins/admin/plugin-metadata.ts index 574d7461b0..4d7b05fbcd 100644 --- a/admin-ui/plugins/admin/plugin-metadata.ts +++ b/admin-ui/plugins/admin/plugin-metadata.ts @@ -132,11 +132,13 @@ const pluginMetadata = { component: RolePermissionMappingPage, path: ROUTES.ADMIN_MAPPING, action: CEDAR_ACTIONS.READ, + resourceKey: ADMIN_UI_RESOURCES.Security, }, { component: CedarlingConfigPage, path: ROUTES.ADMIN_CEDARLING_CONFIG, action: CEDAR_ACTIONS.READ, + resourceKey: ADMIN_UI_RESOURCES.Security, }, { diff --git a/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx b/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx index 15e4183395..0a2efc7919 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/Aliases/Aliases.tsx @@ -49,7 +49,11 @@ const Aliases = ({ onWritePermissionChange, }: AliasesProps): React.ReactElement => { const { t } = useTranslation() - const { canRead: canReadAuth, canWrite: canWriteAuth } = usePermission(AUTH_RESOURCE_ID) + const { + canRead: canReadAuth, + canWrite: canWriteAuth, + canDelete: canDeleteAuth, + } = usePermission(AUTH_RESOURCE_ID) const { state: themeState } = useTheme() const themeColors = useMemo( @@ -220,22 +224,33 @@ const Aliases = ({ ) const actions = useMemo[]>(() => { - if (!canWriteAuth) return [] - return [ - { + const list: ActionDef[] = [] + if (canWriteAuth) { + list.push({ icon: , tooltip: t('messages.edit_acr'), id: 'editAlias', onClick: handleEditClick, - }, - { + }) + } + if (canDeleteAuth) { + list.push({ icon: , tooltip: t('actions.delete'), id: 'deleteAlias', onClick: handleDeleteClick, - }, - ] - }, [canWriteAuth, t, handleEditClick, handleDeleteClick, classes.editIcon, classes.deleteIcon]) + }) + } + return list + }, [ + canWriteAuth, + canDeleteAuth, + t, + handleEditClick, + handleDeleteClick, + classes.editIcon, + classes.deleteIcon, + ]) const getRowKey = useCallback( (row: AcrMappingTableRow, index: number) => row.mapping ?? `alias-${index}`, diff --git a/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx b/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx index bdd38bc963..c8b0d4e1ab 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx @@ -68,12 +68,19 @@ describe('Aliases', () => { expect(screen.getByText('basic_auth')).toBeInTheDocument() }) - it('does not render edit/delete actions when user lacks write permission', () => { + it('does not render the edit action when user lacks write permission', () => { jest .mocked(useCedarling) .mockReturnValue(makeMockCedarling({ hasCedarWritePermission: jest.fn(() => false) })) render(, { wrapper: Wrapper }) expect(screen.queryByTitle(/edit/i)).not.toBeInTheDocument() + }) + + it('does not render the delete action when user lacks delete permission', () => { + jest + .mocked(useCedarling) + .mockReturnValue(makeMockCedarling({ hasCedarDeletePermission: jest.fn(() => false) })) + render(, { wrapper: Wrapper }) expect(screen.queryByTitle(/delete/i)).not.toBeInTheDocument() }) }) diff --git a/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx b/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx index 6a23218353..0f6fd2dadc 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx +++ b/admin-ui/plugins/auth-server/components/Ssa/components/SsaListPage.tsx @@ -273,7 +273,7 @@ const SsaListPage: React.FC = () => { }) } - if (canWriteSsa || canDeleteSsa) { + if (canDeleteSsa) { list.push({ icon: , tooltip: t('tooltips.delete_ssa'), diff --git a/admin-ui/plugins/auth-server/plugin-metadata.tsx b/admin-ui/plugins/auth-server/plugin-metadata.tsx index bcc7ab0bb7..fa21390fc6 100644 --- a/admin-ui/plugins/auth-server/plugin-metadata.tsx +++ b/admin-ui/plugins/auth-server/plugin-metadata.tsx @@ -135,7 +135,7 @@ const pluginMetadata = { { component: AcrsEditPage, path: ROUTES.AUTH_SERVER_AUTHN_EDIT_TEMPLATE, - action: CEDAR_ACTIONS.READ, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.Authentication, }, { @@ -189,7 +189,7 @@ const pluginMetadata = { { component: SsaAddPage, path: ROUTES.AUTH_SERVER_SSA_ADD, - action: CEDAR_ACTIONS.READ, + action: CEDAR_ACTIONS.WRITE, resourceKey: ADMIN_UI_RESOURCES.SSA, }, { diff --git a/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx b/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx index 814458d99e..417403ca97 100644 --- a/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx +++ b/admin-ui/plugins/fido/__tests__/components/Metrics/MetricsPage.test.tsx @@ -49,6 +49,8 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( describe('MetricsPage', () => { beforeEach(() => { jest.clearAllMocks() + const { usePermission } = jest.requireMock('@/cedarling/hooks/usePermission') + usePermission.mockImplementation(() => ({ canRead: true, canWrite: false, canDelete: false })) }) it('renders the General tab with charts by default', () => { diff --git a/admin-ui/plugins/fido/plugin-metadata.ts b/admin-ui/plugins/fido/plugin-metadata.ts index b2c616c6c3..85eca6c672 100644 --- a/admin-ui/plugins/fido/plugin-metadata.ts +++ b/admin-ui/plugins/fido/plugin-metadata.ts @@ -32,7 +32,7 @@ const pluginMetadata = { { component: Fido, path: ROUTES.FIDO_BASE, - action: CEDAR_ACTIONS.WRITE, + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES.FIDO, }, { diff --git a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx index 2a9781f7d7..b60d847162 100644 --- a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx +++ b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderList.tsx @@ -56,6 +56,7 @@ const WebsiteSsoServiceProviderList = React.memo(() => { const { canRead: canReadWebsiteSsoServiceProviders, canWrite: canWriteWebsiteSsoServiceProviders, + canDelete: canDeleteWebsiteSsoServiceProviders, } = usePermission(ADMIN_UI_RESOURCES.SAML) const handleGoToEditPage = useCallback( @@ -141,7 +142,7 @@ const WebsiteSsoServiceProviderList = React.memo(() => { }, }) } - if (canWriteWebsiteSsoServiceProviders) { + if (canDeleteWebsiteSsoServiceProviders) { actions.push({ icon: DeleteOutlinedIcon, iconProps: { color: 'secondary' }, @@ -166,6 +167,7 @@ const WebsiteSsoServiceProviderList = React.memo(() => { }, [ canReadWebsiteSsoServiceProviders, canWriteWebsiteSsoServiceProviders, + canDeleteWebsiteSsoServiceProviders, t, handleGoToEditPage, handleGoToAddPage, @@ -202,7 +204,7 @@ const WebsiteSsoServiceProviderList = React.memo(() => { }} /> - {canWriteWebsiteSsoServiceProviders && ( + {canDeleteWebsiteSsoServiceProviders && ( Date: Tue, 9 Jun 2026 18:08:12 +0500 Subject: [PATCH 03/24] cedarling permission tab fixes Signed-off-by: faisalsiddique4400 --- admin-ui/app/constants/ui.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts index df916d01f4..748e63a1bb 100644 --- a/admin-ui/app/constants/ui.ts +++ b/admin-ui/app/constants/ui.ts @@ -157,7 +157,7 @@ export const MAPPING_SPACING = { PAGE_PADDING_TOP: 53, ALERT_TO_CARD: 24, CARD_PADDING: 33, - CARD_HEADER_HEIGHT: 70, + CARD_HEADER_HEIGHT: 56, CARD_BORDER_RADIUS: 6, CARD_MARGIN_BOTTOM: 16, PERMISSION_ROW_GAP: 20, From c221786c83007d74537a9ac139659c616820df85 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Tue, 9 Jun 2026 18:43:23 +0500 Subject: [PATCH 04/24] storage util added Signed-off-by: faisalsiddique4400 --- admin-ui/app/constants/storageKeys.ts | 1 - admin-ui/app/context/theme/themeContext.tsx | 37 +++-- admin-ui/app/i18n.ts | 5 +- admin-ui/app/layout/default.tsx | 3 +- admin-ui/app/redux/features/logoutSlice.ts | 11 +- admin-ui/app/redux/listeners/authListener.ts | 2 - .../app/routes/Apps/Gluu/LanguageMenu.tsx | 17 +-- .../app/routes/Apps/Gluu/ThemeDropdown.tsx | 17 ++- admin-ui/app/routes/Pages/ByeBye.tsx | 4 +- admin-ui/app/utils/AppAuthProvider.tsx | 5 +- admin-ui/app/utils/TokenController.ts | 5 +- admin-ui/app/utils/pagingUtils.ts | 5 +- admin-ui/app/utils/storage.ts | 61 ++++++++ .../app/utils/types/AppAuthProviderTypes.ts | 1 + admin-ui/docs/auth.md | 27 ++-- admin-ui/docs/cedarling.md | 132 +++++++++--------- admin-ui/docs/onboarding.md | 2 +- admin-ui/docs/recipes.md | 20 +-- admin-ui/docs/testing.md | 14 +- 19 files changed, 212 insertions(+), 157 deletions(-) create mode 100644 admin-ui/app/utils/storage.ts diff --git a/admin-ui/app/constants/storageKeys.ts b/admin-ui/app/constants/storageKeys.ts index 7c2033a6dc..8a74822eea 100644 --- a/admin-ui/app/constants/storageKeys.ts +++ b/admin-ui/app/constants/storageKeys.ts @@ -4,5 +4,4 @@ export const STORAGE_KEYS = { INIT_LANG: 'initLang', USER_INFO: 'userInfo', ISSUER: 'issuer', - POST_LOGOUT_REDIRECT_URI: 'postLogoutRedirectUri', } as const diff --git a/admin-ui/app/context/theme/themeContext.tsx b/admin-ui/app/context/theme/themeContext.tsx index 5bd2c8bf1d..42e2202a8c 100644 --- a/admin-ui/app/context/theme/themeContext.tsx +++ b/admin-ui/app/context/theme/themeContext.tsx @@ -1,6 +1,7 @@ import { createContext, useReducer, useContext, useEffect, useRef, type ReactNode } from 'react' import { DEFAULT_THEME, isValidTheme, type ThemeValue } from './constants' import { devLogger } from '@/utils/devLogger' +import { storage } from '@/utils/storage' import { STORAGE_KEYS } from '@/constants' import type { ThemeState, ThemeAction, ThemeContextType } from './types' @@ -14,12 +15,9 @@ const extractUserTheme = (currentInum?: string | null): ThemeValue => { } try { - const userConfigStr = window.localStorage.getItem(STORAGE_KEYS.USER_CONFIG) - if (!userConfigStr) { - return DEFAULT_THEME - } - - const userConfig = JSON.parse(userConfigStr) as { theme?: string | Record } + const userConfig = storage.getJSON<{ theme?: string | Record }>( + STORAGE_KEYS.USER_CONFIG, + ) if (!userConfig?.theme) { return DEFAULT_THEME } @@ -52,18 +50,18 @@ const getInitialTheme = (): ThemeValue => { } try { - const savedTheme = window.localStorage.getItem(STORAGE_KEYS.INIT_THEME) + const savedTheme = storage.get(STORAGE_KEYS.INIT_THEME) if (savedTheme && isValidTheme(savedTheme)) { return savedTheme } const userTheme = extractUserTheme() if (userTheme !== DEFAULT_THEME) { - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, userTheme) + storage.set(STORAGE_KEYS.INIT_THEME, userTheme) return userTheme } - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) + storage.set(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) return DEFAULT_THEME } catch (e) { devLogger.error( @@ -71,7 +69,7 @@ const getInitialTheme = (): ThemeValue => { e instanceof Error ? e : String(e), ) try { - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) + storage.set(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) } catch (e) { devLogger.warn( 'Failed to write default theme to localStorage:', @@ -89,7 +87,7 @@ const initialState: ThemeState = { const themeReducer = (state: ThemeState, action: ThemeAction): ThemeState => { if (action.type) { if (typeof window !== 'undefined') { - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, action.type) + storage.set(STORAGE_KEYS.INIT_THEME, action.type) } return { theme: action.type } } @@ -102,11 +100,8 @@ interface ThemeProviderProps { const getUserInum = (): string | null => { try { - const userInfoStr = window.localStorage.getItem(STORAGE_KEYS.USER_INFO) - if (userInfoStr) { - const userInfo = JSON.parse(userInfoStr) as { inum?: string } - return userInfo?.inum || null - } + const userInfo = storage.getJSON<{ inum?: string }>(STORAGE_KEYS.USER_INFO) + return userInfo?.inum || null } catch (e) { devLogger.warn( 'Failed to parse userInfo from localStorage:', @@ -124,7 +119,7 @@ export const ThemeProvider = (props: ThemeProviderProps) => { if (typeof window === 'undefined' || hasSyncedRef.current) return try { - const savedTheme = window.localStorage.getItem(STORAGE_KEYS.INIT_THEME) + const savedTheme = storage.get(STORAGE_KEYS.INIT_THEME) if (savedTheme && isValidTheme(savedTheme)) { dispatch({ type: savedTheme }) hasSyncedRef.current = true @@ -135,10 +130,10 @@ export const ThemeProvider = (props: ThemeProviderProps) => { const userTheme = extractUserTheme(currentInum) if (userTheme !== DEFAULT_THEME) { - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, userTheme) + storage.set(STORAGE_KEYS.INIT_THEME, userTheme) dispatch({ type: userTheme }) } else { - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) + storage.set(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) dispatch({ type: DEFAULT_THEME }) } @@ -149,9 +144,9 @@ export const ThemeProvider = (props: ThemeProviderProps) => { e instanceof Error ? e : String(e), ) try { - const currentTheme = window.localStorage.getItem(STORAGE_KEYS.INIT_THEME) + const currentTheme = storage.get(STORAGE_KEYS.INIT_THEME) if (!currentTheme || !isValidTheme(currentTheme)) { - window.localStorage.setItem(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) + storage.set(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) dispatch({ type: DEFAULT_THEME }) } else { dispatch({ type: currentTheme }) diff --git a/admin-ui/app/i18n.ts b/admin-ui/app/i18n.ts index 383fda46fb..70f516ec74 100644 --- a/admin-ui/app/i18n.ts +++ b/admin-ui/app/i18n.ts @@ -4,6 +4,7 @@ import type { InitOptions } from 'i18next' import translationEn from './locales/en/translation.json' import { isDevelopment } from './utils/env' import { devLogger } from './utils/devLogger' +import { storage } from '@/utils/storage' import { hmrAccept } from '@/utils/hmr' import { toast } from 'react-toastify' import { STORAGE_KEYS, LANG_CODES, DEFAULT_LANG } from '@/constants' @@ -30,9 +31,9 @@ const handleMissingKey = (key: string, defaultValue?: string): string => { const getSavedLanguage = (): string => { try { - const initLang = localStorage.getItem(STORAGE_KEYS.INIT_LANG) + const initLang = storage.get(STORAGE_KEYS.INIT_LANG) if (initLang) return initLang - const config = JSON.parse(localStorage.getItem(STORAGE_KEYS.USER_CONFIG) || '{}') + const config = storage.getJSON<{ lang?: Record }>(STORAGE_KEYS.USER_CONFIG) const langs = config?.lang if (langs && typeof langs === 'object') { const values = Object.values(langs) as string[] diff --git a/admin-ui/app/layout/default.tsx b/admin-ui/app/layout/default.tsx index 4ead77333d..6ad416eed4 100755 --- a/admin-ui/app/layout/default.tsx +++ b/admin-ui/app/layout/default.tsx @@ -2,6 +2,7 @@ import React, { ReactNode } from 'react' import { Layout, ThemeProvider } from 'Components' import { DEFAULT_THEME, isValidTheme } from '@/context/theme/constants' import { devLogger } from '@/utils/devLogger' +import { storage } from '@/utils/storage' import { STORAGE_KEYS } from '@/constants' import 'Styles/bootstrap.scss' @@ -33,7 +34,7 @@ const getInitialThemeStyle = (): string => { } try { - const savedTheme = window.localStorage.getItem(STORAGE_KEYS.INIT_THEME) + const savedTheme = storage.get(STORAGE_KEYS.INIT_THEME) if (savedTheme && isValidTheme(savedTheme)) { return savedTheme } diff --git a/admin-ui/app/redux/features/logoutSlice.ts b/admin-ui/app/redux/features/logoutSlice.ts index 58d17991c8..6410ac10fa 100644 --- a/admin-ui/app/redux/features/logoutSlice.ts +++ b/admin-ui/app/redux/features/logoutSlice.ts @@ -2,19 +2,20 @@ import reducerRegistry from 'Redux/reducers/ReducerRegistry' import { createSlice } from '@reduxjs/toolkit' import { DEFAULT_THEME } from '@/context/theme/constants' import { STORAGE_KEYS, DEFAULT_LANG } from '@/constants' +import { storage } from '@/utils/storage' const logoutSlice = createSlice({ initialState: {}, name: 'logout', reducers: { logoutUser: (_state) => { - const userConfig = localStorage.getItem(STORAGE_KEYS.USER_CONFIG) - localStorage.clear() - localStorage.setItem(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) - localStorage.setItem(STORAGE_KEYS.INIT_LANG, DEFAULT_LANG) + const userConfig = storage.get(STORAGE_KEYS.USER_CONFIG) + storage.clear() + storage.set(STORAGE_KEYS.INIT_THEME, DEFAULT_THEME) + storage.set(STORAGE_KEYS.INIT_LANG, DEFAULT_LANG) if (userConfig && userConfig !== 'null') { - localStorage.setItem(STORAGE_KEYS.USER_CONFIG, userConfig) + storage.set(STORAGE_KEYS.USER_CONFIG, userConfig) } }, }, diff --git a/admin-ui/app/redux/listeners/authListener.ts b/admin-ui/app/redux/listeners/authListener.ts index 34802c6820..8497bbcef2 100644 --- a/admin-ui/app/redux/listeners/authListener.ts +++ b/admin-ui/app/redux/listeners/authListener.ts @@ -22,7 +22,6 @@ import { import { isFourZeroThreeError } from 'Utils/TokenController' import { devLogger } from '@/utils/devLogger' import { setApiToken } from 'Orval' -import { STORAGE_KEYS } from '@/constants' import { SESSION_EXPIRED } from '@/audit/messages' import type { Config } from '../features/types/authTypes' import type { PutConfigMeta } from '../features/types/authSliceTypes' @@ -128,7 +127,6 @@ startAppListening({ const response = await fetchServerConfiguration(token ?? undefined) if (response?.postLogoutRedirectUri) { - localStorage.setItem(STORAGE_KEYS.POST_LOGOUT_REDIRECT_URI, response.postLogoutRedirectUri) dispatch(getOAuth2ConfigResponse({ config: response as Config })) return } diff --git a/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx b/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx index 6f0bf56aec..5d50e899ac 100644 --- a/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx +++ b/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx @@ -5,7 +5,7 @@ import { GluuDropdown, type GluuDropdownOption, ChevronIcon } from 'Components' import GluuText from 'Routes/Apps/Gluu/GluuText' import { ThemeContext } from 'Context/theme/themeContext' import { THEME_DARK, DEFAULT_THEME } from '@/context/theme/constants' -import { devLogger } from '@/utils/devLogger' +import { storage } from '@/utils/storage' import { ensureLocaleLoaded } from '@/i18n' import { STORAGE_KEYS, LANG_CODES, DEFAULT_LANG } from '@/constants' import { useStyles } from './styles/LanguageMenu.style' @@ -16,17 +16,11 @@ interface UserConfig { theme?: Record } -const safeParseUserConfig = (): UserConfig => { - try { - return JSON.parse(localStorage.getItem(STORAGE_KEYS.USER_CONFIG) || '{}') || {} - } catch (error) { - devLogger.warn('Failed to parse userConfig:', error instanceof Error ? error : String(error)) - return {} - } -} +const safeParseUserConfig = (): UserConfig => + storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} const getInitialLang = (inum?: string): string => { - const initLang = localStorage.getItem(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG + const initLang = storage.get(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG const config = safeParseUserConfig() return config?.lang?.[inum || ''] || initLang } @@ -71,7 +65,8 @@ const LanguageMenu = memo(({ userInfo }) => { langConfig[inum] = code } const newConfig = { ...config, lang: langConfig } - localStorage.setItem(STORAGE_KEYS.USER_CONFIG, JSON.stringify(newConfig)) + storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) + storage.set(STORAGE_KEYS.INIT_LANG, code) }, [i18n, inum], ) diff --git a/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx b/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx index 792abe465c..2f0799be3f 100644 --- a/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx +++ b/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx @@ -6,6 +6,7 @@ import GluuText from 'Routes/Apps/Gluu/GluuText' import { useTheme } from '@/context/theme/themeContext' import { THEME_LIGHT, THEME_DARK, isValidTheme, type ThemeValue } from '@/context/theme/constants' import { devLogger } from '@/utils/devLogger' +import { storage } from '@/utils/storage' import { STORAGE_KEYS } from '@/constants' import { useStyles } from './styles/ThemeDropdown.style' import type { ThemeDropdownComponentProps } from './types' @@ -38,13 +39,11 @@ export const ThemeDropdownComponent = memo(({ userI } try { - const existingConfigStr = localStorage.getItem(STORAGE_KEYS.USER_CONFIG) - const existingConfig = existingConfigStr - ? (JSON.parse(existingConfigStr) as { - theme?: Record - lang?: Record - }) - : {} + const existingConfig = + storage.getJSON<{ + theme?: Record + lang?: Record + }>(STORAGE_KEYS.USER_CONFIG) ?? {} const updatedTheme = { ...(existingConfig.theme || {}), @@ -57,14 +56,14 @@ export const ThemeDropdownComponent = memo(({ userI theme: updatedTheme, } - localStorage.setItem(STORAGE_KEYS.USER_CONFIG, JSON.stringify(newConfig)) + storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) } catch (e) { devLogger.debug('Failed to parse userConfig:', e instanceof Error ? e : String(e)) const newConfig = { lang: {}, theme: { [inum]: themeValue }, } - localStorage.setItem(STORAGE_KEYS.USER_CONFIG, JSON.stringify(newConfig)) + storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) } dispatch({ type: themeValue }) diff --git a/admin-ui/app/routes/Pages/ByeBye.tsx b/admin-ui/app/routes/Pages/ByeBye.tsx index 908d302ea6..18cb2f30fe 100644 --- a/admin-ui/app/routes/Pages/ByeBye.tsx +++ b/admin-ui/app/routes/Pages/ByeBye.tsx @@ -12,7 +12,6 @@ import { DEFAULT_THEME } from '@/context/theme/constants' import getThemeColor from '@/context/theme/config' import { devLogger } from '@/utils/devLogger' import { buildSafeLogoutUrl, buildSafeNavigationUrl } from '@/utils/urlSecurity' -import { STORAGE_KEYS } from '@/constants' const ByeBye = () => { const config = useAppSelector((state) => state.authReducer.config) as AuthConfig @@ -54,8 +53,7 @@ const ByeBye = () => { return } } else { - const fallbackUri = - buildSafeNavigationUrl(localStorage.getItem(STORAGE_KEYS.POST_LOGOUT_REDIRECT_URI)) || '/' + const fallbackUri = buildSafeNavigationUrl(config.postLogoutRedirectUri) || '/' window.location.href = fallbackUri return } diff --git a/admin-ui/app/utils/AppAuthProvider.tsx b/admin-ui/app/utils/AppAuthProvider.tsx index 46d6e8782b..e8d3b4fa8e 100755 --- a/admin-ui/app/utils/AppAuthProvider.tsx +++ b/admin-ui/app/utils/AppAuthProvider.tsx @@ -42,7 +42,6 @@ import { jwtDecode } from 'jwt-decode' import type { UserInfo } from '@/redux/features/types/authTypes' import type { OAuthConfig, AppAuthProviderProps } from '@/utils/types' import { buildSafeLogoutUrl } from '@/utils/urlSecurity' -import { STORAGE_KEYS } from '@/constants' const LOGOUT_DELAY_SECONDS = 10 @@ -60,6 +59,8 @@ const AppAuthProvider = ({ children }: Readonly) => { hasSession, } = useAppSelector((state) => state.authReducer) const config = rawConfig as OAuthConfig + const configRef = useRef(config) + configRef.current = config const { islicenseCheckResultLoaded, isLicenseValid, isConfigValid, isUnderThresholdLimit } = useAppSelector((state) => state.licenseReducer) @@ -252,7 +253,7 @@ const AppAuthProvider = ({ children }: Readonly) => { const state = uuidv4() const sessionEndpoint = buildSafeLogoutUrl( authConfigs?.endSessionEndpoint || null, - localStorage.getItem(STORAGE_KEYS.POST_LOGOUT_REDIRECT_URI), + configRef.current.postLogoutRedirectUri, state, ) dispatch( diff --git a/admin-ui/app/utils/TokenController.ts b/admin-ui/app/utils/TokenController.ts index bf0b1db6ce..ba59233fc1 100644 --- a/admin-ui/app/utils/TokenController.ts +++ b/admin-ui/app/utils/TokenController.ts @@ -3,6 +3,7 @@ import type { JsonValue } from 'Routes/Apps/Gluu/types/common' import type { AuditRecord } from 'Redux/types/audit' import type { AdditionalPayload, AxiosErrorLike, DirectStatusError, HttpError } from './types' import { STORAGE_KEYS } from '@/constants' +import { storage } from '@/utils/storage' export type { AdditionalPayload } @@ -14,11 +15,11 @@ export const isFourZeroThreeError = (error?: HttpError | Error): boolean => { } export const saveIssuer = (issuer: string): void => { - localStorage.setItem(STORAGE_KEYS.ISSUER, issuer) + storage.set(STORAGE_KEYS.ISSUER, issuer) } export const getIssuer = (): string | null => { - return localStorage.getItem(STORAGE_KEYS.ISSUER) + return storage.get(STORAGE_KEYS.ISSUER) } export const addAdditionalData = ( diff --git a/admin-ui/app/utils/pagingUtils.ts b/admin-ui/app/utils/pagingUtils.ts index 6a2d780a91..ed4c03b0ef 100644 --- a/admin-ui/app/utils/pagingUtils.ts +++ b/admin-ui/app/utils/pagingUtils.ts @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react' import { devLogger } from '@/utils/devLogger' +import { storage } from '@/utils/storage' export const ROWS_PER_PAGE_OPTIONS = [5, 10, 25, 50] as const @@ -15,7 +16,7 @@ const getPagingSize = (defaultSize: number = DEFAULT_PAGING_SIZE): number => { } try { - const stored = localStorage.getItem(STORAGE_KEY) + const stored = storage.get(STORAGE_KEY) if (!stored) return defaultSize @@ -60,7 +61,7 @@ export const savePagingSize = (size: number): void => { } try { - localStorage.setItem(STORAGE_KEY, String(validSize)) + storage.set(STORAGE_KEY, String(validSize)) if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent(PAGING_SIZE_CHANGED_EVENT, { detail: validSize })) } diff --git a/admin-ui/app/utils/storage.ts b/admin-ui/app/utils/storage.ts new file mode 100644 index 0000000000..b1eeac7577 --- /dev/null +++ b/admin-ui/app/utils/storage.ts @@ -0,0 +1,61 @@ +import { devLogger } from '@/utils/devLogger' + +const isAvailable = (): boolean => typeof window !== 'undefined' && !!window.localStorage + +const get = (key: string): string | null => { + if (!isAvailable()) return null + try { + return window.localStorage.getItem(key) + } catch (e) { + devLogger.warn(`storage.get failed for "${key}":`, e instanceof Error ? e : String(e)) + return null + } +} + +const set = (key: string, value: string): void => { + if (!isAvailable()) return + try { + window.localStorage.setItem(key, value) + } catch (e) { + devLogger.warn(`storage.set failed for "${key}":`, e instanceof Error ? e : String(e)) + } +} + +const getJSON = (key: string): T | null => { + const raw = get(key) + if (raw === null) return null + try { + return JSON.parse(raw) as T + } catch (e) { + devLogger.warn(`storage.getJSON failed for "${key}":`, e instanceof Error ? e : String(e)) + return null + } +} + +const setJSON = (key: string, value: T): void => { + try { + set(key, JSON.stringify(value)) + } catch (e) { + devLogger.warn(`storage.setJSON failed for "${key}":`, e instanceof Error ? e : String(e)) + } +} + +const remove = (key: string): void => { + if (!isAvailable()) return + try { + window.localStorage.removeItem(key) + } catch (e) { + devLogger.warn(`storage.remove failed for "${key}":`, e instanceof Error ? e : String(e)) + } +} + +const clear = (): void => { + if (!isAvailable()) return + try { + window.localStorage.clear() + } catch (e) { + devLogger.warn('storage.clear failed:', e instanceof Error ? e : String(e)) + } +} + +export const storage = { get, set, getJSON, setJSON, remove, clear } diff --git a/admin-ui/app/utils/types/AppAuthProviderTypes.ts b/admin-ui/app/utils/types/AppAuthProviderTypes.ts index 825723b544..3fe106932a 100644 --- a/admin-ui/app/utils/types/AppAuthProviderTypes.ts +++ b/admin-ui/app/utils/types/AppAuthProviderTypes.ts @@ -11,6 +11,7 @@ export type OAuthConfig = { clientId?: string redirectUrl?: string scope?: string + postLogoutRedirectUri?: string } export type AppAuthProviderProps = { diff --git a/admin-ui/docs/auth.md b/admin-ui/docs/auth.md index c223ca108f..7d877c0e6c 100644 --- a/admin-ui/docs/auth.md +++ b/admin-ui/docs/auth.md @@ -74,7 +74,7 @@ sequenceDiagram participant Orval as Orval client Listener->>API: fetchApiTokenWithDefaultScopes() - API-->>Listener: { access_token, scopes, issuer, postLogoutRedirectUri } + API-->>Listener: { access_token, scopes, issuer } Listener->>Orval: setApiToken(access_token) Listener->>API: POST /session { ujwt, apiProtectionToken } (withCredentials: true) API-->>Listener: Set-Cookie: admin-ui-session=… @@ -83,9 +83,9 @@ sequenceDiagram ### Explanation of the flow -The flow is driven by [`app/redux/listeners/authListener.ts`](../app/redux/listeners/authListener.ts). After the OAuth/PKCE step writes the user info into Redux, `AppAuthProvider` dispatches `getAPIAccessToken`. The listener calls `fetchApiTokenWithDefaultScopes()` and receives back an `access_token` along with side values (`issuer`, `postLogoutRedirectUri`, the requested scopes). +The flow is driven by [`app/redux/listeners/authListener.ts`](../app/redux/listeners/authListener.ts). After the OAuth/PKCE step writes the user info into Redux, `AppAuthProvider` dispatches `getAPIAccessToken`. The listener calls `fetchApiTokenWithDefaultScopes()` and receives back an `access_token` along with side values (`issuer` and the requested scopes). Separately, the `getOAuth2Config` listener calls `fetchServerConfiguration()` and dispatches the result (including `postLogoutRedirectUri`) into `authReducer.config`. -The listener hands the token to the Orval client by calling `setApiToken(access_token)` so the upcoming `POST /session` call carries the right header. Once the session cookie is set, regular Config API traffic authenticates off the cookie (see [config-api.md](./config-api.md)). The listener also persists `POST_LOGOUT_REDIRECT_URI` to `localStorage` so that [`ByeBye.tsx`](../app/routes/Pages/ByeBye.tsx) can use it as a fallback at logout time. +The listener hands the token to the Orval client by calling `setApiToken(access_token)` so the upcoming `POST /session` call carries the right header. Once the session cookie is set, regular Config API traffic authenticates off the cookie (see [config-api.md](./config-api.md)). The OAuth2 server config (including `postLogoutRedirectUri`) is dispatched into Redux `authReducer.config` and persisted there, so [`ByeBye.tsx`](../app/routes/Pages/ByeBye.tsx) reads it as the logout fallback. Then comes the session creation. The listener dispatches `createAdminUiSession({ ujwt, apiProtectionToken })`: @@ -191,14 +191,15 @@ Never reach into `localStorage` directly for tokens. Go through [`app/utils/Toke `localStorage` is used for state that needs to be readable **before** React and Redux even boot. Theme and language need to be available for the first paint, otherwise the user sees a flash of the wrong theme. Every key lives in [`app/constants/storageKeys.ts`](../app/constants/storageKeys.ts). Inline string keys are a lint regression. -| Key | Feature | Set by | Read by | -| -------------------------- | -------------------------------------------- | ----------------------------------- | ------------------------------------- | -| `USER_CONFIG` | User prefs blob (theme + language) | `LanguageMenu`, `ThemeDropdown` | `i18n.ts`, `themeContext` | -| `INIT_THEME` | UI theme (light/dark) | `themeContext`, `logoutSlice` reset | `themeContext`, `default.tsx` layout | -| `INIT_LANG` | UI language | `LanguageMenu`, `logoutSlice` reset | `i18n.ts`, `LanguageMenu` | -| `USER_INFO` | Decoded OIDC `userinfo` (theme/lang restore) | no current writer | `themeContext` | -| `ISSUER` | OIDC issuer URL | `TokenController` after discovery | `TokenController` on subsequent boots | -| `POST_LOGOUT_REDIRECT_URI` | Where to send the user after end-session | auth listener from OAuth2 config | `ByeBye.tsx` as fallback | +Application code reads and writes through the `storage` helper in [`app/utils/storage.ts`](../app/utils/storage.ts) (`get` / `set` / `getJSON` / `setJSON` / `remove` / `clear`), which guards SSR and swallows quota and parse errors. The only raw `localStorage` access is the pre-paint theme script in `index.html`, which has to run before the bundle loads. + +| Key | Feature | Set by | Read by | +| ------------- | -------------------------------------------- | ----------------------------------- | ------------------------------------- | +| `USER_CONFIG` | User prefs blob (theme + language) | `LanguageMenu`, `ThemeDropdown` | `i18n.ts`, `themeContext` | +| `INIT_THEME` | UI theme (light/dark) | `themeContext`, `logoutSlice` reset | `themeContext`, `default.tsx` layout | +| `INIT_LANG` | UI language | `LanguageMenu`, `logoutSlice` reset | `i18n.ts`, `LanguageMenu` | +| `USER_INFO` | Decoded OIDC `userinfo` (theme/lang restore) | no current writer | `themeContext` | +| `ISSUER` | OIDC issuer URL | `TokenController` after discovery | `TokenController` on subsequent boots | ## 401 vs 403 @@ -231,7 +232,7 @@ Logout is one cleanup path with three different triggers. All of them end up rou 2. If `state.authReducer.hasSession` is `true`, calls `deleteAdminUiSession()` against `ENDPOINTS.SESSION` (DELETE) so the Config API invalidates the admin-UI session cookie. Failures are logged but do not block the rest of logout. 3. `dispatch(logoutUser())`: the `logoutSlice` reducer wipes `localStorage` of tokens and userinfo, **preserves** `USER_CONFIG`, and resets `INIT_THEME` + `INIT_LANG` to defaults so the next visitor lands on a clean theme. 4. If the OAuth config has an `endSessionEndpoint`, builds the end-session URL with `buildSafeLogoutUrl(endSessionEndpoint, postLogoutRedirectUri, state)` (from [`app/utils/urlSecurity.ts`](../app/utils/urlSecurity.ts)) and redirects there. Jans clears its own session and returns the browser to the redirect URI. -5. **Fallback**: if no `endSessionEndpoint` is available, redirects to the URL in `STORAGE_KEYS.POST_LOGOUT_REDIRECT_URI` (validated via `buildSafeNavigationUrl`), or `/` as a last resort. +5. **Fallback**: if no `endSessionEndpoint` is available, redirects to `config.postLogoutRedirectUri` from `authReducer.config` (validated via `buildSafeNavigationUrl`), or `/` as a last resort. `buildSafeLogoutUrl` and `buildSafeNavigationUrl` exist because constructing these URLs by hand is error-prone. A crafted `post_logout_redirect_uri` is a URL-injection vector. Always go through them. @@ -240,7 +241,7 @@ Logout is one cleanup path with three different triggers. All of them end up rou Two unrelated things are both called "scopes": - **OIDC scopes** are declared in the auth config the Jans server hands back. AppAuth lists them in the `AuthorizationRequest`. They control what the OIDC token can be used for. -- **Cedarling resource scopes** are declared in [`app/cedarling/constants/resourceScopes.ts`](../app/cedarling/constants/resourceScopes.ts) and evaluated at the page boundary by `useCedarling()`. They control which UI elements render for which role. +- **Cedarling resource scopes** are derived from [`RESOURCE_ACTIONS`](../app/cedarling/constants/resourceCatalog.ts) and evaluated at the page boundary by `usePermission()`. They control which UI elements render for which role. A user can be signed in (OIDC scopes valid) but unauthorized (Cedarling denies). That is the normal case for role-restricted pages. See [cedarling.md](./cedarling.md). diff --git a/admin-ui/docs/cedarling.md b/admin-ui/docs/cedarling.md index 5773442a0e..1726b72e02 100644 --- a/admin-ui/docs/cedarling.md +++ b/admin-ui/docs/cedarling.md @@ -10,7 +10,7 @@ Three things make this design unusual: - **Policies are data, not code.** The rules ("a user with role X can read resource Y") live in a JSON file called a **policy store**, loaded into Cedarling at startup. Changing who can do what is a policy-store edit, not a component change. - **The UI gate is the first gate, not the only one.** Even when Cedarling says "yes", the Jans Config API revalidates server-side and can still return 403. Cedarling hides UI elements the user can't use. It does not replace server-side authorization. -The check itself is simple: every page that gates a button, an action, or a whole section calls a small hook (`useCedarling`) and asks _can this user read / write / delete this resource?_ The first answer for each `(resource, action)` pair triggers a WASM call. Every later answer is a Redux cache read. +The check itself is simple: every page that gates a button, an action, or a whole section calls `usePermission(resource)` and gets back `{ canRead, canWrite, canDelete }`. The first answer for each `(resource, action)` pair triggers a WASM call. Every later answer is a Redux cache read. ## Flow diagram @@ -47,14 +47,15 @@ sequenceDiagram sequenceDiagram autonumber participant Page as Page (e.g. Clients list) + participant Perm as usePermission(resource) participant Hook as useCedarling() participant Cache as cedarPermissions slice participant Client as cedarlingClient participant Wasm as Cedarling WASM - Page->>Hook: useCedarling() - Page->>Hook: authorizeHelper(CEDAR_RESOURCE_SCOPES[Clients]) in useEffect - Hook->>Hook: dedupe entries by (resourceId, actionLabel) + Page->>Perm: usePermission(ADMIN_UI_RESOURCES.Clients) + Perm->>Hook: authorizeHelper(CEDAR_RESOURCE_SCOPES[Clients]) in useEffect + Hook->>Hook: dedupe entries by (resourceId, action) loop each unique (resource, action) Hook->>Cache: cached? alt cached @@ -66,8 +67,7 @@ sequenceDiagram Hook->>Cache: setCedarlingPermission(key, decision) end end - Page->>Hook: hasCedarReadPermission(Clients) on render - Cache-->>Page: boolean + Perm-->>Page: { canRead, canWrite, canDelete } Page->>Page: render the table or hide it ``` @@ -86,7 +86,7 @@ When the user finishes signing in, [`app/utils/AppAuthProvider.tsx`](../app/util Once all three are ready, it: 1. **Decodes** the base64 string into a `Uint8Array`. -2. **Merges the bootstrap config**: a static JSON file at [`app/cedarling/config/cedarling-bootstrap-TBAC.json`](../app/cedarling/config/cedarling-bootstrap-TBAC.json) gets combined with the runtime-configured log type to produce the full bootstrap configuration that Cedarling will initialize with. The log type comes from the Config API (`authReducer.config.cedarlingLogType`) and is one of the values defined in [`CedarlingLogType`](../app/cedarling/enums/CedarlingLogType.ts): `OFF` (default) or `STD_OUT`. This step tells Cedarling _how_ to behave (logging, schema, token issuers, etc.). The policy store tells it _what to enforce_. +2. **Merges the bootstrap config**: a static JSON file at [`app/cedarling/config/cedarling-bootstrap-TBAC.json`](../app/cedarling/config/cedarling-bootstrap-TBAC.json) gets combined with the runtime-configured log type to produce the full bootstrap configuration that Cedarling will initialize with. The log type comes from the Config API (`authReducer.config.cedarlingLogType`) and is one of the values in [`CEDARLING_LOG_TYPE`](../app/cedarling/constants/cedarlingConstants.ts): `OFF` (default) or `STD_OUT`. This step tells Cedarling _how_ to behave (logging, schema, token issuers). The policy store tells it _what to enforce_. 3. **Initializes Cedarling** by calling `cedarlingClient.initialize(bootstrap config, policy store bytes)`. [`cedarlingClient`](../app/cedarling/client/) is a thin singleton wrapper around the WASM module. Its `initialize` function loads the WASM binary (`initWasm()`) and asks the WASM module to construct a `Cedarling` instance from the bootstrap config and the policy store bytes (`init_from_archive_bytes`). The client guards against double-init using a promise singleton. If Phase 1 re-runs while initialization is mid-flight, the second call returns the in-progress promise instead of starting over. 4. **Marks Cedarling ready** with `setCedarlingInitialized(true)`. From this point on, any component can ask Cedarling for a decision. @@ -94,28 +94,36 @@ If initialization fails, the initializer retries up to **10 times with a 1-secon ### Phase 2: Per-page permission check -Take the OIDC Clients list page as a concrete example. When the page component mounts, it calls the `useCedarling()` hook ([`app/cedarling/hooks/useCedarling.ts`](../app/cedarling/hooks/useCedarling.ts)). The hook pulls the three tokens (`id_token`, `access_token`, `userinfo_token`) out of `authReducer`, reads the existing decision cache out of `cedarPermissions`, and returns a stable object: `{ authorizeHelper, hasCedarReadPermission, hasCedarWritePermission, hasCedarDeletePermission, error, isLoading }`. +Take the OIDC Clients list page as a concrete example. The page calls [`usePermission(ADMIN_UI_RESOURCES.Clients)`](../app/cedarling/hooks/usePermission.ts) and gets back `{ canRead, canWrite, canDelete }`. Those three booleans decide whether the table renders and which action buttons appear. -**`authorizeHelper`** is the one entry point for asking Cedarling about decisions. It takes an array of `ResourceScopeEntry` objects (each shaped like `{ permission: '', resourceId: 'Clients' }`), dedupes by `(resourceId, action)` so it fires exactly one WASM call per unique pair, and returns an `AuthorizationResult[]` aligned to the original input. Single-scope sites just pass a one-element array and destructure the first result. +`usePermission` does two things. On mount it looks up the resource's scope list in `CEDAR_RESOURCE_SCOPES` and passes it to `authorizeHelper` inside a `useEffect`, which fills the decision cache. On every render it reads the cached decisions back through `hasCedarReadPermission`, `hasCedarWritePermission`, and `hasCedarDeletePermission`, defaulting each to `false` when no decision exists yet. -The `permission` field is the OAuth scope URL the Config API uses for that operation. The `resourceId` is the logical resource name in the Cedar policy store. +Under the hood `usePermission` is built on [`useCedarling()`](../app/cedarling/hooks/useCedarling.ts), the lower-level hook that pulls the three tokens (`id_token`, `access_token`, `userinfo_token`) out of `authReducer`, reads the decision cache out of `cedarPermissions`, and returns a stable object: `{ authorizeHelper, hasCedarReadPermission, hasCedarWritePermission, hasCedarDeletePermission }`. Most components use `usePermission`. `useCedarling` is used directly only where a component needs to evaluate many resources at once, such as the sidebar. -**Where do the scopes come from?** Every Cedarling-gated page has an entry in [`CEDAR_RESOURCE_SCOPES`](../app/cedarling/constants/resourceScopes.ts) keyed by `ADMIN_UI_RESOURCES.`. That entry lists every OAuth scope URL the page might need a decision on. The page passes this whole array into `authorizeHelper` once, in a `useEffect`, when the page mounts. +**`authorizeHelper`** is the one entry point for asking Cedarling about decisions. It takes an array of `ResourceScopeEntry` objects (each shaped `{ action, resourceId }`), dedupes by `(resourceId, action)` so it fires exactly one WASM call per unique pair, and returns an `AuthorizationResult[]` aligned to the original input. + +**Where do the scopes come from?** A single catalog, [`RESOURCE_ACTIONS`](../app/cedarling/constants/resourceCatalog.ts), declares the allowed actions per resource, for example `Clients: ['read', 'write', 'delete']`. Two things derive from it in [`app/cedarling/utility/resources.ts`](../app/cedarling/utility/resources.ts): + +- `ADMIN_UI_RESOURCES`: the resource-id map, one key per resource. +- `CEDAR_RESOURCE_SCOPES`: for each resource, its action list turned into `{ action, resourceId }` entries, which is exactly what `authorizeHelper` consumes. + +So adding a resource or changing its allowed actions is a single edit to `RESOURCE_ACTIONS`. **What `authorizeHelper` does internally:** -1. **Derives an action label from each scope URL.** A URL containing `write` becomes the `write` action. One containing `delete` becomes `delete`. Everything else becomes `read`. A handful of special-case URLs (SSA admin/developer, SCIM bulk, revoke session, OpenID) are also forced to `write`. See `getActionLabelFromUrl` inside the hook. -2. **Dedupes by `(resourceId, actionLabel)`.** If two different scope URLs both resolve to `(Clients, read)`: for example, a regular read scope and an admin read scope that both grant read access. The helper fires the underlying authorization exactly once and reuses the decision for both entries. This is the "dedupe" loop in the diagram. -3. **For each unique pair**, the helper first checks the Redux cache. If a decision was already computed in this session, it is returned immediately and no WASM call happens. On a cache miss, the helper builds a `TokenAuthorizationRequest`: +1. **Dedupes by `(resourceId, action)`.** Repeated pairs in the input collapse to one underlying authorization, and the decision is reused for every matching entry. This is the "dedupe" loop in the diagram. +2. **For each unique pair**, the helper first checks the Redux cache. If a decision was already computed in this session, it is returned immediately and no WASM call happens. On a cache miss, the helper builds a `TokenAuthorizationRequest`: - All three tokens, mapped to Cedar entity types (`GluuFlexAdminUI::Access_token`, `::id_token`, `::Userinfo_token`). - The action, formatted as `GluuFlexAdminUI::Action::"read"` (or `"write"` / `"delete"`). - - The resource, as a Cedar entity with the `resourceId`. + - The resource, as a Cedar entity carrying the `resourceId`. + + The entity-type prefixes live in [`CEDARLING_CONSTANTS`](../app/cedarling/constants/cedarlingConstants.ts) and must stay in sync with the policy-store schema. - The entity-type prefixes (`GluuFlexAdminUI::Action::`, `GluuFlexAdminUIResources::Features`) are not magic strings: they live in [`CEDARLING_CONSTANTS`](../app/cedarling/constants/cedarlingConstants.ts) and must stay in sync with the policy-store schema. +3. **Calls `cedarlingClient.token_authorize(request)`**, which calls into WASM (`authorize_multi_issuer`). The WASM evaluates the Cedar policies against the tokens, action, and resource, and returns `{ decision: true | false }`. The decision is cached under the key `${resourceId}::${action}` ([`buildCedarPermissionKey`](../app/cedarling/utility/resources.ts)). -4. **Calls `cedarlingClient.token_authorize(request)`**, which calls into WASM (`authorize_multi_issuer`). The WASM evaluates the Cedar policies against the tokens, action, and resource, and returns `{ decision: true | false }`. The hook caches that decision under the key `${resourceId}::${action}`. +A failed authorization is not cached: the catch path returns `false` for that call but skips the cache write, so a transient WASM or init error retries on the next check instead of sticking as a permanent denial. -After `authorizeHelper` finishes, the page renders. During render, it calls `hasCedarReadPermission(ADMIN_UI_RESOURCES.Clients)` to decide whether to show the clients table at all, and `hasCedarWritePermission(...)` to decide whether to show "Add Client" / "Edit" / "Delete" buttons. Each of these is a pure Redux selector. It reads the cached boolean and returns it. After the first `authorizeHelper` call on a page, every subsequent render costs nothing: no WASM, no network, just a cache hit. +After the first `usePermission` call on a page, every subsequent render costs nothing: no WASM, no network, just a cache hit. A 403 from the Config API is still possible if a Cedarling decision and the server-side policy check disagree. Cedarling is the **early gate** for what the user can see and click, not the final word. The Config API always revalidates. When the API disagrees, the user sees a toast and the affected query fails. Cached decisions stay. @@ -127,10 +135,12 @@ app/cedarling/ ├── config/ # cedarling-bootstrap-TBAC.json │ # policy-store-dev.json │ # policy-store-prod.json -├── constants/ # CEDAR_RESOURCE_SCOPES - scope arrays per resource -├── hooks/ # useCedarling(): the hook every page uses -├── types/ # AdminUiFeatureResource, ResourceScopeEntry, AuthorizationResponse, … -└── utility/ # ADMIN_UI_RESOURCES, CEDARLING_BYPASS, buildCedarPermissionKey +├── components/ # Protected - declarative action gate +├── constants/ # RESOURCE_ACTIONS, CEDAR_ACTIONS, CEDARLING_BYPASS (resourceCatalog) +│ # CEDARLING_CONSTANTS, CEDARLING_LOG_TYPE (cedarlingConstants) +├── hooks/ # useCedarling (low-level), usePermission (per-resource) +├── types/ # cedarTypes: CedarAction, AdminUiFeatureResource, ResourceScopeEntry, … +└── utility/ # ADMIN_UI_RESOURCES, CEDAR_RESOURCE_SCOPES, buildCedarPermissionKey app/redux/features/cedarPermissionsSlice.ts # decision cache, policy-store bytes, init state, retry state @@ -142,84 +152,80 @@ app/utils/AppAuthProvider.tsx # fetches the policy store after sign-in ``` -`vite.config.ts` (`getPolicyStoreConfig`) picks `policy-store-dev.json` or `policy-store-prod.json` by build mode and embeds it into the bundle. The Config API ships the same store at runtime via `fetchPolicyStore()`: that's the runtime override path, used so the policy store can change without rebuilding the UI. +`vite.config.ts` (`getPolicyStoreConfig`) picks `policy-store-dev.json` or `policy-store-prod.json` by build mode and embeds it into the bundle. The Config API ships the same store at runtime via `fetchPolicyStore()`: that is the runtime override path, used so the policy store can change without rebuilding the UI. + +The `app/cedarling` module follows a one-way layering: `constants` ← `types` ← `utility` / `hooks` / `components`. Import from leaf paths (`@/cedarling/hooks/usePermission`, `@/cedarling/constants`, `@/cedarling/utility`, `@/cedarling/types`, `@/cedarling/components`). The top-level `@/cedarling` barrel is reserved for tests; it is blocked by `no-restricted-imports` in app and plugin code to keep Fast Refresh boundaries intact. ## How to use it -To gate a button, a table, or a whole page on a Cedar permission, three things must be in place: +To gate a button, a table, or a whole page on a Cedar permission, two things must be in place: -1. The resource id must exist in [`ADMIN_UI_RESOURCES`](../app/cedarling/utility/resources.ts). -2. The scope array must exist in [`CEDAR_RESOURCE_SCOPES`](../app/cedarling/constants/resourceScopes.ts) under that resource id. -3. The matching Cedar policy must exist in **both** `policy-store-dev.json` and `policy-store-prod.json` (otherwise the answer is always "deny"). +1. The resource must exist in [`RESOURCE_ACTIONS`](../app/cedarling/constants/resourceCatalog.ts) with the actions it supports. `ADMIN_UI_RESOURCES` and `CEDAR_RESOURCE_SCOPES` derive from it. +2. The matching Cedar policy must exist in **both** `policy-store-dev.json` and `policy-store-prod.json` (otherwise the answer is always "deny"). Then in the component: -```ts -import { useEffect, useMemo } from 'react' -import { useCedarling } from '@/cedarling/hooks/useCedarling' +```tsx +import { usePermission } from '@/cedarling/hooks/usePermission' import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes' -const RESOURCE_ID = ADMIN_UI_RESOURCES.Clients -const SCOPES = CEDAR_RESOURCE_SCOPES[RESOURCE_ID] +const resourceId = ADMIN_UI_RESOURCES.Clients const ClientListPage = () => { - const { authorizeHelper, hasCedarReadPermission, hasCedarWritePermission } = useCedarling() - - useEffect(() => { - if (SCOPES?.length) authorizeHelper(SCOPES) - }, [authorizeHelper]) - - const canRead = useMemo(() => hasCedarReadPermission(RESOURCE_ID), [hasCedarReadPermission]) - const canWrite = useMemo(() => hasCedarWritePermission(RESOURCE_ID), [hasCedarWritePermission]) + const { canRead, canWrite, canDelete } = usePermission(resourceId) if (!canRead) return return ( <> {canWrite && } + {canDelete && } ) } ``` +`usePermission` runs the authorization in its own `useEffect`, so the component never calls `authorizeHelper` directly. + +For a single child that should appear only under one action, [`Protected`](../app/cedarling/components/Protected.tsx) is the declarative form: + +```tsx +import { Protected } from '@/cedarling/components' +import { ADMIN_UI_RESOURCES, CEDAR_ACTIONS } from '@/cedarling/constants' +; + + +``` + Rules: -- Import the resource id (`ADMIN_UI_RESOURCES.Clients`). Never inline `'Clients'`: typos compile but always evaluate to `false`. -- Call `authorizeHelper` in `useEffect`, never in the render body. It dispatches Redux actions. -- Wrap `hasCedar*Permission` calls in `useMemo`. +- Match the action to the operation: read for viewing, write for add and edit, delete for delete. Gate the destination page and its confirm dialog on the same action as the button that opens them. +- Import the resource id from `ADMIN_UI_RESOURCES`. Never inline `'Clients'`: typos compile but always evaluate to `false`. +- Use `CEDAR_ACTIONS.READ` / `WRITE` / `DELETE`, never the bare strings. +- Use leaf import paths, not the top `@/cedarling` barrel. ## Adding a new permission check -1. Add the resource id to [`app/cedarling/utility/resources.ts`](../app/cedarling/utility/resources.ts): +1. Add the resource and its allowed actions to [`RESOURCE_ACTIONS`](../app/cedarling/constants/resourceCatalog.ts): ```ts - export const ADMIN_UI_RESOURCES = { + export const RESOURCE_ACTIONS = { // …existing - MyNewFeature: 'MyNewFeature', - } as const - ``` - -2. Add the scopes to [`app/cedarling/constants/resourceScopes.ts`](../app/cedarling/constants/resourceScopes.ts): - - ```ts - [ADMIN_UI_RESOURCES.MyNewFeature]: [ - { permission: MY_FEATURE_READ, resourceId: ADMIN_UI_RESOURCES.MyNewFeature }, - { permission: MY_FEATURE_WRITE, resourceId: ADMIN_UI_RESOURCES.MyNewFeature }, - ], + MyNewFeature: ['read', 'write'], + } as const satisfies Record ``` - `MY_FEATURE_READ` / `MY_FEATURE_WRITE` are the OAuth scope URLs from [`app/utils/PermChecker.ts`](../app/utils/PermChecker.ts). + `ADMIN_UI_RESOURCES.MyNewFeature` and `CEDAR_RESOURCE_SCOPES[MyNewFeature]` are derived automatically. Every action a component reads through `usePermission` (and every action used on a sidebar entry) must be listed here, otherwise that decision is always `false`. -3. Add the Cedar policy to **both** `policy-store-dev.json` and `policy-store-prod.json`. A policy in dev but not prod returns "deny" in production with no obvious error. +2. Add the Cedar policy to **both** `policy-store-dev.json` and `policy-store-prod.json`. A policy in dev but not prod returns "deny" in production with no obvious error. -4. Wire `useCedarling()` into the component as shown above. +3. Gate the component with `usePermission(ADMIN_UI_RESOURCES.MyNewFeature)` or `` as shown above. -5. Verify in the browser. Sign in as a user with the role that should have access. Confirm the page renders. Sign in as a user without it. Confirm the page is gated. +4. Verify in the browser. Sign in as a user with the role that should have access and confirm the page renders. Sign in as a user without it and confirm the page is gated. ## The `CEDARLING_BYPASS` sentinel -`CEDARLING_BYPASS` (`'CEDARLING_BYPASS'`, exported from [`app/cedarling/utility/resources.ts`](../app/cedarling/utility/resources.ts)) is a **production sentinel**, not a debug switch. Use it as the `resourceKey` on a menu item or route whose visibility should not depend on a Cedar decision. The sidebar code recognizes this exact value and short-circuits the permission check, returning the item unconditionally: +`CEDARLING_BYPASS` (`'CEDARLING_BYPASS'`, exported from [`app/cedarling/constants/resourceCatalog.ts`](../app/cedarling/constants/resourceCatalog.ts)) is a **production sentinel**, not a debug switch. Use it as the `resourceKey` on a menu item or route whose visibility should not depend on a Cedar decision. The sidebar code recognizes this exact value and short-circuits the permission check, returning the item unconditionally: ```ts // app/routes/Apps/Gluu/GluuAppSidebar.tsx @@ -234,7 +240,7 @@ Real production usage in [`plugins/admin/plugin-metadata.ts`](../plugins/admin/p { title: 'menus.health', path: ROUTES.ADMIN_HEALTH, - permission: PROPERTIES_READ, + action: CEDAR_ACTIONS.READ, resourceKey: CEDARLING_BYPASS, } ``` diff --git a/admin-ui/docs/onboarding.md b/admin-ui/docs/onboarding.md index 8094eda7b8..be8c0f1a19 100644 --- a/admin-ui/docs/onboarding.md +++ b/admin-ui/docs/onboarding.md @@ -97,7 +97,7 @@ No CI lint or test step. The hook is the only enforcement point. See [build-depl - [config-api.md](./config-api.md): how Orval + React Query talk to the Jans Config API. - [recipes.md](./recipes.md): adding a page, plugin, slice, etc. - [build-deploy.md](./build-deploy.md): Vite build, env injection, Jenkins, Husky. -- [testing.md](./testing.md): Jest unit tests, Playwright e2e. +- [testing.md](./testing.md): Jest unit tests. ## If something doesn't work diff --git a/admin-ui/docs/recipes.md b/admin-ui/docs/recipes.md index 98af35db5d..573aeb123a 100644 --- a/admin-ui/docs/recipes.md +++ b/admin-ui/docs/recipes.md @@ -26,12 +26,12 @@ plugins//components/hooks/useYourPageApi.ts # if needed const YourPage = createLazyRoute(() => import('./components/YourPage')) menus: [{ title: 'menus.your_page', icon: '', path: ROUTES.YOUR_PAGE, - permission: , resourceKey: ADMIN_UI_RESOURCES. }] + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES. }] routes: [{ component: YourPage, path: ROUTES.YOUR_PAGE, - permission: , resourceKey: ADMIN_UI_RESOURCES. }] + action: CEDAR_ACTIONS.READ, resourceKey: ADMIN_UI_RESOURCES. }] ``` -3. Gate the render with `useCedarling()`: see [Add an authorization check](#add-an-authorization-check). +3. Gate the render with `usePermission()`: see [Add an authorization check](#add-an-authorization-check). 4. Add the `menus.your_page` key to **all four** `app/locales/{en,es,fr,pt}/translation.json`. 5. Add a sibling test under `plugins//__tests__/components/YourPage.test.tsx`: see [testing.md](./testing.md). @@ -135,17 +135,17 @@ Rules: Full version in [cedarling.md](./cedarling.md). Short form: ```ts -const RESOURCE_ID = ADMIN_UI_RESOURCES. -const SCOPES = CEDAR_RESOURCE_SCOPES[RESOURCE_ID] +import { usePermission } from '@/cedarling/hooks/usePermission' +import { ADMIN_UI_RESOURCES } from '@/cedarling/utility' -const { authorizeHelper, hasCedarReadPermission, hasCedarWritePermission } = useCedarling() +const resourceId = ADMIN_UI_RESOURCES. -useEffect(() => { if (SCOPES?.length) authorizeHelper(SCOPES) }, [authorizeHelper]) -const canRead = useMemo(() => hasCedarReadPermission(RESOURCE_ID), [hasCedarReadPermission]) -const canWrite = useMemo(() => hasCedarWritePermission(RESOURCE_ID), [hasCedarWritePermission]) +const { canRead, canWrite, canDelete } = usePermission(resourceId) ``` -New resource → add it to `app/cedarling/utility/resources.ts` **and** the policy to both `policy-store-dev.json` and `policy-store-prod.json`. A missing prod policy returns "deny" with no error. +Match the action to the operation: read for viewing, write for add and edit, delete for delete. + +New resource → add it (with its allowed actions) to `RESOURCE_ACTIONS` in `app/cedarling/constants/resourceCatalog.ts` **and** the policy to both `policy-store-dev.json` and `policy-store-prod.json`. A missing prod policy returns "deny" with no error. ## Add an audit record diff --git a/admin-ui/docs/testing.md b/admin-ui/docs/testing.md index 8dff548f84..b54677955a 100644 --- a/admin-ui/docs/testing.md +++ b/admin-ui/docs/testing.md @@ -56,6 +56,7 @@ Each file runs in fresh jsdom. Setup runs before any test code: | ---------------------------------------------- | ------------------------------------------------------------------------------- | | `@janssenproject/cedarling_wasm.ts` | Replaces the WASM bundle (jsdom can't load it) | | `cedarlingHookBridge.ts` | Stub for `@/cedarling/hooks/useCedarling` so components don't hit the real auth | +| `cedarlingPermissionBridge.ts` | Stub for `@/cedarling/hooks/usePermission`; wraps `useCedarling` | | `fileMock.ts` / `styleMock.ts` | Inert stubs for image, font, `.css/.scss` imports | | `hmr.ts`, `utilities.ts`, `loadPluginMetadata` | Inert stubs for runtime-only modules that crash jsdom | @@ -70,13 +71,8 @@ import { render } from '@testing-library/react' import AppTestWrapper from 'Routes/Apps/Gluu/Tests/Components/AppTestWrapper' import YourPage from 'Plugins//components/YourPage' -jest.mock('@/cedarling', () => ({ - useCedarling: () => ({ - hasCedarReadPermission: () => true, - hasCedarWritePermission: () => true, - authorizeHelper: jest.fn(), - }), - ADMIN_UI_RESOURCES: { /* the keys your page reads */ }, +jest.mock('@/cedarling/hooks/usePermission', () => ({ + usePermission: () => ({ canRead: true, canWrite: true, canDelete: true }), })) it('renders', () => { @@ -85,8 +81,8 @@ it('renders', () => { }) ``` -- Don't grant blanket `true` for both read and write in every test. Flip them off in at least one to verify the gated UI actually disappears. -- Mock `useCedarling`, not the underlying tokens. Unmocked, Cedarling sees no tokens, returns `undefined`, and the page hides everything. +- Don't grant blanket `true` for read, write, and delete in every test. Flip them off in at least one to verify the gated UI actually disappears. +- Mock the hook the component uses: `usePermission` for a page, `useCedarling` for the sidebar or any multi-resource caller. Unmocked, Cedarling sees no tokens, returns `false`, and the page hides everything. ## Writing a listener / reducer test From ee446ec6c05d999e8a276217e0f48b4430efabf7 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Tue, 9 Jun 2026 18:48:56 +0500 Subject: [PATCH 05/24] cedarling reivew fixed Signed-off-by: faisalsiddique4400 --- .../Aliases/__tests__/Aliases.test.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx b/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx index c8b0d4e1ab..114fb1d8cf 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/Aliases/__tests__/Aliases.test.tsx @@ -68,19 +68,35 @@ describe('Aliases', () => { expect(screen.getByText('basic_auth')).toBeInTheDocument() }) - it('does not render the edit action when user lacks write permission', () => { + it('hides the edit action but keeps delete when user lacks write permission', () => { + jest.mocked(useAuthServerJsonPropertiesQuery).mockReturnValue({ + data: { acrMappings: { basic: 'basic_auth' } }, + isLoading: false, + isFetching: false, + } as Partial> as ReturnType< + typeof useAuthServerJsonPropertiesQuery + >) jest .mocked(useCedarling) .mockReturnValue(makeMockCedarling({ hasCedarWritePermission: jest.fn(() => false) })) render(, { wrapper: Wrapper }) expect(screen.queryByTitle(/edit/i)).not.toBeInTheDocument() + expect(screen.getByTitle(/delete/i)).toBeInTheDocument() }) - it('does not render the delete action when user lacks delete permission', () => { + it('hides the delete action but keeps edit when user lacks delete permission', () => { + jest.mocked(useAuthServerJsonPropertiesQuery).mockReturnValue({ + data: { acrMappings: { basic: 'basic_auth' } }, + isLoading: false, + isFetching: false, + } as Partial> as ReturnType< + typeof useAuthServerJsonPropertiesQuery + >) jest .mocked(useCedarling) .mockReturnValue(makeMockCedarling({ hasCedarDeletePermission: jest.fn(() => false) })) render(, { wrapper: Wrapper }) expect(screen.queryByTitle(/delete/i)).not.toBeInTheDocument() + expect(screen.getByTitle(/edit/i)).toBeInTheDocument() }) }) From ff816e941debbfec1ef4a253a919dfc736c26d1a Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Tue, 9 Jun 2026 22:43:57 +0500 Subject: [PATCH 06/24] leftover cleanup Signed-off-by: faisalsiddique4400 --- .../app/cedarling/components/Protected.tsx | 22 ------------------ admin-ui/app/cedarling/components/index.ts | 1 - admin-ui/app/cedarling/hooks/index.ts | 1 - admin-ui/app/cedarling/index.ts | 3 +-- admin-ui/app/cedarling/types/cedarTypes.ts | 8 ------- .../app/routes/Apps/Gluu/GluuCommitDialog.tsx | 23 ------------------- .../app/routes/Dashboards/DashboardPage.tsx | 4 ++-- admin-ui/app/utils/AppAuthProvider.tsx | 11 ++++----- .../components/Asset/assetTestUtils.tsx | 1 - .../__tests__/helpers/agamaTestUtils.tsx | 2 +- .../helpers/authenticationTestUtils.tsx | 2 +- .../Ssa/__tests__/helpers/ssaTestUtils.tsx | 2 +- 12 files changed, 11 insertions(+), 69 deletions(-) delete mode 100644 admin-ui/app/cedarling/components/Protected.tsx delete mode 100644 admin-ui/app/cedarling/components/index.ts diff --git a/admin-ui/app/cedarling/components/Protected.tsx b/admin-ui/app/cedarling/components/Protected.tsx deleted file mode 100644 index 117f475e22..0000000000 --- a/admin-ui/app/cedarling/components/Protected.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import { usePermission } from '@/cedarling/hooks/usePermission' -import { CEDAR_ACTIONS } from '@/cedarling/constants' -import type { ProtectedProps } from '@/cedarling/types' - -const Protected: React.FC = ({ - resource, - action = CEDAR_ACTIONS.READ, - fallback = null, - children, -}) => { - const { canRead, canWrite, canDelete } = usePermission(resource) - const allowed = - action === CEDAR_ACTIONS.DELETE - ? canDelete - : action === CEDAR_ACTIONS.WRITE - ? canWrite - : canRead - return <>{allowed ? children : fallback} -} - -export default Protected diff --git a/admin-ui/app/cedarling/components/index.ts b/admin-ui/app/cedarling/components/index.ts deleted file mode 100644 index c325d1f40f..0000000000 --- a/admin-ui/app/cedarling/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Protected } from './Protected' diff --git a/admin-ui/app/cedarling/hooks/index.ts b/admin-ui/app/cedarling/hooks/index.ts index 3165d52010..bb1dc8e5b0 100644 --- a/admin-ui/app/cedarling/hooks/index.ts +++ b/admin-ui/app/cedarling/hooks/index.ts @@ -1,2 +1 @@ export { useCedarling } from './useCedarling' -export { usePermission } from './usePermission' diff --git a/admin-ui/app/cedarling/index.ts b/admin-ui/app/cedarling/index.ts index 793b6e450e..c495a83e92 100644 --- a/admin-ui/app/cedarling/index.ts +++ b/admin-ui/app/cedarling/index.ts @@ -1,3 +1,2 @@ -export { useCedarling, usePermission } from './hooks' -export { Protected } from './components' +export { useCedarling } from './hooks' export type * from './types' diff --git a/admin-ui/app/cedarling/types/cedarTypes.ts b/admin-ui/app/cedarling/types/cedarTypes.ts index 45e87b884c..dd3fcea03b 100644 --- a/admin-ui/app/cedarling/types/cedarTypes.ts +++ b/admin-ui/app/cedarling/types/cedarTypes.ts @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react' import type { JsonValue } from 'Routes/Apps/Gluu/types/common' import { CEDAR_ACTIONS, RESOURCE_ACTIONS, CEDARLING_LOG_TYPE } from '@/cedarling/constants' @@ -10,13 +9,6 @@ export type ResourceScopeEntry = { } export type CedarlingLogType = (typeof CEDARLING_LOG_TYPE)[keyof typeof CEDARLING_LOG_TYPE] -export type ProtectedProps = { - resource: AdminUiFeatureResource - action?: CedarAction - fallback?: ReactNode - children: ReactNode -} - export type ITokenEntry = { mapping: string payload: string diff --git a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx index 2fcc2727a7..df5badbc25 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next' import { useTheme } from 'Context/theme/themeContext' import getThemeColor from '@/context/theme/config' import { THEME_DARK } from '@/context/theme/constants' -import PropTypes from 'prop-types' import { useAppSelector } from '@/redux/hooks' import Alert from '@mui/material/Alert' import { Close } from '@/components/icons' @@ -257,25 +256,3 @@ const GluuCommitDialog = ({ } export default GluuCommitDialog -GluuCommitDialog.propTypes = { - feature: PropTypes.string, - handler: PropTypes.func.isRequired, - modal: PropTypes.bool.isRequired, - onAccept: PropTypes.func.isRequired, - formik: PropTypes.object, - placeholderLabel: PropTypes.string, - label: PropTypes.string, - alertMessage: PropTypes.string, - alertSeverity: PropTypes.oneOf(['error', 'warning', 'info', 'success']), - inputType: PropTypes.oneOf([ - 'text', - 'textarea', - 'email', - 'password', - 'number', - 'tel', - 'url', - 'search', - ]), - isLicenseLabel: PropTypes.bool, -} diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.tsx b/admin-ui/app/routes/Dashboards/DashboardPage.tsx index 668954ab14..61f49fb565 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.tsx +++ b/admin-ui/app/routes/Dashboards/DashboardPage.tsx @@ -1,7 +1,7 @@ import { useState, useMemo, useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' import { useAppSelector, useAppDispatch } from '@/redux/hooks' -import { useMediaQuery } from 'react-responsive' +import useMediaQuery from '@mui/material/useMediaQuery' import type { Dayjs } from 'dayjs' import Grid from '@mui/material/Grid' import Paper from '@mui/material/Paper' @@ -42,7 +42,7 @@ import { } from '@/utils/dayjsUtils' const DASHBOARD_RESOURCE_ID = ADMIN_UI_RESOURCES.Dashboard -const MOBILE_MEDIA_QUERY = { maxWidth: 767 } +const MOBILE_MEDIA_QUERY = '(max-width:767px)' const DashboardPage = () => { const { t } = useTranslation() diff --git a/admin-ui/app/utils/AppAuthProvider.tsx b/admin-ui/app/utils/AppAuthProvider.tsx index e8d3b4fa8e..b404f487c7 100755 --- a/admin-ui/app/utils/AppAuthProvider.tsx +++ b/admin-ui/app/utils/AppAuthProvider.tsx @@ -1,8 +1,7 @@ import React, { useState, useEffect, useRef } from 'react' import ApiKeyRedirect from './ApiKeyRedirect' -import { useLocation } from 'react-router' +import { useLocation } from 'react-router-dom' import { NoHashQueryStringUtils, saveIssuer, getIssuer } from './TokenController' -import queryString from 'query-string' import { uuidv4 } from './Util' import { useAppSelector, useAppDispatch } from '@/redux/hooks' import SessionTimeout from 'Routes/Apps/Gluu/GluuSessionTimeout' @@ -81,8 +80,8 @@ const AppAuthProvider = ({ children }: Readonly) => { const hasDispatchedConfigCheck = useRef(false) useEffect(() => { - const params = queryString.parse(location.search) - const hasCallbackParams = !!(params.code && params.state) + const params = new URLSearchParams(location.search) + const hasCallbackParams = !!(params.get('code') && params.get('state')) if (hasCallbackParams && !getIssuer()) { window.history.replaceState({}, '', window.location.pathname) @@ -98,8 +97,8 @@ const AppAuthProvider = ({ children }: Readonly) => { }, [dispatch]) useEffect(() => { - const params = queryString.parse(location.search) - if (isConfigValid && !(params.code && params.state)) { + const params = new URLSearchParams(location.search) + if (isConfigValid && !(params.get('code') && params.get('state'))) { dispatch(checkLicensePresent(undefined)) } }, [isConfigValid]) diff --git a/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx b/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx index 9867698d36..d18f7240ac 100644 --- a/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx +++ b/admin-ui/plugins/admin/__tests__/components/Asset/assetTestUtils.tsx @@ -6,7 +6,6 @@ import type { Store } from '@reduxjs/toolkit' import AppTestWrapper from 'Routes/Apps/Gluu/Tests/Components/AppTestWrapper' import { SHARED_CEDAR_CONSTANTS as mockSHARED_CEDAR_CONSTANTS } from './assetCedarTestConstants' -export { SHARED_CEDAR_CONSTANTS } from './assetCedarTestConstants' jest.mock('Plugins/PluginReducersResolver', () => ({ __esModule: true, default: jest.fn() })) jest.mock('Plugins/PluginListenersResolver', () => ({ __esModule: true, default: jest.fn() })) diff --git a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx index e7e653a5cc..14d63c6603 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/helpers/agamaTestUtils.tsx @@ -129,7 +129,7 @@ export const createAgamaTestStore = (): Store => }), }) -export const createAgamaQueryClient = (): QueryClient => +const createAgamaQueryClient = (): QueryClient => new QueryClient({ defaultOptions: { queries: { retry: false } }, }) diff --git a/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx b/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx index bc73ea39ce..075862d8a4 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/__tests__/helpers/authenticationTestUtils.tsx @@ -127,7 +127,7 @@ export const createAuthenticationTestStore = (): Store => }), }) -export const createAuthenticationQueryClient = (): QueryClient => +const createAuthenticationQueryClient = (): QueryClient => new QueryClient({ defaultOptions: { queries: { retry: false } }, }) diff --git a/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx b/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx index d7cbc87e9b..896b66ad25 100644 --- a/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx +++ b/admin-ui/plugins/auth-server/components/Ssa/__tests__/helpers/ssaTestUtils.tsx @@ -125,7 +125,7 @@ export const createSsaTestStore = (): Store => }), }) -export const createSsaQueryClient = (): QueryClient => +const createSsaQueryClient = (): QueryClient => new QueryClient({ defaultOptions: { queries: { retry: false } }, }) From af85fa1a6b39ba8ee73ec721036008b34d38fbce Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Tue, 9 Jun 2026 23:01:36 +0500 Subject: [PATCH 07/24] chore(admin-ui): redundant dependencies and unscanned dead test code (#2874) Signed-off-by: faisalsiddique4400 --- .../app/components/Layout/LayoutContent.tsx | 4 -- .../app/components/Layout/LayoutSidebar.tsx | 6 --- .../Apps/Gluu/GluuRemovableInputRow.tsx | 18 ------- admin-ui/app/routes/Apps/Gluu/GluuTabs.tsx | 2 +- admin-ui/docs/testing.md | 6 ++- admin-ui/knip.json | 4 +- admin-ui/package.json | 5 -- .../admin/components/Assets/AssetForm.tsx | 2 +- .../admin/components/Webhook/WebhookForm.tsx | 2 +- .../auth-server/__tests__/api/setup.ts | 47 ------------------- .../__tests__/fixtures/mockAgamaProjects.ts | 20 -------- .../__tests__/fixtures/loggingTestData.ts | 2 +- .../Scopes/__tests__/fixtures/mockScopes.ts | 2 +- .../__tests__/fixtures/mockSessions.ts | 2 +- 14 files changed, 13 insertions(+), 109 deletions(-) delete mode 100644 admin-ui/plugins/auth-server/__tests__/api/setup.ts diff --git a/admin-ui/app/components/Layout/LayoutContent.tsx b/admin-ui/app/components/Layout/LayoutContent.tsx index 66350a4eff..33defc30f9 100755 --- a/admin-ui/app/components/Layout/LayoutContent.tsx +++ b/admin-ui/app/components/Layout/LayoutContent.tsx @@ -1,5 +1,4 @@ import React, { useContext, useEffect, useMemo, useRef } from 'react' -import PropTypes from 'prop-types' import { ThemeContext } from '@/context/theme/themeContext' import getThemeColor, { themeConfig } from '@/context/theme/config' import customColors, { getCustomColorsAsCssVars, getLoadingOverlayRgba } from '@/customColors' @@ -145,9 +144,6 @@ const LayoutContent: React.FC & { layoutPartName: string } = ) } -LayoutContent.propTypes = { - children: PropTypes.node as React.Validator, -} LayoutContent.layoutPartName = 'content' export { LayoutContent } diff --git a/admin-ui/app/components/Layout/LayoutSidebar.tsx b/admin-ui/app/components/Layout/LayoutSidebar.tsx index 2bfa6d8fbe..27b64f63a0 100755 --- a/admin-ui/app/components/Layout/LayoutSidebar.tsx +++ b/admin-ui/app/components/Layout/LayoutSidebar.tsx @@ -1,5 +1,4 @@ import React from 'react' -import PropTypes from 'prop-types' import clsx from 'clsx' import type { LayoutSidebarProps } from './types' @@ -12,11 +11,6 @@ const LayoutSidebar: React.FC & { layoutPartName: string } = return
{props.children}
} -LayoutSidebar.propTypes = { - children: PropTypes.node as React.Validator, - sidebarSlim: PropTypes.bool, - sidebarCollapsed: PropTypes.bool, -} LayoutSidebar.layoutPartName = 'sidebar' export { LayoutSidebar } diff --git a/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx b/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx index 4333f4aead..19822be18e 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx @@ -1,7 +1,6 @@ import { Input } from 'Components' import GluuLabel from './GluuLabel' import GluuToggle from 'Routes/Apps/Gluu/GluuToggle' -import PropTypes from 'prop-types' import type { FormikValues } from 'formik' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -106,21 +105,4 @@ const GluuRemovableInputRow = ({ ) } -GluuRemovableInputRow.propTypes = { - label: PropTypes.string, - name: PropTypes.string, - type: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - formik: PropTypes.object, - required: PropTypes.bool, - lsize: PropTypes.number, - rsize: PropTypes.number, - handler: PropTypes.func, - doc_category: PropTypes.string, - isDirect: PropTypes.bool, - isBoolean: PropTypes.bool, - hideRemoveButton: PropTypes.bool, - modifiedFields: PropTypes.object, - setModifiedFields: PropTypes.func, -} export default GluuRemovableInputRow diff --git a/admin-ui/app/routes/Apps/Gluu/GluuTabs.tsx b/admin-ui/app/routes/Apps/Gluu/GluuTabs.tsx index e53f0429cf..fd472ce118 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuTabs.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuTabs.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useState, type SyntheticEvent } import Tabs from '@mui/material/Tabs' import Tab from '@mui/material/Tab' import Box from '@mui/material/Box' -import { useLocation } from 'react-router' +import { useLocation } from 'react-router-dom' import { useAppNavigation } from '@/helpers/navigation' import { useTheme } from '@/context/theme/themeContext' import type { GluuTabsProps, NavigationTab, TabItem, TabPanelProps } from './types' diff --git a/admin-ui/docs/testing.md b/admin-ui/docs/testing.md index b54677955a..6773a848ed 100644 --- a/admin-ui/docs/testing.md +++ b/admin-ui/docs/testing.md @@ -108,4 +108,8 @@ For a listener, test its effect as a plain async function: pass a `jest.fn()` as ## Knip and tests -knip uses production imports to decide if a module is used. Test files don't count. A `jest.mock(...)` that _replaces_ a module doesn't keep the production file alive in knip's view. If knip flags a module you know is in use, check whether the test mocks it _instead of_ importing it. +knip scans test files, so unused fixtures, helpers, and exports inside `__tests__/` get flagged like any other dead code. + +Liveness still comes from production imports: a `jest.mock(...)` that _replaces_ a module doesn't keep the production file alive in knip's view. If knip flags a module you know is in use, check whether a test mocks it _instead of_ importing it. + +knip can't trace `jest.requireActual('')` (a dynamic string require), so a helper consumed only that way looks unused. List it in `knip.json` `entry`, as `plugins/user-claims/__tests__/cedarTestHelpers.ts` is. diff --git a/admin-ui/knip.json b/admin-ui/knip.json index a79227bf51..e60822b9a3 100644 --- a/admin-ui/knip.json +++ b/admin-ui/knip.json @@ -5,10 +5,10 @@ "plugins/PluginMenuResolver.ts", "plugins/PluginReducersResolver.ts", "plugins/PluginListenersResolver.ts", - "script/*.ts" + "script/*.ts", + "plugins/user-claims/__tests__/cedarTestHelpers.ts" ], "project": ["app/**/*.{ts,tsx,js,jsx}", "plugins/**/*.{ts,tsx,js,jsx}", "script/**/*.ts"], - "ignore": ["**/__tests__/**"], "ignoreDependencies": ["babel-preset-jest", "markdownlint-cli2"], "rules": { "enumMembers": "off", diff --git a/admin-ui/package.json b/admin-ui/package.json index 5a0765f5ba..f48387a56d 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -117,8 +117,6 @@ "jszip": "^3.0.0", "jwt-decode": "^4.0.0", "lodash": "^4.0.0", - "prop-types": "^15.0.0", - "query-string": "^9.0.0", "react": "^18.0.0", "react-ace": "^14.0.0", "react-bootstrap-typeahead": "^6.0.0", @@ -129,14 +127,11 @@ "react-idle-timer": "^5.0.0", "react-json-view-lite": "^2.0.0", "react-redux": "^9.0.0", - "react-responsive": "^10.0.0", - "react-router": "^7.0.0", "react-router-dom": "^7.0.0", "react-toastify": "^11.0.0", "react-toggle": "^4.0.0", "react-tooltip": "^6.0.0", "recharts": "^3.0.0", - "redux": "^5.0.1", "redux-persist": "^6.0.0", "tss-react": "^4.0.0", "yup": "^1.0.0" diff --git a/admin-ui/plugins/admin/components/Assets/AssetForm.tsx b/admin-ui/plugins/admin/components/Assets/AssetForm.tsx index 0755be228e..f84cf50020 100644 --- a/admin-ui/plugins/admin/components/Assets/AssetForm.tsx +++ b/admin-ui/plugins/admin/components/Assets/AssetForm.tsx @@ -11,7 +11,7 @@ import GluuText from 'Routes/Apps/Gluu/GluuText' import Toggle from 'react-toggle' import { useTranslation } from 'react-i18next' import { useAssetServices, useCreateAssetWithAudit, useUpdateAssetWithAudit } from './hooks' -import { useParams } from 'react-router' +import { useParams } from 'react-router-dom' import { useGetAssetByInum } from 'JansConfigApi' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import { ASSET } from 'Utils/ApiResources' diff --git a/admin-ui/plugins/admin/components/Webhook/WebhookForm.tsx b/admin-ui/plugins/admin/components/Webhook/WebhookForm.tsx index 060559e164..8c7355c867 100644 --- a/admin-ui/plugins/admin/components/Webhook/WebhookForm.tsx +++ b/admin-ui/plugins/admin/components/Webhook/WebhookForm.tsx @@ -1,5 +1,5 @@ import React, { Suspense, useCallback, useState, useEffect, useMemo, memo } from 'react' -import { useParams } from 'react-router' +import { useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useFormik } from 'formik' import isEqual from 'lodash/isEqual' diff --git a/admin-ui/plugins/auth-server/__tests__/api/setup.ts b/admin-ui/plugins/auth-server/__tests__/api/setup.ts deleted file mode 100644 index 640a283b8c..0000000000 --- a/admin-ui/plugins/auth-server/__tests__/api/setup.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { devLogger } from '@/utils/devLogger' -import { fetchApiTokenWithDefaultScopes } from 'Redux/api/backend-api' - -type TestGlobal = { - issuer?: string - token?: string -} - -const getTestGlobal = (): TestGlobal => globalThis as TestGlobal & typeof globalThis - -export const beforeAllAsync = async ( - formInitState: (token: string, issuer: string) => void, -): Promise => { - const g = getTestGlobal() - if (!g.issuer && !g.token) { - try { - const accessToken = await fetchApiTokenWithDefaultScopes() - g.token = accessToken.access_token - formInitState(accessToken.access_token, '') - } catch (error) { - formInitState(g.token ?? '', g.issuer ?? '') - const message = error instanceof Error ? error.message : String(error) - devLogger.error(message) - throw new Error('Error during beforeAllAsync: ' + message, { cause: error }) - } - } else { - devLogger.log('Issuer and token already available.') - } -} - -export const authReducerInit = (token: string, issuer: string) => { - return { - userinfo_jwt: token, - config: { - clientId: '', - authServerHost: 'https://admin-ui-test.gluu.org', - }, - location: { - IPv4: '', - }, - userinfo: {}, - issuer, - token: { - access_token: token, - }, - } -} diff --git a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/fixtures/mockAgamaProjects.ts b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/fixtures/mockAgamaProjects.ts index 937c372535..afbc2ee035 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/fixtures/mockAgamaProjects.ts +++ b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/__tests__/fixtures/mockAgamaProjects.ts @@ -1,5 +1,4 @@ import type { Deployment } from 'JansConfigApi' -import type { AgamaProject } from '../../types' export const mockDeployments: Deployment[] = [ { @@ -34,22 +33,3 @@ export const mockDeployments: Deployment[] = [ }, }, ] - -export const mockAgamaProjects: AgamaProject[] = [ - { - ...mockDeployments[0], - deployed_on: '01/15/24, 10:30 AM', - type: 'community', - status: 'Processed', - error: 'No', - } as AgamaProject, - { - ...mockDeployments[1], - deployed_on: '-', - type: 'agama', - status: 'Pending', - error: '', - } as AgamaProject, -] - -export default mockAgamaProjects diff --git a/admin-ui/plugins/auth-server/components/Logging/__tests__/fixtures/loggingTestData.ts b/admin-ui/plugins/auth-server/components/Logging/__tests__/fixtures/loggingTestData.ts index 93022264f0..16469cafaa 100644 --- a/admin-ui/plugins/auth-server/components/Logging/__tests__/fixtures/loggingTestData.ts +++ b/admin-ui/plugins/auth-server/components/Logging/__tests__/fixtures/loggingTestData.ts @@ -8,7 +8,7 @@ export const mockLoggingConfig: Logging = { enabledOAuthAuditLogging: false, } -export const LOGGING_TEST_PERMISSIONS = [ +const LOGGING_TEST_PERMISSIONS = [ 'https://jans.io/oauth/config/logging.readonly', 'https://jans.io/oauth/config/logging.write', 'https://jans.io/oauth/config/logging.delete', diff --git a/admin-ui/plugins/auth-server/components/Scopes/__tests__/fixtures/mockScopes.ts b/admin-ui/plugins/auth-server/components/Scopes/__tests__/fixtures/mockScopes.ts index 709671454c..6f3d6e6c40 100644 --- a/admin-ui/plugins/auth-server/components/Scopes/__tests__/fixtures/mockScopes.ts +++ b/admin-ui/plugins/auth-server/components/Scopes/__tests__/fixtures/mockScopes.ts @@ -1,6 +1,6 @@ import type { Scope } from 'Plugins/auth-server/components/Scopes/types' -export const scopes: Partial[] = [ +const scopes: Partial[] = [ { dn: 'inum=F0C4,ou=scopes,o=jans', inum: 'F0C4', diff --git a/admin-ui/plugins/auth-server/components/Sessions/__tests__/fixtures/mockSessions.ts b/admin-ui/plugins/auth-server/components/Sessions/__tests__/fixtures/mockSessions.ts index e655ff197a..9fad4771b1 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/__tests__/fixtures/mockSessions.ts +++ b/admin-ui/plugins/auth-server/components/Sessions/__tests__/fixtures/mockSessions.ts @@ -1,6 +1,6 @@ import { AUTHENTICATED_SESSION_STATE, type Session } from '../../types' -export const mockSessions: Session[] = [ +const mockSessions: Session[] = [ { id: 'session-001', userDn: 'inum=baea0439-d11f-4fd7-a349-b9cd80fda871,ou=people,o=jans', From 514b3174981d6ced0ca00cb56f0c175c51c60827 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Wed, 10 Jun 2026 20:19:37 +0500 Subject: [PATCH 08/24] further clenaup Signed-off-by: faisalsiddique4400 --- .../app/hooks/__tests__/useFileDrop.test.tsx | 115 ++++++++++ .../app/hooks/__tests__/useIdleTimer.test.ts | 88 +++++++ admin-ui/app/hooks/useFileDrop.ts | 214 ++++++++++++++++++ admin-ui/app/hooks/useIdleTimer.ts | 101 +++++++++ .../routes/Apps/Gluu/GluuSessionTimeout.tsx | 26 +-- .../app/routes/Apps/Gluu/GluuUploadFile.tsx | 4 +- .../Gluu/types/GluuComponentPropsTypes.ts | 2 +- admin-ui/app/styles/plugins/plugins.scss | 1 - admin-ui/app/utils/AppAuthProvider.tsx | 4 +- admin-ui/app/utils/UploadSSA.tsx | 4 +- .../app/utils/__tests__/jwtDecode.test.ts | 39 ++++ admin-ui/app/utils/__tests__/zip.test.ts | 134 +++++++++++ admin-ui/app/utils/jwtDecode.ts | 37 +++ admin-ui/app/utils/regex.ts | 4 + admin-ui/app/utils/zip.ts | 90 ++++++++ admin-ui/jest.config.ts | 4 +- admin-ui/jest/setup.ts | 32 +-- admin-ui/package.json | 4 - .../Authentication/AgamaFlows/AgamaFlows.tsx | 12 +- admin-ui/vite.config.ts | 22 +- 20 files changed, 869 insertions(+), 68 deletions(-) create mode 100644 admin-ui/app/hooks/__tests__/useFileDrop.test.tsx create mode 100644 admin-ui/app/hooks/__tests__/useIdleTimer.test.ts create mode 100644 admin-ui/app/hooks/useFileDrop.ts create mode 100644 admin-ui/app/hooks/useIdleTimer.ts create mode 100644 admin-ui/app/utils/__tests__/jwtDecode.test.ts create mode 100644 admin-ui/app/utils/__tests__/zip.test.ts create mode 100644 admin-ui/app/utils/jwtDecode.ts create mode 100644 admin-ui/app/utils/zip.ts diff --git a/admin-ui/app/hooks/__tests__/useFileDrop.test.tsx b/admin-ui/app/hooks/__tests__/useFileDrop.test.tsx new file mode 100644 index 0000000000..d3771e12c2 --- /dev/null +++ b/admin-ui/app/hooks/__tests__/useFileDrop.test.tsx @@ -0,0 +1,115 @@ +import { fireEvent, render } from '@testing-library/react' +import { useFileDrop } from '../useFileDrop' +import type { Accept, FileRejection } from '../useFileDrop' + +type HarnessProps = { + onDrop: (files: File[]) => void + accept?: Accept + maxSize?: number + onDropRejected?: (rejections: FileRejection[]) => void +} + +const Harness = (props: HarnessProps) => { + const { getInputProps } = useFileDrop(props) + return +} + +const changeFiles = (input: HTMLElement, files: File[]) => { + fireEvent.change(input, { target: { files } }) +} + +describe('useFileDrop', () => { + it('passes a single accepted file to onDrop', () => { + const onDrop = jest.fn() + const { getByTestId } = render( + , + ) + const file = new File(['payload'], 'token.jwt', { type: 'application/jwt' }) + + changeFiles(getByTestId('file-input'), [file]) + + expect(onDrop).toHaveBeenCalledWith([file]) + }) + + it('accepts files by extension when the browser reports an empty mime type', () => { + const onDrop = jest.fn() + const { getByTestId } = render( + , + ) + const file = new File(['x'], 'project.gama', { type: '' }) + + changeFiles(getByTestId('file-input'), [file]) + + expect(onDrop).toHaveBeenCalledWith([file]) + }) + + it('rejects files larger than maxSize with a file-too-large error', () => { + const onDrop = jest.fn() + const onDropRejected = jest.fn() + const { getByTestId } = render( + , + ) + const file = new File([new Uint8Array(100)], 'big.zip', { type: 'application/zip' }) + + changeFiles(getByTestId('file-input'), [file]) + + expect(onDrop).toHaveBeenCalledWith([]) + expect(onDropRejected).toHaveBeenCalledTimes(1) + const [[rejections]] = onDropRejected.mock.calls + expect(rejections[0].file).toBe(file) + expect(rejections[0].errors[0].code).toBe('file-too-large') + }) + + it('configures the input as a single-file picker', () => { + const { getByTestId } = render( + , + ) + const input = getByTestId('file-input') as HTMLInputElement + + expect(input.multiple).toBe(false) + expect(input.type).toBe('file') + expect(input.accept).toContain('.txt') + }) + + it('stops the hidden input click from bubbling to the root', () => { + const rootClick = jest.fn() + const RootHarness = () => { + const { getRootProps, getInputProps } = useFileDrop({ onDrop: jest.fn() }) + return ( +
+ +
+ ) + } + const { getByTestId } = render() + + fireEvent.click(getByTestId('root-input')) + + expect(rootClick).not.toHaveBeenCalled() + }) + + it('keeps isDragActive true until the matching number of drag-leaves', () => { + const DragHarness = () => { + const { getRootProps, isDragActive } = useFileDrop({ onDrop: jest.fn() }) + return ( +
+ {String(isDragActive)} + child +
+ ) + } + const { getByTestId } = render() + const root = getByTestId('root') + const child = getByTestId('child') + + fireEvent.dragEnter(root) + fireEvent.dragEnter(child) + expect(getByTestId('active').textContent).toBe('true') + + fireEvent.dragLeave(child) + expect(getByTestId('active').textContent).toBe('true') + + fireEvent.dragLeave(root) + expect(getByTestId('active').textContent).toBe('false') + }) +}) diff --git a/admin-ui/app/hooks/__tests__/useIdleTimer.test.ts b/admin-ui/app/hooks/__tests__/useIdleTimer.test.ts new file mode 100644 index 0000000000..c7659db66c --- /dev/null +++ b/admin-ui/app/hooks/__tests__/useIdleTimer.test.ts @@ -0,0 +1,88 @@ +import { act, renderHook } from '@testing-library/react' +import { useIdleTimer } from '../useIdleTimer' + +const fireActivity = (eventName = 'keydown') => { + act(() => { + window.dispatchEvent(new Event(eventName)) + }) +} + +describe('useIdleTimer', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + it('fires onIdle after the timeout elapses', () => { + const onIdle = jest.fn() + const { result } = renderHook(() => useIdleTimer({ timeout: 1000, onIdle })) + + expect(result.current.isIdle).toBe(false) + act(() => { + jest.advanceTimersByTime(1000) + }) + + expect(onIdle).toHaveBeenCalledTimes(1) + expect(result.current.isIdle).toBe(true) + }) + + it('resets the timer on activity so onIdle does not fire early', () => { + const onIdle = jest.fn() + renderHook(() => useIdleTimer({ timeout: 1000, onIdle })) + + act(() => { + jest.advanceTimersByTime(900) + }) + fireActivity() + act(() => { + jest.advanceTimersByTime(900) + }) + + expect(onIdle).not.toHaveBeenCalled() + }) + + it('fires onActive when activity follows an idle state', () => { + const onActive = jest.fn() + const { result } = renderHook(() => useIdleTimer({ timeout: 1000, onActive })) + + act(() => { + jest.advanceTimersByTime(1000) + }) + expect(result.current.isIdle).toBe(true) + + fireActivity() + expect(onActive).toHaveBeenCalledTimes(1) + expect(result.current.isIdle).toBe(false) + }) + + it('does not fire onIdle when disabled', () => { + const onIdle = jest.fn() + renderHook(() => useIdleTimer({ timeout: 1000, onIdle, disabled: true })) + + act(() => { + jest.advanceTimersByTime(5000) + }) + + expect(onIdle).not.toHaveBeenCalled() + }) + + it('collapses rapid activity within the debounce window', () => { + const onActive = jest.fn() + const { result } = renderHook(() => useIdleTimer({ timeout: 1000, onActive, debounce: 250 })) + + act(() => { + jest.advanceTimersByTime(1000) + }) + expect(result.current.isIdle).toBe(true) + + fireActivity() + fireActivity() + fireActivity() + + expect(onActive).toHaveBeenCalledTimes(1) + }) +}) diff --git a/admin-ui/app/hooks/useFileDrop.ts b/admin-ui/app/hooks/useFileDrop.ts new file mode 100644 index 0000000000..df9a0ad960 --- /dev/null +++ b/admin-ui/app/hooks/useFileDrop.ts @@ -0,0 +1,214 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import type { + ChangeEvent, + DragEvent, + HTMLAttributes, + InputHTMLAttributes, + KeyboardEvent, + MouseEvent, + Ref, +} from 'react' + +export type Accept = Record +export type FileError = { code: string; message: string } +export type FileRejection = { file: File; errors: FileError[] } + +type UseFileDropOptions = { + onDrop: (acceptedFiles: File[]) => void + accept?: Accept + maxSize?: number + disabled?: boolean + onDropRejected?: (fileRejections: FileRejection[]) => void +} + +type RootProps = HTMLAttributes & { + onClick: (event: MouseEvent) => void + onKeyDown: (event: KeyboardEvent) => void + onDrop: (event: DragEvent) => void + onDragOver: (event: DragEvent) => void + onDragEnter: (event: DragEvent) => void + onDragLeave: (event: DragEvent) => void +} + +type InputProps = InputHTMLAttributes & { + ref: Ref +} + +type UseFileDropResult = { + getRootProps: (props?: HTMLAttributes) => RootProps + getInputProps: (props?: InputHTMLAttributes) => InputProps + isDragActive: boolean + open: () => void +} + +const ERROR_FILE_INVALID_TYPE = 'file-invalid-type' +const ERROR_FILE_TOO_LARGE = 'file-too-large' + +const matchesMimePattern = (fileType: string, pattern: string): boolean => { + if (pattern === fileType) { + return true + } + if (pattern.endsWith('/*')) { + return fileType.startsWith(pattern.slice(0, -1)) + } + return false +} + +const isAccepted = (file: File, accept?: Accept): boolean => { + if (!accept || Object.keys(accept).length === 0) { + return true + } + const fileName = file.name.toLowerCase() + return Object.entries(accept).some(([mimePattern, extensions]) => { + if (file.type && matchesMimePattern(file.type, mimePattern)) { + return true + } + return extensions.some((extension) => fileName.endsWith(extension.toLowerCase())) + }) +} + +const buildAcceptAttribute = (accept?: Accept): string | undefined => { + if (!accept) { + return undefined + } + const tokens = Object.entries(accept).flatMap(([mimePattern, extensions]) => [ + mimePattern, + ...extensions, + ]) + return tokens.length > 0 ? tokens.join(',') : undefined +} + +const validateFile = (file: File, accept?: Accept, maxSize?: number): FileError[] => { + const errors: FileError[] = [] + if (!isAccepted(file, accept)) { + errors.push({ code: ERROR_FILE_INVALID_TYPE, message: 'File type is not accepted' }) + } + if (maxSize != null && file.size > maxSize) { + errors.push({ code: ERROR_FILE_TOO_LARGE, message: `File is larger than ${maxSize} bytes` }) + } + return errors +} + +export const useFileDrop = ({ + onDrop, + accept, + maxSize, + disabled = false, + onDropRejected, +}: UseFileDropOptions): UseFileDropResult => { + const inputRef = useRef(null) + const dragDepthRef = useRef(0) + const [isDragActive, setIsDragActive] = useState(false) + + useEffect(() => { + const preventDefault = (event: Event) => event.preventDefault() + document.addEventListener('dragover', preventDefault, false) + document.addEventListener('drop', preventDefault, false) + return () => { + document.removeEventListener('dragover', preventDefault) + document.removeEventListener('drop', preventDefault) + } + }, []) + + const processFiles = useCallback( + (fileList: FileList | null) => { + const files = fileList ? Array.from(fileList) : [] + const accepted: File[] = [] + const rejected: FileRejection[] = [] + for (const file of files) { + const errors = validateFile(file, accept, maxSize) + if (errors.length === 0) { + accepted.push(file) + } else { + rejected.push({ file, errors }) + } + } + if (rejected.length > 0) { + onDropRejected?.(rejected) + } + onDrop(accepted) + }, + [accept, maxSize, onDrop, onDropRejected], + ) + + const open = useCallback(() => { + if (!disabled) { + inputRef.current?.click() + } + }, [disabled]) + + const getRootProps = useCallback( + (props: HTMLAttributes = {}): RootProps => ({ + ...props, + role: 'presentation', + tabIndex: disabled ? -1 : 0, + onClick: (event: MouseEvent) => { + props.onClick?.(event) + open() + }, + onKeyDown: (event: KeyboardEvent) => { + props.onKeyDown?.(event) + if (event.target !== event.currentTarget) { + return + } + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault() + open() + } + }, + onDrop: (event: DragEvent) => { + event.preventDefault() + dragDepthRef.current = 0 + setIsDragActive(false) + if (!disabled) { + processFiles(event.dataTransfer.files) + } + }, + onDragOver: (event: DragEvent) => { + event.preventDefault() + }, + onDragEnter: (event: DragEvent) => { + event.preventDefault() + dragDepthRef.current += 1 + if (!disabled) { + setIsDragActive(true) + } + }, + onDragLeave: (event: DragEvent) => { + event.preventDefault() + dragDepthRef.current -= 1 + if (dragDepthRef.current <= 0) { + dragDepthRef.current = 0 + setIsDragActive(false) + } + }, + }), + [open, disabled, processFiles], + ) + + const getInputProps = useCallback( + (props: InputHTMLAttributes = {}): InputProps => ({ + ...props, + ref: inputRef, + type: 'file', + multiple: false, + accept: buildAcceptAttribute(accept), + disabled, + style: { display: 'none', ...props.style }, + onClick: (event: MouseEvent) => { + props.onClick?.(event) + event.stopPropagation() + }, + onChange: (event: ChangeEvent) => { + props.onChange?.(event) + processFiles(event.target.files) + event.target.value = '' + }, + }), + [accept, disabled, processFiles], + ) + + return { getRootProps, getInputProps, isDragActive, open } +} + +export default useFileDrop diff --git a/admin-ui/app/hooks/useIdleTimer.ts b/admin-ui/app/hooks/useIdleTimer.ts new file mode 100644 index 0000000000..497aad1a6b --- /dev/null +++ b/admin-ui/app/hooks/useIdleTimer.ts @@ -0,0 +1,101 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +type UseIdleTimerOptions = { + timeout: number + onIdle?: () => void + onActive?: () => void + debounce?: number + events?: string[] + disabled?: boolean +} + +type UseIdleTimerResult = { + reset: () => void + isIdle: boolean +} + +const DEFAULT_EVENTS = [ + 'mousemove', + 'mousedown', + 'keydown', + 'wheel', + 'touchstart', + 'touchmove', + 'visibilitychange', +] + +const eventTarget = (eventName: string): Window | Document => + eventName === 'visibilitychange' ? document : window + +export const useIdleTimer = ({ + timeout, + onIdle, + onActive, + debounce = 0, + events = DEFAULT_EVENTS, + disabled = false, +}: UseIdleTimerOptions): UseIdleTimerResult => { + const [isIdle, setIsIdle] = useState(false) + const onIdleRef = useRef(onIdle) + const onActiveRef = useRef(onActive) + const isIdleRef = useRef(false) + const timeoutIdRef = useRef | null>(null) + const lastActivityRef = useRef(Number.NEGATIVE_INFINITY) + + onIdleRef.current = onIdle + onActiveRef.current = onActive + + const clearTimer = useCallback(() => { + if (timeoutIdRef.current !== null) { + clearTimeout(timeoutIdRef.current) + timeoutIdRef.current = null + } + }, []) + + const startTimer = useCallback(() => { + clearTimer() + timeoutIdRef.current = setTimeout(() => { + isIdleRef.current = true + setIsIdle(true) + onIdleRef.current?.() + }, timeout) + }, [clearTimer, timeout]) + + const reset = useCallback(() => { + if (isIdleRef.current) { + isIdleRef.current = false + setIsIdle(false) + onActiveRef.current?.() + } + startTimer() + }, [startTimer]) + + useEffect(() => { + if (disabled) { + clearTimer() + return undefined + } + startTimer() + const handleActivity = () => { + const now = Date.now() + if (debounce > 0 && now - lastActivityRef.current < debounce) { + return + } + lastActivityRef.current = now + reset() + } + events.forEach((eventName) => + eventTarget(eventName).addEventListener(eventName, handleActivity, { passive: true }), + ) + return () => { + clearTimer() + events.forEach((eventName) => + eventTarget(eventName).removeEventListener(eventName, handleActivity), + ) + } + }, [disabled, debounce, events, reset, startTimer, clearTimer]) + + return { reset, isIdle } +} + +export default useIdleTimer diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx index 43d63945ef..79f280c76b 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import SessionTimeoutDialog from './GluuSessionTimeoutDialog' -import { withIdleTimer, type IIdleTimer } from 'react-idle-timer' +import { useIdleTimer } from '@/hooks/useIdleTimer' import { useAppDispatch, useAppSelector } from '@/redux/hooks' import { auditLogoutLogs } from 'Redux/features/sessionSlice' import { useAppNavigation, ROUTES } from '@/helpers/navigation' @@ -14,10 +14,6 @@ const COUNTDOWN_SECONDS = 10 const DEFAULT_TIMEOUT_MINS = 5 const IDLE_TO_MODAL_DELAY = 1000 -const IdleTimerComponent = ({ children }: IIdleTimer & { children?: React.ReactNode }) => - (children as React.ReactElement) ?? null -const IdleTimer = withIdleTimer(IdleTimerComponent) - const SessionTimeout = ({ isAuthenticated }: SessionTimeoutProps) => { const [timeoutModalOpen, setTimeoutModalOpen] = useState(false) const [timeoutCountdown, setTimeoutCountdown] = useState(0) @@ -111,15 +107,19 @@ const SessionTimeout = ({ isAuthenticated }: SessionTimeoutProps) => { } }, [logoutAuditSucceeded, navigateToRoute]) + const { reset } = useIdleTimer({ + timeout: sessionTimeout * 60 * 1000, + onActive, + onIdle, + debounce: 250, + }) + + useEffect(() => { + reset() + }, [sessionTimeout, isAuthenticated, reset]) + return ( <> - = ({ getRootProps: getRootProps1, getInputProps: getInputProps1, isDragActive: isDragActive1, - } = useDropzone({ + } = useFileDrop({ onDrop: handleDrop, accept, disabled, diff --git a/admin-ui/app/routes/Apps/Gluu/types/GluuComponentPropsTypes.ts b/admin-ui/app/routes/Apps/Gluu/types/GluuComponentPropsTypes.ts index 0e59e50256..d0f258668f 100644 --- a/admin-ui/app/routes/Apps/Gluu/types/GluuComponentPropsTypes.ts +++ b/admin-ui/app/routes/Apps/Gluu/types/GluuComponentPropsTypes.ts @@ -1,7 +1,7 @@ import type { CSSProperties, ReactNode, HTMLAttributes } from 'react' import type { AlertProps } from '@mui/material/Alert' import type { FormikProps } from 'formik' -import type { Accept } from 'react-dropzone' +import type { Accept } from '@/hooks/useFileDrop' import type { JsonPatch } from 'JansConfigApi' import type { JsonValue } from './common' diff --git a/admin-ui/app/styles/plugins/plugins.scss b/admin-ui/app/styles/plugins/plugins.scss index 51f80c3e04..8ca2fb781a 100755 --- a/admin-ui/app/styles/plugins/plugins.scss +++ b/admin-ui/app/styles/plugins/plugins.scss @@ -1,4 +1,3 @@ @import "react-toastify"; @import "react-toggle"; @import "react-bootstrap-typeahead"; -@import "react-dropzone"; diff --git a/admin-ui/app/utils/AppAuthProvider.tsx b/admin-ui/app/utils/AppAuthProvider.tsx index b404f487c7..78e2f2c8b7 100755 --- a/admin-ui/app/utils/AppAuthProvider.tsx +++ b/admin-ui/app/utils/AppAuthProvider.tsx @@ -37,7 +37,7 @@ import { type FetchUserInfoResult, } from 'Redux/api/backend-api' import { useTranslation } from 'react-i18next' -import { jwtDecode } from 'jwt-decode' +import { decodeJwt } from '@/utils/jwtDecode' import type { UserInfo } from '@/redux/features/types/authTypes' import type { OAuthConfig, AppAuthProviderProps } from '@/utils/types' import { buildSafeLogoutUrl } from '@/utils/urlSecurity' @@ -232,7 +232,7 @@ const AppAuthProvider = ({ children }: Readonly) => { if (value === -1) return const ujwt = value - const decoded = jwtDecode(ujwt) + const decoded = decodeJwt(ujwt) dispatch( getUserInfoResponse({ userinfo: decoded, diff --git a/admin-ui/app/utils/UploadSSA.tsx b/admin-ui/app/utils/UploadSSA.tsx index 0c0de13225..28d144ce7d 100644 --- a/admin-ui/app/utils/UploadSSA.tsx +++ b/admin-ui/app/utils/UploadSSA.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { Container } from 'Components' -import { useDropzone } from 'react-dropzone' +import { useFileDrop } from '@/hooks/useFileDrop' import logo from 'Images/logos/logo192.png' import { ThemeContext } from 'Context/theme/themeContext' import { useTranslation } from 'react-i18next' @@ -69,7 +69,7 @@ const UploadSSA = () => { getRootProps: getRootProps1, getInputProps: getInputProps1, isDragActive: isDragActive1, - } = useDropzone({ + } = useFileDrop({ onDrop: onDrop, accept: { 'application/jwt': ['.jwt'], diff --git a/admin-ui/app/utils/__tests__/jwtDecode.test.ts b/admin-ui/app/utils/__tests__/jwtDecode.test.ts new file mode 100644 index 0000000000..3bc12423d1 --- /dev/null +++ b/admin-ui/app/utils/__tests__/jwtDecode.test.ts @@ -0,0 +1,39 @@ +import { decodeJwt } from '../jwtDecode' + +const toJwt = (payload: Record): string => { + const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url') + const body = Buffer.from(JSON.stringify(payload)).toString('base64url') + return `${header}.${body}.` +} + +describe('decodeJwt', () => { + it('decodes a standard payload', () => { + const token = toJwt({ sub: '123', name: 'Jane', admin: true }) + expect(decodeJwt(token)).toEqual({ sub: '123', name: 'Jane', admin: true }) + }) + + it('decodes multibyte UTF-8 values', () => { + const token = toJwt({ name: 'Ünîcödé 名前 😀' }) + expect(decodeJwt<{ name: string }>(token).name).toBe('Ünîcödé 名前 😀') + }) + + it('decodes base64url payloads that need padding', () => { + const token = toJwt({ a: 1 }) + const body = token.split('.')[1] + expect(body.length % 4).not.toBe(0) + expect(decodeJwt(token)).toEqual({ a: 1 }) + }) + + it('throws when the value has fewer than two segments', () => { + expect(() => decodeJwt('not-a-jwt')).toThrow('not a valid JWT') + }) + + it('throws when the payload segment is not valid base64url', () => { + expect(() => decodeJwt('aaa.@@@@.ccc')).toThrow('not valid base64url') + }) + + it('throws when the payload is not valid JSON', () => { + const body = Buffer.from('not json').toString('base64url') + expect(() => decodeJwt(`aaa.${body}.ccc`)).toThrow('not valid JSON') + }) +}) diff --git a/admin-ui/app/utils/__tests__/zip.test.ts b/admin-ui/app/utils/__tests__/zip.test.ts new file mode 100644 index 0000000000..15d85a4b18 --- /dev/null +++ b/admin-ui/app/utils/__tests__/zip.test.ts @@ -0,0 +1,134 @@ +/** @jest-environment node */ +import { deflateRawSync } from 'node:zlib' +import { readZip } from '../zip' + +const STORED_CONTENT = '{"stored":true}' +const DEFLATED_CONTENT = 'deflate me '.repeat(50) +const NESTED_CONTENT = '{"inner":1}' + +type Entry = { name: string; content: string; method: 'store' | 'deflate' } + +const buildLocalHeader = (entry: Entry, data: Uint8Array, raw: Uint8Array, name: Uint8Array) => { + const method = entry.method === 'store' || raw.length === 0 ? 0 : 8 + const header = new Uint8Array(30 + name.length + data.length) + const view = new DataView(header.buffer) + view.setUint32(0, 0x04034b50, true) + view.setUint16(4, 20, true) + view.setUint16(8, method, true) + view.setUint32(18, data.length, true) + view.setUint32(22, raw.length, true) + view.setUint16(26, name.length, true) + header.set(name, 30) + header.set(data, 30 + name.length) + return header +} + +const buildCentralHeader = ( + entry: Entry, + data: Uint8Array, + raw: Uint8Array, + name: Uint8Array, + localOffset: number, +) => { + const method = entry.method === 'store' || raw.length === 0 ? 0 : 8 + const header = new Uint8Array(46 + name.length) + const view = new DataView(header.buffer) + view.setUint32(0, 0x02014b50, true) + view.setUint16(4, 20, true) + view.setUint16(6, 20, true) + view.setUint16(10, method, true) + view.setUint32(20, data.length, true) + view.setUint32(24, raw.length, true) + view.setUint16(28, name.length, true) + view.setUint32(42, localOffset, true) + header.set(name, 46) + return header +} + +const buildZip = (entries: Entry[]): Uint8Array => { + const encoder = new TextEncoder() + const locals: Uint8Array[] = [] + const centrals: Uint8Array[] = [] + let offset = 0 + for (const entry of entries) { + const name = encoder.encode(entry.name) + const raw = encoder.encode(entry.content) + const data = + entry.method === 'store' || raw.length === 0 ? raw : new Uint8Array(deflateRawSync(raw)) + const local = buildLocalHeader(entry, data, raw, name) + locals.push(local) + centrals.push(buildCentralHeader(entry, data, raw, name, offset)) + offset += local.length + } + + const centralSize = centrals.reduce((sum, header) => sum + header.length, 0) + const eocd = new Uint8Array(22) + const eocdView = new DataView(eocd.buffer) + eocdView.setUint32(0, 0x06054b50, true) + eocdView.setUint16(8, entries.length, true) + eocdView.setUint16(10, entries.length, true) + eocdView.setUint32(12, centralSize, true) + eocdView.setUint32(16, offset, true) + + const out = new Uint8Array(offset + centralSize + eocd.length) + let cursor = 0 + for (const local of locals) { + out.set(local, cursor) + cursor += local.length + } + for (const central of centrals) { + out.set(central, cursor) + cursor += central.length + } + out.set(eocd, cursor) + return out +} + +const FIXTURE: Entry[] = [ + { name: 'stored.json', content: STORED_CONTENT, method: 'store' }, + { name: 'deflated.txt', content: DEFLATED_CONTENT, method: 'deflate' }, + { name: 'nested/', content: '', method: 'store' }, + { name: 'nested/inner.json', content: NESTED_CONTENT, method: 'deflate' }, +] + +describe('readZip', () => { + it('lists every entry including directories', async () => { + const archive = await readZip(buildZip(FIXTURE)) + expect(Object.keys(archive.files).sort()).toEqual([ + 'deflated.txt', + 'nested/', + 'nested/inner.json', + 'stored.json', + ]) + expect(archive.files['nested/'].dir).toBe(true) + expect(archive.files['stored.json'].dir).toBe(false) + }) + + it('reads stored (uncompressed) entries', async () => { + const archive = await readZip(buildZip(FIXTURE)) + expect(await archive.files['stored.json'].text()).toBe(STORED_CONTENT) + }) + + it('reads deflate-compressed entries', async () => { + const archive = await readZip(buildZip(FIXTURE)) + expect(await archive.files['deflated.txt'].text()).toBe(DEFLATED_CONTENT) + expect(await archive.files['nested/inner.json'].text()).toBe(NESTED_CONTENT) + }) + + it('returns an empty string for directory entries', async () => { + const archive = await readZip(buildZip(FIXTURE)) + expect(await archive.files['nested/'].text()).toBe('') + }) + + it('accepts a Blob input', async () => { + const blob = new Blob([new Uint8Array(buildZip(FIXTURE))]) + const archive = await readZip(blob) + expect(await archive.files['stored.json'].text()).toBe(STORED_CONTENT) + }) + + it('throws on input that is not a zip', async () => { + await expect(readZip(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))).rejects.toThrow( + 'not a valid zip', + ) + }) +}) diff --git a/admin-ui/app/utils/jwtDecode.ts b/admin-ui/app/utils/jwtDecode.ts new file mode 100644 index 0000000000..6f17e1aad0 --- /dev/null +++ b/admin-ui/app/utils/jwtDecode.ts @@ -0,0 +1,37 @@ +import { REGEX_BASE64URL_MINUS, REGEX_BASE64URL_UNDERSCORE } from '@/utils/regex' + +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } +type JsonObject = { [key: string]: JsonValue } + +const BASE64_GROUP_SIZE = 4 + +export const decodeJwt = (token: string): T => { + const segments = typeof token === 'string' ? token.split('.') : [] + const payloadSegment = segments[1] + if (segments.length < 2 || !payloadSegment) { + throw new Error('decodeJwt: value is not a valid JWT') + } + const base64 = payloadSegment + .replace(REGEX_BASE64URL_MINUS, '+') + .replace(REGEX_BASE64URL_UNDERSCORE, '/') + const remainder = base64.length % BASE64_GROUP_SIZE + const padded = remainder + ? base64.padEnd(base64.length + (BASE64_GROUP_SIZE - remainder), '=') + : base64 + let json: string + try { + const binary = atob(padded) + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)) + json = new TextDecoder().decode(bytes) + } catch { + throw new Error('decodeJwt: token payload is not valid base64url') + } + try { + const payload: T = JSON.parse(json) + return payload + } catch { + throw new Error('decodeJwt: token payload is not valid JSON') + } +} + +export default decodeJwt diff --git a/admin-ui/app/utils/regex.ts b/admin-ui/app/utils/regex.ts index 1cd1ea7177..45582fa5ed 100644 --- a/admin-ui/app/utils/regex.ts +++ b/admin-ui/app/utils/regex.ts @@ -176,3 +176,7 @@ export const regexForOrvalQueryHook = (hookName: string): RegExp => { export const regexForOrvalHookDecl = (hookName: string): RegExp => { return new RegExp(`export (?:const|function) ${escapeRegexSpecialChars(hookName)}\\b`) } +/** Matches base64url '-' characters; replace with '+' to convert base64url → standard base64 before atob (JWT decode). */ +export const REGEX_BASE64URL_MINUS = /-/g +/** Matches base64url '_' characters; replace with '/' to convert base64url → standard base64 before atob (JWT decode). */ +export const REGEX_BASE64URL_UNDERSCORE = /_/g diff --git a/admin-ui/app/utils/zip.ts b/admin-ui/app/utils/zip.ts new file mode 100644 index 0000000000..a1bdbe2de9 --- /dev/null +++ b/admin-ui/app/utils/zip.ts @@ -0,0 +1,90 @@ +type ZipEntry = { name: string; dir: boolean; text: () => Promise } +type ZipArchive = { files: Record } + +const EOCD_SIGNATURE = 0x06054b50 +const EOCD_MIN_SIZE = 22 +const MAX_COMMENT_SIZE = 0xffff +const CENTRAL_HEADER_FIXED_SIZE = 46 +const LOCAL_HEADER_FIXED_SIZE = 30 +const COMPRESSION_STORED = 0 +const COMPRESSION_DEFLATE = 8 + +const toBytes = async (input: Blob | ArrayBuffer | Uint8Array): Promise => { + if (input instanceof Uint8Array) { + return input + } + if (input instanceof ArrayBuffer) { + return new Uint8Array(input) + } + return new Uint8Array(await input.arrayBuffer()) +} + +const findEndOfCentralDirectory = ( + view: DataView, + length: number, +): { entryCount: number; centralDirectoryOffset: number } => { + const minPosition = Math.max(0, length - EOCD_MIN_SIZE - MAX_COMMENT_SIZE) + for (let position = length - EOCD_MIN_SIZE; position >= minPosition; position -= 1) { + if (view.getUint32(position, true) === EOCD_SIGNATURE) { + return { + entryCount: view.getUint16(position + 10, true), + centralDirectoryOffset: view.getUint32(position + 16, true), + } + } + } + throw new Error('readZip: input is not a valid zip (no end-of-central-directory record)') +} + +const inflateRaw = async (data: Uint8Array): Promise => { + const stream = new Blob([new Uint8Array(data)]) + .stream() + .pipeThrough(new DecompressionStream('deflate-raw')) + return new Uint8Array(await new Response(stream).arrayBuffer()) +} + +export const readZip = async (input: Blob | ArrayBuffer | Uint8Array): Promise => { + const bytes = await toBytes(input) + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + const decoder = new TextDecoder() + const { entryCount, centralDirectoryOffset } = findEndOfCentralDirectory(view, bytes.byteLength) + + const files: Record = {} + let cursor = centralDirectoryOffset + for (let index = 0; index < entryCount; index += 1) { + const method = view.getUint16(cursor + 10, true) + const compressedSize = view.getUint32(cursor + 20, true) + const fileNameLength = view.getUint16(cursor + 28, true) + const extraFieldLength = view.getUint16(cursor + 30, true) + const fileCommentLength = view.getUint16(cursor + 32, true) + const localHeaderOffset = view.getUint32(cursor + 42, true) + const nameStart = cursor + CENTRAL_HEADER_FIXED_SIZE + const name = decoder.decode(bytes.subarray(nameStart, nameStart + fileNameLength)) + const dir = name.endsWith('/') + + const text = async (): Promise => { + if (dir || compressedSize === 0) { + return '' + } + const localNameLength = view.getUint16(localHeaderOffset + 26, true) + const localExtraLength = view.getUint16(localHeaderOffset + 28, true) + const dataStart = + localHeaderOffset + LOCAL_HEADER_FIXED_SIZE + localNameLength + localExtraLength + const data = bytes.subarray(dataStart, dataStart + compressedSize) + if (method === COMPRESSION_STORED) { + return decoder.decode(data) + } + if (method === COMPRESSION_DEFLATE) { + return decoder.decode(await inflateRaw(data)) + } + throw new Error(`readZip: unsupported compression method ${method} for "${name}"`) + } + + files[name] = { name, dir, text } + cursor = nameStart + fileNameLength + extraFieldLength + fileCommentLength + } + + return { files } +} + +export default readZip +export type { ZipArchive, ZipEntry } diff --git a/admin-ui/jest.config.ts b/admin-ui/jest.config.ts index af012a1ce7..5fa1306738 100644 --- a/admin-ui/jest.config.ts +++ b/admin-ui/jest.config.ts @@ -35,9 +35,7 @@ const config: Config = { 'loadPluginMetadata$': '/__mocks__/loadPluginMetadata.ts', }, testMatch: ['**/__tests__/**/*.test.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], - transformIgnorePatterns: [ - 'node_modules/(?!(query-string|decode-uri-component|uuid|split-on-first|filter-obj)/)', - ], + transformIgnorePatterns: ['node_modules/(?!(uuid)/)'], } export default config diff --git a/admin-ui/jest/setup.ts b/admin-ui/jest/setup.ts index ae4ec1cbe0..a757a9e53b 100644 --- a/admin-ui/jest/setup.ts +++ b/admin-ui/jest/setup.ts @@ -39,7 +39,9 @@ jest jest.setTimeout(30000) -jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(jest.fn()) +if (typeof HTMLAnchorElement !== 'undefined') { + jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(jest.fn()) +} if (typeof globalThis.URL !== 'undefined') { if ('createObjectURL' in globalThis.URL) { jest.spyOn(globalThis.URL, 'createObjectURL').mockImplementation(jest.fn()) @@ -58,16 +60,18 @@ global.ResizeObserver = class ResizeObserver { disconnect() {} } -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation((query: string) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}) +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }) +} diff --git a/admin-ui/package.json b/admin-ui/package.json index f48387a56d..f9a413f2dd 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -114,17 +114,13 @@ "dayjs": "^1.0.0", "formik": "^2.0.0", "i18next": "^26.0.0", - "jszip": "^3.0.0", - "jwt-decode": "^4.0.0", "lodash": "^4.0.0", "react": "^18.0.0", "react-ace": "^14.0.0", "react-bootstrap-typeahead": "^6.0.0", "react-dom": "^18.0.0", - "react-dropzone": "^15.0.0", "react-error-boundary": "^6.0.0", "react-i18next": "^17.0.0", - "react-idle-timer": "^5.0.0", "react-json-view-lite": "^2.0.0", "react-redux": "^9.0.0", "react-router-dom": "^7.0.0", diff --git a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx index 65ef9849ba..322299edde 100644 --- a/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx +++ b/admin-ui/plugins/auth-server/components/Authentication/AgamaFlows/AgamaFlows.tsx @@ -26,8 +26,8 @@ import Radio from '@mui/material/Radio' import FormGroup from '@mui/material/FormGroup' import FormControlLabel from '@mui/material/FormControlLabel' import FormLabel from '@mui/material/FormLabel' -import { useDropzone, type FileRejection } from 'react-dropzone' -import JSZip from 'jszip' +import { useFileDrop, type FileRejection } from '@/hooks/useFileDrop' +import { readZip } from '@/utils/zip' import AgamaProjectConfigModal from './AgamaProjectConfigModal' import { updateToast } from 'Redux/features/toastSlice' import { useAuthServerJsonPropertiesQuery } from 'Plugins/auth-server/hooks/useAuthServerJsonProperties' @@ -222,14 +222,14 @@ const AgamaFlows: React.FC = () => { let foundProjectName = false try { - const zip = await JSZip.loadAsync(file) + const zip = await readZip(file) const jsonFiles = Object.keys(zip.files).filter((filename) => filename.endsWith('.json')) for (const filename of jsonFiles) { const zipEntry = zip.files[filename] if (!zipEntry.dir) { try { - const jsonStr = await zipEntry.async('string') + const jsonStr = await zipEntry.text() const jsonData = JSON.parse(jsonStr) as { projectName?: string } if (jsonData?.projectName) { setProjectName(jsonData.projectName) @@ -279,7 +279,7 @@ const AgamaFlows: React.FC = () => { getRootProps: getRootProps1, getInputProps: getInputProps1, isDragActive: isDragActive1, - } = useDropzone({ + } = useFileDrop({ onDrop: onDrop, accept: ACCEPTED_PROJECT_TYPES, maxSize: 50 * 1024 * 1024, @@ -290,7 +290,7 @@ const AgamaFlows: React.FC = () => { getRootProps: getRootProps2, getInputProps: getInputProps2, isDragActive: isDragActive2, - } = useDropzone({ + } = useFileDrop({ onDrop: onSHA256FileDrop, accept: ACCEPTED_SHA_TYPES, maxSize: 1024 * 1024, diff --git a/admin-ui/vite.config.ts b/admin-ui/vite.config.ts index a9f1154b00..b3e3cfee28 100644 --- a/admin-ui/vite.config.ts +++ b/admin-ui/vite.config.ts @@ -137,8 +137,6 @@ const REACT_RUNTIME_PACKAGES = new Set([ 'react-redux', 'react-i18next', 'react-error-boundary', - 'react-responsive', - 'react-idle-timer', ]) const FEATURE_GROUPS: ReadonlyArray = [ @@ -184,30 +182,14 @@ const FEATURE_GROUPS: ReadonlyArray { From 858c0f349358dab7ff2504d8522c4bd2dbdbc056 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Wed, 10 Jun 2026 21:34:56 +0500 Subject: [PATCH 09/24] tooltip implemented Signed-off-by: faisalsiddique4400 --- admin-ui/app/constants/ui.ts | 9 ++ admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx | 82 ++++++++++++++----- .../Apps/Gluu/Tests/GluuTooltip.test.tsx | 19 +++++ .../Apps/Gluu/styles/GluuTooltip.style.ts | 47 +++++++++++ admin-ui/package.json | 1 - 5 files changed, 138 insertions(+), 20 deletions(-) diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts index 748e63a1bb..5ffb0c026e 100644 --- a/admin-ui/app/constants/ui.ts +++ b/admin-ui/app/constants/ui.ts @@ -120,6 +120,15 @@ export const ICON_SIZE = { export const ICON_BUTTON_SIZE = 32 +export const TOOLTIP = { + ARROW_SIZE: 8, + PADDING_VERTICAL: 8, + PADDING_HORIZONTAL: 16, + BORDER_RADIUS: 3, + OFFSET: 10, + FONT_SIZE: '90%', +} as const + export const TOOLBAR = { MIN_WIDTH: 130, SEARCH_MIN_WIDTH: 220, diff --git a/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx b/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx index d9b430231a..4656a96aba 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx @@ -1,10 +1,15 @@ -import { useMemo } from 'react' -import 'react-tooltip/dist/react-tooltip.css' -import { Tooltip as ReactTooltip } from 'react-tooltip' +import { useEffect, useMemo, useState } from 'react' +import Popper from '@mui/material/Popper' import { useTranslation } from 'react-i18next' import { useTheme } from '@/context/theme/themeContext' -import { THEME_DARK, THEME_LIGHT } from '@/context/theme/constants' -import { DEFAULT_Z_INDEX, getLabelTooltipStyle } from './styles/GluuTooltip.style' +import { THEME_DARK } from '@/context/theme/constants' +import { TOOLTIP } from '@/constants' +import { + DEFAULT_Z_INDEX, + getLabelTooltipStyle, + useStyles, + TOOLTIP_ARROW_CLASS, +} from './styles/GluuTooltip.style' import type { GluuTooltipProps } from './types' const GluuTooltip = ({ @@ -16,13 +21,15 @@ const GluuTooltip = ({ zIndex = DEFAULT_Z_INDEX, place = 'bottom', content: contentOverride, - positionStrategy, + positionStrategy = 'absolute', offset, }: GluuTooltipProps) => { const { t } = useTranslation() const { state: themeState } = useTheme() - const selectedTheme = themeState?.theme - const isDarkTheme = selectedTheme === THEME_DARK + const isDarkTheme = themeState?.theme === THEME_DARK + const { classes } = useStyles() + + const [anchorEl, setAnchorEl] = useState(null) const tooltipStyle = useMemo( () => getLabelTooltipStyle(isDarkTheme, zIndex), @@ -38,19 +45,56 @@ const GluuTooltip = ({ ? t(`documentation.${doc_category}.${doc_entry}`) : doc_entry + useEffect(() => { + const resolveAnchor = (target: EventTarget | null): Element | null => { + if (!(target instanceof Element)) return null + const el = target.closest('[data-tooltip-id]') + return el?.getAttribute('data-tooltip-id') === doc_entry ? el : null + } + const show = (event: Event) => { + const el = resolveAnchor(event.target) + if (el) setAnchorEl(el) + } + const hide = (event: Event) => { + if (resolveAnchor(event.target)) setAnchorEl(null) + } + document.addEventListener('mouseover', show) + document.addEventListener('mouseout', hide) + document.addEventListener('focusin', show) + document.addEventListener('focusout', hide) + return () => { + document.removeEventListener('mouseover', show) + document.removeEventListener('mouseout', hide) + document.removeEventListener('focusin', show) + document.removeEventListener('focusout', hide) + } + }, [doc_entry]) + + const modifiers = useMemo( + () => [{ name: 'offset', options: { offset: [0, offset ?? TOOLTIP.OFFSET] } }], + [offset], + ) + const tooltipElement = ( - - {tooltipContent} - +
+
+ {tooltipContent} +
+ +
+ ) if (tooltipOnly) { diff --git a/admin-ui/app/routes/Apps/Gluu/Tests/GluuTooltip.test.tsx b/admin-ui/app/routes/Apps/Gluu/Tests/GluuTooltip.test.tsx index e04f3c44bd..7a17ff2872 100644 --- a/admin-ui/app/routes/Apps/Gluu/Tests/GluuTooltip.test.tsx +++ b/admin-ui/app/routes/Apps/Gluu/Tests/GluuTooltip.test.tsx @@ -24,3 +24,22 @@ it('Test gluutooltip', async () => { expect(screen.getByRole('tooltip')).toHaveTextContent(/Kind of the application/i) }) }) + +it('shows a tooltipOnly tooltip when a detached data-tooltip-id anchor is hovered', async () => { + render( + + <> + + + + , + ) + + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() + + await userEvent.hover(screen.getByText('trigger')) + + await waitFor(() => { + expect(screen.getByRole('tooltip')).toHaveTextContent(/Kind of the application/i) + }) +}) diff --git a/admin-ui/app/routes/Apps/Gluu/styles/GluuTooltip.style.ts b/admin-ui/app/routes/Apps/Gluu/styles/GluuTooltip.style.ts index f1ed2d17cf..f3e16ded10 100644 --- a/admin-ui/app/routes/Apps/Gluu/styles/GluuTooltip.style.ts +++ b/admin-ui/app/routes/Apps/Gluu/styles/GluuTooltip.style.ts @@ -1,6 +1,8 @@ import type { CSSProperties } from 'react' +import { makeStyles } from 'tss-react/mui' import getThemeColor from '@/context/theme/config' import { THEME_LIGHT } from '@/context/theme/constants' +import { OPACITY, TOOLTIP } from '@/constants' export const DEFAULT_Z_INDEX = 101 const TOOLTIP_MAX_WIDTH = '45vw' @@ -19,3 +21,48 @@ export const getLabelTooltipStyle = ( ...colors, } } + +const ARROW_OFFSET = -TOOLTIP.ARROW_SIZE / 2 + +export const TOOLTIP_ARROW_CLASS = 'gluu-tooltip-arrow' + +export const useStyles = makeStyles()(() => ({ + popper: { + [`& .${TOOLTIP_ARROW_CLASS}`]: { + position: 'absolute', + width: TOOLTIP.ARROW_SIZE, + height: TOOLTIP.ARROW_SIZE, + }, + [`&[data-popper-placement*="bottom"] .${TOOLTIP_ARROW_CLASS}`]: { + top: ARROW_OFFSET, + left: '50%', + transform: 'translateX(-50%) rotate(45deg)', + }, + [`&[data-popper-placement*="top"] .${TOOLTIP_ARROW_CLASS}`]: { + bottom: ARROW_OFFSET, + left: '50%', + transform: 'translateX(-50%) rotate(45deg)', + }, + [`&[data-popper-placement*="right"] .${TOOLTIP_ARROW_CLASS}`]: { + left: ARROW_OFFSET, + top: '50%', + transform: 'translateY(-50%) rotate(45deg)', + }, + [`&[data-popper-placement*="left"] .${TOOLTIP_ARROW_CLASS}`]: { + right: ARROW_OFFSET, + top: '50%', + transform: 'translateY(-50%) rotate(45deg)', + }, + }, + inner: { + position: 'relative', + opacity: OPACITY.OVERLAY, + }, + tooltip: { + position: 'relative', + padding: `${TOOLTIP.PADDING_VERTICAL}px ${TOOLTIP.PADDING_HORIZONTAL}px`, + borderRadius: TOOLTIP.BORDER_RADIUS, + fontSize: TOOLTIP.FONT_SIZE, + width: 'max-content', + }, +})) diff --git a/admin-ui/package.json b/admin-ui/package.json index f9a413f2dd..a5042de530 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -126,7 +126,6 @@ "react-router-dom": "^7.0.0", "react-toastify": "^11.0.0", "react-toggle": "^4.0.0", - "react-tooltip": "^6.0.0", "recharts": "^3.0.0", "redux-persist": "^6.0.0", "tss-react": "^4.0.0", From 068ea8519e8b875c9025983d57b1cbe982b8bff0 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Wed, 10 Jun 2026 22:54:52 +0500 Subject: [PATCH 10/24] coderabbit fixes Signed-off-by: faisalsiddique4400 --- .../routes/Apps/Gluu/GluuSessionTimeout.tsx | 5 +- admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx | 25 ++------- admin-ui/app/utils/tooltipManager.ts | 52 +++++++++++++++++++ admin-ui/app/utils/zip.ts | 5 ++ 4 files changed, 64 insertions(+), 23 deletions(-) create mode 100644 admin-ui/app/utils/tooltipManager.ts diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx index 79f280c76b..9cac60f32c 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeout.tsx @@ -112,10 +112,13 @@ const SessionTimeout = ({ isAuthenticated }: SessionTimeoutProps) => { onActive, onIdle, debounce: 250, + disabled: !isAuthenticated, }) useEffect(() => { - reset() + if (isAuthenticated) { + reset() + } }, [sessionTimeout, isAuthenticated, reset]) return ( diff --git a/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx b/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx index 4656a96aba..4c6656d5fd 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx @@ -10,6 +10,7 @@ import { useStyles, TOOLTIP_ARROW_CLASS, } from './styles/GluuTooltip.style' +import { tooltipManager } from '@/utils/tooltipManager' import type { GluuTooltipProps } from './types' const GluuTooltip = ({ @@ -46,28 +47,8 @@ const GluuTooltip = ({ : doc_entry useEffect(() => { - const resolveAnchor = (target: EventTarget | null): Element | null => { - if (!(target instanceof Element)) return null - const el = target.closest('[data-tooltip-id]') - return el?.getAttribute('data-tooltip-id') === doc_entry ? el : null - } - const show = (event: Event) => { - const el = resolveAnchor(event.target) - if (el) setAnchorEl(el) - } - const hide = (event: Event) => { - if (resolveAnchor(event.target)) setAnchorEl(null) - } - document.addEventListener('mouseover', show) - document.addEventListener('mouseout', hide) - document.addEventListener('focusin', show) - document.addEventListener('focusout', hide) - return () => { - document.removeEventListener('mouseover', show) - document.removeEventListener('mouseout', hide) - document.removeEventListener('focusin', show) - document.removeEventListener('focusout', hide) - } + tooltipManager.addListener(doc_entry, setAnchorEl) + return () => tooltipManager.removeListener(doc_entry, setAnchorEl) }, [doc_entry]) const modifiers = useMemo( diff --git a/admin-ui/app/utils/tooltipManager.ts b/admin-ui/app/utils/tooltipManager.ts new file mode 100644 index 0000000000..b6247d1fa5 --- /dev/null +++ b/admin-ui/app/utils/tooltipManager.ts @@ -0,0 +1,52 @@ +type TooltipCallback = (anchor: Element | null) => void + +const registry = new Map>() +let listening = false + +const resolveAnchor = (target: EventTarget | null): Element | null => + target instanceof Element ? target.closest('[data-tooltip-id]') : null + +const notify = (target: EventTarget | null, show: boolean) => { + const el = resolveAnchor(target) + const docEntry = el?.getAttribute('data-tooltip-id') + if (!docEntry) return + registry.get(docEntry)?.forEach((callback) => callback(show ? el : null)) +} + +const handleShow = (event: Event) => notify(event.target, true) +const handleHide = (event: Event) => notify(event.target, false) + +const startListening = () => { + if (listening) return + listening = true + document.addEventListener('mouseover', handleShow) + document.addEventListener('mouseout', handleHide) + document.addEventListener('focusin', handleShow) + document.addEventListener('focusout', handleHide) +} + +const stopListening = () => { + if (!listening) return + listening = false + document.removeEventListener('mouseover', handleShow) + document.removeEventListener('mouseout', handleHide) + document.removeEventListener('focusin', handleShow) + document.removeEventListener('focusout', handleHide) +} + +const addListener = (docEntry: string, callback: TooltipCallback) => { + const callbacks = registry.get(docEntry) ?? new Set() + callbacks.add(callback) + registry.set(docEntry, callbacks) + startListening() +} + +const removeListener = (docEntry: string, callback: TooltipCallback) => { + const callbacks = registry.get(docEntry) + if (!callbacks) return + callbacks.delete(callback) + if (callbacks.size === 0) registry.delete(docEntry) + if (registry.size === 0) stopListening() +} + +export const tooltipManager = { addListener, removeListener } diff --git a/admin-ui/app/utils/zip.ts b/admin-ui/app/utils/zip.ts index a1bdbe2de9..5e1fa2ce79 100644 --- a/admin-ui/app/utils/zip.ts +++ b/admin-ui/app/utils/zip.ts @@ -8,6 +8,7 @@ const CENTRAL_HEADER_FIXED_SIZE = 46 const LOCAL_HEADER_FIXED_SIZE = 30 const COMPRESSION_STORED = 0 const COMPRESSION_DEFLATE = 8 +const MAX_UNCOMPRESSED_SIZE = 100 * 1024 * 1024 const toBytes = async (input: Blob | ArrayBuffer | Uint8Array): Promise => { if (input instanceof Uint8Array) { @@ -53,6 +54,7 @@ export const readZip = async (input: Blob | ArrayBuffer | Uint8Array): Promise MAX_UNCOMPRESSED_SIZE) { + throw new Error(`readZip: entry "${name}" exceeds the maximum uncompressed size`) + } const localNameLength = view.getUint16(localHeaderOffset + 26, true) const localExtraLength = view.getUint16(localHeaderOffset + 28, true) const dataStart = From a8b58c73e0511bf9dcdd26a978861569749d3788 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Wed, 10 Jun 2026 23:13:44 +0500 Subject: [PATCH 11/24] coderabbit fixes Signed-off-by: faisalsiddique4400 --- admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx b/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx index 4c6656d5fd..06eb415714 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuTooltip.tsx @@ -48,7 +48,10 @@ const GluuTooltip = ({ useEffect(() => { tooltipManager.addListener(doc_entry, setAnchorEl) - return () => tooltipManager.removeListener(doc_entry, setAnchorEl) + return () => { + tooltipManager.removeListener(doc_entry, setAnchorEl) + setAnchorEl(null) + } }, [doc_entry]) const modifiers = useMemo( From b848ffb72436794f50a653587b890f5512ecb538 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Thu, 11 Jun 2026 11:19:06 +0500 Subject: [PATCH 12/24] idletimer and file picker design fixed Signed-off-by: faisalsiddique4400 --- admin-ui/app/hooks/useFileDrop/index.ts | 2 + admin-ui/app/hooks/useFileDrop/types/index.ts | 40 +++++++ .../hooks/{ => useFileDrop}/useFileDrop.ts | 59 ++++------ admin-ui/app/hooks/useIdleTimer/index.ts | 2 + .../app/hooks/useIdleTimer/types/index.ts | 13 +++ .../hooks/{ => useIdleTimer}/useIdleTimer.ts | 27 ++--- .../Apps/Gluu/GluuSessionTimeoutDialog.tsx | 108 ++++++++++-------- .../app/routes/Apps/Gluu/GluuUploadFile.tsx | 3 +- .../styles/GluuSessionTimeoutDialog.style.ts | 94 +++------------ .../Apps/Gluu/styles/GluuUploadFile.style.ts | 10 +- .../app/styles/plugins/_react-dropzone.scss | 14 --- .../Cedarling/CedarlingConfigPage.tsx | 15 ++- 12 files changed, 191 insertions(+), 196 deletions(-) create mode 100644 admin-ui/app/hooks/useFileDrop/index.ts create mode 100644 admin-ui/app/hooks/useFileDrop/types/index.ts rename admin-ui/app/hooks/{ => useFileDrop}/useFileDrop.ts (79%) create mode 100644 admin-ui/app/hooks/useIdleTimer/index.ts create mode 100644 admin-ui/app/hooks/useIdleTimer/types/index.ts rename admin-ui/app/hooks/{ => useIdleTimer}/useIdleTimer.ts (83%) delete mode 100755 admin-ui/app/styles/plugins/_react-dropzone.scss diff --git a/admin-ui/app/hooks/useFileDrop/index.ts b/admin-ui/app/hooks/useFileDrop/index.ts new file mode 100644 index 0000000000..61e828c822 --- /dev/null +++ b/admin-ui/app/hooks/useFileDrop/index.ts @@ -0,0 +1,2 @@ +export { useFileDrop, default } from './useFileDrop' +export type * from './types' diff --git a/admin-ui/app/hooks/useFileDrop/types/index.ts b/admin-ui/app/hooks/useFileDrop/types/index.ts new file mode 100644 index 0000000000..93a11adb56 --- /dev/null +++ b/admin-ui/app/hooks/useFileDrop/types/index.ts @@ -0,0 +1,40 @@ +import type { + DragEvent, + HTMLAttributes, + InputHTMLAttributes, + KeyboardEvent, + MouseEvent, + Ref, +} from 'react' + +export type Accept = Record +export type FileError = { code: string; message: string } +export type FileRejection = { file: File; errors: FileError[] } + +export type UseFileDropOptions = { + onDrop: (acceptedFiles: File[]) => void + accept?: Accept + maxSize?: number + disabled?: boolean + onDropRejected?: (fileRejections: FileRejection[]) => void +} + +export type RootProps = HTMLAttributes & { + onClick: (event: MouseEvent) => void + onKeyDown: (event: KeyboardEvent) => void + onDrop: (event: DragEvent) => void + onDragOver: (event: DragEvent) => void + onDragEnter: (event: DragEvent) => void + onDragLeave: (event: DragEvent) => void +} + +export type InputProps = InputHTMLAttributes & { + ref: Ref +} + +export type UseFileDropResult = { + getRootProps: (props?: HTMLAttributes) => RootProps + getInputProps: (props?: InputHTMLAttributes) => InputProps + isDragActive: boolean + open: () => void +} diff --git a/admin-ui/app/hooks/useFileDrop.ts b/admin-ui/app/hooks/useFileDrop/useFileDrop.ts similarity index 79% rename from admin-ui/app/hooks/useFileDrop.ts rename to admin-ui/app/hooks/useFileDrop/useFileDrop.ts index df9a0ad960..9c055cf254 100644 --- a/admin-ui/app/hooks/useFileDrop.ts +++ b/admin-ui/app/hooks/useFileDrop/useFileDrop.ts @@ -6,40 +6,16 @@ import type { InputHTMLAttributes, KeyboardEvent, MouseEvent, - Ref, } from 'react' - -export type Accept = Record -export type FileError = { code: string; message: string } -export type FileRejection = { file: File; errors: FileError[] } - -type UseFileDropOptions = { - onDrop: (acceptedFiles: File[]) => void - accept?: Accept - maxSize?: number - disabled?: boolean - onDropRejected?: (fileRejections: FileRejection[]) => void -} - -type RootProps = HTMLAttributes & { - onClick: (event: MouseEvent) => void - onKeyDown: (event: KeyboardEvent) => void - onDrop: (event: DragEvent) => void - onDragOver: (event: DragEvent) => void - onDragEnter: (event: DragEvent) => void - onDragLeave: (event: DragEvent) => void -} - -type InputProps = InputHTMLAttributes & { - ref: Ref -} - -type UseFileDropResult = { - getRootProps: (props?: HTMLAttributes) => RootProps - getInputProps: (props?: InputHTMLAttributes) => InputProps - isDragActive: boolean - open: () => void -} +import type { + Accept, + FileError, + FileRejection, + InputProps, + RootProps, + UseFileDropOptions, + UseFileDropResult, +} from './types' const ERROR_FILE_INVALID_TYPE = 'file-invalid-type' const ERROR_FILE_TOO_LARGE = 'file-too-large' @@ -97,7 +73,7 @@ export const useFileDrop = ({ onDropRejected, }: UseFileDropOptions): UseFileDropResult => { const inputRef = useRef(null) - const dragDepthRef = useRef(0) + const dragTargetsRef = useRef([]) const [isDragActive, setIsDragActive] = useState(false) useEffect(() => { @@ -158,7 +134,7 @@ export const useFileDrop = ({ }, onDrop: (event: DragEvent) => { event.preventDefault() - dragDepthRef.current = 0 + dragTargetsRef.current = [] setIsDragActive(false) if (!disabled) { processFiles(event.dataTransfer.files) @@ -169,16 +145,21 @@ export const useFileDrop = ({ }, onDragEnter: (event: DragEvent) => { event.preventDefault() - dragDepthRef.current += 1 + if (event.target instanceof Node) { + dragTargetsRef.current = [...dragTargetsRef.current, event.target] + } if (!disabled) { setIsDragActive(true) } }, onDragLeave: (event: DragEvent) => { event.preventDefault() - dragDepthRef.current -= 1 - if (dragDepthRef.current <= 0) { - dragDepthRef.current = 0 + const root = event.currentTarget + const remaining = dragTargetsRef.current.filter( + (target) => target !== event.target && root.contains(target), + ) + dragTargetsRef.current = remaining + if (remaining.length === 0) { setIsDragActive(false) } }, diff --git a/admin-ui/app/hooks/useIdleTimer/index.ts b/admin-ui/app/hooks/useIdleTimer/index.ts new file mode 100644 index 0000000000..ee0218eef5 --- /dev/null +++ b/admin-ui/app/hooks/useIdleTimer/index.ts @@ -0,0 +1,2 @@ +export { useIdleTimer, default } from './useIdleTimer' +export type * from './types' diff --git a/admin-ui/app/hooks/useIdleTimer/types/index.ts b/admin-ui/app/hooks/useIdleTimer/types/index.ts new file mode 100644 index 0000000000..650ece84cf --- /dev/null +++ b/admin-ui/app/hooks/useIdleTimer/types/index.ts @@ -0,0 +1,13 @@ +export type UseIdleTimerOptions = { + timeout: number + onIdle?: () => void + onActive?: () => void + debounce?: number + events?: string[] + disabled?: boolean +} + +export type UseIdleTimerResult = { + reset: () => void + isIdle: boolean +} diff --git a/admin-ui/app/hooks/useIdleTimer.ts b/admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts similarity index 83% rename from admin-ui/app/hooks/useIdleTimer.ts rename to admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts index 497aad1a6b..99529689db 100644 --- a/admin-ui/app/hooks/useIdleTimer.ts +++ b/admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts @@ -1,18 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' - -type UseIdleTimerOptions = { - timeout: number - onIdle?: () => void - onActive?: () => void - debounce?: number - events?: string[] - disabled?: boolean -} - -type UseIdleTimerResult = { - reset: () => void - isIdle: boolean -} +import type { UseIdleTimerOptions, UseIdleTimerResult } from './types' const DEFAULT_EVENTS = [ 'mousemove', @@ -38,12 +25,16 @@ export const useIdleTimer = ({ const [isIdle, setIsIdle] = useState(false) const onIdleRef = useRef(onIdle) const onActiveRef = useRef(onActive) + const timeoutRef = useRef(timeout) + const debounceRef = useRef(debounce) const isIdleRef = useRef(false) const timeoutIdRef = useRef | null>(null) const lastActivityRef = useRef(Number.NEGATIVE_INFINITY) onIdleRef.current = onIdle onActiveRef.current = onActive + timeoutRef.current = timeout + debounceRef.current = debounce const clearTimer = useCallback(() => { if (timeoutIdRef.current !== null) { @@ -58,8 +49,8 @@ export const useIdleTimer = ({ isIdleRef.current = true setIsIdle(true) onIdleRef.current?.() - }, timeout) - }, [clearTimer, timeout]) + }, timeoutRef.current) + }, [clearTimer]) const reset = useCallback(() => { if (isIdleRef.current) { @@ -78,7 +69,7 @@ export const useIdleTimer = ({ startTimer() const handleActivity = () => { const now = Date.now() - if (debounce > 0 && now - lastActivityRef.current < debounce) { + if (debounceRef.current > 0 && now - lastActivityRef.current < debounceRef.current) { return } lastActivityRef.current = now @@ -93,7 +84,7 @@ export const useIdleTimer = ({ eventTarget(eventName).removeEventListener(eventName, handleActivity), ) } - }, [disabled, debounce, events, reset, startTimer, clearTimer]) + }, [disabled, events, reset, startTimer, clearTimer]) return { reset, isIdle } } diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeoutDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeoutDialog.tsx index 342634ded0..fcfb150f59 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeoutDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuSessionTimeoutDialog.tsx @@ -1,14 +1,16 @@ import { useContext, useMemo } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import clsx from 'clsx' import styles from './styles/GluuSessionTimeoutDialog.style' import { useStyles as useCommitDialogStyles } from './styles/GluuCommitDialog.style' +import { BUTTON_STYLES } from './styles/GluuThemeFormFooter.style' import { ThemeContext } from 'Context/theme/themeContext' import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' import getThemeColor from '@/context/theme/config' +import { OPACITY } from '@/constants' +import { Close } from '@/components/icons' import GluuText from './GluuText' -import { GluuButton } from '@/components/GluuButton' +import { GluuButton } from '@/components' import type { SessionTimeoutDialogProps } from './types' const SessionTimeoutDialog = ({ @@ -26,57 +28,71 @@ const SessionTimeoutDialog = ({ const { classes } = styles({ themeColors, isDark }) const { classes: commitClasses } = useCommitDialogStyles({ isDark, themeColors }) + const { back: continueColors, cancel: logoutColors } = themeColors.formFooter + if (!open) return null const modalContent = ( <>
-
-
- - {t('sessionTimeout.title')} - -
-
- - {t('sessionTimeout.messageExpire', { count: countdown })} - - - {t('sessionTimeout.messageContinue')} - -
-
- - {t('sessionTimeout.logout')} - - +
+ +
+ + {t('sessionTimeout.title')} + +
+ + {t('sessionTimeout.messageExpire', { count: countdown })} + + + {t('sessionTimeout.messageContinue')} + +
+
+ + {t('sessionTimeout.continueSession')} + + + {t('sessionTimeout.logout')} + +
+
diff --git a/admin-ui/app/routes/Apps/Gluu/GluuUploadFile.tsx b/admin-ui/app/routes/Apps/Gluu/GluuUploadFile.tsx index ecc7a6d9a9..01926794ac 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuUploadFile.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuUploadFile.tsx @@ -75,7 +75,8 @@ const GluuUploadFile: React.FC = ({ const hasFile = Boolean(selectedFile || preDefinedFileName) const dropzoneClassName = [ - isDragActive1 ? 'active' : 'dropzone', + 'dropzone', + isDragActive1 ? 'active' : '', hasFile ? classes.dropzoneWithFile : '', ] .filter(Boolean) diff --git a/admin-ui/app/routes/Apps/Gluu/styles/GluuSessionTimeoutDialog.style.ts b/admin-ui/app/routes/Apps/Gluu/styles/GluuSessionTimeoutDialog.style.ts index 729e07c4e5..7b19b62245 100644 --- a/admin-ui/app/routes/Apps/Gluu/styles/GluuSessionTimeoutDialog.style.ts +++ b/admin-ui/app/routes/Apps/Gluu/styles/GluuSessionTimeoutDialog.style.ts @@ -1,47 +1,21 @@ import { makeStyles } from 'tss-react/mui' import type { ThemeConfig } from '@/context/theme/config' -import { BORDER_RADIUS, MAPPING_SPACING } from '@/constants/ui' +import { MODAL, SPACING } from '@/constants/ui' import { fontFamily, fontSizes, fontWeights, letterSpacing, lineHeights } from '@/styles/fonts' -import { getCardBorderStyle } from '@/styles/cardBorderStyles' +import { BUTTON_STYLES } from './GluuThemeFormFooter.style' -const DIALOG_BORDER_RADIUS = BORDER_RADIUS.DEFAULT +const SESSION_DIALOG_WIDTH = 580 -const styles = makeStyles<{ themeColors: ThemeConfig; isDark: boolean }>()(( - theme, - { themeColors, isDark }, -) => { - const cardBorderStyle = getCardBorderStyle({ isDark, borderRadius: DIALOG_BORDER_RADIUS }) - - return { - dialog: { - ...cardBorderStyle, - padding: theme.spacing(5), - borderRadius: DIALOG_BORDER_RADIUS, - width: 'min(550px, calc(100vw - 32px))', - minHeight: 259, +const styles = makeStyles<{ themeColors: ThemeConfig; isDark: boolean }>()( + (theme, { themeColors }) => ({ + modalContainer: { + width: `min(${SESSION_DIALOG_WIDTH}px, ${MODAL.MAX_VW})`, + maxWidth: `${SESSION_DIALOG_WIDTH}px`, + }, + contentWrapper: { display: 'flex', flexDirection: 'column', - backgroundColor: - themeColors.card?.background ?? themeColors.menu?.background ?? themeColors.background, - backgroundImage: 'none', - boxSizing: 'border-box', - position: 'fixed', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - zIndex: 1050, - overflow: 'visible', - }, - titleWrapper: { - paddingBottom: 0, - }, - title: { - fontFamily, - fontSize: fontSizes['3xl'], - fontStyle: 'normal', - fontWeight: fontWeights.semiBold, - lineHeight: '45px', - color: themeColors.fontColor, + gap: theme.spacing(0.5), }, contentText: { fontFamily, @@ -51,50 +25,18 @@ const styles = makeStyles<{ themeColors: ThemeConfig; isDark: boolean }>()(( lineHeight: lineHeights.base, letterSpacing: letterSpacing.content, color: themeColors.textMuted, - marginBottom: theme.spacing(0.5), }, - contentWrapper: { - marginBottom: theme.spacing(1), - }, - actionArea: { + buttonRow: { display: 'flex', alignItems: 'center', - padding: theme.spacing(0.5, 0), justifyContent: 'flex-start', gap: theme.spacing(2), - marginTop: 0, - }, - button: { - textTransform: 'none', - padding: theme.spacing(1, 4), - fontFamily, - fontSize: fontSizes.md, - fontWeight: fontWeights.semiBold, - }, - logout: { - borderRadius: BORDER_RADIUS.SMALL_MEDIUM, - color: themeColors.formFooter?.back?.textColor, - backgroundColor: themeColors.formFooter?.back?.backgroundColor, - borderColor: themeColors.formFooter?.back?.backgroundColor, - }, - continue: { - 'borderRadius': MAPPING_SPACING.INFO_ALERT_BORDER_RADIUS, - 'border': `1px solid ${themeColors.formFooter?.cancel?.borderColor ?? themeColors.borderColor}`, - fontFamily, - 'fontSize': fontSizes.base, - 'fontStyle': 'normal', - 'fontWeight': fontWeights.bold, - 'lineHeight': lineHeights.normal, - 'letterSpacing': letterSpacing.button, - 'color': themeColors.formFooter?.cancel?.textColor ?? themeColors.fontColor, - 'backgroundColor': themeColors.formFooter?.cancel?.backgroundColor ?? 'transparent', - '&:hover': { - color: themeColors.formFooter?.cancel?.textColor ?? themeColors.fontColor, - backgroundColor: themeColors.formFooter?.cancel?.backgroundColor ?? 'transparent', - borderColor: themeColors.formFooter?.cancel?.borderColor ?? themeColors.borderColor, - }, + marginTop: SPACING.SECTION_GAP, + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(1), + letterSpacing: BUTTON_STYLES.letterSpacing, }, - } -}) + }), +) export default styles diff --git a/admin-ui/app/routes/Apps/Gluu/styles/GluuUploadFile.style.ts b/admin-ui/app/routes/Apps/Gluu/styles/GluuUploadFile.style.ts index 1d40a63941..4cc9f64558 100644 --- a/admin-ui/app/routes/Apps/Gluu/styles/GluuUploadFile.style.ts +++ b/admin-ui/app/routes/Apps/Gluu/styles/GluuUploadFile.style.ts @@ -10,7 +10,15 @@ export const useStyles = makeStyles<{ removeDisabled: boolean }>()((theme: Theme, { fontColor, removeColor, removeDisabled }) => ({ root: { - width: '100%', + 'width': '100%', + '& .dropzone': { + cursor: 'pointer', + transition: 'border-color 0.15s ease, background-color 0.15s ease', + }, + '&& .dropzone.active, && .dropzone:has(strong):hover': { + borderColor: customColors.lightBlue, + backgroundColor: alpha(customColors.lightBlue, OPACITY.HOVER_LIGHT), + }, }, dropzoneWithFile: { justifyContent: 'flex-start', diff --git a/admin-ui/app/styles/plugins/_react-dropzone.scss b/admin-ui/app/styles/plugins/_react-dropzone.scss deleted file mode 100755 index e87c39aab6..0000000000 --- a/admin-ui/app/styles/plugins/_react-dropzone.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import "../miltonbo/scss/variables"; - -.dropzone { - border: 2px dashed $gray-300; - padding: 20px; - text-align: center; - cursor: pointer; - - &:hover, - &--active { - border-color: $primary; - background-color: rgba($primary, 0.1); - } -} diff --git a/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx b/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx index 0ca8872c38..ddc3b9a651 100644 --- a/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx +++ b/admin-ui/plugins/admin/components/Cedarling/CedarlingConfigPage.tsx @@ -13,6 +13,7 @@ import { updateToast } from '@/redux/features/toastSlice' import { getErrorMessage, type ApiError } from '@/utils/errorHandler' import { logAuditUserAction } from '@/utils/AuditLogger' import { devLogger } from '@/utils/devLogger' +import apiAxios from '@/redux/api/axios' import { UPDATE } from '@/audit/UserActionType' import { Box, Link } from '@mui/material' import { Close, InfoOutlined } from '@/components/icons' @@ -37,6 +38,18 @@ const CJAR_ACCEPT = { 'application/zip': ['.cjar'], } +const buildPolicyStoreFileName = (): string => { + const base = apiAxios.defaults.baseURL + if (!base) { + return 'policy-store.cjar' + } + try { + return `${new URL(base).hostname}-policy-store.cjar` + } catch { + return 'policy-store.cjar' + } +} + const CedarlingConfigPage: React.FC = () => { const { canRead: canReadSecurity, canWrite: canWriteSecurity } = usePermission(SECURITY_RESOURCE_ID) @@ -183,7 +196,7 @@ const CedarlingConfigPage: React.FC = () => { const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url - link.download = 'policy-store.cjar' + link.download = buildPolicyStoreFileName() document.body.appendChild(link) link.click() document.body.removeChild(link) From 30ec8e4d134238bbf1fbd62fa80de59771d532d2 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Thu, 11 Jun 2026 12:04:52 +0500 Subject: [PATCH 13/24] coderabbit fixes Signed-off-by: faisalsiddique4400 --- admin-ui/app/hooks/useFileDrop/index.ts | 2 +- admin-ui/app/hooks/useFileDrop/useFileDrop.ts | 3 +-- admin-ui/app/hooks/useIdleTimer/index.ts | 2 +- admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts | 3 +-- admin-ui/app/utils/AppAuthProvider.tsx | 2 +- admin-ui/app/utils/__tests__/jwtDecode.test.ts | 2 +- admin-ui/app/utils/jwtDecode.ts | 3 +-- admin-ui/app/utils/zip.ts | 6 +++--- 8 files changed, 10 insertions(+), 13 deletions(-) diff --git a/admin-ui/app/hooks/useFileDrop/index.ts b/admin-ui/app/hooks/useFileDrop/index.ts index 61e828c822..a70e8c6490 100644 --- a/admin-ui/app/hooks/useFileDrop/index.ts +++ b/admin-ui/app/hooks/useFileDrop/index.ts @@ -1,2 +1,2 @@ -export { useFileDrop, default } from './useFileDrop' +export { default as useFileDrop } from './useFileDrop' export type * from './types' diff --git a/admin-ui/app/hooks/useFileDrop/useFileDrop.ts b/admin-ui/app/hooks/useFileDrop/useFileDrop.ts index 9c055cf254..42ad9e4268 100644 --- a/admin-ui/app/hooks/useFileDrop/useFileDrop.ts +++ b/admin-ui/app/hooks/useFileDrop/useFileDrop.ts @@ -65,7 +65,7 @@ const validateFile = (file: File, accept?: Accept, maxSize?: number): FileError[ return errors } -export const useFileDrop = ({ +const useFileDrop = ({ onDrop, accept, maxSize, @@ -191,5 +191,4 @@ export const useFileDrop = ({ return { getRootProps, getInputProps, isDragActive, open } } - export default useFileDrop diff --git a/admin-ui/app/hooks/useIdleTimer/index.ts b/admin-ui/app/hooks/useIdleTimer/index.ts index ee0218eef5..e93965e8a1 100644 --- a/admin-ui/app/hooks/useIdleTimer/index.ts +++ b/admin-ui/app/hooks/useIdleTimer/index.ts @@ -1,2 +1,2 @@ -export { useIdleTimer, default } from './useIdleTimer' +export { default as useIdleTimer } from './useIdleTimer' export type * from './types' diff --git a/admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts b/admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts index 99529689db..770a755984 100644 --- a/admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts +++ b/admin-ui/app/hooks/useIdleTimer/useIdleTimer.ts @@ -14,7 +14,7 @@ const DEFAULT_EVENTS = [ const eventTarget = (eventName: string): Window | Document => eventName === 'visibilitychange' ? document : window -export const useIdleTimer = ({ +const useIdleTimer = ({ timeout, onIdle, onActive, @@ -88,5 +88,4 @@ export const useIdleTimer = ({ return { reset, isIdle } } - export default useIdleTimer diff --git a/admin-ui/app/utils/AppAuthProvider.tsx b/admin-ui/app/utils/AppAuthProvider.tsx index 78e2f2c8b7..1d02a40e84 100755 --- a/admin-ui/app/utils/AppAuthProvider.tsx +++ b/admin-ui/app/utils/AppAuthProvider.tsx @@ -37,7 +37,7 @@ import { type FetchUserInfoResult, } from 'Redux/api/backend-api' import { useTranslation } from 'react-i18next' -import { decodeJwt } from '@/utils/jwtDecode' +import decodeJwt from '@/utils/jwtDecode' import type { UserInfo } from '@/redux/features/types/authTypes' import type { OAuthConfig, AppAuthProviderProps } from '@/utils/types' import { buildSafeLogoutUrl } from '@/utils/urlSecurity' diff --git a/admin-ui/app/utils/__tests__/jwtDecode.test.ts b/admin-ui/app/utils/__tests__/jwtDecode.test.ts index 3bc12423d1..c0a7bd97bf 100644 --- a/admin-ui/app/utils/__tests__/jwtDecode.test.ts +++ b/admin-ui/app/utils/__tests__/jwtDecode.test.ts @@ -1,4 +1,4 @@ -import { decodeJwt } from '../jwtDecode' +import decodeJwt from '../jwtDecode' const toJwt = (payload: Record): string => { const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url') diff --git a/admin-ui/app/utils/jwtDecode.ts b/admin-ui/app/utils/jwtDecode.ts index 6f17e1aad0..530293d280 100644 --- a/admin-ui/app/utils/jwtDecode.ts +++ b/admin-ui/app/utils/jwtDecode.ts @@ -5,7 +5,7 @@ type JsonObject = { [key: string]: JsonValue } const BASE64_GROUP_SIZE = 4 -export const decodeJwt = (token: string): T => { +const decodeJwt = (token: string): T => { const segments = typeof token === 'string' ? token.split('.') : [] const payloadSegment = segments[1] if (segments.length < 2 || !payloadSegment) { @@ -33,5 +33,4 @@ export const decodeJwt = (token: string): T => { throw new Error('decodeJwt: token payload is not valid JSON') } } - export default decodeJwt diff --git a/admin-ui/app/utils/zip.ts b/admin-ui/app/utils/zip.ts index 5e1fa2ce79..f6b9a5d0d8 100644 --- a/admin-ui/app/utils/zip.ts +++ b/admin-ui/app/utils/zip.ts @@ -43,7 +43,7 @@ const inflateRaw = async (data: Uint8Array): Promise => { return new Uint8Array(await new Response(stream).arrayBuffer()) } -export const readZip = async (input: Blob | ArrayBuffer | Uint8Array): Promise => { +const readZip = async (input: Blob | ArrayBuffer | Uint8Array): Promise => { const bytes = await toBytes(input) const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) const decoder = new TextDecoder() @@ -91,5 +91,5 @@ export const readZip = async (input: Blob | ArrayBuffer | Uint8Array): Promise Date: Thu, 11 Jun 2026 13:52:26 +0500 Subject: [PATCH 14/24] chore(admin-ui): removal two more redundant UI dependencies (#2878) Signed-off-by: faisalsiddique4400 --- .../app/components/GluuTable/GluuTable.tsx | 6 +- .../app/components/GluuTable/constants.ts | 2 + admin-ui/app/components/Layout/Layout.tsx | 6 +- admin-ui/app/components/icons/index.ts | 1 - admin-ui/app/config/site.ts | 2 - admin-ui/app/constants/index.ts | 1 + admin-ui/app/constants/site.ts | 2 + admin-ui/app/constants/ui.ts | 18 +++ .../app/layout/components/DefaultSidebar.tsx | 4 +- admin-ui/app/layout/default.tsx | 1 - .../app/routes/Apps/Gluu/GluuAutocomplete.tsx | 9 +- .../routes/Apps/Gluu/GluuBooleanSelectBox.tsx | 61 -------- .../app/routes/Apps/Gluu/GluuInlineInput.tsx | 28 ++-- .../routes/Apps/Gluu/GluuPermissionModal.tsx | 54 ------- .../app/routes/Apps/Gluu/GluuSecretDetail.tsx | 64 -------- .../routes/Apps/Gluu/GluuSuspenseLoader.tsx | 7 - admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx | 96 +++++++----- .../app/routes/Apps/Gluu/GluuTypeAhead.tsx | 104 ------------- .../routes/Apps/Gluu/GluuTypeAheadForDn.tsx | 146 ------------------ .../routes/Apps/Gluu/GluuTypeAheadWithAdd.tsx | 112 -------------- .../Apps/Gluu/Tests/GluuAutocomplete.test.tsx | 85 ++++++++++ .../Apps/Gluu/Tests/GluuSecretDetail.test.tsx | 31 ---- .../Apps/Gluu/Tests/GluuToggle.test.tsx | 31 +++- .../Apps/Gluu/Tests/GluuToggleRow.test.tsx | 63 ++++++++ .../Apps/Gluu/Tests/GluuTypeAhead.test.tsx | 28 ---- .../Gluu/Tests/GluuTypeAheadForDn.test.tsx | 38 ----- .../Gluu/Tests/GluuTypeAheadWithAdd.test.tsx | 55 ------- .../Gluu/styles/GluuPermissionModal.style.ts | 37 ----- .../Gluu/styles/GluuSecretDetail.style.ts | 12 -- .../Apps/Gluu/styles/GluuToggle.style.ts | 106 +++++++++++++ .../Apps/Gluu/styles/GluuTypeAhead.style.ts | 12 -- .../Gluu/styles/GluuTypeAheadWithAdd.style.ts | 18 --- .../Gluu/types/GluuComponentPropsTypes.ts | 24 --- .../Gluu/types/GluuPermissionModal.types.ts | 4 - .../Apps/Gluu/types/GluuToggle.types.ts | 12 ++ .../Apps/Gluu/types/GluuTypeAhead.types.ts | 35 ----- .../Gluu/types/GluuTypeAheadForDn.types.ts | 40 ----- .../Gluu/types/GluuTypeAheadWithAdd.types.ts | 20 --- admin-ui/app/routes/Apps/Gluu/types/index.ts | 1 - .../plugins/_react-bootstrap-typeahead.scss | 10 -- .../app/styles/plugins/_react-toggle.scss | 17 -- admin-ui/app/styles/plugins/plugins.css | 2 - admin-ui/app/styles/plugins/plugins.scss | 2 - admin-ui/app/utils/__tests__/zip.test.ts | 2 +- admin-ui/app/utils/menuFilters.ts | 2 +- admin-ui/app/utils/zip.ts | 3 +- admin-ui/package.json | 3 - .../admin/components/Assets/AssetForm.tsx | 8 +- .../admin/components/Webhook/WebhookForm.tsx | 8 +- .../components/Authentication/Acrs/Acrs.tsx | 6 +- .../Authentication/Acrs/AcrsEditPage.tsx | 3 +- .../Authentication/Acrs/AcrsForm.style.ts | 57 +++---- .../Authentication/Acrs/AcrsForm.tsx | 122 +++++++-------- .../Authentication/AgamaFlows/AgamaFlows.tsx | 2 +- .../Authentication/Aliases/Aliases.tsx | 6 +- .../Authentication/Authentication.tsx | 7 +- .../Authentication/DefaultAcr/DefaultAcr.tsx | 5 +- .../types/authenticationTypes.ts | 55 +++++++ .../components/ClientTokensPanel.tsx | 3 +- .../WebsiteSsoIdentityProviderForm.tsx | 9 +- .../WebsiteSsoServiceProviderForm.tsx | 67 +++++--- .../scripts/components/CustomScriptForm.tsx | 8 +- admin-ui/plugins/smtp/components/SmtpForm.tsx | 20 +-- .../components/UserClaimsListPage.test.tsx | 16 +- .../__tests__/utils/attributes.test.ts | 19 --- .../plugins/user-claims/utils/attributes.ts | 34 ---- 66 files changed, 636 insertions(+), 1236 deletions(-) delete mode 100644 admin-ui/app/config/site.ts create mode 100644 admin-ui/app/constants/site.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuTypeAhead.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuTypeAheadForDn.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/GluuTypeAheadWithAdd.tsx create mode 100644 admin-ui/app/routes/Apps/Gluu/Tests/GluuAutocomplete.test.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/Tests/GluuSecretDetail.test.tsx create mode 100644 admin-ui/app/routes/Apps/Gluu/Tests/GluuToggleRow.test.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/Tests/GluuTypeAhead.test.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/Tests/GluuTypeAheadForDn.test.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/Tests/GluuTypeAheadWithAdd.test.tsx delete mode 100644 admin-ui/app/routes/Apps/Gluu/styles/GluuPermissionModal.style.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/styles/GluuSecretDetail.style.ts create mode 100644 admin-ui/app/routes/Apps/Gluu/styles/GluuToggle.style.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/styles/GluuTypeAhead.style.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/styles/GluuTypeAheadWithAdd.style.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/types/GluuPermissionModal.types.ts create mode 100644 admin-ui/app/routes/Apps/Gluu/types/GluuToggle.types.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/types/GluuTypeAhead.types.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/types/GluuTypeAheadForDn.types.ts delete mode 100644 admin-ui/app/routes/Apps/Gluu/types/GluuTypeAheadWithAdd.types.ts delete mode 100755 admin-ui/app/styles/plugins/_react-bootstrap-typeahead.scss delete mode 100755 admin-ui/app/styles/plugins/_react-toggle.scss delete mode 100755 admin-ui/app/styles/plugins/plugins.css delete mode 100644 admin-ui/plugins/user-claims/__tests__/utils/attributes.test.ts delete mode 100644 admin-ui/plugins/user-claims/utils/attributes.ts diff --git a/admin-ui/app/components/GluuTable/GluuTable.tsx b/admin-ui/app/components/GluuTable/GluuTable.tsx index 2bd19ca590..7fe6f783d0 100644 --- a/admin-ui/app/components/GluuTable/GluuTable.tsx +++ b/admin-ui/app/components/GluuTable/GluuTable.tsx @@ -9,7 +9,7 @@ import GluuText from '@/routes/Apps/Gluu/GluuText' import { GluuButton } from '@/components/GluuButton' import { GluuSpinner } from '@/components/GluuSpinner' import { useStyles, TABLE_MIN_WIDTH, TABLE_RESPONSIVE_BREAKPOINT } from './GluuTable.style' -import { T_KEYS } from './constants' +import { T_KEYS, EMPTY_CELL_PLACEHOLDER } from './constants' import type { CellValue, ColumnKey, GluuTableProps, SortDirection } from './types' import { ChevronIcon } from '@/components/SVG' import { @@ -652,7 +652,9 @@ const GluuTable = (props: Readonly>) => { }) ) : ( - {String(value ?? '')} + {value === null || value === undefined || value === '' + ? EMPTY_CELL_PLACEHOLDER + : String(value)} )} diff --git a/admin-ui/app/components/GluuTable/constants.ts b/admin-ui/app/components/GluuTable/constants.ts index 7ab32a6d18..fc48bc5b24 100644 --- a/admin-ui/app/components/GluuTable/constants.ts +++ b/admin-ui/app/components/GluuTable/constants.ts @@ -4,6 +4,8 @@ export const COLUMN_WIDTHS = { PILL_LIST: 280, } as const +export const EMPTY_CELL_PLACEHOLDER = '—' + export const T_KEYS = { FIELDS_ACTIONS: 'fields.actions', FIELDS_OF: 'fields.of', diff --git a/admin-ui/app/components/Layout/Layout.tsx b/admin-ui/app/components/Layout/Layout.tsx index ce44af3d57..2bc38d2f91 100755 --- a/admin-ui/app/components/Layout/Layout.tsx +++ b/admin-ui/app/components/Layout/Layout.tsx @@ -13,7 +13,7 @@ import { LayoutNavbar } from './LayoutNavbar' import { LayoutSidebar } from './LayoutSidebar' import { PageConfigContext } from './PageConfigContext' import { ThemeClass } from './../Theme' -import { siteDescription, siteKeywords } from '@/config/site' +import { SITE_DESCRIPTION, SITE_KEYWORDS } from '@/constants' import type { LayoutPartComponentType, LayoutProps, @@ -89,8 +89,8 @@ const initialLayoutState: LayoutState = { screenSize: (typeof window !== 'undefined' ? getScreenSize() : '') as ScreenSize, animationsDisabled: true, pageTitle: null, - pageDescription: siteDescription, - pageKeywords: siteKeywords, + pageDescription: SITE_DESCRIPTION, + pageKeywords: SITE_KEYWORDS, } const Layout: React.FC = (props) => { diff --git a/admin-ui/app/components/icons/index.ts b/admin-ui/app/components/icons/index.ts index 533d06dc00..56b0f5add2 100644 --- a/admin-ui/app/components/icons/index.ts +++ b/admin-ui/app/components/icons/index.ts @@ -1,6 +1,5 @@ export { default as AccessTimeIcon } from '@mui/icons-material/AccessTime' export { default as Add } from '@mui/icons-material/Add' -export { default as AddCircleOutline } from '@mui/icons-material/AddCircleOutlineOutlined' export { default as CachedIcon } from '@mui/icons-material/Cached' export { default as Check } from '@mui/icons-material/Check' export { default as CheckCircleOutline } from '@mui/icons-material/CheckCircleOutlineOutlined' diff --git a/admin-ui/app/config/site.ts b/admin-ui/app/config/site.ts deleted file mode 100644 index 895ac3bd4e..0000000000 --- a/admin-ui/app/config/site.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const siteDescription = 'Jans-server admin Ui' -export const siteKeywords = 'jans-server oauth jans gluu' diff --git a/admin-ui/app/constants/index.ts b/admin-ui/app/constants/index.ts index f6ac170919..f51c17f486 100644 --- a/admin-ui/app/constants/index.ts +++ b/admin-ui/app/constants/index.ts @@ -1,4 +1,5 @@ export * from './ui' +export * from './site' export * from './status' export * from './storageKeys' export * from './logging' diff --git a/admin-ui/app/constants/site.ts b/admin-ui/app/constants/site.ts new file mode 100644 index 0000000000..5d400b783b --- /dev/null +++ b/admin-ui/app/constants/site.ts @@ -0,0 +1,2 @@ +export const SITE_DESCRIPTION = 'Jans-server admin Ui' +export const SITE_KEYWORDS = 'jans-server oauth jans gluu' diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts index 5ffb0c026e..24b39bfdff 100644 --- a/admin-ui/app/constants/ui.ts +++ b/admin-ui/app/constants/ui.ts @@ -120,6 +120,24 @@ export const ICON_SIZE = { export const ICON_BUTTON_SIZE = 32 +export const TOGGLE = { + TRACK_WIDTH: 50, + TRACK_HEIGHT: 24, + TRACK_RADIUS: 30, + THUMB_SIZE: 22, + THUMB_INSET: 1, + THUMB_CHECKED_LEFT: 27, + ICON_HEIGHT: 10, + CHECK_WIDTH: 14, + CHECK_LEFT: 8, + X_WIDTH: 10, + X_RIGHT: 10, + FOCUS_RING_BLUR: 2, + FOCUS_RING_SPREAD: 3, + ACTIVE_RING_BLUR: 5, + ACTIVE_RING_SPREAD: 5, +} as const + export const TOOLTIP = { ARROW_SIZE: 8, PADDING_VERTICAL: 8, diff --git a/admin-ui/app/layout/components/DefaultSidebar.tsx b/admin-ui/app/layout/components/DefaultSidebar.tsx index 922250de6c..dbb8fc745a 100755 --- a/admin-ui/app/layout/components/DefaultSidebar.tsx +++ b/admin-ui/app/layout/components/DefaultSidebar.tsx @@ -3,13 +3,13 @@ import { Link } from 'react-router-dom' import { useAppSelector } from '@/redux/hooks' import { Sidebar } from 'Components' import { LogoThemed } from 'Routes/components/LogoThemed/LogoThemed' -import GluuSuspenseLoader from 'Routes/Apps/Gluu/GluuSuspenseLoader' import GluuText from '@/routes/Apps/Gluu/GluuText' import GluuAppSidebar from 'Routes/Apps/Gluu/GluuAppSidebar' import type { DefaultSidebarProps } from './types' import { useTranslation } from 'react-i18next' import { ROUTES } from '@/helpers/navigation' import { useStyles } from './DefaultSidebar.style' +import { GluuSpinner } from '@/components/GluuSpinner' const DefaultSidebar: React.FC = () => { const { t } = useTranslation() @@ -26,7 +26,7 @@ const DefaultSidebar: React.FC = () => {
) : (
- +
) diff --git a/admin-ui/app/layout/default.tsx b/admin-ui/app/layout/default.tsx index 6ad416eed4..13bedc7b73 100755 --- a/admin-ui/app/layout/default.tsx +++ b/admin-ui/app/layout/default.tsx @@ -7,7 +7,6 @@ import { STORAGE_KEYS } from '@/constants' import 'Styles/bootstrap.scss' import 'Styles/main.scss' -import 'Styles/plugins/plugins.css' import 'Styles/plugins/plugins.scss' import { RoutedNavbars, RoutedSidebars } from '../routes' diff --git a/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx b/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx index 239975bf8b..91fc0bb3e3 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx @@ -50,7 +50,7 @@ const GluuAutocomplete = ({ const { t } = useTranslation() const { state: themeState } = useTheme() const selectedTheme = themeState?.theme ?? DEFAULT_THEME - const themeColors = getThemeColor(selectedTheme) + const themeColors = React.useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) const { classes } = useStyles({ themeColors, allowCustom, @@ -81,9 +81,10 @@ const GluuAutocomplete = ({ [labelByValue], ) - const selectedItems = Array.isArray(value) - ? value.filter((v): v is string => typeof v === 'string') - : [] + const selectedItems = React.useMemo( + () => (Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []), + [value], + ) const [inputValue, setInputValue] = React.useState('') const lockedPlacementRef = React.useRef(null) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx b/admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx deleted file mode 100644 index d3059fcbfb..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import GluuLabel from './GluuLabel' -import GluuToggle from './GluuToggle' -import { Col, FormGroup, CustomInput, InputGroup } from 'Components' -import { useTranslation } from 'react-i18next' -import { useMemo } from 'react' -import type { JsonValue } from './types/common' -import type { GluuBooleanSelectBoxProps } from './types' - -const GluuBooleanSelectBox = >({ - label, - name, - value, - formik, - handler, - lsize, - rsize, - doc_category, - disabled, - toToggle = true, -}: GluuBooleanSelectBoxProps): JSX.Element => { - const { t } = useTranslation() - const normalizedValue = useMemo(() => { - return typeof value === 'string' ? value === 'true' : value - }, [value]) - return ( - - - - {!toToggle && ( - - - - - - - )} - {toToggle && ( - - )} - - - ) -} - -export default GluuBooleanSelectBox diff --git a/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx b/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx index 147952353d..93b1bc1962 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx @@ -3,7 +3,8 @@ import { Check, Close } from '@/components/icons' import { OPACITY } from '@/constants' import GluuLabel from './GluuLabel' import GluuToggle from './GluuToggle' -import { Typeahead } from 'react-bootstrap-typeahead' +import GluuAutocomplete from './GluuAutocomplete' +import type { AutocompleteOption } from './types/GluuAutocomplete.types' import applicationStyle from '@/routes/Apps/Gluu/styles/applicationStyle' import { useStyles } from '@/routes/Apps/Gluu/styles/GluuInlineInput.style' import { Col, FormGroup, Input, Button } from 'Components' @@ -137,8 +138,11 @@ const GluuInlineInput = ({ [value], ) - const filteredOptions = useMemo( - () => (Array.isArray(options) ? options.filter((item) => item != null) : []), + const filteredOptions = useMemo( + () => + Array.isArray(options) + ? options.filter((item) => item != null).map((item) => ({ value: item, label: item })) + : [], [options], ) const resolvedId = id || name @@ -173,21 +177,19 @@ const GluuInlineInput = ({ data-testid={resolvedId} name={name} handler={onValueChanged} - value={value as boolean} + value={Boolean(data)} disabled={disabled} /> )} {isArray && ( - )} diff --git a/admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx b/admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx deleted file mode 100644 index a2aecdbbcb..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { useTheme } from 'Context/theme/themeContext' -import getThemeColor from '@/context/theme/config' -import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' -import GluuText from './GluuText' -import { GluuButton } from '@/components' -import { GluuModalShell } from '@/components/GluuModalShell' -import { useStyles } from './styles/GluuPermissionModal.style' -import type { GluuPermissionModalProps } from './types/GluuPermissionModal.types' - -const GluuPermissionModal = ({ handler, isOpen }: GluuPermissionModalProps) => { - const { t } = useTranslation() - const { state: themeState } = useTheme() - const selectedTheme = themeState?.theme ?? DEFAULT_THEME - const isDark = selectedTheme === THEME_DARK - const themeColors = useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) - const { classes } = useStyles({ isDark, themeColors }) - - if (!isOpen) return null - - return ( - - - {t('dashboard.access_denied')} - -
- - 🚫 {t('dashboard.access_denied_message')} - - {t('dashboard.access_contact_admin')} -
-
- - {t('menus.signout')} - -
-
- ) -} - -export default GluuPermissionModal diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx deleted file mode 100644 index 4e803968e9..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useState, memo, useCallback, CSSProperties, useMemo } from 'react' -import { FormGroup, Label, Col } from 'Components' -import Toggle from 'react-toggle' -import GluuTooltip from './GluuTooltip' -import { useTranslation } from 'react-i18next' -import customColors from '@/customColors' -import type { GluuSecretDetailProps } from './types' -import { useStyles } from './styles/GluuSecretDetail.style' - -const defaultLabelStyle: CSSProperties = { fontWeight: 'bold', color: customColors.black } - -const GluuSecretDetail = ({ - label, - value, - doc_category, - doc_entry, - lsize = 6, - rsize = 6, - labelStyle, - rowClassName, -}: GluuSecretDetailProps) => { - const { t } = useTranslation() - const [up, setUp] = useState(false) - - const handleSecret = useCallback(() => { - setUp((prev) => !prev) - }, []) - - const appliedLabelStyle: CSSProperties = useMemo( - () => ({ ...defaultLabelStyle, ...labelStyle }), - [labelStyle], - ) - const { classes } = useStyles() - const appliedRowClassName = rowClassName || classes.rowDefault - - const valueStyle: CSSProperties = useMemo( - () => ({ fontWeight: 'bold', color: customColors.black }), - [], - ) - - return ( - - - - - {value !== '-' && } - {(value === '-' || up) && ( - - {value} - - )} - - - - ) -} - -export default memo(GluuSecretDetail) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx deleted file mode 100644 index 569618f933..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { GluuSpinner } from '@/components/GluuSpinner' - -const GluuSuspenseLoader = () => { - return -} - -export default GluuSuspenseLoader diff --git a/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx b/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx index 6a8f203b99..2c7544af0a 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx @@ -1,52 +1,74 @@ -import React, { useEffect, useState } from 'react' -import type { FormikProps } from 'formik' -import Toggle from 'react-toggle' +import { useState } from 'react' +import type { ChangeEvent } from 'react' +import clsx from 'clsx' import type { JsonValue } from './types/common' - -type GluuToggleProps> = { - id?: string - name: string - formik?: FormikProps | null - value?: boolean - handler?: (event: React.ChangeEvent) => void - disabled?: boolean -} +import type { GluuToggleProps } from './types/GluuToggle.types' +import { useStyles } from './styles/GluuToggle.style' const GluuToggle = ,>({ id, name, formik, - value, + value = false, handler, - disabled, + disabled = false, }: GluuToggleProps) => { - const [checked, setChecked] = useState(value || false) + const { classes } = useStyles() + const [focused, setFocused] = useState(false) - useEffect(() => { - if (value !== undefined) { - setChecked(value) + const handleChange = (event: ChangeEvent) => { + if (formik) { + formik.handleChange(event) } - }, [value]) + handler?.(event) + } return ( - ) => { - setChecked(event.target.checked) - if (formik) { - formik.handleChange(event) - if (handler) { - handler(event) - } - } else { - handler?.(event) - } - }} - disabled={disabled} - checked={checked} - /> + +