From aed7bbcddb0847989b3d4efe704cea70a6362487 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 14 Nov 2024 08:48:58 -0500 Subject: [PATCH] STCOR-910 provide IfAnyPermission, stripes.ifAnyPerm `` and `stripes.ifPerm()` return `true` when the user has ALL the given permissions. `` and `stripes.ifAnyPerm()` return `true` when the user has ANY of the given permission. Refs STCOR-910 --- CHANGELOG.md | 1 + index.js | 1 + src/Stripes.js | 29 +++++++++++++ src/Stripes.test.js | 42 +++++++++++++++++++ .../IfAnyPermission/IfAnyPermission.js | 20 +++++++++ .../IfAnyPermission/IfAnyPermission.test.js | 33 +++++++++++++++ src/components/IfAnyPermission/index.js | 1 + src/components/IfAnyPermission/readme.md | 34 +++++++++++++++ src/components/IfPermission/IfPermission.js | 22 ++++------ .../IfPermission/IfPermission.test.js | 33 +++++++++++++++ src/components/IfPermission/readme.md | 7 ++-- 11 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 src/components/IfAnyPermission/IfAnyPermission.js create mode 100644 src/components/IfAnyPermission/IfAnyPermission.test.js create mode 100644 src/components/IfAnyPermission/index.js create mode 100644 src/components/IfAnyPermission/readme.md create mode 100644 src/components/IfPermission/IfPermission.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a09e5be..f635f6ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Conditionally use `/users-keycloak/_self` endpoint when `users-keycloak` interface is present. Refs STCOR-835. * Wait longer before declaring a rotation request to be stale. Refs STCOR-895. * Send the stored central tenant name in the header on logout. Refs STCOR-900. +* Provide `` and `stripes.hasAnyPermission()`. Refs STCOR-910. ## [10.2.0](https://github.com/folio-org/stripes-core/tree/v10.2.0) (2024-10-11) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.1.1...v10.2.0) diff --git a/index.js b/index.js index 67fd4d1d0..3a9510d40 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ export { default as createReactQueryClient } from './src/createReactQueryClient' export { default as AppContextMenu } from './src/components/MainNav/CurrentApp/AppContextMenu'; export { default as IfInterface } from './src/components/IfInterface'; export { default as IfPermission } from './src/components/IfPermission'; +export { default as IfAnyPermission } from './src/components/IfAnyPermission'; export { default as TitleManager } from './src/components/TitleManager'; export { default as HandlerManager } from './src/components/HandlerManager'; export { default as IntlConsumer } from './src/components/IntlConsumer'; diff --git a/src/Stripes.js b/src/Stripes.js index 5f91fe238..08c264d64 100644 --- a/src/Stripes.js +++ b/src/Stripes.js @@ -8,6 +8,7 @@ export const stripesShape = PropTypes.shape({ clone: PropTypes.func.isRequired, hasInterface: PropTypes.func.isRequired, hasPerm: PropTypes.func.isRequired, + hasAnyPerm: PropTypes.func.isRequired, // Properties passed into the constructor by the caller actionNames: PropTypes.arrayOf( @@ -91,6 +92,12 @@ class Stripes { Object.assign(this, properties); } + /** + * hasPerm + * Return true if user has every permission on the given list; false otherwise. + * @param {string} perm comma-separated list of permissions + * @returns boolean + */ hasPerm(perm) { const logger = this.logger; if (this.config && this.config.hasAllPerms) { @@ -107,6 +114,28 @@ class Stripes { return ok; } + /** + * hasAnyPerm + * Return true if user has any permission on the given list; false otherwise. + * @param {string} perm comma-separated list of permissions + * @returns boolean + */ + hasAnyPerm(perm) { + const logger = this.logger; + if (this.config && this.config.hasAllPerms) { + logger.log('perm', `assuming perm '${perm}': hasAllPerms is true`); + return true; + } + if (!this.user.perms) { + logger.log('perm', `not checking perm '${perm}': no user permissions yet`); + return undefined; + } + + const ok = _.some(perm.split(','), p => !!this.user.perms[p]); + logger.log('perm', `checking any perm '${perm}': `, ok); + return ok; + } + hasInterface(name, versionWanted) { const logger = this.logger; if (!this.discovery || !this.discovery.interfaces) { diff --git a/src/Stripes.test.js b/src/Stripes.test.js index fdc03c6df..49e67339e 100644 --- a/src/Stripes.test.js +++ b/src/Stripes.test.js @@ -100,4 +100,46 @@ describe('Stripes', () => { }); }); }); + + describe('hasAnyPerm', () => { + describe('returns true', () => { + it('given hasAllPerms', () => { + const logger = { log: jest.fn() }; + const s = new Stripes({ logger, config: { hasAllPerms: true } }); + expect(s.hasAnyPerm('monkey')).toBe(true); + }); + + it('when any requested permission is assigned', () => { + const logger = { log: jest.fn() }; + const s = new Stripes({ + logger, + user: { + perms: { + 'monkey': true, 'funky': true, 'chicken': true + } + } + }); + expect(s.hasAnyPerm('monkey,bagel')).toBe(true); + }); + }); + + describe('returns falsy', () => { + it('when no requested permissions are assigned [boolean, false]', () => { + const logger = { log: jest.fn() }; + const s = new Stripes({ + logger, + user: { + perms: { 'bagel': true } + } + }); + expect(s.hasAnyPerm('monkey,funky')).toBe(false); + }); + + it('when user perms are uninitialized [undefined]', () => { + const logger = { log: jest.fn() }; + const s = new Stripes({ logger, user: {} }); + expect(s.hasAnyPerm('monkey')).toBeUndefined(); + }); + }); + }); }); diff --git a/src/components/IfAnyPermission/IfAnyPermission.js b/src/components/IfAnyPermission/IfAnyPermission.js new file mode 100644 index 000000000..e331555ca --- /dev/null +++ b/src/components/IfAnyPermission/IfAnyPermission.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import { useStripes } from '../../StripesContext'; + +const IfAnyPermission = ({ children, perm }) => { + const stripes = useStripes(); + const hasPermission = stripes.hasAnyPerm(perm); + + if (typeof children === 'function') { + return children({ hasPermission }); + } + + return hasPermission ? children : null; +}; + +IfAnyPermission.propTypes = { + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + perm: PropTypes.string.isRequired +}; + +export default IfAnyPermission; diff --git a/src/components/IfAnyPermission/IfAnyPermission.test.js b/src/components/IfAnyPermission/IfAnyPermission.test.js new file mode 100644 index 000000000..d9c4bfdee --- /dev/null +++ b/src/components/IfAnyPermission/IfAnyPermission.test.js @@ -0,0 +1,33 @@ +import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; + +import { useStripes } from '../../StripesContext'; +import Stripes from '../../Stripes'; +import IfAnyPermission from './IfAnyPermission'; + +jest.mock('../../StripesContext'); +const stripes = new Stripes({ + user: { + perms: { + john: true, + george: true, + ringo: true, + } + }, + logger: { + log: jest.fn(), + } +}); + +describe('IfAnyPermission', () => { + it('returns true if any permission matches', () => { + useStripes.mockReturnValue(stripes); + render(monkey); + expect(screen.queryByText(/monkey/)).toBeTruthy(); + }); + + it('returns false if no permissions match', () => { + useStripes.mockReturnValue(stripes); + render(monkey); + expect(screen.queryByText(/monkey/)).toBeFalsy(); + }); +}); diff --git a/src/components/IfAnyPermission/index.js b/src/components/IfAnyPermission/index.js new file mode 100644 index 000000000..1bfbe3418 --- /dev/null +++ b/src/components/IfAnyPermission/index.js @@ -0,0 +1 @@ +export { default } from './IfAnyPermission'; diff --git a/src/components/IfAnyPermission/readme.md b/src/components/IfAnyPermission/readme.md new file mode 100644 index 000000000..6a04e199c --- /dev/null +++ b/src/components/IfAnyPermission/readme.md @@ -0,0 +1,34 @@ +# IfAnyPermission + +A wrapper component that facilitates conditional rendering based on +whether the currently authentiated user has _any_ of the permissions +named in the given comma-delimited string. + +Supports children in the form of React nodes or as a render-prop function. + +## Usage (children as nodes) + +``` + + + +``` + +## Usage (children as function) + +``` + + {({ hasPermission }) => hasPermission ? + + : + You do not have permission to edit this user! + } + +``` + +## Properties + +A single property is supported: + +* `perm`: a comma-delimited string of permissions to check. + diff --git a/src/components/IfPermission/IfPermission.js b/src/components/IfPermission/IfPermission.js index 5e0566643..f53f4d33d 100644 --- a/src/components/IfPermission/IfPermission.js +++ b/src/components/IfPermission/IfPermission.js @@ -1,20 +1,16 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { StripesContext } from '../../StripesContext'; +import { useStripes } from '../../StripesContext'; -const IfPermission = ({ children, perm }) => ( - - {stripes => { - const hasPermission = stripes.hasPerm(perm); +const IfPermission = ({ children, perm }) => { + const stripes = useStripes(); + const hasPermission = stripes.hasPerm(perm); - if (typeof children === 'function') { - return children({ hasPermission }); - } + if (typeof children === 'function') { + return children({ hasPermission }); + } - return hasPermission ? children : null; - }} - -); + return hasPermission ? children : null; +}; IfPermission.propTypes = { children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), diff --git a/src/components/IfPermission/IfPermission.test.js b/src/components/IfPermission/IfPermission.test.js new file mode 100644 index 000000000..5af0aac75 --- /dev/null +++ b/src/components/IfPermission/IfPermission.test.js @@ -0,0 +1,33 @@ +import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; + +import { useStripes } from '../../StripesContext'; +import Stripes from '../../Stripes'; +import IfPermission from './IfPermission'; + +jest.mock('../../StripesContext'); +const stripes = new Stripes({ + user: { + perms: { + john: true, + george: true, + ringo: true, + } + }, + logger: { + log: jest.fn(), + } +}); + +describe('IfPermission', () => { + it('returns true if all permissions match', () => { + useStripes.mockReturnValue(stripes); + render(monkey); + expect(screen.queryByText(/monkey/)).toBeTruthy(); + }); + + it('returns false unless all permissions match', () => { + useStripes.mockReturnValue(stripes); + render(monkey); + expect(screen.queryByText(/monkey/)).toBeFalsy(); + }); +}); diff --git a/src/components/IfPermission/readme.md b/src/components/IfPermission/readme.md index 5190040d7..8976f30d2 100644 --- a/src/components/IfPermission/readme.md +++ b/src/components/IfPermission/readme.md @@ -1,6 +1,8 @@ # IfPermission -A wrapper component that facilitates conditional rendering based on the existence of a permission. +A wrapper component that facilitates conditional rendering based on +whether the currently authentiated user has _all_ the permissions +named in the given comma-delimited string. Supports children in the form of React nodes or as a render-prop function. @@ -28,5 +30,4 @@ Supports children in the form of React nodes or as a render-prop function. A single property is supported: -* `perm`: a short string containing the name of the permission that is required. - +* `perm`: a comma-delimited string of permissions to check.