Skip to content
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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,4 @@ FS_REPO=local

# Location of virus scanning binary (ClamAV - https://www.clamav.net/)
#CLAMSCAN_BINARY=/usr/bin/clamscan
RESTRICT_CREATE_ORGANISATION=true
15 changes: 8 additions & 7 deletions api/src/routes/HttpRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Promise from 'bluebird';
import { omit, findIndex } from 'lodash';
import getAuthFromRequest from 'lib/helpers/getAuthFromRequest';
import getScopesFromRequest from 'lib/services/auth/authInfoSelectors/getScopesFromAuthInfo';
import { SITE_ADMIN } from 'lib/constants/scopes';
import getUserIdFromAuthInfo from 'lib/services/auth/authInfoSelectors/getUserIdFromAuthInfo';
import { SITE_ADMIN } from 'lib/constants/scopes';
import { jsonSuccess, serverError } from 'api/utils/responses';
import passport from 'api/auth/passport';
import {
Expand Down Expand Up @@ -209,12 +209,13 @@ restify.serve(router, User, {
const authInfo = getAuthFromRequest(req);
const scopes = getScopesFromRequest(authInfo);

if (
findIndex(scopes, item => item === SITE_ADMIN) < 0 &&
(req.body._id !== getUserIdFromAuthInfo(authInfo).toString())
) {
// Don't allow changing of passwords
req.body = omit(req.body, 'password');
if (findIndex(scopes, item => item === SITE_ADMIN) < 0) {
// remove scope changes
req.body = omit(req.body, 'scopes');
if (req.body._id !== getUserIdFromAuthInfo(authInfo).toString()){
// Don't allow changing of passwords
req.body = omit(req.body, 'password');
}
}

next();
Expand Down
4 changes: 2 additions & 2 deletions api/src/routes/tests/scopeFiltering/downloadLogo-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const TEST_FILE = `${process.cwd()}/api/src/routes/tests/fixtures/favicon.png`;
describe('DownloadController.downloadLogo scope filtering', () => {
const apiApp = setup();

const createMasterToken = async () => createOrgToken([ALL], [], masterId);
const createMasterToken = async () => createOrgToken([ALL], [SITE_ADMIN], masterId);

const uploadLogo = async () => {
const orgId = testId;
Expand Down Expand Up @@ -54,7 +54,7 @@ describe('DownloadController.downloadLogo scope filtering', () => {
expectedCode: 200
});

it('should not allow action when no scopes are used', async () => {
it('should allow action when no scopes are used', async () => {
const token = await createOrgToken([]);
await assertAuthorised(token);
});
Expand Down
3 changes: 3 additions & 0 deletions lib/constants/scopes.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ export const CLIENT_SCOPES = {

// USERS
export const SITE_ADMIN = 'site_admin';
export const SITE_CAN_CREATE_ORG = 'site_can_create_org';

export const SITE_SCOPES = {
[SITE_ADMIN]: 'Site Administrator',
[SITE_CAN_CREATE_ORG]: 'Can create organisation'
};

// ORGANISATION_USER
Expand Down
20 changes: 20 additions & 0 deletions lib/services/auth/canCreateOrganisations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SITE_ADMIN, SITE_CAN_CREATE_ORG, ALL } from 'lib/constants/scopes';
import intersection from 'lodash/intersection';
import boolean from 'boolean';
import defaultTo from 'lodash/defaultTo';


export default (activeScopes, {
RESTRICT_CREATE_ORGANISATION = boolean(defaultTo(process.env.RESTRICT_CREATE_ORGANISATION, true))
}) => {
const viewScope = 'org/all/organisation/manage';
const requiredScopes = [
viewScope,
SITE_ADMIN,
SITE_CAN_CREATE_ORG,
...(RESTRICT_CREATE_ORGANISATION ? [] : [ALL])
];

const matchingScopes = intersection(requiredScopes, activeScopes);
return matchingScopes.length !== 0;
};
1 change: 1 addition & 0 deletions lib/services/auth/canViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import intersection from 'lodash/intersection';
export default (modelName, activeScopes) => {
const viewScope = `org/all/${modelName}/manage`;
const requiredScopes = [viewScope, SITE_ADMIN, ALL];

const matchingScopes = intersection(requiredScopes, activeScopes);
return matchingScopes.length !== 0;
};
8 changes: 6 additions & 2 deletions lib/services/auth/modelFilters/organisation.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import includes from 'lodash/includes';
import get from 'lodash/get';
// import chain from 'lodash/chain';
import { chain } from 'lodash';
import { chain, defaultTo } from 'lodash';
import map from 'lodash/map';
import intersection from 'lodash/intersection';
import {
SITE_ADMIN,
SITE_CAN_CREATE_ORG,
ALL
} from 'lib/constants/scopes';
import {
Expand All @@ -16,6 +17,7 @@ import getOrgFromAuthInfo from 'lib/services/auth/authInfoSelectors/getOrgFromAu
import getTokenTypeFromAuthInfo from 'lib/services/auth/authInfoSelectors/getTokenTypeFromAuthInfo';
import NoAccessError from 'lib/errors/NoAccessError';
import Role from 'lib/models/role';
import boolean from 'boolean';

const getUserViewableOrgs = async (authInfo, scopes) => {
let organisationIds = [];
Expand Down Expand Up @@ -97,9 +99,10 @@ const createFilterQuery = organisationIds =>
({ _id: { $in: organisationIds } });

const modelFiltersOrganisation = async ({ actionName, authInfo }) => {
const superAdminOnly = boolean(defaultTo(process.env.RESTRICT_CREATE_ORGANISATION, true));
const scopes = getScopesFromAuthInfo(authInfo);

if (includes(scopes, SITE_ADMIN)) {
if (includes(scopes, SITE_ADMIN) || includes(scopes, SITE_CAN_CREATE_ORG)) {
return {};
}

Expand All @@ -109,6 +112,7 @@ const modelFiltersOrganisation = async ({ actionName, authInfo }) => {
return createFilterQuery(organisationIds);
}
case 'create': {
if (superAdminOnly) throw new NoAccessError();
checkCreationScope(authInfo, scopes);
break;
}
Expand Down
14 changes: 3 additions & 11 deletions lib/services/auth/tests/modelFilters/organisation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,21 @@ describe('model scope filters organisation', () => {

// org admin tests
testOrgScopeFilter(modelName, 'view', [ALL], TEST_USER_ORGS_FILTER);
testOrgScopeFilter(modelName, 'create', [ALL], undefined);
testOrgScopeFilterError(modelName, 'create', [ALL], undefined);

// @todo: tests do not use roles properly. Assign a role to the org setting on the user
// testOrgScopeFilter(modelName, 'edit', [ALL], TEST_USER_ORGS_FILTER);
// testOrgScopeFilter(modelName, 'delete', [ALL], TEST_USER_NO_ORGS_FILTER);

testOrgScopeFilter(modelName, 'view', [], TEST_USER_ORGS_FILTER);
testOrgScopeFilter(modelName, 'edit', [], TEST_USER_NO_ORGS_FILTER);
testOrgScopeFilterError(modelName, 'create', []);
testOrgScopeFilter(modelName, 'delete', [], TEST_USER_NO_ORGS_FILTER);

testOrgScopeFilter(
modelName,
'view',
[MANAGE_ALL_ORGANISATIONS],
TEST_USER_ORGS_FILTER
);
testOrgScopeFilter(
modelName,
'edit',
[MANAGE_ALL_ORGANISATIONS],
TEST_USER_NO_ORGS_FILTER
);
testOrgScopeFilter(modelName, 'create', [MANAGE_ALL_ORGANISATIONS], undefined);
testOrgScopeFilter(modelName, 'delete', [MANAGE_ALL_ORGANISATIONS], TEST_USER_NO_ORGS_FILTER);

it('should return the correct view filter when using a owner token', async () => {
const user = createUser(TEST_OWNER_ID);
Expand All @@ -84,6 +75,7 @@ describe('model scope filters organisation', () => {
testClientBasicScopeFilterError(modelName, 'delete', [ALL]);
testClientBasicScopeFilterError(modelName, 'delete', []);


// site admin tests
testSiteAdminScopeFilter(modelName, 'view', {});
testSiteAdminScopeFilter(modelName, 'edit', {});
Expand Down
16 changes: 10 additions & 6 deletions ui/src/containers/SideNav/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ class SideNav extends Component {
const activeScopes = this.props.activeScopes.toJS();
const canViewStores = canViewModel('store', activeScopes);
const canViewUsers = canViewModel('user', activeScopes);

const canViewOrganisations = canViewModel('organisation', activeScopes);

const canViewClients = canViewModel('client', activeScopes);
const canViewRoles = canViewModel('role', activeScopes);
const canViewSettings = (
Expand Down Expand Up @@ -166,12 +168,14 @@ class SideNav extends Component {

export default compose(
withStyles(styles),
connect(state => ({
activeRoute: routeNodeSelector('organisation')(state).route,
activeOrganisationSettings: activeOrganisationSettingsSelector(state),
activeScopes: currentScopesSelector(state),
id: activeOrgIdSelector(state)
})),
connect(state =>
({
activeRoute: routeNodeSelector('organisation')(state).route,
activeOrganisationSettings: activeOrganisationSettingsSelector(state),
activeScopes: currentScopesSelector(state),
id: activeOrgIdSelector(state)
})
),
withProps(() => ({
schema: 'organisation'
})),
Expand Down
70 changes: 42 additions & 28 deletions ui/src/containers/SiteOrgs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import SearchBox from 'ui/containers/SearchBox';
import { withModels, withModel } from 'ui/utils/hocs';
import SiteOrgItem from 'ui/containers/SiteOrgs/SiteOrgItem';
import { addModel as addModelAction } from 'ui/redux/modules/models';
import { loggedInUserSelector } from 'ui/redux/modules/auth';
import { loggedInUserSelector, currentScopesSelector } from 'ui/redux/modules/auth';
import ModelList from 'ui/containers/ModelList';
import DeleteButton from 'ui/containers/DeleteButton';
import OrgMemberButton from 'ui/containers/OrgMemberButton';
import canCreateOrganisationsFn from 'lib/services/auth/canCreateOrganisations';
import { getAppDataSelector } from 'ui/redux/modules/app';
import { routeNodeSelector } from 'redux-router5';

const schema = 'organisation';
Expand All @@ -31,6 +33,8 @@ const enhance = compose(
state => ({
searchString: modelQueryStringSelector(schema)(state),
authUser: loggedInUserSelector(state),
activeScopes: currentScopesSelector(state),
RESTRICT_CREATE_ORGANISATION: getAppDataSelector('RESTRICT_CREATE_ORGANISATION')(state),
organisationId: routeNodeSelector('admin.organisations.id')(state).route.params.organisationId
}),
{ addModel: addModelAction }
Expand All @@ -51,35 +55,45 @@ const render = ({
params,
searchString,
handleAdd,
organisationId
}) => (
<div>
<header id="topbar">
<div className="heading heading-light">
All Organisations
<span className="pull-right open_panel_btn">
<button className="btn btn-primary btn-sm" onClick={handleAdd}>
<i className="ion ion-plus" /> Add new
</button>
</span>
<span className="pull-right open_panel_btn" style={{ width: '25%' }}>
<SearchBox schema={schema} />
</span>
</div>
</header>
activeScopes,
RESTRICT_CREATE_ORGANISATION,
organisationId,
}) => {
const activeScopesJs = activeScopes.toJS();

const canCreateOrg = canCreateOrganisationsFn(activeScopesJs, {
RESTRICT_CREATE_ORGANISATION
});

<div className="row">
<div className="col-md-12">
<OrgList
id={organisationId}
filter={queryStringToQuery(searchString, schema)}
params={params}
getDescription={model => model.get('name')}
buttons={[OrgMemberButton, DeleteButton]}
ModelForm={SiteOrgItem} />
return (
<div>
<header id="topbar">
<div className="heading heading-light">
All Organisations
{canCreateOrg && <span className="pull-right open_panel_btn">
<button className="btn btn-primary btn-sm" onClick={handleAdd}>
<i className="ion ion-plus" /> Add new
</button>
</span>}
<span className="pull-right open_panel_btn" style={{ width: '25%' }}>
<SearchBox schema={schema} />
</span>
</div>
</header>

<div className="row">
<div className="col-md-12">
<OrgList
id={organisationId}
filter={queryStringToQuery(searchString, schema)}
params={params}
getDescription={model => model.get('name')}
buttons={[OrgMemberButton, DeleteButton]}
ModelForm={SiteOrgItem} />
</div>
</div>
</div>
</div>
);
);
};

export default enhance(render);
62 changes: 37 additions & 25 deletions ui/src/containers/SubOrgs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { withProps, compose } from 'recompose';
import { addModel } from 'ui/redux/modules/models';
import { queryStringToQuery, modelQueryStringSelector } from 'ui/redux/modules/search';
import { routeNodeSelector } from 'redux-router5';
import { loggedInUserId } from 'ui/redux/modules/auth';
import { loggedInUserId, currentScopesSelector } from 'ui/redux/modules/auth';
import SearchBox from 'ui/containers/SearchBox';
import ModelList from 'ui/containers/ModelList';
import SubOrgForm from 'ui/containers/SubOrgForm';
import { withModels } from 'ui/utils/hocs';
import canCreateOrganisationsFn from 'lib/services/auth/canCreateOrganisations';
import { getAppDataSelector } from 'ui/redux/modules/app';

const schema = 'organisation';
const OrgList = compose(
Expand All @@ -25,7 +27,7 @@ class SubOrgs extends Component {
userId: PropTypes.string,
organisationId: PropTypes.string,
addModel: PropTypes.func,
searchString: PropTypes.string,
searchString: PropTypes.string
};

onClickAdd = () => {
Expand All @@ -40,31 +42,39 @@ class SubOrgs extends Component {
});
}

render = () => (
<div>
<header id="topbar">
<div className="heading heading-light">
<span className="pull-right open_panel_btn">
<button className="btn btn-primary btn-sm" ref={(ref) => { this.addButton = ref; }} onClick={this.onClickAdd}>
<i className="ion ion-plus" /> Add new
</button>
</span>
<span className="pull-right open_panel_btn" style={{ width: '25%' }}>
<SearchBox schema={schema} />
</span>
Organisations
</div>
</header>
<div className="row">
<div className="col-md-12">
<OrgList
filter={queryStringToQuery(this.props.searchString, schema)}
getDescription={model => model.get('name')}
ModelForm={SubOrgForm} />
render = () => {
const activeScopesJs = this.props.activeScopes.toJS();

const canCreateOrg = canCreateOrganisationsFn(activeScopesJs, {
RESTRICT_CREATE_ORGANISATION: this.props.RESTRICT_CREATE_ORGANISATION
});

return (
<div>
<header id="topbar">
<div className="heading heading-light">
{ canCreateOrg && <span className="pull-right open_panel_btn">
<button className="btn btn-primary btn-sm" ref={(ref) => { this.addButton = ref; }} onClick={this.onClickAdd}>
<i className="ion ion-plus" /> Add new
</button>
</span> }
<span className="pull-right open_panel_btn" style={{ width: '25%' }}>
<SearchBox schema={schema} />
</span>
Organisations
</div>
</header>
<div className="row">
<div className="col-md-12">
<OrgList
filter={queryStringToQuery(this.props.searchString, schema)}
getDescription={model => model.get('name')}
ModelForm={SubOrgForm} />
</div>
</div>
</div>
</div>
);
);
}
}

export default connect((state) => {
Expand All @@ -74,5 +84,7 @@ export default connect((state) => {
organisationId: params.organisationId,
userId,
searchString: modelQueryStringSelector(schema)(state),
activeScopes: currentScopesSelector(state),
RESTRICT_CREATE_ORGANISATION: getAppDataSelector('RESTRICT_CREATE_ORGANISATION')(state)
};
}, { addModel })(SubOrgs);
Loading