Skip to content

Commit

Permalink
STCOR-910 provide IfAnyPermission, stripes.ifAnyPerm (#1560)
Browse files Browse the repository at this point in the history
`<IfPermission>` and `stripes.ifPerm()` return `true` when the user has
ALL the given permissions. `<IfAnyPermission>` and `stripes.ifAnyPerm()`
return `true` when the user has ANY of the given permissions.

Refs STCOR-910

(cherry picked from commit 921f0ab)
  • Loading branch information
zburke committed Nov 15, 2024
1 parent a3f00b4 commit 4340810
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

* Conditionally use `/users-keycloak/_self` endpoint when `users-keycloak` interface is present. Refs STCOR-835.
* Send the stored central tenant name in the header on logout. Refs STCOR-900.
* Provide `<IfAnyPermission>` and `stripes.hasAnyPermission()`. Refs STCOR-910.

## [10.2.1](https://github.com/folio-org/stripes-core/tree/v10.2.1) (2024-10-30)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.2.0...v10.2.1)
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
29 changes: 29 additions & 0 deletions src/Stripes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
42 changes: 42 additions & 0 deletions src/Stripes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
20 changes: 20 additions & 0 deletions src/components/IfAnyPermission/IfAnyPermission.js
Original file line number Diff line number Diff line change
@@ -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;
33 changes: 33 additions & 0 deletions src/components/IfAnyPermission/IfAnyPermission.test.js
Original file line number Diff line number Diff line change
@@ -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(<IfAnyPermission perm="john,paul">monkey</IfAnyPermission>);
expect(screen.queryByText(/monkey/)).toBeTruthy();
});

it('returns false if no permissions match', () => {
useStripes.mockReturnValue(stripes);
render(<IfAnyPermission perm="paul,is,dead">monkey</IfAnyPermission>);
expect(screen.queryByText(/monkey/)).toBeFalsy();
});
});
1 change: 1 addition & 0 deletions src/components/IfAnyPermission/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './IfAnyPermission';
34 changes: 34 additions & 0 deletions src/components/IfAnyPermission/readme.md
Original file line number Diff line number Diff line change
@@ -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)

```
<IfAnyPermission perm="users.edit,users.manage">
<button onClick={this.onClickEditUser}>Edit</button>
</IfAnyPermission>
```

## Usage (children as function)

```
<IfAnyPermission perm="users.edit,users.manage">
{({ hasPermission }) => hasPermission ?
<button onClick={this.onClickEditUser}>Edit</button>
:
<span>You do not have permission to edit this user!</span>
}
</IfAnyPermission>
```

## Properties

A single property is supported:

* `perm`: a comma-delimited string of permissions to check.

22 changes: 9 additions & 13 deletions src/components/IfPermission/IfPermission.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<StripesContext.Consumer>
{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;
}}
</StripesContext.Consumer>
);
return hasPermission ? children : null;
};

IfPermission.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
Expand Down
33 changes: 33 additions & 0 deletions src/components/IfPermission/IfPermission.test.js
Original file line number Diff line number Diff line change
@@ -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(<IfPermission perm="john,george">monkey</IfPermission>);
expect(screen.queryByText(/monkey/)).toBeTruthy();
});

it('returns false unless all permissions match', () => {
useStripes.mockReturnValue(stripes);
render(<IfPermission perm="john,paul">monkey</IfPermission>);
expect(screen.queryByText(/monkey/)).toBeFalsy();
});
});
7 changes: 4 additions & 3 deletions src/components/IfPermission/readme.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.

0 comments on commit 4340810

Please sign in to comment.