Skip to content

Commit

Permalink
Merge branch 'master' into CVE-2024-47764
Browse files Browse the repository at this point in the history
  • Loading branch information
zburke authored Jan 7, 2025
2 parents beb1a36 + f7d2b36 commit 008d091
Show file tree
Hide file tree
Showing 62 changed files with 548 additions and 477 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* @folio-org/stripes-force

5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* 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.
* Use the `users-keycloak/_self` endpoint conditionally when the `users-keycloak` interface is present; otherwise, use `bl-users/_self` within `useUserTenantPermissions`. Refs STCOR-905.
* Don't override initial discovery and okapi data in test mocks. Refs STCOR-913.
* `<Logout>` must consume `QueryClient` in order to supply it to `loginServices::logout()`. Refs STCOR-907.
* On resuming session, spread session and `_self` together to preserve session values. Refs STCOR-912.

## [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();
});
});
});
});
16 changes: 16 additions & 0 deletions src/components/BadRequestScreen/BadRequestScreen.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';

import BadRequestScreen from './BadRequestScreen';

jest.mock('../../Pluggable', () => (props) => props.children);

describe('BadRequestScreen', () => {
it('renders expected message', () => {
render(<BadRequestScreen />);

screen.getByText('stripes-core.front.error.header');
screen.getByText(/stripes-core.front.error.general.message/);
});
});


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.

20 changes: 11 additions & 9 deletions src/components/IfInterface/IfInterface.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StripesContext } from '../../StripesContext';
import { useStripes } from '../../StripesContext';

const IfInterface = ({ children, name, version }) => (
<StripesContext.Consumer>
{stripes => (
stripes.hasInterface(name, version) ? children : null
)}
</StripesContext.Consumer>
);
const IfInterface = ({ children, name, version }) => {
const stripes = useStripes();
const hasInterface = stripes.hasInterface(name, version);

if (typeof children === 'function') {
return children({ hasInterface });
}

return hasInterface ? children : null;
};

IfInterface.propTypes = {
children: PropTypes.node,
Expand Down
33 changes: 33 additions & 0 deletions src/components/IfInterface/IfInterface.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 IfInterface from './IfInterface';

jest.mock('../../StripesContext');
const stripes = new Stripes({
discovery: {
interfaces: {
foo: '1.0'
}
},
logger: {
log: jest.fn(),
}
});

// IfInterface is just a component version of Stripes::hasInterface
// See more extensive tests there.
describe('IfInterface', () => {
it('returns true if interface is present', () => {
useStripes.mockReturnValue(stripes);
render(<IfInterface name="foo">monkey</IfInterface>);
expect(screen.queryByText(/monkey/)).toBeTruthy();
});

it('returns false if interface is absent', () => {
useStripes.mockReturnValue(stripes);
render(<IfInterface name="paul,is,dead">monkey</IfInterface>);
expect(screen.queryByText(/monkey/)).toBeFalsy();
});
});
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.
4 changes: 3 additions & 1 deletion src/components/Logout/Logout.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { FormattedMessage } from 'react-intl';
import { useQueryClient } from 'react-query';
import { branding } from 'stripes-config';

import {
Expand Down Expand Up @@ -35,14 +36,15 @@ const Logout = () => {
const stripes = useStripes();
const [didLogout, setDidLogout] = useState(false);
const location = useLocation();
const queryClient = useQueryClient();

const messageId = location.pathName === '/logout-timeout' ? 'stripes-core.rtr.idleSession.sessionExpiredSoSad' : 'stripes-core.logoutComplete';

useEffect(
() => {
if (stripes.okapi.isAuthenticated) {
// returns a promise, which we ignore
logout(stripes.okapi.url, stripes.store)
logout(stripes.okapi.url, stripes.store, queryClient)
.then(setDidLogout(true));
} else {
setDidLogout(true);
Expand Down
Loading

0 comments on commit 008d091

Please sign in to comment.