Skip to content

Commit

Permalink
UIU-3054 use users-keycloak interface for central-tenant permissions (#…
Browse files Browse the repository at this point in the history
…2632)

When the `users-keycloak` interface is present, use its endpoints to
retrieve the logged-in user's permissions in the central tenant.

Refs UIU-3054

Co-authored-by: Ryan Berger <[email protected]>
  • Loading branch information
zburke and ryandberger committed Mar 28, 2024
1 parent 9264df3 commit 51f6114
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 20 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@
* Fix lint issues. Refs UIU-3072.
* Leverage `users-keycloak` interface endpoints for user data when available.
* Fix problem with `Reset password email sent` , application points to bl-users endpoint, even if `users-keycloak` interface provided. Refs UIU-3031.
* Add `users-keycloak` permissions. Refs UIU-3068
* Add `users-keycloak` permissions. Refs UIU-3068.
* Omit permissions accordions and queries when `roles` interface is present. Refs UIU-3061, UIU-3062.
* Retrieve user's central-tenant permission from `users-keycloak` endpoints when available. Refs UIU-3054.

## [10.0.4](https://github.com/folio-org/ui-users/tree/v10.0.4) (2023-11-10)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v10.0.3...v10.0.4)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"loan-storage": "4.0 5.0 6.0 7.0",
"notes": "2.0 3.0",
"request-storage": "2.5 3.0 4.0 5.0 6.0",
"roles": "1.0"
"roles": "1.0",
"users-keycloak": "1.0"
},
"permissionSets": [
{
Expand Down
63 changes: 45 additions & 18 deletions src/hooks/useConsortiumPermissions/useConsortiumPermissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import {
OKAPI_TENANT_HEADER,
} from '../../constants';

/**
* useConsortiumPermissions
* Retrieve a list of consortia-related permissions assigned to the current
* user in their central tenant.
*
* @returns {object} shaped like { foo: true, bar: true } where foo, bar are permissions
*/
const useConsortiumPermissions = () => {
const stripes = useStripes();
const ky = useOkapiKy();
Expand All @@ -21,6 +28,43 @@ const useConsortiumPermissions = () => {

const enabled = Boolean(user?.id && consortium?.id);

// retrieve permissions via permissions interface
const permissionsPermissions = async (api) => {
const { id } = await api.get(
`perms/users/${user.id}`,
{ searchParams: { indexField: 'userId' } },
).json();
const { permissions } = await api.get(
'perms/permissions',
{ searchParams: { limit: MAX_RECORDS, query: `(grantedTo=${id})`, expanded: true } },
).json();

return permissions
.map(({ subPermissions = [] }) => subPermissions)
.flat()
.filter(permission => permission.includes('consortia'))
.reduce((acc, permission) => {
acc[permission] = true;

return acc;
}, {});
};

// retrieve permissions via users-keycloak interface
const capabilitiesPermissions = async (api) => {
const { permissions } = await api.get(
'users-keycloak/_self',
).json();

return permissions?.permissions
.filter(permission => permission.includes('consortia'))
.reduce((acc, permission) => {
acc[permission] = true;

return acc;
}, {});
};

const {
isLoading,
data = {},
Expand All @@ -38,24 +82,7 @@ const useConsortiumPermissions = () => {
});

try {
const { id } = await api.get(
`perms/users/${user.id}`,
{ searchParams: { indexField: 'userId' } },
).json();
const { permissions } = await api.get(
'perms/permissions',
{ searchParams: { limit: MAX_RECORDS, query: `(grantedTo=${id})`, expanded: true } },
).json();

return permissions
.map(({ subPermissions = [] }) => subPermissions)
.flat()
.filter(permission => permission.includes('consortia'))
.reduce((acc, permission) => {
acc[permission] = true;

return acc;
}, {});
return stripes.hasInterface('users-keycloak', '1.0') ? capabilitiesPermissions(api) : permissionsPermissions(api);
} catch {
return {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('useConsortiumPermissions', () => {
mockGet.mockClear();
useOkapiKy.mockClear().mockReturnValue(kyMock);
useStripes.mockClear().mockReturnValue({
hasInterface: () => (false),
user: {
user: { ...user, consortium },
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
import {
QueryClient,
QueryClientProvider,
} from 'react-query';

import {
useOkapiKy,
useStripes,
} from '@folio/stripes/core';

import consortia from '../../../test/jest/fixtures/consortia';
import {
MAX_RECORDS,

Check warning on line 14 in src/hooks/useConsortiumPermissions/useConsortiumPermissionsUsersKeycloak.test.js

View workflow job for this annotation

GitHub Actions / build-npm

'MAX_RECORDS' is defined but never used. Allowed unused vars must match /React/u
} from '../../constants';
import useConsortiumPermissions from './useConsortiumPermissions';

jest.mock('@folio/stripes/core', () => ({
...jest.requireActual('@folio/stripes/core'),
useNamespace: jest.fn(() => ['test']),
useOkapiKy: jest.fn(),
useStripes: jest.fn(),
}));

const queryClient = new QueryClient();

// eslint-disable-next-line react/prop-types
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

const user = { id: 'userId' };
const consortium = {
...consortia[0],
centralTenantId: 'mobius',
};

const response = {

Check warning on line 40 in src/hooks/useConsortiumPermissions/useConsortiumPermissionsUsersKeycloak.test.js

View workflow job for this annotation

GitHub Actions / build-npm

'response' is assigned a value but never used. Allowed unused vars must match /React/u
id: 'userPermissionId',
permissions: [
{ subPermissions: ['users.view'] },
{ subPermissions: ['consortia.view'] },
],
};

describe('useConsortiumPermissions with users-keycloak', () => {
const keycloakResponse = {
'user': {
'username': 'circ-admin',
'id': '7cf60c03-1a83-4d40-9aec-961f2e55963f',
},
'permissions': {
'userId': '7cf60c03-1a83-4d40-9aec-961f2e55963f',
'permissions': [
'monkey.bagel',
'thunder.chicken',
'consortia.view',
]
}
};

const mockGet = jest.fn(() => ({
json: () => Promise.resolve(keycloakResponse),
}));
const setHeaderMock = jest.fn();
const kyMock = {
extend: jest.fn(({ hooks: { beforeRequest } }) => {
beforeRequest.forEach(handler => handler({ headers: { set: setHeaderMock } }));

return {
get: mockGet,
};
}),
};

beforeEach(() => {
mockGet.mockClear();
useOkapiKy.mockClear().mockReturnValue(kyMock);
useStripes.mockClear().mockReturnValue({
hasInterface: () => (true),
user: {
user: { ...user, consortium },
}
});
});

it('should fetch consortium permissions via capabilities interface', async () => {
const { result } = renderHook(() => useConsortiumPermissions(), { wrapper });

await waitFor(() => !result.current.isLoading);

expect(mockGet).toHaveBeenCalledWith('users-keycloak/_self');
});

it('should return consortia permissions', async () => {
const { result } = renderHook(() => useConsortiumPermissions(), { wrapper });

await waitFor(() => !result.current.isLoading);

expect(result.current.permissions).toEqual({ 'consortia.view': true });
});
});

0 comments on commit 51f6114

Please sign in to comment.