-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
STCOR-830 user-tenant-permissions hooks/functions (#1453)
Provide user-tenant-permissions functionality, both centralizing this functionality and insulating other applications from needing to depend on the permissions interface. * `useUserTenantPermissions` provides permissions for the currently authenticated user in a single tenant * `getUserTenantsPermissions` provides permissions for the currently authenticated user across an array of tenants Refs STCOR-830
- Loading branch information
Showing
10 changed files
with
300 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as useUserTenantPermissions } from './useUserTenantPermissions'; // eslint-disable-line import/prefer-default-export |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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, | ||
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, | ||
}, | ||
); | ||
|
||
return ({ | ||
isFetching, | ||
isLoading, | ||
userPermissions: data.permissionNames || INITIAL_DATA, | ||
totalRecords: data.totalRecords, | ||
}); | ||
}; | ||
|
||
export default useUserTenantPermissions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 useUserTenantPermissions from './useUserTenantPermissions'; | ||
import useOkapiKy from '../useOkapiKy'; | ||
|
||
jest.mock('../useOkapiKy'); | ||
jest.mock('../components', () => ({ | ||
useNamespace: () => ([]), | ||
})); | ||
jest.mock('../StripesContext', () => ({ | ||
useStripes: () => ({ | ||
user: { | ||
user: { | ||
id: 'userId' | ||
} | ||
} | ||
}), | ||
})); | ||
|
||
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('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 } })); | ||
|
||
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(() => useUserTenantPermissions(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({})); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/** | ||
* getUserTenantsPermissions | ||
* Retrieve the currently-authenticated user's permissions in each of the | ||
* given tenants | ||
* @param {object} stripes | ||
* @param {array} tenants array of tenantIds | ||
* @returns [] | ||
*/ | ||
const getUserTenantsPermissions = async (stripes, tenants = []) => { | ||
const { | ||
user: { user: { id } }, | ||
okapi: { | ||
url, | ||
token, | ||
} | ||
} = stripes; | ||
const userTenantIds = tenants.map(tenant => tenant.id || tenant); | ||
|
||
const promises = userTenantIds.map(async (tenantId) => { | ||
const result = await fetch(`${url}/perms/users/${id}/permissions?full=true&indexField=userId`, { | ||
headers: { | ||
'X-Okapi-Tenant': tenantId, | ||
'Content-Type': 'application/json', | ||
...(token && { 'X-Okapi-Token': token }), | ||
}, | ||
credentials: 'include', | ||
}); | ||
|
||
const json = await result.json(); | ||
|
||
return { tenantId, ...json }; | ||
}); | ||
|
||
const userTenantsPermissions = await Promise.allSettled(promises); | ||
|
||
return userTenantsPermissions.map(userTenantsPermission => userTenantsPermission.value); | ||
}; | ||
|
||
export default getUserTenantsPermissions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import getUserTenantsPermissions from './getUserTenantsPermissions'; | ||
|
||
const mockFetch = jest.fn(); | ||
|
||
describe('getUserTenantsPermissions', () => { | ||
beforeEach(() => { | ||
global.fetch = mockFetch; | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it('X-Okapi-Token header is present if present in stripes', async () => { | ||
const stripes = { | ||
user: { user: { id: 'userId' } }, | ||
okapi: { | ||
url: 'http://okapiUrl', | ||
token: 'elevensies', | ||
}, | ||
}; | ||
mockFetch.mockResolvedValueOnce('non-okapi-success'); | ||
|
||
await getUserTenantsPermissions(stripes, ['tenantId']); | ||
await expect(mockFetch.mock.calls).toHaveLength(1); | ||
|
||
expect(mockFetch.mock.lastCall[0]).toMatch(/[stripes.okapi.url]/); | ||
expect(mockFetch.mock.lastCall[1]).toMatchObject({ | ||
headers: { | ||
'X-Okapi-Tenant': 'tenantId' | ||
} | ||
}); | ||
}); | ||
|
||
it('X-Okapi-Token header is absent if absent from stripes', async () => { | ||
const stripes = { | ||
user: { user: { id: 'userId' } }, | ||
okapi: { | ||
url: 'http://okapiUrl', | ||
}, | ||
}; | ||
mockFetch.mockResolvedValueOnce('non-okapi-success'); | ||
|
||
await getUserTenantsPermissions(stripes, ['tenantId']); | ||
await expect(mockFetch.mock.calls).toHaveLength(1); | ||
|
||
expect(mockFetch.mock.lastCall[0]).toMatch(/[stripes.okapi.url]/); | ||
expect(mockFetch.mock.lastCall[1].headers.keys).toEqual(expect.not.arrayContaining(['X-Okapi-Token'])); | ||
}); | ||
|
||
it('response aggregates permissions across tenants', async () => { | ||
const stripes = { | ||
user: { user: { id: 'userId' } }, | ||
okapi: { | ||
url: 'http://okapiUrl', | ||
}, | ||
}; | ||
|
||
const t1 = { p: ['t1-p1', 't1-p2'] }; | ||
const t2 = { p: ['t2-p3', 't2-p4'] }; | ||
|
||
mockFetch | ||
.mockResolvedValueOnce(Promise.resolve({ json: () => Promise.resolve(t1) })) | ||
.mockResolvedValueOnce(Promise.resolve({ json: () => Promise.resolve(t2) })); | ||
|
||
const response = await getUserTenantsPermissions(stripes, ['t1', 't2']); | ||
|
||
expect(response[0]).toMatchObject(t1); | ||
expect(response[1]).toMatchObject(t2); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
|
||
export { default as getUserTenantsPermissions } from './getUserTenantsPermissions'; | ||
export { default as useChunkedCQLFetch } from './useChunkedCQLFetch'; | ||
export { default as useConfigurations } from './useConfigurations'; | ||
export { default as useOkapiEnv } from './useOkapiEnv'; | ||
export { default as useChunkedCQLFetch } from './useChunkedCQLFetch'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
[ | ||
{ | ||
"deprecated": false, | ||
"description": "Grants all permissions included in Agreements: Search & view agreements plus the ability to delete agreements. This does not include the ability to edit agreements, only to delete them", | ||
"displayName": "Agreements: Delete agreements", | ||
"grantedTo": ["8fafcfdb-419c-483e-a698-d7f8f46ea694"], | ||
"id": "026bc082-add6-4cdd-a4fd-a588439a57b6", | ||
"moduleName": "folio_agreements", | ||
"moduleVersion": "8.1.1000825", | ||
"mutable": false, | ||
"permissionName": "ui-agreements.agreements.delete", | ||
"subPermissions": ["ui-agreements.agreements.view", "erm.agreements.item.delete"], | ||
"tags": [], | ||
"visible": true | ||
}, | ||
{ | ||
"deprecated": false, | ||
"description": "Grants all permissions included in Agreements: Search & view agreements plus the ability to delete agreements. This does not include the ability to edit agreements, only to delete them", | ||
"displayName": "Agreements: Edit agreements", | ||
"grantedTo": ["8fafcfdb-419c-483e-a698-d7f8f46ea694"], | ||
"id": "b52da718-7770-407a-af6d-668d258b2309", | ||
"moduleName": "folio_agreements", | ||
"moduleVersion": "8.1.1000825", | ||
"mutable": false, | ||
"permissionName": "ui-agreements.agreements.edit", | ||
"subPermissions": ["ui-agreements.agreements.view", "erm.agreements.item.delete"], | ||
"tags": [], | ||
"visible": true | ||
} | ||
] |