Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STCOR-910 provide IfAnyPermission, stripes.ifAnyPerm #1560

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
* 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 `<IfAnyPermission>` 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)
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.
Loading