From bec39d570c7990d42ca17b87195caafb2d739043 Mon Sep 17 00:00:00 2001
From: aidynoJ <121284650+aidynoJ@users.noreply.github.com>
Date: Mon, 15 Jul 2024 16:32:25 +0500
Subject: [PATCH 1/3] STCOR-834: refactor useUserTenantPermissions to use _self
endpoint permissions instead of okapi permissions if roles interface is
presented (#1491)
Refs STCOR-834.
---
src/hooks/useUserSelfTenantPermissions.js | 51 ++++++++
.../useUserSelfTenantPermissions.test.js | 71 +++++++++++
src/hooks/useUserTenantPermissionNames.js | 59 +++++++++
.../useUserTenantPermissionNames.test.js | 72 +++++++++++
src/hooks/useUserTenantPermissions.js | 61 +++------
src/hooks/useUserTenantPermissions.test.js | 117 +++++++++---------
6 files changed, 333 insertions(+), 98 deletions(-)
create mode 100644 src/hooks/useUserSelfTenantPermissions.js
create mode 100644 src/hooks/useUserSelfTenantPermissions.test.js
create mode 100644 src/hooks/useUserTenantPermissionNames.js
create mode 100644 src/hooks/useUserTenantPermissionNames.test.js
diff --git a/src/hooks/useUserSelfTenantPermissions.js b/src/hooks/useUserSelfTenantPermissions.js
new file mode 100644
index 000000000..7ffde2e84
--- /dev/null
+++ b/src/hooks/useUserSelfTenantPermissions.js
@@ -0,0 +1,51 @@
+import { useQuery } from 'react-query';
+
+import { useStripes } from '../StripesContext';
+import { useNamespace } from '../components';
+import useOkapiKy from '../useOkapiKy';
+
+const INITIAL_DATA = [];
+
+const useUserSelfTenantPermissions = (
+ { tenantId },
+ options = {},
+) => {
+ const stripes = useStripes();
+ const ky = useOkapiKy();
+ const api = ky.extend({
+ hooks: {
+ beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', tenantId)]
+ }
+ });
+ const [namespace] = useNamespace({ key: 'user-self-permissions' });
+
+ const user = stripes.user.user;
+
+ const {
+ isFetching,
+ isLoading,
+ data,
+ } = useQuery(
+ [namespace, user?.id, tenantId],
+ ({ signal }) => {
+ return api.get(
+ 'users-keycloak/_self',
+ { signal },
+ ).json();
+ },
+ {
+ enabled: Boolean(user?.id && tenantId) && stripes.hasInterface('users-keycloak'),
+ keepPreviousData: true,
+ ...options,
+ },
+ );
+
+ return ({
+ isFetching,
+ isLoading,
+ userPermissions: data?.permissions.permissions || INITIAL_DATA,
+ totalRecords: data?.permissions.permissions.length || 0,
+ });
+};
+
+export default useUserSelfTenantPermissions;
diff --git a/src/hooks/useUserSelfTenantPermissions.test.js b/src/hooks/useUserSelfTenantPermissions.test.js
new file mode 100644
index 000000000..e8604bab7
--- /dev/null
+++ b/src/hooks/useUserSelfTenantPermissions.test.js
@@ -0,0 +1,71 @@
+import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from 'react-query';
+
+import permissions from 'fixtures/permissions';
+import useUserSelfTenantPermissions from './useUserSelfTenantPermissions';
+import useOkapiKy from '../useOkapiKy';
+
+jest.mock('../useOkapiKy');
+jest.mock('../components', () => ({
+ useNamespace: () => ([]),
+}));
+jest.mock('../StripesContext', () => ({
+ useStripes: () => ({
+ user: {
+ user: {
+ id: 'userId'
+ }
+ },
+ hasInterface: () => true
+ }),
+}));
+
+const queryClient = new QueryClient();
+
+// eslint-disable-next-line react/prop-types
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const response = {
+ permissions: { permissions },
+};
+
+describe('useUserSelfTenantPermissions', () => {
+ const getMock = jest.fn(() => ({
+ json: () => Promise.resolve(response),
+ }));
+ const setHeaderMock = jest.fn();
+ const kyMock = {
+ extend: jest.fn(({ hooks: { beforeRequest } }) => {
+ beforeRequest.forEach(handler => handler({ headers: { set: setHeaderMock } }));
+
+ return {
+ get: getMock,
+ };
+ }),
+ };
+
+ beforeEach(() => {
+ getMock.mockClear();
+ useOkapiKy.mockClear().mockReturnValue(kyMock);
+ });
+
+ it('should fetch user permissions for specified tenant', async () => {
+ const options = {
+ userId: 'userId',
+ tenantId: 'tenantId',
+ };
+ const { result } = renderHook(() => useUserSelfTenantPermissions(options), { wrapper });
+
+ await waitFor(() => !result.current.isLoading);
+
+ expect(setHeaderMock).toHaveBeenCalledWith('X-Okapi-Tenant', options.tenantId);
+ expect(getMock).toHaveBeenCalledWith('users-keycloak/_self', expect.objectContaining({}));
+ });
+});
diff --git a/src/hooks/useUserTenantPermissionNames.js b/src/hooks/useUserTenantPermissionNames.js
new file mode 100644
index 000000000..dc7c2057a
--- /dev/null
+++ b/src/hooks/useUserTenantPermissionNames.js
@@ -0,0 +1,59 @@
+import { useQuery } from 'react-query';
+
+import { useStripes } from '../StripesContext';
+import { useNamespace } from '../components';
+import useOkapiKy from '../useOkapiKy';
+
+const INITIAL_DATA = [];
+
+const useUserTenantPermissionNames = (
+ { tenantId },
+ options = {},
+) => {
+ const stripes = useStripes();
+ const ky = useOkapiKy();
+ const api = ky.extend({
+ hooks: {
+ beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', tenantId)]
+ }
+ });
+ const [namespace] = useNamespace({ key: 'user-affiliation-permissions' });
+
+ const user = stripes.user.user;
+
+ const searchParams = {
+ full: 'true',
+ indexField: 'userId',
+ };
+
+ const {
+ isFetching,
+ isLoading,
+ data = {},
+ } = useQuery(
+ [namespace, user?.id, tenantId],
+ ({ signal }) => {
+ return api.get(
+ `perms/users/${user.id}/permissions`,
+ {
+ searchParams,
+ signal,
+ },
+ ).json();
+ },
+ {
+ enabled: Boolean(user?.id && tenantId) && !stripes.hasInterface('roles'),
+ keepPreviousData: true,
+ ...options,
+ },
+ );
+
+ return ({
+ isFetching,
+ isLoading,
+ userPermissions: data.permissionNames || INITIAL_DATA,
+ totalRecords: data.totalRecords,
+ });
+};
+
+export default useUserTenantPermissionNames;
diff --git a/src/hooks/useUserTenantPermissionNames.test.js b/src/hooks/useUserTenantPermissionNames.test.js
new file mode 100644
index 000000000..cc15aecda
--- /dev/null
+++ b/src/hooks/useUserTenantPermissionNames.test.js
@@ -0,0 +1,72 @@
+import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from 'react-query';
+
+import permissions from 'fixtures/permissions';
+import useUserTenantPermissionNames from './useUserTenantPermissionNames';
+import useOkapiKy from '../useOkapiKy';
+
+jest.mock('../useOkapiKy');
+jest.mock('../components', () => ({
+ useNamespace: () => ([]),
+}));
+jest.mock('../StripesContext', () => ({
+ useStripes: () => ({
+ user: {
+ user: {
+ id: 'userId'
+ }
+ },
+ hasInterface: () => false
+ }),
+}));
+
+const queryClient = new QueryClient();
+
+// eslint-disable-next-line react/prop-types
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const response = {
+ permissionNames: permissions,
+ totalRecords: permissions.length,
+};
+
+describe('useUserTenantPermissionNames', () => {
+ const getMock = jest.fn(() => ({
+ json: () => Promise.resolve(response),
+ }));
+ const setHeaderMock = jest.fn();
+ const kyMock = {
+ extend: jest.fn(({ hooks: { beforeRequest } }) => {
+ beforeRequest.forEach(handler => handler({ headers: { set: setHeaderMock } }));
+
+ return {
+ get: getMock,
+ };
+ }),
+ };
+
+ beforeEach(() => {
+ getMock.mockClear();
+ useOkapiKy.mockClear().mockReturnValue(kyMock);
+ });
+
+ it('should fetch user permissions for specified tenant', async () => {
+ const options = {
+ userId: 'userId',
+ tenantId: 'tenantId',
+ };
+ const { result } = renderHook(() => useUserTenantPermissionNames(options), { wrapper });
+
+ await waitFor(() => !result.current.isLoading);
+
+ expect(setHeaderMock).toHaveBeenCalledWith('X-Okapi-Tenant', options.tenantId);
+ expect(getMock).toHaveBeenCalledWith(`perms/users/${options.userId}/permissions`, expect.objectContaining({}));
+ });
+});
diff --git a/src/hooks/useUserTenantPermissions.js b/src/hooks/useUserTenantPermissions.js
index 80030eea1..4173160c3 100644
--- a/src/hooks/useUserTenantPermissions.js
+++ b/src/hooks/useUserTenantPermissions.js
@@ -1,58 +1,37 @@
-import { useQuery } from 'react-query';
-
import { useStripes } from '../StripesContext';
-import { useNamespace } from '../components';
-import useOkapiKy from '../useOkapiKy';
-
-const INITIAL_DATA = [];
+import useUserSelfTenantPermissions from './useUserSelfTenantPermissions';
+import useUserTenantPermissionNames from './useUserTenantPermissionNames';
const useUserTenantPermissions = (
{ tenantId },
options = {},
) => {
const stripes = useStripes();
- const ky = useOkapiKy();
- const api = ky.extend({
- hooks: {
- beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', tenantId)]
- }
- });
- const [namespace] = useNamespace({ key: 'user-affiliation-permissions' });
-
- const user = stripes.user.user;
- const searchParams = {
- full: 'true',
- indexField: 'userId',
- };
+ const {
+ isFetching: isPermissionsFetching,
+ isLoading: isPermissionsLoading,
+ userPermissions: permissionsData = {},
+ totalRecords: permissionsTotalRecords
+ } = useUserTenantPermissionNames({ tenantId }, options);
const {
- isFetching,
- isLoading,
- data = {},
- } = useQuery(
- [namespace, user?.id, tenantId],
- ({ signal }) => {
- return api.get(
- `perms/users/${user.id}/permissions`,
- {
- searchParams,
- signal,
- },
- ).json();
- },
- {
- enabled: Boolean(user?.id && tenantId),
- keepPreviousData: true,
- ...options,
- },
- );
+ isFetching: isSelfPermissionsFetching,
+ isLoading: isSelfPermissionsLoading,
+ userPermissions:selfPermissionsData = {},
+ totalRecords: selfPermissionsTotalRecords
+ } = useUserSelfTenantPermissions({ tenantId }, options);
+
+ const isFetching = stripes.hasInterface('roles') ? isSelfPermissionsFetching : isPermissionsFetching;
+ const isLoading = stripes.hasInterface('roles') ? isSelfPermissionsLoading : isPermissionsLoading;
+ const userPermissions = stripes.hasInterface('roles') ? selfPermissionsData : permissionsData;
+ const totalRecords = stripes.hasInterface('roles') ? selfPermissionsTotalRecords : permissionsTotalRecords;
return ({
isFetching,
isLoading,
- userPermissions: data.permissionNames || INITIAL_DATA,
- totalRecords: data.totalRecords,
+ userPermissions,
+ totalRecords
});
};
diff --git a/src/hooks/useUserTenantPermissions.test.js b/src/hooks/useUserTenantPermissions.test.js
index e64b9c440..c3f4aa9bd 100644
--- a/src/hooks/useUserTenantPermissions.test.js
+++ b/src/hooks/useUserTenantPermissions.test.js
@@ -1,71 +1,74 @@
-import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
-import {
- QueryClient,
- QueryClientProvider,
-} from 'react-query';
-
-import permissions from 'fixtures/permissions';
+import { renderHook } from '@folio/jest-config-stripes/testing-library/react';
+import { useStripes } from '../StripesContext';
+import useUserSelfTenantPermissions from './useUserSelfTenantPermissions';
+import useUserTenantPermissionNames from './useUserTenantPermissionNames';
import useUserTenantPermissions from './useUserTenantPermissions';
-import useOkapiKy from '../useOkapiKy';
-jest.mock('../useOkapiKy');
-jest.mock('../components', () => ({
- useNamespace: () => ([]),
-}));
-jest.mock('../StripesContext', () => ({
- useStripes: () => ({
- user: {
- user: {
- id: 'userId'
- }
- }
- }),
-}));
+jest.mock('../StripesContext');
+jest.mock('./useUserSelfTenantPermissions');
+jest.mock('./useUserTenantPermissionNames');
-const queryClient = new QueryClient();
+describe('useUserTenantPermissions', () => {
+ const tenantId = 'tenant-id';
+ const options = {};
-// eslint-disable-next-line react/prop-types
-const wrapper = ({ children }) => (
-
- {children}
-
-);
+ beforeEach(() => {
+ useStripes.mockReturnValue({
+ hasInterface: jest.fn()
+ });
+ });
-const response = {
- permissionNames: permissions,
- totalRecords: permissions.length,
-};
+ it('should return _self permissions data when "roles" interface is present', () => {
+ useStripes().hasInterface.mockReturnValue(true);
-describe('useUserTenantPermissions', () => {
- const getMock = jest.fn(() => ({
- json: () => Promise.resolve(response),
- }));
- const setHeaderMock = jest.fn();
- const kyMock = {
- extend: jest.fn(({ hooks: { beforeRequest } }) => {
- beforeRequest.forEach(handler => handler({ headers: { set: setHeaderMock } }));
+ useUserSelfTenantPermissions.mockReturnValue({
+ isFetching: true,
+ isLoading: true,
+ userPermissions: ['self'],
+ totalRecords: 1
+ });
- return {
- get: getMock,
- };
- }),
- };
+ useUserTenantPermissionNames.mockReturnValue({
+ isFetching: false,
+ isLoading: false,
+ userPermissions: ['permission name'],
+ totalRecords: 1
+ });
- beforeEach(() => {
- getMock.mockClear();
- useOkapiKy.mockClear().mockReturnValue(kyMock);
+ const { result } = renderHook(() => useUserTenantPermissions({ tenantId }, options));
+
+ expect(result.current).toStrictEqual({
+ isFetching: true,
+ isLoading: true,
+ userPermissions: ['self'],
+ totalRecords: 1
+ });
});
- it('should fetch user permissions for specified tenant', async () => {
- const options = {
- userId: 'userId',
- tenantId: 'tenantId',
- };
- const { result } = renderHook(() => useUserTenantPermissions(options), { wrapper });
+ it('should return tenant permissions data when "roles" interface is NOT present', () => {
+ useStripes().hasInterface.mockReturnValue(false);
+
+ useUserSelfTenantPermissions.mockReturnValue({
+ isFetching: true,
+ isLoading: true,
+ userPermissions: ['self'],
+ totalRecords: 1
+ });
+
+ useUserTenantPermissionNames.mockReturnValue({
+ isFetching: false,
+ isLoading: false,
+ userPermissions: ['permission name'],
+ totalRecords: 1
+ });
- await waitFor(() => !result.current.isLoading);
+ const { result } = renderHook(() => useUserTenantPermissions({ tenantId }, options));
- expect(setHeaderMock).toHaveBeenCalledWith('X-Okapi-Tenant', options.tenantId);
- expect(getMock).toHaveBeenCalledWith(`perms/users/${options.userId}/permissions`, expect.objectContaining({}));
+ expect(result.current).toStrictEqual({
+ isFetching: false,
+ isLoading: false,
+ userPermissions: ['permission name'],
+ totalRecords: 1
+ });
});
});
From f93f21d68c8c4b30a5a748ab5b1bf6db16339417 Mon Sep 17 00:00:00 2001
From: Zak Burke
Date: Mon, 15 Jul 2024 23:15:16 -0400
Subject: [PATCH 2/3] STCOR-866 include `/users-keycloak/_self` in auth-n
requests (#1502)
The RTR cycle is kicked off when processing the response from an
authentication-related request. `/users-keycloak/_self` was missing
from the list, which meant that RTR would never kick off when a new tab
was opened for an existing session.
Refs STCOR-866
---
CHANGELOG.md | 1 +
src/components/Root/token-util.js | 1 +
2 files changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a348814c..28ce8ffdb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
* Always retrieve `clientId` and `tenant` values from `config.tenantOptions` in stripes.config.js. Retires `okapi.tenant`, `okapi.clientId`, and `config.isSingleTenant`. Refs STCOR-787.
* List UI apps in "Applications/modules/interfaces" column. STCOR-773
* Correctly evaluate `stripes.okapi` before rendering ``. Refs STCOR-864.
+* `/users-keycloak/_self` is an authentication request. Refs STCOR-866.
## [10.1.1](https://github.com/folio-org/stripes-core/tree/v10.1.1) (2024-03-25)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.1.0...v10.1.1)
diff --git a/src/components/Root/token-util.js b/src/components/Root/token-util.js
index 32ede2604..0e70ba012 100644
--- a/src/components/Root/token-util.js
+++ b/src/components/Root/token-util.js
@@ -74,6 +74,7 @@ export const isAuthenticationRequest = (resource, oUrl) => {
'/authn/token',
'/bl-users/login-with-expiry',
'/bl-users/_self',
+ '/users-keycloak/_self',
];
return !!permissible.find(i => string.startsWith(`${oUrl}${i}`));
From f150e29dff4404319cf2ed7f6300da40dec9eebb Mon Sep 17 00:00:00 2001
From: Ryan Berger
Date: Mon, 22 Jul 2024 11:31:06 -0400
Subject: [PATCH 3/3] STCOR-867 Add permission display names lookup table to
Redux (#1505)
* Add permission display names lookup table to Redux
* Sonar fixes
---
src/discoverServices.js | 17 +++++++++++++++--
src/discoverServices.test.js | 20 ++++++++++++++++++++
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/src/discoverServices.js b/src/discoverServices.js
index 6ea2a63b9..0da05c8a9 100644
--- a/src/discoverServices.js
+++ b/src/discoverServices.js
@@ -25,6 +25,7 @@ function parseApplicationDescriptor(store, descriptor) {
const dispatchDescriptor = (d) => {
return Promise.all([
store.dispatch({ type: 'DISCOVERY_INTERFACES', data: d }),
+ store.dispatch({ type: 'DISCOVERY_PERMISSION_DISPLAY_NAMES', data: d }),
store.dispatch({ type: 'DISCOVERY_PROVIDERS', data: d }),
]);
};
@@ -42,12 +43,17 @@ function parseApplicationDescriptor(store, descriptor) {
data: descriptor.moduleDescriptors,
})
);
+
if (descriptor.moduleDescriptors) {
- list.push(...descriptor.moduleDescriptors?.map((i) => dispatchDescriptor(i)));
+ list.push(...descriptor.moduleDescriptors.map((i) => dispatchDescriptor(i)));
+ }
+
+ if (descriptor.uiModuleDescriptors) {
+ list.push(...descriptor.uiModuleDescriptors.map((i) => dispatchDescriptor(i)));
}
if (descriptor.uiModules) {
- list.push(...descriptor.uiModules?.map((i) => dispatchDescriptor(i)));
+ list.push(...descriptor.uiModules.map((i) => dispatchDescriptor(i)));
}
list.push(dispatchApplication(descriptor));
@@ -286,6 +292,13 @@ export function discoveryReducer(state = {}, action) {
interfaces: Object.assign(state.interfaces || {}, interfaces),
});
}
+ case 'DISCOVERY_PERMISSION_DISPLAY_NAMES': {
+ const permissions = {};
+ for (const entry of action.data.permissionSets || []) {
+ permissions[entry.permissionName] = entry.displayName;
+ }
+ return { permissionDisplayNames: { ...state.permissionDisplayNames, ...permissions } };
+ }
case 'DISCOVERY_PROVIDERS': {
if (action.data.provides?.length > 0) {
return Object.assign({}, state, {
diff --git a/src/discoverServices.test.js b/src/discoverServices.test.js
index 3edbf5ebd..f1c42486f 100644
--- a/src/discoverServices.test.js
+++ b/src/discoverServices.test.js
@@ -140,6 +140,26 @@ describe('discoveryReducer', () => {
expect(state).toMatchObject(mapped);
});
+ it('handles DISCOVERY_PERMISSION_DISPLAY_NAMES', () => {
+ let state = {
+ permissionDisplayNames: {}
+ };
+ const action = {
+ type: 'DISCOVERY_PERMISSION_DISPLAY_NAMES',
+ data: {
+ permissionSets: [
+ { 'permissionName': 'perm1', 'displayName': 'Admin Permission' },
+ { 'permissionName': 'perm2', 'displayName': 'Read-only Permission' }
+ ]
+ },
+ };
+
+ state = discoveryReducer(state, action);
+
+ expect(state.permissionDisplayNames.perm1).toBe(action.data.permissionSets[0].displayName);
+ expect(state.permissionDisplayNames.perm2).toBe(action.data.permissionSets[1].displayName);
+ });
+
it('handles DISCOVERY_OKAPI', () => {
let state = {
okapi: '0.0.0'