From ce2e2977c2fcecc8a9941c544cb301cf6a5dcc2a Mon Sep 17 00:00:00 2001 From: Chris Bishop Date: Tue, 20 Feb 2018 13:13:03 +0000 Subject: [PATCH 1/6] Only super admins can manage organisations. --- .env.example | 1 + lib/services/auth/canViewModel.js | 1 + lib/services/auth/canViewOrganisations.js | 15 +++++++++++ .../auth/modelFilters/organisation.js | 7 +++++- ui/src/containers/SideNav/index.js | 25 +++++++++++++------ ui/src/controllers/renderApp.js | 9 ++++++- 6 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 lib/services/auth/canViewOrganisations.js diff --git a/.env.example b/.env.example index c48c55b0df..4c355d21d9 100644 --- a/.env.example +++ b/.env.example @@ -179,3 +179,4 @@ FS_REPO=local # Location of virus scanning binary (ClamAV - https://www.clamav.net/) #CLAMSCAN_BINARY=/usr/bin/clamscan +SUPERADMIN_EDIT_ORGANISATION_ONLY=true \ No newline at end of file diff --git a/lib/services/auth/canViewModel.js b/lib/services/auth/canViewModel.js index 3208d79df1..a25e3f3c0a 100644 --- a/lib/services/auth/canViewModel.js +++ b/lib/services/auth/canViewModel.js @@ -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; }; diff --git a/lib/services/auth/canViewOrganisations.js b/lib/services/auth/canViewOrganisations.js new file mode 100644 index 0000000000..a22071500a --- /dev/null +++ b/lib/services/auth/canViewOrganisations.js @@ -0,0 +1,15 @@ +import { SITE_ADMIN, ALL } from 'lib/constants/scopes'; +import intersection from 'lodash/intersection'; +import boolean from 'boolean'; +import defaultTo from 'lodash/defaultTo'; + + +export default (activeScopes, { + SUPERADMIN_EDIT_ORGANISATION_ONLY = boolean(defaultTo(process.env.SUPERADMIN_EDIT_ORGANISATION_ONLY, true)) +}) => { + const viewScope = 'org/all/organisation/manage'; + const requiredScopes = [viewScope, SITE_ADMIN, ...(SUPERADMIN_EDIT_ORGANISATION_ONLY ? [] : [ALL])]; + + const matchingScopes = intersection(requiredScopes, activeScopes); + return matchingScopes.length !== 0; +}; diff --git a/lib/services/auth/modelFilters/organisation.js b/lib/services/auth/modelFilters/organisation.js index 50e0b884b2..f580e64942 100644 --- a/lib/services/auth/modelFilters/organisation.js +++ b/lib/services/auth/modelFilters/organisation.js @@ -1,7 +1,7 @@ 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 { @@ -16,6 +16,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 = []; @@ -97,6 +98,7 @@ const createFilterQuery = organisationIds => ({ _id: { $in: organisationIds } }); const modelFiltersOrganisation = async ({ actionName, authInfo }) => { + const superAdminOnly = boolean(defaultTo(process.env.SUPERADMIN_EDIT_ORGANISATION_ONLY, true)); const scopes = getScopesFromAuthInfo(authInfo); if (includes(scopes, SITE_ADMIN)) { @@ -109,14 +111,17 @@ const modelFiltersOrganisation = async ({ actionName, authInfo }) => { return createFilterQuery(organisationIds); } case 'create': { + if (superAdminOnly) throw new NoAccessError(); checkCreationScope(authInfo, scopes); break; } case 'delete': { + if (superAdminOnly) throw new NoAccessError(); const organisationIds = await getUserDeletableOrgs(authInfo, scopes); return createFilterQuery(organisationIds); } case 'edit': { + if (superAdminOnly) throw new NoAccessError(); const organisationIds = await getUserEditableOrgs(authInfo, scopes); return createFilterQuery(organisationIds); } diff --git a/ui/src/containers/SideNav/index.js b/ui/src/containers/SideNav/index.js index 1d9935c70a..e084968152 100644 --- a/ui/src/containers/SideNav/index.js +++ b/ui/src/containers/SideNav/index.js @@ -13,6 +13,8 @@ import CollapsibleNav from 'ui/containers/SideNav/CollapsibleNav'; import { activeOrganisationSettingsSelector, currentScopesSelector } from 'ui/redux/modules/auth'; import { activeOrgIdSelector } from 'ui/redux/modules/router'; import canViewModel from 'lib/services/auth/canViewModel'; +import canViewOrganisationsFn from 'lib/services/auth/canViewOrganisations'; +import { getAppDataSelector } from 'ui/redux/modules/app'; import styles from './sidenav.css'; class SideNav extends Component { @@ -68,13 +70,17 @@ class SideNav extends Component { } renderSettings = () => { - const { activeRoute } = this.props; + const { activeRoute, SUPERADMIN_EDIT_ORGANISATION_ONLY } = this.props; const { groups } = this.state; const organisationId = activeRoute.params.organisationId; const activeScopes = this.props.activeScopes.toJS(); const canViewStores = canViewModel('store', activeScopes); const canViewUsers = canViewModel('user', activeScopes); - const canViewOrganisations = canViewModel('organisation', activeScopes); + + const canViewOrganisations = canViewOrganisationsFn(activeScopes, { + SUPERADMIN_EDIT_ORGANISATION_ONLY + }); + const canViewClients = canViewModel('client', activeScopes); const canViewRoles = canViewModel('role', activeScopes); const canViewSettings = ( @@ -166,12 +172,15 @@ 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), + SUPERADMIN_EDIT_ORGANISATION_ONLY: getAppDataSelector('SUPERADMIN_EDIT_ORGANISATION_ONLY')(state) + }) + ), withProps(() => ({ schema: 'organisation' })), diff --git a/ui/src/controllers/renderApp.js b/ui/src/controllers/renderApp.js index bd3ec3c6a7..4b24c4f67a 100644 --- a/ui/src/controllers/renderApp.js +++ b/ui/src/controllers/renderApp.js @@ -3,6 +3,8 @@ import logger from 'lib/logger'; import Html from 'ui/components/Html'; import PrettyError from 'pretty-error'; import { renderToString } from 'react-dom/server'; +import boolean from 'boolean'; +import defaultTo from 'lodash/defaultTo'; // this is the assets manifest for the client build // it describes the location of all the compiled assets (js, css) @@ -19,7 +21,12 @@ export default async (req, res, next) => { clientAssets.client.js, ]; - data.state = {}; + data.state = { + app: { + SUPERADMIN_EDIT_ORGANISATION_ONLY: boolean(defaultTo(process.env.SUPERADMIN_EDIT_ORGANISATION_ONLY, true)) + } + }; + const html = renderToString(); global.navigator = { userAgent: req.headers['user-agent'] }; res.status(200); From 7d2fe4c073691f79d72077c6068e5ce5f6989dd1 Mon Sep 17 00:00:00 2001 From: Chris Bishop Date: Tue, 20 Feb 2018 13:39:54 +0000 Subject: [PATCH 2/6] Fixed tests. --- api/src/routes/tests/scopeFiltering/downloadLogo-test.js | 4 ++-- api/src/routes/tests/scopeFiltering/uploadLogo-test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/routes/tests/scopeFiltering/downloadLogo-test.js b/api/src/routes/tests/scopeFiltering/downloadLogo-test.js index 7140896c51..647c80420f 100644 --- a/api/src/routes/tests/scopeFiltering/downloadLogo-test.js +++ b/api/src/routes/tests/scopeFiltering/downloadLogo-test.js @@ -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; @@ -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); }); diff --git a/api/src/routes/tests/scopeFiltering/uploadLogo-test.js b/api/src/routes/tests/scopeFiltering/uploadLogo-test.js index 3104259688..1033030027 100644 --- a/api/src/routes/tests/scopeFiltering/uploadLogo-test.js +++ b/api/src/routes/tests/scopeFiltering/uploadLogo-test.js @@ -45,9 +45,9 @@ describe('UploadController.uploadLogo scope filtering', () => { await assertUnauthorised(token); }); - it('should allow action when ALL org scope is used', async () => { + it('should not allow action when ALL org scope is used', async () => { const token = await createOrgToken([ALL]); - await assertAuthorised(token); + await assertUnauthorised(token); }); it('should not allow action when MANAGE_ALL_ORGANISATIONS org scope is used', async () => { From 882ba59259c0d6f22f0a63a14df3cb6a6333e631 Mon Sep 17 00:00:00 2001 From: Chris Bishop Date: Wed, 21 Feb 2018 08:39:06 +0000 Subject: [PATCH 3/6] Fixed tests. --- .../tests/modelFilters/organisation-test.js | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/services/auth/tests/modelFilters/organisation-test.js b/lib/services/auth/tests/modelFilters/organisation-test.js index c9650992c2..d8a568ef51 100644 --- a/lib/services/auth/tests/modelFilters/organisation-test.js +++ b/lib/services/auth/tests/modelFilters/organisation-test.js @@ -21,6 +21,12 @@ import { } from 'lib/services/auth/tests/utils/constants'; import createUser from 'lib/services/auth/tests/utils/createUser'; import createUserToken from 'lib/services/auth/tests/utils/createUserToken'; +import createClient from 'lib/services/auth/tests/utils/createClient'; +import createClientToken from 'lib/services/auth/tests/utils/createClientToken'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +chai.use(chaiAsPromised); const modelName = 'organisation'; @@ -33,16 +39,14 @@ describe('model scope filters organisation', () => { testAllActions(modelName, [SITE_ADMIN], {}); // org admin tests - testOrgScopeFilter(modelName, 'view', [ALL], TEST_USER_ORGS_FILTER); - testOrgScopeFilter(modelName, 'create', [ALL], undefined); + // testOrgScopeFilter(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, @@ -50,14 +54,6 @@ describe('model scope filters organisation', () => { [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); @@ -75,8 +71,23 @@ describe('model scope filters organisation', () => { testClientBasicScopeFilter(modelName, 'view', [ALL], TEST_USER_ORGS_FILTER); testClientBasicScopeFilter(modelName, 'view', [], TEST_USER_NO_ORGS_FILTER); - testClientBasicScopeFilter(modelName, 'edit', [ALL], TEST_USER_ORGS_FILTER); - testClientBasicScopeFilter(modelName, 'edit', [], TEST_USER_NO_ORGS_FILTER); + it('should error on All scope', async () => { + const client = createClient(); + const token = createClientToken([ALL]); + + getModelScopeFilter('organisation', 'edit', token, undefined, client) + .should.eventually.be.rejectedWith(Error); + }); + // testClientBasicScopeFilter(modelName, 'edit', [ALL], TEST_USER_ORGS_FILTER); + + it('should error with no scopes', async () => { + const client = createClient(); + const token = createClientToken([]); + + getModelScopeFilter('organisation', 'edit', token, undefined, client) + .should.eventually.be.rejectedWith(Error); + }); + // testClientBasicScopeFilter(modelName, 'edit', [], TEST_USER_NO_ORGS_FILTER); testClientBasicScopeFilterError(modelName, 'create', []); testClientBasicScopeFilterError(modelName, 'create', []); @@ -84,6 +95,7 @@ describe('model scope filters organisation', () => { testClientBasicScopeFilterError(modelName, 'delete', [ALL]); testClientBasicScopeFilterError(modelName, 'delete', []); + // site admin tests testSiteAdminScopeFilter(modelName, 'view', {}); testSiteAdminScopeFilter(modelName, 'edit', {}); From 1562b7efc191f73c71d8f7fc31c964a342576dd0 Mon Sep 17 00:00:00 2001 From: Chris Bishop Date: Fri, 23 Feb 2018 08:14:21 +0000 Subject: [PATCH 4/6] Added new scope for restricting organisation. --- .env.example | 2 +- api/src/routes/HttpRoutes.js | 18 ++++- lib/constants/scopes.js | 3 + ...nisations.js => canCreateOrganisations.js} | 11 ++- .../auth/modelFilters/organisation.js | 7 +- ui/src/containers/SideNav/index.js | 11 +-- ui/src/containers/SiteOrgs/index.js | 74 +++++++++++------- ui/src/containers/SubOrgs/index.js | 62 +++++++++------ ui/src/containers/UserOrgForm/index.js | 77 ++++++++++++++++++- ui/src/controllers/renderApp.js | 2 +- 10 files changed, 195 insertions(+), 72 deletions(-) rename lib/services/auth/{canViewOrganisations.js => canCreateOrganisations.js} (50%) diff --git a/.env.example b/.env.example index 4c355d21d9..84c02f6999 100644 --- a/.env.example +++ b/.env.example @@ -179,4 +179,4 @@ FS_REPO=local # Location of virus scanning binary (ClamAV - https://www.clamav.net/) #CLAMSCAN_BINARY=/usr/bin/clamscan -SUPERADMIN_EDIT_ORGANISATION_ONLY=true \ No newline at end of file +RESTRICT_CREATE_ORGANISATION=true \ No newline at end of file diff --git a/api/src/routes/HttpRoutes.js b/api/src/routes/HttpRoutes.js index d84dd79d00..189489cf77 100644 --- a/api/src/routes/HttpRoutes.js +++ b/api/src/routes/HttpRoutes.js @@ -3,6 +3,10 @@ import express from 'express'; import restify from 'express-restify-mongoose'; import git from 'git-rev'; 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 { jsonSuccess, serverError } from 'api/utils/responses'; import passport from 'api/auth/passport'; import { @@ -198,7 +202,19 @@ restify.serve(router, PersonaIdentifier); restify.serve(router, Query); restify.serve(router, ImportCsv); restify.serve(router, ScoringScheme); -restify.serve(router, User); +restify.serve(router, User, { + preUpdate: (req, res, next) => { + const authInfo = getAuthFromRequest(req); + const scopes = getScopesFromRequest(authInfo); + + if ( + findIndex(scopes, item => item === SITE_ADMIN) < 0 + ) { + req.body = omit(req.body, 'scopes'); + } + next(); + } +}); restify.serve(router, Client); restify.serve(router, Visualisation); restify.serve(router, Dashboard); diff --git a/lib/constants/scopes.js b/lib/constants/scopes.js index 89a5c4960d..25d738f7b2 100644 --- a/lib/constants/scopes.js +++ b/lib/constants/scopes.js @@ -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 diff --git a/lib/services/auth/canViewOrganisations.js b/lib/services/auth/canCreateOrganisations.js similarity index 50% rename from lib/services/auth/canViewOrganisations.js rename to lib/services/auth/canCreateOrganisations.js index a22071500a..3f73001057 100644 --- a/lib/services/auth/canViewOrganisations.js +++ b/lib/services/auth/canCreateOrganisations.js @@ -1,14 +1,19 @@ -import { SITE_ADMIN, ALL } from 'lib/constants/scopes'; +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, { - SUPERADMIN_EDIT_ORGANISATION_ONLY = boolean(defaultTo(process.env.SUPERADMIN_EDIT_ORGANISATION_ONLY, true)) + RESTRICT_CREATE_ORGANISATION = boolean(defaultTo(process.env.RESTRICT_CREATE_ORGANISATION, true)) }) => { const viewScope = 'org/all/organisation/manage'; - const requiredScopes = [viewScope, SITE_ADMIN, ...(SUPERADMIN_EDIT_ORGANISATION_ONLY ? [] : [ALL])]; + const requiredScopes = [ + viewScope, + SITE_ADMIN, + SITE_CAN_CREATE_ORG, + ...(RESTRICT_CREATE_ORGANISATION ? [] : [ALL]) + ]; const matchingScopes = intersection(requiredScopes, activeScopes); return matchingScopes.length !== 0; diff --git a/lib/services/auth/modelFilters/organisation.js b/lib/services/auth/modelFilters/organisation.js index f580e64942..62d2f2845c 100644 --- a/lib/services/auth/modelFilters/organisation.js +++ b/lib/services/auth/modelFilters/organisation.js @@ -6,6 +6,7 @@ import map from 'lodash/map'; import intersection from 'lodash/intersection'; import { SITE_ADMIN, + SITE_CAN_CREATE_ORG, ALL } from 'lib/constants/scopes'; import { @@ -98,10 +99,10 @@ const createFilterQuery = organisationIds => ({ _id: { $in: organisationIds } }); const modelFiltersOrganisation = async ({ actionName, authInfo }) => { - const superAdminOnly = boolean(defaultTo(process.env.SUPERADMIN_EDIT_ORGANISATION_ONLY, true)); + 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 {}; } @@ -116,12 +117,10 @@ const modelFiltersOrganisation = async ({ actionName, authInfo }) => { break; } case 'delete': { - if (superAdminOnly) throw new NoAccessError(); const organisationIds = await getUserDeletableOrgs(authInfo, scopes); return createFilterQuery(organisationIds); } case 'edit': { - if (superAdminOnly) throw new NoAccessError(); const organisationIds = await getUserEditableOrgs(authInfo, scopes); return createFilterQuery(organisationIds); } diff --git a/ui/src/containers/SideNav/index.js b/ui/src/containers/SideNav/index.js index e084968152..220b093490 100644 --- a/ui/src/containers/SideNav/index.js +++ b/ui/src/containers/SideNav/index.js @@ -13,8 +13,6 @@ import CollapsibleNav from 'ui/containers/SideNav/CollapsibleNav'; import { activeOrganisationSettingsSelector, currentScopesSelector } from 'ui/redux/modules/auth'; import { activeOrgIdSelector } from 'ui/redux/modules/router'; import canViewModel from 'lib/services/auth/canViewModel'; -import canViewOrganisationsFn from 'lib/services/auth/canViewOrganisations'; -import { getAppDataSelector } from 'ui/redux/modules/app'; import styles from './sidenav.css'; class SideNav extends Component { @@ -70,16 +68,14 @@ class SideNav extends Component { } renderSettings = () => { - const { activeRoute, SUPERADMIN_EDIT_ORGANISATION_ONLY } = this.props; + const { activeRoute } = this.props; const { groups } = this.state; const organisationId = activeRoute.params.organisationId; const activeScopes = this.props.activeScopes.toJS(); const canViewStores = canViewModel('store', activeScopes); const canViewUsers = canViewModel('user', activeScopes); - const canViewOrganisations = canViewOrganisationsFn(activeScopes, { - SUPERADMIN_EDIT_ORGANISATION_ONLY - }); + const canViewOrganisations = canViewModel('organisation', activeScopes); const canViewClients = canViewModel('client', activeScopes); const canViewRoles = canViewModel('role', activeScopes); @@ -177,8 +173,7 @@ export default compose( activeRoute: routeNodeSelector('organisation')(state).route, activeOrganisationSettings: activeOrganisationSettingsSelector(state), activeScopes: currentScopesSelector(state), - id: activeOrgIdSelector(state), - SUPERADMIN_EDIT_ORGANISATION_ONLY: getAppDataSelector('SUPERADMIN_EDIT_ORGANISATION_ONLY')(state) + id: activeOrgIdSelector(state) }) ), withProps(() => ({ diff --git a/ui/src/containers/SiteOrgs/index.js b/ui/src/containers/SiteOrgs/index.js index 5ceb312df6..f31a5d0c57 100644 --- a/ui/src/containers/SiteOrgs/index.js +++ b/ui/src/containers/SiteOrgs/index.js @@ -10,10 +10,12 @@ import SearchBox from 'ui/containers/SearchBox'; import { withModels } 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'; const schema = 'organisation'; const OrgList = compose( @@ -28,7 +30,9 @@ const enhance = compose( connect( state => ({ searchString: modelQueryStringSelector(schema)(state), - authUser: loggedInUserSelector(state) + authUser: loggedInUserSelector(state), + activeScopes: currentScopesSelector(state), + RESTRICT_CREATE_ORGANISATION: getAppDataSelector('RESTRICT_CREATE_ORGANISATION')(state) }), { addModel: addModelAction } ), @@ -44,33 +48,49 @@ const enhance = compose( }) ); -const render = ({ params, searchString, handleAdd }) => ( -
-
-
- All Organisations - - - - - - -
-
+const render = ({ + params, + searchString, + handleAdd, + activeScopes, + RESTRICT_CREATE_ORGANISATION +}) => { + const activeScopesJs = activeScopes.toJS(); + + const canCreateOrg = canCreateOrganisationsFn(activeScopesJs, { + RESTRICT_CREATE_ORGANISATION + }); + + console.log('001 canCreateOrg', canCreateOrg); -
-
- model.get('name')} - buttons={[OrgMemberButton, DeleteButton]} - ModelForm={SiteOrgItem} /> + return ( +
+
+
+ All Organisations + {canCreateOrg && + + } + + + +
+
+ +
+
+ model.get('name')} + buttons={[OrgMemberButton, DeleteButton]} + ModelForm={SiteOrgItem} /> +
-
-); + ); +}; export default enhance(render); diff --git a/ui/src/containers/SubOrgs/index.js b/ui/src/containers/SubOrgs/index.js index 1be6af8490..d651193033 100644 --- a/ui/src/containers/SubOrgs/index.js +++ b/ui/src/containers/SubOrgs/index.js @@ -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( @@ -25,7 +27,7 @@ class SubOrgs extends Component { userId: PropTypes.string, organisationId: PropTypes.string, addModel: PropTypes.func, - searchString: PropTypes.string, + searchString: PropTypes.string }; onClickAdd = () => { @@ -40,31 +42,39 @@ class SubOrgs extends Component { }); } - render = () => ( -
-
-
- - - - - - - Organisations -
-
-
-
- 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 ( +
+
+
+ { canCreateOrg && + + } + + + + Organisations +
+
+
+
+ model.get('name')} + ModelForm={SubOrgForm} /> +
-
- ); + ); + } } export default connect((state) => { @@ -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); diff --git a/ui/src/containers/UserOrgForm/index.js b/ui/src/containers/UserOrgForm/index.js index 68fa41837b..0fe4bf6196 100644 --- a/ui/src/containers/UserOrgForm/index.js +++ b/ui/src/containers/UserOrgForm/index.js @@ -8,6 +8,9 @@ import UserForm from 'ui/containers/UserForm'; import { activeOrgIdSelector } from 'ui/redux/modules/router'; import Checkbox from 'ui/components/Material/Checkbox'; import { connect } from 'react-redux'; +import { getAppDataSelector } from 'ui/redux/modules/app'; +import { currentScopesSelector } from 'ui/redux/modules/auth'; +import { SITE_ADMIN, SITE_CAN_CREATE_ORG, SITE_SCOPES } from 'lib/constants/scopes'; const ORG_SETTINGS = 'organisationSettings'; @@ -42,6 +45,42 @@ const RolesList = compose(
) ); +const SiteRolesList = compose( + setPropTypes({ + selectedRoles: PropTypes.instanceOf(List).isRequired, + handleRolesChange: PropTypes.func.isRequired, + }), + withProps({ + sort: new Map({ + title: 1, + _id: 1, + }), + filter: new Map({}), + }), +)(({ selectedRoles, handleRolesChange }) => { + const models = new List([ + new Map({ + _id: SITE_CAN_CREATE_ORG, + title: SITE_SCOPES[SITE_CAN_CREATE_ORG] + }) + ]); + return (
+ { + models.map((model) => { + const modelId = model.get('_id'); + const isChecked = selectedRoles.includes(modelId); + return ( + + ); + }).valueSeq() + } +
); +}); + const getDefaultOrgSettings = organisation => fromJS({ organisation, @@ -63,7 +102,9 @@ const enhance = compose( id: PropTypes.string.isRequired, }), connect(state => ({ - organisationId: activeOrgIdSelector(state) + organisationId: activeOrgIdSelector(state), + activeScopes: currentScopesSelector(state), + RESTRICT_CREATE_ORGANISATION: getAppDataSelector('RESTRICT_CREATE_ORGANISATION')(state) })), withProps({ schema: 'user', @@ -108,6 +149,13 @@ const enhance = compose( ); updateOrgSettings('roles', newRoles.toList()); }, + handleSiteRolesChange: ({ model, updateModel }) => (role, checked) => { + const scopes = model.get('scopes', new List()).toSet(); + const newScopes = checked ? + scopes.add(role) : + scopes.delete(role); + updateModel({ path: 'scopes', value: newScopes.toList() }); + }, handleFilterChange: ({ updateOrgSettings }) => (filter) => { updateOrgSettings('filter', filter); @@ -116,13 +164,25 @@ const enhance = compose( ); const render = (props) => { - const { model, organisationId, handleRolesChange, handleFilterChange } = props; + const { + model, + organisationId, + handleRolesChange, + handleFilterChange, + handleSiteRolesChange, + RESTRICT_CREATE_ORGANISATION + } = props; const userOrgSettings = getActiveOrgSettings({ model, organisationId }); const roles = userOrgSettings.get('roles', new List()); const rolesId = uuid.v4(); + const siteRolesId = uuid.v4(); const filterId = uuid.v4(); const filter = fromJS(userOrgSettings.get('filter', new Map({}))); + const siteRoles = model.get('scopes', new List()); + const canEditSiteRoles = RESTRICT_CREATE_ORGANISATION && + props.activeScopes.includes(SITE_ADMIN); + return (
@@ -136,6 +196,19 @@ const render = (props) => {
+ + + {canEditSiteRoles &&
+
+
+ +
+ +
+
+
+
} +
{ data.state = { app: { - SUPERADMIN_EDIT_ORGANISATION_ONLY: boolean(defaultTo(process.env.SUPERADMIN_EDIT_ORGANISATION_ONLY, true)) + RESTRICT_CREATE_ORGANISATION: boolean(defaultTo(process.env.RESTRICT_CREATE_ORGANISATION, true)) } }; From 41c80e2fa7b216305363d505896b5556d07f9992 Mon Sep 17 00:00:00 2001 From: Chris Bishop Date: Fri, 23 Feb 2018 08:31:57 +0000 Subject: [PATCH 5/6] Fixed tests. --- api/src/routes/tests/scopeFiltering/uploadLogo-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/routes/tests/scopeFiltering/uploadLogo-test.js b/api/src/routes/tests/scopeFiltering/uploadLogo-test.js index 1033030027..3104259688 100644 --- a/api/src/routes/tests/scopeFiltering/uploadLogo-test.js +++ b/api/src/routes/tests/scopeFiltering/uploadLogo-test.js @@ -45,9 +45,9 @@ describe('UploadController.uploadLogo scope filtering', () => { await assertUnauthorised(token); }); - it('should not allow action when ALL org scope is used', async () => { + it('should allow action when ALL org scope is used', async () => { const token = await createOrgToken([ALL]); - await assertUnauthorised(token); + await assertAuthorised(token); }); it('should not allow action when MANAGE_ALL_ORGANISATIONS org scope is used', async () => { From 9dc25ec562c053096158ad9356c5eee68a1e7bcc Mon Sep 17 00:00:00 2001 From: James Mullaney Date: Thu, 22 Mar 2018 17:01:38 +0000 Subject: [PATCH 6/6] test: Fix scoping tests for org --- .../tests/modelFilters/organisation-test.js | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/lib/services/auth/tests/modelFilters/organisation-test.js b/lib/services/auth/tests/modelFilters/organisation-test.js index d8a568ef51..5134d8e169 100644 --- a/lib/services/auth/tests/modelFilters/organisation-test.js +++ b/lib/services/auth/tests/modelFilters/organisation-test.js @@ -21,12 +21,6 @@ import { } from 'lib/services/auth/tests/utils/constants'; import createUser from 'lib/services/auth/tests/utils/createUser'; import createUserToken from 'lib/services/auth/tests/utils/createUserToken'; -import createClient from 'lib/services/auth/tests/utils/createClient'; -import createClientToken from 'lib/services/auth/tests/utils/createClientToken'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; - -chai.use(chaiAsPromised); const modelName = 'organisation'; @@ -39,7 +33,8 @@ describe('model scope filters organisation', () => { testAllActions(modelName, [SITE_ADMIN], {}); // org admin tests - // testOrgScopeFilter(modelName, 'create', [ALL], undefined); + testOrgScopeFilter(modelName, 'view', [ALL], TEST_USER_ORGS_FILTER); + 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); @@ -71,23 +66,8 @@ describe('model scope filters organisation', () => { testClientBasicScopeFilter(modelName, 'view', [ALL], TEST_USER_ORGS_FILTER); testClientBasicScopeFilter(modelName, 'view', [], TEST_USER_NO_ORGS_FILTER); - it('should error on All scope', async () => { - const client = createClient(); - const token = createClientToken([ALL]); - - getModelScopeFilter('organisation', 'edit', token, undefined, client) - .should.eventually.be.rejectedWith(Error); - }); - // testClientBasicScopeFilter(modelName, 'edit', [ALL], TEST_USER_ORGS_FILTER); - - it('should error with no scopes', async () => { - const client = createClient(); - const token = createClientToken([]); - - getModelScopeFilter('organisation', 'edit', token, undefined, client) - .should.eventually.be.rejectedWith(Error); - }); - // testClientBasicScopeFilter(modelName, 'edit', [], TEST_USER_NO_ORGS_FILTER); + testClientBasicScopeFilter(modelName, 'edit', [ALL], TEST_USER_ORGS_FILTER); + testClientBasicScopeFilter(modelName, 'edit', [], TEST_USER_NO_ORGS_FILTER); testClientBasicScopeFilterError(modelName, 'create', []); testClientBasicScopeFilterError(modelName, 'create', []);