Skip to content

Commit

Permalink
Merge branch 'keycloak-ramsons' into STCOR-862
Browse files Browse the repository at this point in the history
  • Loading branch information
zburke authored Jul 22, 2024
2 parents 0684955 + f150e29 commit 4b179b3
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 100 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<RootWithIntl>`. Refs STCOR-864.
* `/users-keycloak/_self` is an authentication request. Refs STCOR-866.
* Terminate the session when the fixed-length session expires. Refs STCOR-862.

## [10.1.1](https://github.com/folio-org/stripes-core/tree/v10.1.1) (2024-03-25)
Expand Down
17 changes: 15 additions & 2 deletions src/discoverServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
]);
};
Expand All @@ -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));
Expand Down Expand Up @@ -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, {
Expand Down
20 changes: 20 additions & 0 deletions src/discoverServices.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
51 changes: 51 additions & 0 deletions src/hooks/useUserSelfTenantPermissions.js
Original file line number Diff line number Diff line change
@@ -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;
71 changes: 71 additions & 0 deletions src/hooks/useUserSelfTenantPermissions.test.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

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({}));
});
});
59 changes: 59 additions & 0 deletions src/hooks/useUserTenantPermissionNames.js
Original file line number Diff line number Diff line change
@@ -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;
72 changes: 72 additions & 0 deletions src/hooks/useUserTenantPermissionNames.test.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

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({}));
});
});
Loading

0 comments on commit 4b179b3

Please sign in to comment.