diff --git a/.travis.yml b/.travis.yml index 83779b04..744dcbda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: node_js node_js: - - '6' - - '7' + - '8.9' dist: trusty sudo: required diff --git a/lerna.json b/lerna.json index 69c21112..dcea2c0d 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,10 @@ "packages": [ "packages/*" ], - "version": "independent" + "version": "independent", + "command": { + "run": { + "concurrency": 1 + } + } } diff --git a/package.json b/package.json index dc998edc..b8c853c9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "bugs": { "url": "https://github.com/nearform/udaru/issues" }, - "bin": "packages/udaru-server/start.js", + "bin": { + "udaru": "packages/udaru-server/start.js" + }, "engines": { "node": ">=6.0.0" }, @@ -43,6 +45,7 @@ "postinstall": "lerna bootstrap", "doc:lint": "remark .", "start": "node packages/udaru-server/start.js", + "start-v17": "node packages/hapi-auth-udaru/start.js", "test": "lerna run test", "test:commit-check": "npm run doc:lint && npm run lint && npm run depcheck && npm run test", "swagger-gen": "node scripts/getSwaggerJson.js | swagger-gen -d docs/swagger && node scripts/injectUdaruSwaggerCss.js" diff --git a/packages/hapi-auth-udaru/.npmrc b/packages/hapi-auth-udaru/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/packages/hapi-auth-udaru/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/hapi-auth-udaru/LICENSE.md b/packages/hapi-auth-udaru/LICENSE.md new file mode 100644 index 00000000..fe8a2877 --- /dev/null +++ b/packages/hapi-auth-udaru/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 nearForm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/hapi-auth-udaru/README.md b/packages/hapi-auth-udaru/README.md new file mode 100644 index 00000000..652129e4 --- /dev/null +++ b/packages/hapi-auth-udaru/README.md @@ -0,0 +1,335 @@ +# Hapi Udaru Authentication Plugin +[![npm][npm-badge]][npm-url] +[![travis][travis-badge]][travis-url] +[![coveralls][coveralls-badge]][coveralls-url] +[![snyk][snyk-badge]][snyk-url] + + + +Udaru is a Policy Based Access Control (PBAC) authorization module. It supports Organizations, Teams and User entities that are used to build the access model. The policies attached to these entities define the 'Actions' that can be performed by an entity on various 'Resources'. + +See the Udaru [website](https://nearform.github.io/udaru/) for complete documentation on Udaru. + +## Install +To install via npm: + +``` +npm install @nearform/hapi-auth-udaru +``` + +## Usage +```js +const Hapi = require('hapi') +const HapiAuthUdaruPlugin = require('@nearform/hapi-auth-udaru') + +... + +const server = Hapi.server() +server.register({register: HapiAuthUdaruPlugin}) +``` + +### Stand alone server + +``` +$ npx udaru +``` + +The server will be listening on `localhost:8080`. + +### Database support + +Udaru requires an instance of Postgres (version 9.5+) to function correctly. For simplicity, a preconfigured `docker-compose` file has been provided. To run: + +``` +docker-compose up +``` + +- **Note:** Ensure you are using the latest version of Docker for (Linux/OSX/Windows) +- **Note:** Udaru needs PostgreSQL >= 9.5 + +#### Populate the database +The Authorization database, system user and initial tables can be created by executing: + +``` +npm run pg:init +``` + +Test data can be added with: + +``` +npm run pg:load-test-data +``` + +- **Note:** Running a test or coverage command will auto run these commands + +#### Volume data set installation and bench tests +The Authorization database can be further initialized with a larger volume of data, which can be tested using autocannon bench tests in order to demonstrate the potential throughput of the authorization API. + +To populate the database with volume data, execute the following command: + +``` +npm run pg:init-volume-db +``` + +- **Note:** Running this command will auto run the standard database population commands also + +All volume data sits under the organization 'CONCH' and has the following default setup: +- 500 teams +- 100 users per team (the first of every 100 being the parent of subsequent 99) +- 10 policies per team + +After loading the data, the autocannon bench tests can be run by executing: + +``` +npm run bench-volume +``` + +This will run 15 second autocannon tests, which fire multiple concurrent requests at 2 frequently used endpoints. This results in the database being queried randomly across the entire set of data giving a good indication of average end-to-end latency and potential requests per second for a database containing 50K users. + + +### pgAdmin database access +As the Postgresql docker container has its 5432 port forwarded on the local machine the database can be accessed with pgAdmin. + +To access the database using the pgAdmin you have to fill in also the container IP beside the database names and access credentials. The container IP can be seen with `docker ps`. Use IP 127.0.0.1 and use postgres as username/password to connect to database server. + +### Migrations +We use [`postgrator`][postgrator] for database migrations. You can find the sql files in the [`database/migrations`](https://github.com/nearform/udaru/tree/master/database/migrations) folder. To run the migrations manually: + +``` +node database/migrate.js --version=` +``` + +- **Note:** Running the tests or init commands will automaticaly bring the db to the latest version. + +To get more information see [Service Api documentation](#service-api-documentation) + +### Setup SuperUser + +The init script needs to be run in order to setup the SuperUser: `node scripts/init` + +If you want to specify a better SuperUser id (default is `SuperUserId`) you can prefix the script as follow: + +``` +UDARU_SERVICE_authorization_superUser_id=myComplexId12345 node scripts/init +``` + +- **Note:** if you have already ran some tests or loaded the test data, you will need to run `npm run pg:init` again to reset the db. + +### Load policies from file + +Use the following script to load policies from a file: + +Usage: `node scripts/loadPolicies --org=FOO policies.json` + +JSON structure: + +``` +{ + "policies": [ + { + "id": "unique-string", // <== optional + "version": "", + "name": "policy name", + "organizationId": "your_organization" // <== optional, if present will override the "--org=FOO" parameter + "statements": [ + { + "Effect": "Allow/Deny", + "Action": "act", + "Resource": "res" + }, + { /*...*/ } + ] + }, + { /*...*/ } + ] +} +``` + +## Documentation + +The Udaru documentation site can be found at [nearform.github.io/udaru][docs-site]. + +### Swagger API Documentation + +The Swagger API documentation gives explanations on the exposed API. The documentation can be found at [nearform.github.io/udaru/swagger/][swagger-docs-url]. + +It is also possible to access the Swagger documentation from Udaru itself. Simply start the server: + +``` +npm run start +``` + +and then go to [`http://localhost:8080/documentation`][swagger-link] + +The Swagger documentation also gives the ability to execute calls to the API and see their results. If you're using the test database, you can use 'ROOTid' as the required authorization parameter and 'WONKA' as the organisation. + +### ENV variables to set configuration options +There are three default configuration files, one per "level": [`packages/udaru-core/config.js`][core-config], [`packages/udaru-hapi-plugin/config.js`][plugin-config] and [`packages/udaru-hapi-server/config.js`][server-config]. + +They are cumulative: when running udaru as a standalone server all the three files will be loaded; when using it as an Hapi plugin, plugin and core will be loaded. + +This configuration is the one used in dev environment and we are quite sure the production one will be different :) To override this configuration you can: + +- provide a config object when using it as a standalone module or hapi server +- ENV variables on the server/container/machine you will run Udaru on. + +### Config object + +**Standalone module** +```js +const buildUdaru = require('@nearform/udaru-core') +const udaru = buildUdaru(dbPool, { + logger: { + pino: { + level: 'warn' + } + } +}}) +``` + +**Hapi plugin** +```js +const Hapi = require('hapi') +const UdaruPlugin = require('@nearform/udaru-hapi-plugin') +const server = Hapi.server() +server.register({ + register: UdaruPlugin, + options: {dbPool, config: { + api: { + servicekeys: { + private: ['123456789'] + } + } +}}}) +``` + +### Env variables + +To override those configuration settings you will have to specify your ENV variables with a [prefix][prefix-link] and then the "path" to the property you want to override. + +**Configuration** +``` +{ + security: { + api: { + servicekeys: { + private: [ + '123456789' + ] + } + } + } +} +``` + +**ENV variable override** +``` +UDARU_SERVICE_security_api_servicekeys_private_0=jerfkgfjdedfkg3j213i43u31jk2erwegjndf +``` + +To achieve this we use the [`reconfig`][reconfig] module. + +## Testing, benching & linting + +Before running tests, ensure a valid Postgres database is running. The simplest way to do this is via Docker. Assuming docker is installed on your machine, in the root folder, run: + +``` +docker-compose up -d +``` + +This will start a Postgres database. Running test or coverage runs will automatically populate the database with the information it needs. + +- **Note:** you can tail the Postgres logs if needed with `docker-compose logs --tail=100 -f` + +To run tests: + +``` +npm run test +``` + +- **Note:** running the tests will output duplicate keys errors in Postgres logs, this is expected, as the error handling of those cases is part of what is tested. + + +To lint the repository: + +``` +npm run lint +``` + +To fix (most) linting issues: + +``` +npm run lint -- --fix +``` + +To run a bench test on a given route: + +``` +npm run bench -- "METHOD swagger/route/template/path" +``` + +To create coverage reports: + +``` +npm run coverage +``` + +To populate the database with large volume of data: + +``` +npm run pg:init-volume-db +``` + +To run bench test against populated volume data (2 endpoints) + +``` +npm run bench:volume +``` + +For convenience, you can load the volume db and run the bench tests with the single command. + +``` +npm run bench:load-volume +``` + +This command will: +- initialise the db & migrate to latest db schema +- load the standard test fixtures +- load the volume fixtures +- spawn an instance of udaru server +- run the autocannon tests & display results +- shut down + +## Security + +Udaru has been thoroughly evaluated against SQL injection, a detailed description of this can be found in the [SQL Injection][] document. + +To automatically run [sqlmap][] injection tests run: +``` +npm run test:security +``` + +- **Note:** before running this, make sure you have a version of [`Python 2.x`](https://www.python.org) installed in your path. + +These tests are not included in the main test suite. The security test spawns a hapi.js server exposing the Udaru routes. It only needs the DB to be running and being initialized with data. + +The injection tests can be configured in the [sqlmap config][]. A few output configuration changes that can be made: +- `level` can be set to 5 for more aggressive testing +- `risk` can be set to 3 for more testing options. Note: this level might alter the DB data +- `verbose` can be set to level 1-5. Level 1 displays info about the injections tried + +See the [sqlmap][] repository for more details. + +## License + +Copyright nearForm Ltd 2017. Licensed under [MIT][license]. + +[license]: ./LICENSE.md +[travis-badge]: https://travis-ci.org/nearform/hapi-auth-udaru.svg?branch=master +[travis-url]: https://travis-ci.org/nearform/hapi-auth-udaru +[npm-badge]: https://badge.fury.io/js/hapi-auth-udaru.svg +[npm-url]: https://npmjs.org/package/hapi-auth-udaru +[coveralls-badge]: https://coveralls.io/repos/nearform/hapi-auth-udaru/badge.svg?branch=master&service=github +[coveralls-url]: https://coveralls.io/github/nearform/hapi-auth-udaru?branch=master +[snyk-badge]: https://snyk.io/test/github/nearform/hapi-auth-udaru/badge.svg +[snyk-url]: https://snyk.io/test/github/nearform/hapi-auth-udaru diff --git a/packages/hapi-auth-udaru/bench/access.bench.js b/packages/hapi-auth-udaru/bench/access.bench.js new file mode 100644 index 00000000..f07e371c --- /dev/null +++ b/packages/hapi-auth-udaru/bench/access.bench.js @@ -0,0 +1,41 @@ +'use strict' + +module.exports = [ + { + tag: 'GET authorization/access/{userId}/{action}/{resource}', + handler: () => { + return { + path: '/authorization/access/ROOTid/action-a/resource-a', + method: 'GET', + headers: { + authorization: 'ROOTid' + } + } + } + }, + { + tag: 'GET authorization/list/{userId}/{resource}', + handler: () => { + return { + path: '/authorization/list/ROOTid/resource-a', + method: 'GET', + headers: { + authorization: 'ROOTid' + } + } + } + }, + { + tag: 'GET authorization/list/{userId}', + handler: () => { + return { + path: '/authorization/list/ManyPoliciesId?resources=/myapp/users/filippo&resources=/myapp/documents/no_access&resources=/myapp/teams/foo&resources=/myapp/teams/foo1&resources=/myapp/teams/foo2&resources=/myapp/teams/foo3&resources=/myapp/teams/foo4&resources=/myapp/teams/foo5&resources=/myapp/teams/foo6&resources=/myapp/teams/foo7', + method: 'GET', + headers: { + authorization: 'ROOTid', + org: 'WONKA' + } + } + } + } +] diff --git a/packages/hapi-auth-udaru/bench/list.bench.js b/packages/hapi-auth-udaru/bench/list.bench.js new file mode 100644 index 00000000..2db5b430 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/list.bench.js @@ -0,0 +1,16 @@ +'use strict' + +module.exports = [ + { + tag: 'GET authorization/list/{userId}/{resource}', + handler: () => { + return { + path: '/authorization/list/ROOTid/resource_a', + method: 'GET', + headers: { + authorization: 'ROOTid' + } + } + } + } +] diff --git a/packages/hapi-auth-udaru/bench/orgs.bench.js b/packages/hapi-auth-udaru/bench/orgs.bench.js new file mode 100644 index 00000000..45133836 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/orgs.bench.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = [ + // Todo.. +] diff --git a/packages/hapi-auth-udaru/bench/ping.bench.js b/packages/hapi-auth-udaru/bench/ping.bench.js new file mode 100644 index 00000000..f0be18f8 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/ping.bench.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports = [ + { + tag: 'GET ping', + handler: () => { + return { + path: '/ping', + method: 'GET' + } + } + } +] diff --git a/packages/hapi-auth-udaru/bench/policies.bench.js b/packages/hapi-auth-udaru/bench/policies.bench.js new file mode 100644 index 00000000..45178357 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/policies.bench.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports = [ + { + tag: 'GET authorization/policies', + handler: () => { + return { + path: '/authorization/policies?page=1&limit=100', + method: 'GET', + headers: { + authorization: 'ROOTid', + org: 'WONKA' + } + } + } + } +] diff --git a/packages/hapi-auth-udaru/bench/teams.bench.js b/packages/hapi-auth-udaru/bench/teams.bench.js new file mode 100644 index 00000000..f45d12d1 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/teams.bench.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports = [ + { + tag: 'GET authorization/teams', + handler: () => { + return { + path: '/authorization/teams?page=1&limit=100', + method: 'GET', + headers: { + authorization: 'ROOTid', + org: 'WONKA' + } + } + } + } +] diff --git a/packages/hapi-auth-udaru/bench/users.bench.js b/packages/hapi-auth-udaru/bench/users.bench.js new file mode 100644 index 00000000..c6321f46 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/users.bench.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports = [ + { + tag: 'GET authorization/users', + handler: () => { + return { + path: '/authorization/users?page=1&limit=100', + method: 'GET', + headers: { + authorization: 'ROOTid', + org: 'WONKA' + } + } + } + } +] diff --git a/packages/hapi-auth-udaru/bench/util/loadVolumeData.js b/packages/hapi-auth-udaru/bench/util/loadVolumeData.js new file mode 100755 index 00000000..315f9be2 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/util/loadVolumeData.js @@ -0,0 +1,211 @@ +#!/usr/bin/env node + +'use strict' + +/* /bench/util/volumeRunner.js needs to have same values as here for teams, users etc. */ +const NUM_TEAMS = 500 // total number of teams + +const USER_START_ID = 1 // user start id, we may want a few super users +const TEAM_START_ID = 7 // user start id, so as not to interfere with other test data +const SUB_TEAM_MOD = 100 // 1 parent for every X-1 teams +const NUM_USERS_PER_TEAM = 100 // put this many users in each team +const NUM_POLICIES_PER_TEAM = 10 // :-| +const ADD_METADATA = true + +const pg = require('pg') +const chalk = require('chalk') +const minimist = require('minimist') +const argv = minimist(process.argv.slice(2)) + +const pgConf = { + user: argv.user || 'postgres', + database: argv.database || 'authorization', + password: argv.password || 'postgres', + host: argv.host || 'localhost', + port: argv.port || 5432 +} + +const client = new pg.Client(pgConf) + +var startTime +var endTime + +function loadVolumeDataBegin (callback) { + startTime = Date.now() + console.log('loadVolume data started at ' + startTime) + + client.connect(() => { // connect first, then set off daisy chain of queries on success + loadTeams('CONCH', callback) // loads load everything into WONKA org + }) +} + +function loadTeams (orgId, callback) { + // insert teams + console.log('inserting teams') + let fixturesSQL = 'INSERT INTO teams (id, name, description, team_parent_id, org_id, path)\nVALUES\n' + for (var i = TEAM_START_ID; i < (NUM_TEAMS + TEAM_START_ID); i++) { + // root level teams + if (((i - TEAM_START_ID + 1) % SUB_TEAM_MOD) === 1) { + fixturesSQL += '(' + i + ", 'TEAM_" + i + + "', 'Root level team', NULL, '" + orgId + "', TEXT2LTREE('" + i + "'))" + var currentParentId = i + } else { + fixturesSQL += '(' + i + ", 'TEAM_" + i + + "', 'Subordinate of TEAM_" + currentParentId + "', " + currentParentId + ", '" + orgId + "', TEXT2LTREE('" + i + "'))" + } + + if (i === (NUM_TEAMS + TEAM_START_ID - 1)) { + fixturesSQL += ';' + } else { + fixturesSQL += ',\n' + } + } + + console.log(fixturesSQL) + client.query(fixturesSQL, function (err, result) { + if (err) { + callback(err) + } else { + console.log(chalk.green('success inserting teams')) + loadPolicies(1, orgId, TEAM_START_ID, callback) + } + }) +} + +function getPolicyTemplate () { + var policyTemplate = {} + var statement = [] + statement[0] = {} + statement[0].Effect = 'Allow' + statement[0].Action = ['Read'] + statement[0].Resource = ['x'] + statement[1] = {} + statement[1].Effect = 'Allow' + statement[1].Action = ['Read'] + statement[1].Resource = ['y'] + policyTemplate.Statement = statement + return policyTemplate +} + +function loadPolicies (startId, orgId, teamId, callback) { + // insert policies, for each team we need 10 per team + console.log('inserting policies for team ' + teamId) + + var policyTemplate = getPolicyTemplate() + + let policiesSql = 'INSERT INTO policies (id, version, name, org_id, statements)\nVALUES\n' + let teamPoliciesSql = 'INSERT INTO team_policies(team_id, policy_id, variables)\nVALUES\n' + + // 10 policies per team + var count = 1 + for (var id = startId; id < startId + NUM_POLICIES_PER_TEAM; id++) { + // modify policy here... + policyTemplate.Statement[0].Resource[0] = 'resource_' + count + ':org/$' + '{udaru:organizationId}' + policyTemplate.Statement[1].Resource[0] = 'resource_' + count + ':user/$' + '{udaru:userId}' + + policiesSql += "('" + id + "', '2012-10-17', 'POLICY_" + id + "', '" + orgId + "', '" + + JSON.stringify(policyTemplate) + "'::JSONB)" + + teamPoliciesSql += "('" + teamId + "','" + id + "','" + JSON.stringify({var: count}) + "'::JSONB)" + + if (id === (startId + NUM_POLICIES_PER_TEAM - 1)) { + policiesSql += ';' + teamPoliciesSql += ';' + } else { + policiesSql += ',\n' + teamPoliciesSql += ',\n' + } + + count++ + } + + var fixturesSQL = 'BEGIN;\n' + fixturesSQL += policiesSql + '\n' + fixturesSQL += teamPoliciesSql + '\n' + fixturesSQL += 'COMMIT;\n' + + client.query(fixturesSQL, function (err, result) { + if (err) { + callback(err) + } else { + console.log(chalk.green('success inserting policies for team ' + teamId)) + + if (teamId < NUM_TEAMS + TEAM_START_ID - 1) { + // load policies for next team + loadPolicies(id, orgId, teamId + 1, callback) + } else { + // move on to loading users + loadUsers(USER_START_ID, orgId, TEAM_START_ID, callback) + } + } + }) +} + +function getMetaData (val1, val2) { + if (ADD_METADATA) { + var obj = { + key1: val1, + key2: val2 + } + return "'" + JSON.stringify(obj) + "'::JSONB" + } + + return null +} + +// insert users and add them to teams in batches +function loadUsers (startId, orgId, teamId, callback) { + // insert users + console.log('inserting users ' + startId + ' to ' + (startId + NUM_USERS_PER_TEAM - 1) + ' into team: ' + teamId) + + var userSql = 'INSERT INTO users (id, name, org_id, metadata)\nVALUES\n' + var userTeamSql = 'INSERT INTO team_members (user_id, team_id)\nVALUES\n' + for (var id = startId; id < (startId + NUM_USERS_PER_TEAM); id++) { + userSql += "('" + id + "', 'USER_" + id + "', '" + orgId + "'," + getMetaData(id, orgId) + ')' + userTeamSql += "('" + id + "', '" + teamId + "')" + + if (id === startId + NUM_USERS_PER_TEAM - 1) { + userSql += ';' + userTeamSql += ';' + } else { + userSql += ',\n' + userTeamSql += ',\n' + } + } + + var fixturesSQL = 'BEGIN;\n' + fixturesSQL += userSql + '\n' + fixturesSQL += userTeamSql + '\n' + fixturesSQL += 'COMMIT;\n' + + client.query(fixturesSQL, function (err, result) { + if (err) { + callback(err) + } else { + console.log(chalk.green('success inserting users ' + startId + + ' to ' + (startId + NUM_USERS_PER_TEAM - 1))) + if (teamId < NUM_TEAMS + TEAM_START_ID - 1) { + loadUsers(id, orgId, teamId + 1, callback) + } else { + loadVolumeDataEnd(callback) + } + } + }) +} + +function loadVolumeDataEnd (callback) { + endTime = Date.now() + console.log(chalk.green('loadVolumeData completed in ' + (endTime - startTime) + 'ms')) + + callback() // done +} + +module.exports = loadVolumeDataBegin + +if (require.main === module) { + loadVolumeDataBegin((err) => { + if (err) console.log(chalk.red(err, 'error')) + try { client.end() } catch (e) { } + process.exit(err ? 1 : 0) + }) +} diff --git a/packages/hapi-auth-udaru/bench/util/runner.js b/packages/hapi-auth-udaru/bench/util/runner.js new file mode 100644 index 00000000..c8b15d6e --- /dev/null +++ b/packages/hapi-auth-udaru/bench/util/runner.js @@ -0,0 +1,65 @@ +'use strict' + +// Note: you must run the server separately. + +const Autocannon = require('autocannon') +const Bloomrun = require('bloomrun') +const Minimist = require('minimist') + +// Our test config +const Policies = require('../policies.bench.js') +const Ping = require('../ping.bench.js') +const Orgs = require('../orgs.bench.js') +const Teams = require('../teams.bench.js') +const Users = require('../users.bench.js') +const List = require('../list.bench.js') +const Access = require('../access.bench.js') + +// Apply each test handler to bloomrun with a tag +function configureTests (tests) { + const container = Bloomrun() + + tests.forEach((route) => { + route.forEach((test) => { + container.add({tag: test.tag}, test.handler) + }) + }) + + return container +} + +// Prime the test suite, get the tag from cli, eg, `npm run bench -- get/orgs` +const tests = configureTests([Ping, Orgs, Policies, Teams, Users, List, Access]) +const tag = (Minimist(process.argv.slice(2))._ || '').toString() + +// If we don't have a matching tag we exit early +const getParams = tests.lookup({tag: tag}) +if (typeof getParams !== 'function') { + console.error(`No test found for tag: ${tag}`) + process.exit() +} + +// Generate params for the bench test +const params = getParams() +const opts = { + connections: 500, + duration: 10, + title: tag, + url: `http://localhost:8080${params.path}`, + method: params.method, + headers: params.headers, + body: params.body +} + +// Create the test and finish handler. +const instance = Autocannon(opts, (err, result) => { + if (err) { + console.error(err) + process.exit(-1) + } + + console.log('Detailed Result:', '\n\n', result) +}) + +// Starts the test and shows a pretty progress bar while it runs +Autocannon.track(instance) diff --git a/packages/hapi-auth-udaru/bench/util/volumeRunner.js b/packages/hapi-auth-udaru/bench/util/volumeRunner.js new file mode 100644 index 00000000..ea356c11 --- /dev/null +++ b/packages/hapi-auth-udaru/bench/util/volumeRunner.js @@ -0,0 +1,193 @@ +'use strict' +// run this AFTER /database/loadVolumeData.js has populated db successfully +const DEBUG = false // prints request/response details +const START_SERVER = true // true = fork udaru server + +// ensure variables same as /database/loadVolumeData.js +const NUM_TEAMS = 500 // total number of teams +const TEAM_START_ID = 7 // user start id offset +const NUM_USERS_PER_TEAM = 100 +const NUM_POLICIES_PER_TEAM = 10 + +// autocannon settings +const DURATION = 15 // how long to run tests for in seconds +const CONNECTIONS = 10 // number of concurrent connections + +const autocannon = require('autocannon') +const path = require('path') +const chalk = require('chalk') + +// part of initial route, this changes for second run +var partialRoute = '/authorization/access/' + +var child = null +if (START_SERVER) { + const spawn = require('child_process') + // start udaru server + console.log('Starting UDARU server...') + // need stdin to determine when server starts + child = spawn.fork(path.join(__dirname, '../../start'), [], ['ignore', 'ignore', 'ignore', 'ipc']) + + child.on('message', (m) => { + if (m.indexOf('Server started on:') !== -1) { + console.log(chalk.green(m)) + startBench() + } else { + console.log(chalk.red(m)) + process.exit(0) + } + }) + + child.on('close', (code, signal) => { + console.log('Server received close event, shutting down') + // as part of graceful exit after bench run, child.kill will cause this + // might as well kill here if the server isn't running for any reason + process.exit(0) + }) +} else { + startBench() +} + +var instance +function startBench () { + console.log('Running autocannon against: ' + partialRoute + ', num teams: ' + NUM_TEAMS) + instance = autocannon({ + title: 'Random requests to ' + partialRoute, + url: 'http://localhost:8080', + duration: DURATION, + connections: CONNECTIONS, + headers: { + authorization: 'ROOTid', + org: 'CONCH' + }, + requests: [ + { + method: 'GET', + path: getPath() + } + ], + setupClient: setupClient + }, onComplete) + + autocannon.track(instance, {renderProgressBar: !DEBUG}) + + instance.on('response', onResponse) +} + +// allows for ctrl+c safely shut down +process.once('SIGINT', () => { + // this will be called if we haven't kicked off a child process for UDARU + console.log('\nStopping instance of autocannon...') + instance.stop() + + if (child) { + console.log('Kill server child process...') + child.kill() + } +}) + +function getRandomIntInclusive (min, max) { + min = Math.ceil(min) + max = Math.floor(max) + return Math.floor(Math.random() * (max - min + 1)) + min // The maximum is inclusive and the minimum is inclusive +} + +function getPath () { + if (partialRoute === '/authorization/access/') { + // get a random team + var team = getRandomIntInclusive(TEAM_START_ID, TEAM_START_ID + NUM_TEAMS - 1) + // get a random user within the team + var user = ((team - TEAM_START_ID) * NUM_USERS_PER_TEAM) + getRandomIntInclusive(1, NUM_USERS_PER_TEAM) + // get a random policy within the team + var policy = getRandomIntInclusive(1, NUM_POLICIES_PER_TEAM) + + var dynamicPath = partialRoute + user + '/Read/resource_' + policy + ':user/' + user + + debug('Next request: ' + dynamicPath) + return dynamicPath + } else if (partialRoute === '/authorization/users/') { + // get a random user within the database, all we want to know is if a team is returned + user = getRandomIntInclusive(1, NUM_TEAMS * NUM_USERS_PER_TEAM) + dynamicPath = partialRoute + user + '/teams' + + debug('Next request: ' + dynamicPath) + return dynamicPath + } else { + onComplete('invalid partial route') + return null + } +} + +function debug (message) { + if (DEBUG) { + console.log(message) + } +} + +function onResponse (client, statusCode, returnBytes, responseTime) { + // change path for next request + var dynamicPath = getPath() + + var requests = [ + { + method: 'GET', + path: dynamicPath + } + ] + + client.setRequests(requests) + + debug(statusCode + ':' + returnBytes + 'b:' + responseTime + 'ms') +} + +function setupClient (client) { + client.on('body', onBodyReceived) // console.log a response body when its received +} + +function onBodyReceived (buffer) { + // here we are testing for valid response to ensure end-to-end has happened + // not for any assertions etc., exit if failure occurs + var s = buffer.toString('utf8') + + var testString = '"name":"TEAM_' + if (partialRoute === '/authorization/access/') { + testString = '{"access":true}' + } + + if (s.indexOf(testString) === -1) { + onComplete('Test failed: ' + s) + } else { + debug('Test Passed: contains ' + testString) + } +} + +function onComplete (err, res) { + var shutDown = false + if (err) { + console.log(chalk.red( + '\nExiting due to invalid response, ensure database loaded with correct number of teams etc. ' + + '(see /database/loadVolumeData.js)')) + console.error(err) + shutDown = true + } else { + console.log('Results for: ' + partialRoute) + console.log(res) + if (partialRoute === '/authorization/access/') { + // start second set of tests with users route to list teams + partialRoute = '/authorization/users/' + startBench() + } else { + shutDown = true + } + } + + if (shutDown) { + if (child) { + debug('Stopping UDARU server') + child.kill() + // process.exit will be called by close event handler for child process + } else { + process.exit(0) + } + } +} diff --git a/packages/hapi-auth-udaru/index.js b/packages/hapi-auth-udaru/index.js new file mode 100644 index 00000000..fce6b719 --- /dev/null +++ b/packages/hapi-auth-udaru/index.js @@ -0,0 +1,33 @@ +'use strict' + +const buildUdaru = require('@nearform/udaru-core') +const buildConfig = require('./lib/config') +const buildAuthorization = require('./lib/authorization') +const HapiAuthService = require('./lib/authentication') + +module.exports = { + pkg: require('./package'), + + async register (server, options) { + const config = buildConfig(options.config) + const udaru = buildUdaru(options.dbPool, config) + + server.decorate('server', 'udaru', udaru) + server.decorate('server', 'udaruConfig', config) + server.decorate('server', 'udaruAuthorization', buildAuthorization(config)) + server.decorate('request', 'udaruCore', udaru) + + await server.register([ + require('./lib/routes/users'), + require('./lib/routes/policies'), + require('./lib/routes/teams'), + require('./lib/routes/authorization'), + require('./lib/routes/organizations'), + require('./lib/routes/monitor'), + HapiAuthService + ]) + + server.auth.strategy('udaru', 'udaru') + server.auth.default({ mode: 'required', strategy: 'udaru' }) + } +} diff --git a/packages/hapi-auth-udaru/lib/authentication.js b/packages/hapi-auth-udaru/lib/authentication.js new file mode 100644 index 00000000..9071f525 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/authentication.js @@ -0,0 +1,144 @@ +'use strict' + +const Hoek = require('hoek') +const Boom = require('boom') + +async function loadUser (job) { + const { userId } = job + + try { + const organizationId = await job.udaru.getUserOrganizationId(userId) + job.currentUser = await job.udaru.users.read({ id: userId, organizationId }) + } catch (e) { + throw Boom.unauthorized('Bad credentials') + } +} + +function canImpersonate (request, user) { + return user.organizationId === request.server.udaruConfig.get('authorization.superUser.organization.id') +} + +async function impersonate (job) { + const { currentUser } = job + job.organizationId = currentUser.organizationId + + if (canImpersonate(job.request, currentUser) && job.requestedOrganizationId) job.organizationId = job.requestedOrganizationId +} + +async function checkAuthorization (udaru, userId, action, organizationId, resource, done) { + const result = await udaru.authorize.isUserAuthorized({ userId, action, organizationId, resource }) + + return result.access +} + +async function buildResourcesForUser (udaru, builder, buildParams, organizationId) { + const resources = [builder(buildParams)] + + try { + const user = await udaru.users.read({ id: buildParams.userId, organizationId: organizationId }) + + for (const team of user.teams) { + buildParams.teamId = team.id + resources.push(builder(buildParams)) + } + + return resources + } catch (err) { + if (err.output && err.output.statusCode === 404) return resources + throw err + } +} + +function buildResources (options, udaru, authParams, request, organizationId) { + let resource = authParams.resource + + if (resource) return [resource] + + const resourceType = request.route.path.split('/')[2] + const resourceBuilder = request.server.udaruConfig.get('AuthConfig.resources')[resourceType] + if (!resourceBuilder) throw new Error('Resource builder not found') + + const requestParams = authParams.getParams ? authParams.getParams(request) : {} + const buildParams = Object.assign({}, {organizationId}, requestParams) + + if (resourceType === 'users' && buildParams.userId) return buildResourcesForUser(udaru, resourceBuilder, buildParams, organizationId) + + return [resourceBuilder(buildParams)] +} + +async function authorize (job) { + const resources = await buildResources(job.options, job.udaru, job.authParams, job.request, job.organizationId) + + const action = job.authParams.action + const userId = job.currentUser.id + const organizationId = job.currentUser.organizationId + + const valids = await Promise.all(resources.map(resource => checkAuthorization(job.udaru, userId, action, organizationId, resource))) + + if (!valids.includes(true)) throw Boom.forbidden('Invalid credentials', 'udaru') +} + +async function validate (options, server, request, userId, callback) { + const authParams = server.udaruAuthorization.getAuthParams(request) + const udaru = request.udaruCore + + if (!authParams) throw Boom.forbidden('Invalid credentials', 'udaru') + + const job = {udaru, options, userId, request, authParams, requestedOrganizationId: request.headers.org} + + await loadUser(job) + await impersonate(job) + await authorize(job) + + return {user: job.currentUser, organizationId: job.organizationId} +} + +async function authenticate (server, settings, request) { + const req = request.raw.req + const authorization = req.headers.authorization + + if (!authorization) throw Boom.unauthorized('Missing authorization', 'udaru') + + const userId = String(authorization) + + const credentials = await settings.validateFunc(settings, server, request, userId) + request.udaru = credentials + + return {credentials: {scope: 'udaru'}} +} + +module.exports = { + name: 'Udaru Authentication', + version: '0.0.1', + + validate, + + register (server, options) { + server.auth.scheme('udaru', function (server, options) { + Hoek.assert(options, 'Missing service auth strategy options') + + if (typeof options.validateFunc === 'undefined' || options.validateFunc === null) options.validateFunc = validate + Hoek.assert(typeof options.validateFunc === 'function', 'options.validateFunc must be a valid function') + + const settings = Hoek.clone(options) + + const scheme = { + async authenticate (request, h) { + return h.authenticated(await authenticate(server, settings, request)) + }, + + payload (request, h) { + if (server.udaruAuthorization.needTeamsValidation(request)) return request.server.udaruAuthorization.validateTeamsInPayload(server, request, h) + + return h.continue + }, + + options: { + payload: true + } + } + + return scheme + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/authorization.js b/packages/hapi-auth-udaru/lib/authorization.js new file mode 100644 index 00000000..cd529921 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/authorization.js @@ -0,0 +1,75 @@ +'use strict' + +const Boom = require('boom') +const getProperty = require('lodash/get') + +module.exports = function (config) { + const Action = config.get('AuthConfig.Action') + const actionsNeedingValidation = [Action.ReplaceUserTeams, Action.DeleteUserTeams] + + function getAuthParams (request, def = null) { + return getProperty(request, 'route.settings.plugins.auth', def) + } + + async function getTeams (server, request) { + const authParams = getAuthParams(request) + + switch (authParams.action) { + case Action.ReplaceUserTeams: + // This is called before we get to routing so needs to be validated first + const teams = getProperty(request, 'payload.teams') + if (!teams || !Array.isArray(teams)) throw Boom.badRequest('No teams found in payload', 'udaru') + + return teams + case Action.DeleteUserTeams: + const { user: currentUser } = request.udaru + + const requestParams = authParams.getParams(request) + const organizationId = request.headers.org || currentUser.organizationId + const user = await request.udaruCore.users.read({ id: requestParams.userId, organizationId }) + + return user.teams.map(t => t.id) + } + } + + function needTeamsValidation (request) { + const authParams = getAuthParams(request, {}) + + return actionsNeedingValidation.includes(authParams.action) + } + + async function validateTeamsInPayload (server, request, h) { + const teams = await getTeams(server, request) + const { user: currentUser } = request.udaru + const authParams = getAuthParams(request) + const resourceType = request.route.path.split('/')[2] + const resourceBuilder = server.udaruConfig.get('AuthConfig.resources')[resourceType] + + if (!resourceBuilder) throw new Error('Resource builder not found') + + for (const teamId of teams) { + const { id: userId, organizationId } = currentUser + const resource = resourceBuilder({ userId, teamId, organizationId }) + const { action } = authParams + + const result = await request.udaruCore.authorize.isUserAuthorized({ userId, action, resource, organizationId }) + + if (!getProperty(result, 'access')) throw Boom.forbidden(`Not enough permissions to modify team ${teamId}`) + } + + return h.continue + } + + function hasValidServiceKey (request) { + const validKeys = config.get('security.api.servicekeys.private') + + return validKeys.includes(getProperty(request, 'query.sig', '')) + } + + return { + getAuthParams, + needTeamsValidation, + validateTeamsInPayload, + hasValidServiceKey + } +} diff --git a/packages/hapi-auth-udaru/lib/config.js b/packages/hapi-auth-udaru/lib/config.js new file mode 100644 index 00000000..28b75c02 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/config.js @@ -0,0 +1,45 @@ +const config = require('@nearform/udaru-core/config') + +module.exports = (...amendments) => config({ + hapi: { + port: 8080, + host: 'localhost' + }, + logger: { + pino: { + level: 'warn' + } + }, + security: { + api: { + servicekeys: { + private: [ + '123456789' + ] + } + } + }, + authorization: { + superUser: { + organization: { + id: 'ROOT', + name: 'SuperOrganization', + description: 'SuperUser Organization' + }, + id: 'SuperUserId', + name: 'SuperUser', + defaultPolicy: { + version: '1', + name: 'SuperUser', + organizationId: ':organizationId', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['*'], + Resource: ['*'] + }] + } + } + } + } +}, ...amendments) diff --git a/packages/hapi-auth-udaru/lib/headers.js b/packages/hapi-auth-udaru/lib/headers.js new file mode 100644 index 00000000..9b0c1184 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/headers.js @@ -0,0 +1,10 @@ +'use strict' + +const Joi = require('joi') + +const headers = Joi.object({ + 'authorization': Joi.any().required().description('User ID of the enpoint caller'), + 'org': Joi.any().description('Specify a different organization for the user who is calling the endpoint (works only for SuperUser, it\'s like impersonation).') +}).unknown() + +module.exports = headers diff --git a/packages/hapi-auth-udaru/lib/routes/authorization.js b/packages/hapi-auth-udaru/lib/routes/authorization.js new file mode 100644 index 00000000..a963a89e --- /dev/null +++ b/packages/hapi-auth-udaru/lib/routes/authorization.js @@ -0,0 +1,99 @@ +'use strict' + +const pick = require('lodash/pick') +const validation = require('@nearform/udaru-core/lib/ops/validation').authorize +const swagger = require('../swagger') +const headers = require('../headers') + +module.exports = { + name: 'authorization', + version: '0.0.1', + register (server, options) { + const Action = server.udaruConfig.get('AuthConfig.Action') + + server.route({ + method: 'GET', + path: '/authorization/access/{userId}/{action}/{resource*}', + async handler (request) { + const { organizationId } = request.udaru + const { resource, action, userId } = request.params + const { remoteAddress: sourceIpAddress, remotePort: sourcePort } = request.info + + return request.udaruCore.authorize.isUserAuthorized({userId, action, resource, organizationId, sourceIpAddress, sourcePort}) + }, + config: { + plugins: { + auth: { + action: Action.CheckAccess, + resource: 'authorization/access' + } + }, + validate: { + params: pick(validation.isUserAuthorized, ['userId', 'action', 'resource']), + headers + }, + description: 'Authorize user action against a resource', + notes: 'The GET /authorization/access/{userId}/{action}/{resource} endpoint answers if a user can perform an action\non a resource.\n', + tags: ['api', 'authorization'], + response: {schema: swagger.Access} + } + }) + + server.route({ + method: 'GET', + path: '/authorization/list/{userId}/{resource*}', + async handler (request) { + const { organizationId } = request.udaru + const { resource, userId } = request.params + const { remoteAddress: sourceIpAddress, remotePort: sourcePort } = request.info + + return request.udaruCore.authorize.listActions({ userId, resource, organizationId, sourceIpAddress, sourcePort }) + }, + config: { + plugins: { + auth: { + action: Action.ListActions, + resource: 'authorization/actions' + } + }, + validate: { + params: pick(validation.listAuthorizations, ['userId', 'resource']), + headers + }, + description: 'List all the actions a user can perform on a resource', + notes: 'The GET /authorization/list/{userId}/{resource} endpoint returns a list of all the actions a user\ncan perform on a given resource.\n', + tags: ['api', 'authorization'], + response: {schema: swagger.UserActions} + } + }) + + server.route({ + method: 'GET', + path: '/authorization/list/{userId}', + async handler (request) { + const { organizationId } = request.udaru + const { userId } = request.params + const { resources } = request.query + + return request.udaruCore.authorize.listAuthorizationsOnResources({ userId, resources, organizationId }) + }, + config: { + plugins: { + auth: { + action: Action.ListActionsOnResources, + resource: 'authorization/actions/resources' + } + }, + validate: { + params: pick(validation.listAuthorizationsOnResources, ['userId']), + query: pick(validation.listAuthorizationsOnResources, ['resources']), + headers + }, + description: 'List all the actions a user can perform on a list of resources', + notes: 'The GET /authorization/list/{userId} endpoint returns a list of all the actions a user\ncan perform on a given list of resources.\n', + tags: ['api', 'authorization'], + response: {schema: swagger.UserActionsOnResources} + } + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/routes/monitor.js b/packages/hapi-auth-udaru/lib/routes/monitor.js new file mode 100644 index 00000000..4c93f721 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/routes/monitor.js @@ -0,0 +1,21 @@ +'use strict' + +module.exports = { + name: 'monitor', + version: '0.0.1', + register (server, options) { + server.route({ + method: 'GET', + path: '/ping', + async handler (request, h) { + return h.response() + }, + config: { + auth: false, + description: 'Ping endpoint', + notes: 'The GET /ping endpoint will return 200 if the server is up and running.', + tags: ['api', 'monitoring'] + } + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/routes/organizations.js b/packages/hapi-auth-udaru/lib/routes/organizations.js new file mode 100644 index 00000000..64ed7498 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/routes/organizations.js @@ -0,0 +1,254 @@ +'use strict' + +const Joi = require('joi') +const pick = require('lodash/pick') +const validation = require('@nearform/udaru-core/lib/ops/validation').organizations +const swagger = require('../swagger') +const headers = require('../headers') + +module.exports = { + name: 'organizations', + version: '0.0.1', + register (server, options) { + const Action = server.udaruConfig.get('AuthConfig.Action') + + server.route({ + method: 'GET', + path: '/authorization/organizations', + async handler (request) { + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + return { page, limit, ...(await request.udaruCore.organizations.list({ limit, page })) } + }, + config: { + description: 'List all the organizations', + notes: 'The GET /authorization/organizations endpoint returns a list of all organizations.\n\nThe results are paginated. Page numbering and page limit start from 1.\n', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.ListOrganizations + } + }, + validate: { + headers, + query: validation.list + }, + response: { schema: swagger.PagedOrganizations } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/organizations/{id}', + async handler (request) { + return request.udaruCore.organizations.read(request.params.id) + }, + config: { + description: 'Get organization', + notes: 'The GET /authorization/organizations/{id} endpoint returns a single organization data.\n', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.ReadOrganization, + getParams: (request) => ({ organizationId: request.params.id }) + } + }, + validate: { + params: { + id: validation.readById + }, + headers + }, + response: { schema: swagger.Organization } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/organizations', + async handler (request, h) { + const { id, name, description, metadata, user } = request.payload + + return h.response(await request.udaruCore.organizations.create({ id, name, description, metadata, user })).code(201) + }, + config: { + validate: { + payload: Joi.object(validation.create).label('CreateOrgPayload'), + headers + }, + description: 'Create an organization', + notes: 'The POST /authorization/organizations endpoint creates a new organization, the default organization admin policy and (if provided) its admin.', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.CreateOrganization + } + }, + response: { schema: swagger.OrganizationAndUser } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/organizations/{id}', + async handler (request) { + const { id } = request.params + const { name, description, metadata } = request.payload + + return request.udaruCore.organizations.update({ id, name, description, metadata }) + }, + config: { + validate: { + params: pick(validation.update, ['id']), + payload: Joi.object(pick(validation.update, ['name', 'description', 'metadata'])).label('UpdateOrgPayload'), + headers + }, + description: 'Update an organization', + notes: 'The PUT /authorization/organizations/{id} endpoint will update an organization name and description', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.UpdateOrganization, + getParams: (request) => ({ organizationId: request.params.id }) + } + }, + response: { schema: swagger.Organization } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/organizations/{id}', + async handler (request, h) { + return h.response(await request.udaruCore.organizations.delete(request.params.id)).code(204) + }, + config: { + description: 'DELETE an organization', + notes: 'The DELETE /authorization/organizations/{id} endpoint will delete an organization.', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.DeleteOrganization, + getParams: (request) => ({ organizationId: request.params.id }) + } + }, + validate: { + params: { + id: validation.deleteById + }, + headers + } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/organizations/{id}/policies', + async handler (request) { + const { id } = request.params + const { policies } = request.payload + + return request.udaruCore.organizations.replacePolicies({id, policies}) + }, + config: { + validate: { + params: pick(validation.replaceOrganizationPolicies, ['id']), + payload: Joi.object(pick(validation.replaceOrganizationPolicies, ['policies'])).label('ReplaceOrgPoliciesPayload'), + headers + }, + description: 'Clear and replace the policies of an organization', + notes: 'The POST /authorization/organizations/{id}/policies endpoint replaces all the organization policies. The existing organization policies are removed.', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.ReplaceOrganizationPolicy, + getParams: (request) => ({ organizationId: request.params.id }) + } + }, + response: { schema: swagger.Organization } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/organizations/{id}/policies', + async handler (request) { + const { id } = request.params + const { policies } = request.payload + + return request.udaruCore.organizations.addPolicies({ id, policies }) + }, + config: { + validate: { + params: pick(validation.addOrganizationPolicies, ['id']), + payload: Joi.object(pick(validation.addOrganizationPolicies, ['policies'])).label('AddPoliciesToOrgPayload'), + headers + }, + description: 'Add one or more policies to an organization', + notes: 'The PUT /authorization/organizations/{id}/policies endpoint adds one or more policies to an organization.', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.AddOrganizationPolicy, + getParams: (request) => ({ organizationId: request.params.id }) + } + }, + response: { schema: swagger.Organization } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/organizations/{id}/policies', + async handler (request, h) { + return h.response(await request.udaruCore.organizations.deletePolicies({ id: request.params.id })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteOrganizationPolicies, ['id']), + headers + }, + description: 'Clear all policies of the organization', + notes: 'The DELETE /authorization/organizations/{id}/policies endpoint removes all the organization policies.\n', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.RemoveOrganizationPolicy, + getParams: (request) => ({ organizationId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/organizations/{id}/policies/{policyId}', + async handler (request, h) { + const { id, policyId } = request.params + const { instance } = request.query + + return h.response(await request.udaruCore.organizations.deletePolicy({ id, policyId, instance })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteOrganizationPolicy, ['id', 'policyId']), + headers + }, + description: 'Remove a policy associated with an organization', + notes: 'The DELETE /authorization/organizations/{id}/policies/{policyId} endpoint disassociates a specific policy from an organization.\n' + + 'Set optional parameter instance to delete a specific policy instance with variables, or leave blank to remove all instances with this policyId.\n', + tags: ['api', 'organizations'], + plugins: { + auth: { + action: Action.RemoveOrganizationPolicy, + getParams: (request) => ({ + organizationId: request.params.id, + policyId: request.params.policyId + }) + } + } + } + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/routes/policies.js b/packages/hapi-auth-udaru/lib/routes/policies.js new file mode 100644 index 00000000..0b797f6f --- /dev/null +++ b/packages/hapi-auth-udaru/lib/routes/policies.js @@ -0,0 +1,308 @@ +'use strict' + +const Joi = require('joi') +const Boom = require('boom') +const pick = require('lodash/pick') +const validation = require('@nearform/udaru-core/lib/ops/validation').policies +const swagger = require('../swagger') +const headers = require('../headers') + +module.exports = { + name: 'policies', + version: '0.0.1', + register (server, options) { + const hasValidServiceKey = server.udaruAuthorization.hasValidServiceKey + const Action = server.udaruConfig.get('AuthConfig.Action') + + server.route({ + method: 'GET', + path: '/authorization/policies', + async handler (request) { + const { organizationId } = request.udaru + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + return { page, limit, ...(await request.udaruCore.policies.list({ organizationId, limit, page })) } + }, + config: { + description: 'Fetch all the defined policies', + notes: 'The GET /authorization/policies endpoint returns a list of all the defined policies\nthe policies will contain only the ID, version and name. No statements.\n\nThe results are paginated. Page numbering and page limit start from 1.\n', + tags: ['api', 'policies'], + plugins: { + auth: { + action: Action.ListPolicies + } + }, + validate: { + headers, + query: pick(validation.listByOrganization, ['page', 'limit']) + }, + response: { schema: swagger.PagedPolicies } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/policies/{id}', + async handler (request) { + const { organizationId } = request.udaru + const { id } = request.params + + return request.udaruCore.policies.read({ id, organizationId }) + }, + config: { + validate: { + params: pick(validation.readPolicy, ['id']), + headers + }, + description: 'Fetch a single policy by ID', + notes: 'The GET /authorization/policies/{id} endpoint returns a policy based on its ID.\n', + tags: ['api', 'policies'], + plugins: { + auth: { + action: Action.ReadPolicy, + getParams: (request) => ({ policyId: request.params.id }) + } + }, + response: { schema: swagger.Policy } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/policies', + async handler (request, h) { + if (!hasValidServiceKey(request)) throw Boom.forbidden() + + const { id, version, name, statements } = request.payload + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.policies.create({ id, version, name, organizationId, statements })).code(201) + }, + config: { + validate: { + payload: Joi.object(pick(validation.createPolicy, ['id', 'name', 'version', 'statements'])).label('CreatePolicyPayload'), + query: { + sig: Joi.string().required() + }, + headers + }, + description: 'Create a policy for the current user organization', + notes: 'The POST /authorization/policies endpoint is a private endpoint. It can be accessed only using a service key.\nThis service key needs to be passed as a query string in the form "sig="\n', + tags: ['api', 'policies', 'private'], + plugins: { + auth: { + action: Action.CreatePolicy + } + }, + response: { schema: swagger.Policy } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/policies/{id}', + async handler (request) { + if (!hasValidServiceKey(request)) throw Boom.forbidden() + + const { id } = request.params + const { organizationId } = request.udaru + const { version, name, statements } = request.payload + + return request.udaruCore.policies.update({ id, organizationId, version, name, statements }) + }, + config: { + validate: { + params: pick(validation.updatePolicy, ['id']), + payload: Joi.object(pick(validation.updatePolicy, ['version', 'name', 'statements'])).label('UpdatePolicyPayload'), + query: { + sig: Joi.string().required() + }, + headers + }, + description: 'Update a policy of the current user organization', + notes: 'The PUT /authorization/policies/{id} endpoint is a private endpoint. It can be accessed only using a service key.\nThis service key needs to be passed as a query string in the form "sig="\n', + tags: ['api', 'policies', 'private'], + plugins: { + auth: { + action: Action.UpdatePolicy, + getParams: (request) => ({ policyId: request.params.id }) + } + }, + response: { schema: swagger.Policy } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/policies/{id}', + async handler (request, h) { + if (!hasValidServiceKey(request)) throw Boom.forbidden() + + const { id } = request.params + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.policies.delete({ id, organizationId })).code(204) + }, + config: { + validate: { + params: pick(validation.deletePolicy, ['id']), + query: { + sig: Joi.string().required() + }, + headers + }, + description: 'Delete a policy', + notes: 'The DELETE /authorization/policies/{id} endpoint is a private endpoint. It can be accessed only using a service key.\n\nThis service key needs to be passed as a query string in the form "sig="\n', + tags: ['api', 'policies', 'private'], + plugins: { + auth: { + action: Action.DeletePolicy, + getParams: (request) => ({ policyId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/shared-policies', + async handler (request) { + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + return { page, limit, ...(await request.udaruCore.policies.listShared({ limit, page })) } + }, + config: { + description: 'Fetch all the defined shared policies', + notes: 'The GET /authorization/shared-policies endpoint returns a list of all the defined policies\nthe policies will contain only the ID, version and name. No statements.\n\nThe results are paginated. Page numbering and page limit start from 1.\n', + tags: ['api', 'policies'], + plugins: { + auth: { + action: Action.ListPolicies + } + }, + validate: { + headers, + query: pick(validation.listSharedPolicies, ['page', 'limit']) + }, + response: { schema: swagger.PagedPolicies } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/shared-policies/{id}', + async handler (request) { + return request.udaruCore.policies.readShared({ id: request.params.id }) + }, + config: { + validate: { + params: pick(validation.readSharedPolicy, ['id']), + headers + }, + description: 'Fetch a single shared policy', + notes: 'The GET /authorization/shared-policies/{id} endpoint returns a policy based on its ID.\n', + tags: ['api', 'policies'], + plugins: { + auth: { + action: Action.ReadPolicy, + getParams: (request) => ({ policyId: request.params.id }) + } + }, + response: { schema: swagger.Policy } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/shared-policies', + async handler (request, h) { + if (!hasValidServiceKey(request)) throw Boom.forbidden() + + const params = pick(request.payload, ['id', 'version', 'name', 'statements']) + + return h.response(await request.udaruCore.policies.createShared(params)).code(201) + }, + config: { + validate: { + payload: Joi.object(pick(validation.createSharedPolicy, ['id', 'name', 'version', 'statements'])).label('CreateSharedPoliciesPayload'), + query: { + sig: Joi.string().required() + }, + headers + }, + description: 'Create a policy shared across organizations', + notes: 'The POST /authorization/shared-policies endpoint is a private endpoint. It can be accessed only using a service key.\nThis service key needs to be passed as a query string in the form "sig="\n', + tags: ['api', 'policies', 'private'], + plugins: { + auth: { + action: Action.CreatePolicy + } + }, + response: { schema: swagger.Policy } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/shared-policies/{id}', + async handler (request) { + if (!hasValidServiceKey(request)) throw Boom.forbidden() + + const { id } = request.params + const { version, name, statements } = request.payload + + return request.udaruCore.policies.updateShared({ id, version, name, statements }) + }, + config: { + validate: { + params: pick(validation.updateSharedPolicy, ['id']), + payload: Joi.object(pick(validation.updateSharedPolicy, ['version', 'name', 'statements'])).label('UpdateSharedPolicyPayload'), + query: { + sig: Joi.string().required() + }, + headers + }, + description: 'Update a shared policy', + notes: 'The PUT /authorization/shared-policies/{id} endpoint is a private endpoint. It can be accessed only using a service key.\nThis service key needs to be passed as a query string in the form "sig="\n', + tags: ['api', 'policies', 'private'], + plugins: { + auth: { + action: Action.UpdatePolicy, + getParams: (request) => ({ policyId: request.params.id }) + } + }, + response: { schema: swagger.Policy } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/shared-policies/{id}', + async handler (request, h) { + if (!hasValidServiceKey(request)) throw Boom.forbidden() + + return h.response(await request.udaruCore.policies.deleteShared({ id: request.params.id })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteSharedPolicy, ['id']), + query: { + sig: Joi.string().required() + }, + headers + }, + description: 'Delete a shared policy', + notes: 'The DELETE /authorization/shared-policies/{id} endpoint is a private endpoint. It can be accessed only using a service key.\n\nThis service key needs to be passed as a query string in the form "sig="\n', + tags: ['api', 'policies', 'private'], + plugins: { + auth: { + action: Action.DeletePolicy, + getParams: (request) => ({ policyId: request.params.id }) + } + } + } + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/routes/teams.js b/packages/hapi-auth-udaru/lib/routes/teams.js new file mode 100644 index 00000000..bfbaf935 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/routes/teams.js @@ -0,0 +1,546 @@ +'use strict' + +const Joi = require('joi') +const pick = require('lodash/pick') +const validation = require('@nearform/udaru-core/lib/ops/validation').teams +const swagger = require('../swagger') +const headers = require('../headers') + +module.exports = { + name: 'teams', + version: '0.0.1', + register (server, options) { + const Action = server.udaruConfig.get('AuthConfig.Action') + + server.route({ + method: 'GET', + path: '/authorization/teams/search', + async handler (request) { + const { organizationId } = request.udaru + const query = request.query.query + + return request.udaruCore.teams.search({ organizationId, query }) + }, + config: { + description: 'Search for teams from the current user organization', + notes: 'The GET /authorization/teams/search endpoint returns a filtered list of teams from the current organization.\n\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.SearchTeams + } + }, + validate: { + headers, + query: pick(validation.searchTeam, ['query']) + }, + response: { schema: swagger.Search(swagger.ShortTeam).label('FilteredTeams') } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/teams', + async handler (request) { + const { organizationId } = request.udaru + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + return { page, limit, ...(await request.udaruCore.teams.list({ organizationId, limit, page })) } + }, + config: { + description: 'Fetch all teams from the current user organization', + notes: 'The GET /authorization/teams endpoint returns a list of all teams from the current organization.\n\nThe results are paginated. Page numbering and page limit start from 1.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ListTeams + } + }, + validate: { + headers, + query: pick(validation.listOrgTeams, ['page', 'limit']) + }, + response: { schema: swagger.PagedTeams } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/teams/{id}', + async handler (request) { + const { organizationId } = request.udaru + const { id } = request.params + + return request.udaruCore.teams.read({ id, organizationId }) + }, + config: { + validate: { + params: pick(validation.readTeam, ['id']), + headers + }, + description: 'Fetch a team given its identifier', + notes: 'The GET /authorization/teams/{id} endpoint returns a single team data.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ReadTeam, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/teams', + async handler (request, h) { + const { id, name, description, metadata, user } = request.payload + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.teams.create({ id, name, description, metadata, parentId: null, organizationId, user })).code(201) + }, + config: { + validate: { + payload: Joi.object(pick(validation.createTeam, ['id', 'name', 'description', 'metadata', 'user'])).label('CreateTeamPayload'), + headers + }, + description: 'Create a team', + notes: 'The POST /authorization/teams endpoint creates a new team from its payload data.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.CreateTeam + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/teams/{id}', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { name, description, metadata } = request.payload + + return request.udaruCore.teams.update({ id, name, description, metadata, organizationId }) + }, + config: { + validate: { + params: pick(validation.updateTeam, ['id']), + payload: Joi.object(pick(validation.updateTeam, ['name', 'description', 'metadata'])).or('name', 'description', 'metadata').label('UpdateTeamPayload'), + headers + }, + description: 'Update a team', + notes: 'The PUT /authorization/teams/{id} endpoint updates a team data.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.UpdateTeam, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/teams/{id}', + async handler (request, h) { + const { id } = request.params + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.teams.delete({ id, organizationId })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteTeam, ['id']), + headers + }, + description: 'Delete a team', + notes: 'The DELETE /authorization/teams endpoint deletes a team.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.DeleteTeam, + getParams: (request) => ({ teamId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/teams/{id}/nested', + async handler (request) { + const { organizationId } = request.udaru + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + const { id } = request.params + + await request.udaruCore.teams.read({ id, organizationId }) + return { page, limit, ...(await request.udaruCore.teams.listNestedTeams({ organizationId, id, limit, page })) } + }, + config: { + validate: { + query: pick(validation.listNestedTeams, ['page', 'limit']), + params: pick(validation.listNestedTeams, ['id']), + headers + }, + description: 'Fetch a nested team given its identifier', + notes: 'The GET /authorization/teams/{id}/nested endpoint returns a list of team data.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ListNestedTeams, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.NestedPagedTeams } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/teams/{id}/nest', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { parentId } = request.payload + + return request.udaruCore.teams.move({ id, parentId, organizationId }) + }, + config: { + validate: { + params: pick(validation.moveTeam, ['id']), + payload: Joi.object({ parentId: validation.moveTeam.parentId.required() }).label('NestTeamPayload'), + headers + }, + description: 'Nest a team', + notes: 'The PUT /authorization/teams/{id}/nest endpoint nests a team.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ManageTeams, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/teams/{id}/unnest', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + + return request.udaruCore.teams.move({ id, parentId: null, organizationId }) + }, + config: { + validate: { + params: pick(validation.moveTeam, ['id']), + headers + }, + description: 'Unnest a team', + notes: 'The PUT /authorization/teams/{id}/unnest endpoint unnests a team.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ManageTeams, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/teams/{id}/policies', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { policies } = request.payload + + return request.udaruCore.teams.replacePolicies({ id, organizationId, policies }) + }, + config: { + validate: { + params: pick(validation.replaceTeamPolicies, ['id']), + payload: Joi.object(pick(validation.replaceTeamPolicies, ['policies'])).label('ReplaceTeamPoliciesPayload'), + headers + }, + description: 'Clear and replace policies for a team', + notes: 'The POST /authorization/teams/{id}/policies endpoint replaces all the team policies. Existing policies are removed.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ReplaceTeamPolicy, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/teams/{id}/policies', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { policies } = request.payload + + return request.udaruCore.teams.addPolicies({ id, organizationId, policies }) + }, + config: { + validate: { + params: pick(validation.addTeamPolicies, ['id']), + payload: Joi.object(pick(validation.addTeamPolicies, ['policies'])).label('AddPoliciesToTeamPayload'), + headers + }, + description: 'Add one or more policies to a team', + notes: 'The PUT /authorization/teams/{id}/policies endpoint adds one or more new policies to a team.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.AddTeamPolicy, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/teams/{id}/policies', + async handler (request, h) { + const { id } = request.params + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.teams.deletePolicies({ id, organizationId })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteTeamPolicies, ['id']), + headers + }, + description: 'Clear all team policies', + notes: 'The DELETE /authorization/teams/{id}/policies endpoint removes all the team policies.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.RemoveTeamPolicy, + getParams: (request) => ({ teamId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/teams/{teamId}/policies/{policyId}', + async handler (request, h) { + const { teamId, policyId } = request.params + const { organizationId } = request.udaru + const { instance } = request.query + + return h.response(await request.udaruCore.teams.deletePolicy({ teamId, policyId, organizationId, instance })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteTeamPolicy, ['teamId', 'policyId']), + headers + }, + description: 'Remove a policy associated with a team', + notes: 'The DELETE /authorization/teams/{teamId}/policies/{policyId} endpoint disassociates a policy from a team.\n' + + 'Set optional parameter instance to delete a specific policy instance with variables, or leave blank to remove all instances with this policyId.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.RemoveTeamPolicy, + getParams: (request) => ({ teamId: request.params.teamId }) + } + } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/teams/{id}/users', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + await request.udaruCore.teams.read({ id, organizationId }) + return request.udaruCore.teams.listUsers({ id, page, limit, organizationId }) + }, + config: { + validate: { + params: pick(validation.readTeamUsers, ['id']), + query: pick(validation.readTeamUsers, ['page', 'limit']), + headers + }, + description: 'Fetch team users given its identifier', + notes: 'The GET /authorization/teams/{id}/users endpoint returns the team users and pagination metadata.\n\nThe results are paginated. Page numbering and page limit start from 1.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ReadTeam, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.PagedUsers } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/teams/{id}/users', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { users } = request.payload + + return request.udaruCore.teams.replaceUsers({ id, users, organizationId }) + }, + config: { + validate: { + params: pick(validation.replaceUsersInTeam, ['id']), + payload: Joi.object(pick(validation.replaceUsersInTeam, ['users'])).label('ReplaceTeamUsersPayload'), + headers + }, + description: 'Replace team users with the given ones', + notes: 'The POST /authorization/teams/{id}/users endpoint replaces all team users. Existing team users are removed.', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.ReplaceTeamMember, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/teams/{id}/users', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { users } = request.payload + + return request.udaruCore.teams.addUsers({ id, users, organizationId }) + }, + config: { + validate: { + params: pick(validation.addUsersToTeam, ['id']), + payload: Joi.object(pick(validation.addUsersToTeam, ['users'])).label('AddTeamUsersPayload'), + headers + }, + description: 'Add team users', + notes: 'The PUT /authorization/teams/{id}/users endpoint adds one or more team users.', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.AddTeamMember, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.Team } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/teams/{id}/users', + async handler (request, h) { + const { id } = request.params + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.teams.deleteMembers({ id, organizationId })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteTeamMembers, ['id']), + headers + }, + description: 'Delete all team users', + notes: 'The DELETE /authorization/teams/{id}/users endpoint removes all team users.', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.RemoveTeamMember, + getParams: (request) => ({ teamId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/teams/{id}/users/{userId}', + async handler (request, h) { + const { id, userId } = request.params + const { organizationId } = request.udaru + + return h.response(await request.udaruCore.teams.deleteMember({ id, userId, organizationId })).code(204) + }, + config: { + validate: { + params: pick(validation.deleteTeamMember, ['id', 'userId']), + headers + }, + description: 'Delete one team member', + notes: 'The DELETE /authorization/teams/{id}/users/{userId} endpoint removes one user from a team.', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.RemoveTeamMember, + getParams: (request) => ({ teamId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/teams/{id}/users/search', + async handler (request) { + const { id } = request.params + const query = request.query.query + const { organizationId } = request.udaru + + await request.udaruCore.teams.read({ id, organizationId }) + return request.udaruCore.teams.searchUsers({ id, query, organizationId }) + }, + config: { + validate: { + params: pick(validation.searchTeamUsers, ['id']), + query: pick(validation.searchTeamUsers, ['query']), + headers + }, + description: 'Search for users in a team from the current user organization', + notes: 'The GET /authorization/teams/{id}/users/search endpoint returns the team users matching the query.\n', + tags: ['api', 'teams'], + plugins: { + auth: { + action: Action.SearchTeamsUsers, + getParams: (request) => ({ teamId: request.params.id }) + } + }, + response: { schema: swagger.SearchUser } + } + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/routes/users.js b/packages/hapi-auth-udaru/lib/routes/users.js new file mode 100644 index 00000000..9561f8ba --- /dev/null +++ b/packages/hapi-auth-udaru/lib/routes/users.js @@ -0,0 +1,382 @@ +'use strict' + +const Joi = require('joi') +const pick = require('lodash/pick') +const validation = require('@nearform/udaru-core/lib/ops/validation').users +const swagger = require('../swagger') +const headers = require('../headers') + +module.exports = { + name: 'users', + version: '0.0.1', + register (server, options) { + const Action = server.udaruConfig.get('AuthConfig.Action') + + server.route({ + method: 'GET', + path: '/authorization/users/search', + async handler (request) { + const { organizationId } = request.udaru + const query = request.query.query + + return request.udaruCore.users.search({ organizationId, query }) + }, + config: { + description: 'Search for users from the current organization', + notes: 'The get /authorization/users/search endpoint returns a filtered list of users from the current organization.\n\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.SearchUsers + } + }, + validate: { + headers, + query: pick(validation.searchUser, ['query']) + }, + response: { schema: swagger.SearchUser } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/users', + async handler (request, h) { + const { organizationId } = request.udaru + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + return { page, limit, ...(await request.udaruCore.users.list({ organizationId, limit, page })) } + }, + config: { + description: 'Fetch all users from the current user organization', + notes: 'The GET /authorization/users endpoint returns a list of all users from the current organization.\n\nThe results are paginated. Page numbering and page limit start from 1.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.ListUsers + } + }, + validate: { + headers, + query: pick(validation.listOrgUsers, ['page', 'limit']) + }, + response: { schema: swagger.PagedUsers } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/users/{id}', + async handler (request) { + const { organizationId } = request.udaru + const { id } = request.params + + return request.udaruCore.users.read({ id, organizationId }) + }, + config: { + validate: { + params: pick(validation.readUser, ['id']), + headers + }, + description: 'Fetch a user given its identifier', + notes: 'The GET /authorization/users/{id} endpoint returns a single user data.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.ReadUser, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.User } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/users', + async handler (request, h) { + const { organizationId } = request.udaru + const { id, name, metadata } = request.payload + + return h.response(await request.udaruCore.users.create({ id, name, organizationId, metadata })).code(201) + }, + config: { + validate: { + payload: Joi.object(pick(validation.createUser, ['id', 'name', 'metadata'])).label('CreateUserPayload'), + headers + }, + description: 'Create a new user', + notes: 'The POST /authorization/users endpoint creates a new user given its data.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.CreateUser + } + }, + response: { schema: swagger.User } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/users/{id}', + async handler (request) { + const { organizationId } = request.udaru + const { id } = request.params + const { name, metadata } = request.payload + + return request.udaruCore.users.update({ id, organizationId, name, metadata }) + }, + config: { + validate: { + params: pick(validation.updateUser, ['id']), + payload: Joi.object(pick(validation.updateUser, ['name', 'metadata'])).label('UpdateUserPayload'), + headers + }, + description: 'Update a user', + notes: 'The PUT /authorization/users/{id} endpoint updates the user data.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.UpdateUser, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.User } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/users/{id}', + async handler (request, h) { + const { organizationId } = request.udaru + const { id } = request.params + + await request.udaruCore.users.delete({ id, organizationId }) + + return h.response().code(204) + }, + config: { + validate: { + params: pick(validation.deleteUser, ['id']), + headers + }, + description: 'Delete a user', + notes: 'The DELETE /authorization/users/{id} endpoint deletes a user.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.DeleteUser, + getParams: (request) => ({ userId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/users/{id}/policies', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { policies } = request.payload + + return request.udaruCore.users.replacePolicies({ id, organizationId, policies }) + }, + config: { + validate: { + params: pick(validation.replaceUserPolicies, ['id']), + payload: Joi.object(pick(validation.replaceUserPolicies, ['policies'])).label('ReplaceUserPoliciesPayload'), + headers + }, + description: 'Clear and replace policies for a user', + notes: 'The POST /authorization/users/{id}/policies endpoint replaces all the user policies. Existing user policies are removed.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.ReplaceUserPolicy, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.User } + } + }) + + server.route({ + method: 'PUT', + path: '/authorization/users/{id}/policies', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { policies } = request.payload + + return request.udaruCore.users.addPolicies({ id, organizationId, policies }) + }, + config: { + validate: { + params: pick(validation.addUserPolicies, ['id']), + payload: Joi.object(pick(validation.addUserPolicies, ['policies'])).label('AddUserPoliciesPayload'), + headers + }, + description: 'Add one or more policies to a user', + notes: 'The PUT /authorization/users/{id}/policies endpoint adds one or more policies to a user.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.AddUserPolicy, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.User } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/users/{id}/policies', + async handler (request, h) { + const { id } = request.params + const { organizationId } = request.udaru + + await request.udaruCore.users.deletePolicies({ id, organizationId }) + return h.response().code(204) + }, + config: { + validate: { + params: pick(validation.deleteUserPolicies, ['id']), + headers + }, + description: 'Clear all user\'s policies', + notes: 'The DELETE /authorization/users/{id}/policies endpoint removes all the user policies.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.RemoveUserPolicy, + getParams: (request) => ({ userId: request.params.id }) + } + } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/users/{userId}/policies/{policyId}', + async handler (request, h) { + const { userId, policyId } = request.params + const { organizationId } = request.udaru + const { instance } = request.query + + await request.udaruCore.users.deletePolicy({ userId, policyId, organizationId, instance }) + return h.response().code(204) + }, + config: { + validate: { + params: pick(validation.deleteUserPolicy, ['userId', 'policyId']), + headers + }, + description: 'Remove a policy associated with a user', + notes: 'The DELETE /authorization/users/{userId}/policies/{policyId} disassociates a policy from a user.\n' + + 'Set optional parameter instance to delete a specific policy instance with variables, or leave blank to remove all instances with this policyId.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.RemoveUserPolicy, + getParams: (request) => ({ + userId: request.params.userId, + policyId: request.params.policyId + }) + } + } + } + }) + + server.route({ + method: 'GET', + path: '/authorization/users/{id}/teams', + async handler (request, h) { + const { id } = request.params + const { organizationId } = request.udaru + const limit = request.query.limit || server.udaruConfig.get('authorization.defaultPageSize') + const page = request.query.page || 1 + + await request.udaruCore.users.read({ id, organizationId }) + return { page, limit, ...(await request.udaruCore.users.listUserTeams({ id, organizationId, limit, page })) } + }, + config: { + validate: { + headers, + params: pick(validation.listUserTeams, ['id']), + query: pick(validation.listUserTeams, ['page', 'limit']) + }, + description: 'Fetch all teams to which the user belongs to. Does not fetch parent teams.', + notes: 'The GET /authorization/users/{id}/teams endpoint returns a list of teams to which the user belongs to.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.ListUserTeams, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.PagedTeamRefs } + } + }) + + server.route({ + method: 'POST', + path: '/authorization/users/{id}/teams', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + const { teams } = request.payload + + return request.udaruCore.users.replaceTeams({ id, organizationId, teams }) + }, + config: { + validate: { + params: pick(validation.replaceUserTeams, ['id']), + payload: Joi.object(pick(validation.replaceUserTeams, ['teams'])).label('ReplaceUserTeamsPayload'), + headers + }, + description: 'Clear and replace user teams', + notes: 'The POST /authorization/users/{id}/teams endpoint replaces all the user teams. This can be use to move a user from a team to another (or a set of teams to another).\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.ReplaceUserTeams, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.User } + } + }) + + server.route({ + method: 'DELETE', + path: '/authorization/users/{id}/teams', + async handler (request) { + const { id } = request.params + const { organizationId } = request.udaru + + return request.udaruCore.users.deleteTeams({ id, organizationId }) + }, + config: { + validate: { + params: pick(validation.deleteUserFromTeams, ['id']), + headers + }, + description: 'Delete teams for a user', + notes: 'The DELETE /authorization/users/{id}/teams endpoint deletes user from all her teams.\n', + tags: ['api', 'users'], + plugins: { + auth: { + action: Action.DeleteUserTeams, + getParams: (request) => ({ userId: request.params.id }) + } + }, + response: { schema: swagger.User } + } + }) + } +} diff --git a/packages/hapi-auth-udaru/lib/standalone/config.js b/packages/hapi-auth-udaru/lib/standalone/config.js new file mode 100644 index 00000000..b86a2044 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/standalone/config.js @@ -0,0 +1,13 @@ +const config = require('../config') + +module.exports = (...amendments) => config({ + hapi: { + port: 8080, + host: 'localhost' + }, + logger: { + pino: { + level: 'warn' + } + } +}, ...amendments) diff --git a/packages/hapi-auth-udaru/lib/standalone/server.js b/packages/hapi-auth-udaru/lib/standalone/server.js new file mode 100644 index 00000000..79441c61 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/standalone/server.js @@ -0,0 +1,48 @@ +'use strict' + +const Hapi = require('hapi') +const config = require('./config')() + +module.exports = async function () { + const server = Hapi.Server({ + port: Number(config.get('hapi.port')), + host: config.get('hapi.host'), + routes: { + cors: { + additionalHeaders: ['org'] + }, + validate: { // This is to propagate validation keys in Hapi v17 - https://github.com/hapijs/hapi/issues/3706#issuecomment-349765943 + async failAction (request, h, err) { + throw err + } + } + } + }) + + await server.register( + [ + { + plugin: require('hapi-pino'), + options: config.get('logger.pino') + }, + { + plugin: require('inert') + }, + { + plugin: require('vision') + }, + { + plugin: require('hapi-swagger'), + options: require('./swagger') + }, + { + plugin: require('../..'), + options: {config} + } + ] + ) + + await server.start() + + return server +} diff --git a/packages/hapi-auth-udaru/lib/standalone/swagger.js b/packages/hapi-auth-udaru/lib/standalone/swagger.js new file mode 100644 index 00000000..8cf5ee8c --- /dev/null +++ b/packages/hapi-auth-udaru/lib/standalone/swagger.js @@ -0,0 +1,43 @@ +const packageJson = require('../../package.json') + +module.exports = { + jsonEditor: true, + reuseDefinitions: false, + grouping: 'tags', + info: { + title: 'Udaru API Documentation', + description: 'This page documents Udaru\'s API endpoints, along with their various inputs and outputs. For more information about Udaru please see the Documentation Site.', + version: packageJson.version + }, + sortEndpoints: 'path', + tags: [ + { + name: 'policies', + description: 'Manage policy objects' + }, + { + name: 'organizations', + description: 'Manage organizations' + }, + { + name: 'teams', + description: 'Manage teams within an organization' + }, + { + name: 'users', + description: 'Manage users within an organization' + }, + { + name: 'authorization', + description: 'Manage the actions a user can perform against a resource' + }, + { + name: 'private', + description: 'Endpoints that require a service key' + }, + { + name: 'monitoring', + description: 'Endpoints for monitoring and uptime' + } + ] +} diff --git a/packages/hapi-auth-udaru/lib/swagger.js b/packages/hapi-auth-udaru/lib/swagger.js new file mode 100644 index 00000000..0d07c108 --- /dev/null +++ b/packages/hapi-auth-udaru/lib/swagger.js @@ -0,0 +1,170 @@ +'use strict' + +const Joi = require('joi') + +const MetaData = Joi.object().optional().description('Metadata').label('MetaData') + +const PolicyStatements = Joi.object({ + Statement: Joi.array().items(Joi.object({ + Effect: Joi.string().valid('Allow', 'Deny').description('Statement result').label('Effect'), + Action: Joi.array().items(Joi.string()).description('Action to perform on resource').label('Actions'), + Resource: Joi.array().items(Joi.string()).description('Resource that the statement covers').label('Resources'), + Sid: Joi.string().description('Statement ID').label('Sid'), + Condition: Joi.object().description('Condition operator used when evaluating effect') + }).label('Statement')).label('Statements') +}).label('PolicyStatements') + +const Policy = Joi.object({ + id: Joi.string().description('Policy ID'), + version: Joi.string().description('Policy version'), + name: Joi.string().description('Policy name'), + statements: PolicyStatements +}).label('Policy') +const Policies = Joi.array().items(Policy).description('items').label('Policies') + +const PolicyRef = Joi.object({ + id: Joi.string().description('Policy ID'), + version: Joi.string().description('Policy version'), + name: Joi.string().description('Policy name'), + variables: Joi.object().description('List of fixed values for variables').label('Variables'), + instance: Joi.number().integer().description('Policy unique instance') +}).label('PolicyRef') +const PolicyRefs = Joi.array().items(PolicyRef).description('Policy Refs').label('PolicyRefs') + +const UserRef = Joi.object({ + id: Joi.string().description('User ID'), + name: Joi.string().description('User name') +}).label('UserRef') +const UserRefs = Joi.array().items(UserRef).description('User refs').label('UserRefs') + +const TeamRef = Joi.object({ + id: Joi.string().description('Team ID'), + name: Joi.string().description('Team name') +}).label('TeamRef') +const TeamRefs = Joi.array().items(TeamRef).description('Team refs').label('TeamRefs') + +const ShortTeam = Joi.object({ + id: Joi.string().description('Team ID'), + name: Joi.string().description('Team name'), + description: Joi.string().description('Team description'), + path: Joi.string(), + organizationId: Joi.string().description('Organization ID to which the team belongs to') +}).label('Short Team') + +const Team = Joi.object({ + id: Joi.string().description('Team ID'), + name: Joi.string().description('Team name'), + description: Joi.string().description('Team description'), + path: Joi.string(), + metadata: MetaData, + users: UserRefs, + policies: PolicyRefs, + organizationId: Joi.string().description('Organization ID to which the team belongs to'), + usersCount: Joi.number().description('Number of team users. Sub team users not counted.') +}).label('Team') + +const Teams = Joi.array().items(Team).description('items').label('Teams') + +const NestedTeam = Joi.object({ + id: Joi.string().description('Team ID'), + name: Joi.string().description('Team name'), + description: Joi.string().description('Team description'), + parentId: Joi.string().description('Parent Team ID'), + path: Joi.string(), + organizationId: Joi.string().description('Organization ID to which the team belongs to'), + usersCount: Joi.number().description('Number of team users. Sub team users not counted.') +}).label('Nested Team') + +const NestedTeams = Joi.array().items(NestedTeam).description('items').label('Nested Teams') + +const User = Joi.object({ + id: Joi.string().description('User ID'), + name: Joi.string().description('User name'), + organizationId: Joi.string().description('Organization ID to which the user belongs to'), + metadata: MetaData, + teams: TeamRefs, + policies: PolicyRefs +}).label('User') +const Users = Joi.array().items(User).description('items').label('Users') + +const Organization = Joi.object({ + id: Joi.string().description('Organization ID'), + name: Joi.string().description('Organization name'), + description: Joi.string().description('Organization description'), + metadata: MetaData, + policies: PolicyRefs +}).label('Organization') +const Organizations = Joi.array().items(Organization).description('items').label('Organizations') + +const OrganizationAndUser = Joi.object({ + organization: Organization, + user: UserRef +}).label('OrganizationAndUser') + +const List = (data) => { + return Joi.object({ + page: Joi.number().integer().min(1).description('Page number, starts from 1'), + limit: Joi.number().integer().min(1).description('Items per page'), + total: Joi.number().integer().description('Total number of entries matched by the query'), + data: data + }).label('PagedList') +} + +const PagedPolicies = List(Policies).label('PagedPolicies') +const PagedTeams = List(Teams).label('PagedTeams').description('Note: teams users and policies are not populated in paged teams list') +const NestedPagedTeams = List(NestedTeams).label('NestedPagedTeams').description('Note: teams users and policies are not populated in nested paged teams list') +const PagedTeamRefs = List(TeamRefs).label('PagedTeamRefs') +const PagedUsers = List(Users).label('PagedUsers') +const PagedOrganizations = List(Organizations).label('PagedOrganizations') + +const Search = (data) => { + return Joi.object({ + total: Joi.number().integer().description('Total number of entries matched by the query'), + data: Joi.array().items(data).label('Data') + }).label('SearchList') +} + +const SearchUser = Joi.object({ + total: Joi.number().integer().description('Total number of entries matched by the query'), + data: Users +}).label('SearchUserList') + +const Access = Joi.object({ + access: Joi.boolean() +}).label('Access') + +const UserActionsArray = Joi.array().items(Joi.string()).label('UserActionsArray') + +const UserActions = Joi.object({ + actions: UserActionsArray +}).label('UserActions') + +const UserActionsOnResources = Joi.array().items(Joi.object({ + resource: Joi.string().label('UserResource'), + actions: UserActionsArray +}).label('UserActionOnResource')).label('UserActionsOnResources') + +module.exports = { + List, + User, + UserActions, + UserActionsOnResources, + Team, + NestedTeam, + TeamRef, + Policy, + PagedPolicies, + PagedTeams, + NestedPagedTeams, + PagedTeamRefs, + PagedUsers, + PagedOrganizations, + PolicyRef, + Organization, + OrganizationAndUser, + PolicyStatements, + Access, + Search, + SearchUser, + ShortTeam +} diff --git a/packages/hapi-auth-udaru/package.json b/packages/hapi-auth-udaru/package.json new file mode 100644 index 00000000..94e52dda --- /dev/null +++ b/packages/hapi-auth-udaru/package.json @@ -0,0 +1,84 @@ +{ + "name": "@nearform/hapi-auth-udaru", + "version": "5.0.0", + "bin": { + "udaru": "./start.js" + }, + "main": "index.js", + "description": "Hapi authentication plugin that allows using udaru for policy based authorization", + "author": "nearForm Ltd", + "license": "MIT", + "contributors": [ + "Andrew Cashmore (https://github.com/andrewcashmore)", + "Damian Beresford (https://github.com/dberesford)", + "Dean McDonnell (https://github.com/mcdonnelldean)", + "Filippo De Santis (https://github.com/p16)", + "Florian Traverse (https://github.com/temsa)", + "Mihai Dima (https://github.com/mihaidma)", + "Paolo Chiodi (https://github.com/paolochiodi)", + "Paolo Insogna (https://github.com/ShogunPanda)", + "Paul Negrutiu (https://github.com/floridemai)", + "Mark Ireland (https://github.com/irelandm)", + "Michael O'Brien (https://github.com/mobri3n)", + "Michele Capra (https://github.com/piccoloaiutante)", + "Nicolas Herment (https://github.com/nherment)", + "Salman Mitha (https://github.com/salmanm)", + "William Riley-Land (https://github.com/wprl)" + ], + "homepage": "https://github.com/nearform/udaru#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/nearform/udaru.git" + }, + "bugs": { + "url": "https://github.com/nearform/udaru/issues" + }, + "engines": { + "node": ">=8.9.0" + }, + "scripts": { + "bench": "node ./bench/util/runner.js", + "bench:volume": "node ./bench/util/volumeRunner.js", + "bench:load-volume": "npm run pg:init-volume-db && node ./bench/util/volumeRunner.js", + "coverage": "UDARU_SERVICE_logger_pino_level=silent lab -c -t 96 -r html -o coverage/coverage.html", + "coveralls": "UDARU_SERVICE_logger_pino_level=silent lab -c -t 96 -r lcov | COVERALLS_REPO_TOKEN='?' coveralls", + "depcheck": "npx depcheck", + "pg:init": "UDARU_SERVICE_local=true npx udaru-init && npm run pg:migrate", + "pg:init-test-db": "npm run pg:init && npm run pg:load-test-data", + "pg:init-volume-db": "npm run pg:init-test-db && ./bench/util/loadVolumeData.js", + "pg:load-test-data": "UDARU_SERVICE_local=true npx udaru-loadTestData", + "pg:migrate": "npx udaru-migrate --version=max", + "start": "node ./start.js", + "test": "npm run pg:init-test-db && UDARU_SERVICE_logger_pino_level=silent lab -c -t 96", + "test:security": "node ./security/runner.js", + "pretest:security": "napa sqlmapproject/sqlmap" + }, + "dependencies": { + "@nearform/udaru-core": "^5.0.0", + "boom": "^7.2.0", + "hapi": "^17.2.3", + "hapi-pino": "^4.0.4", + "hapi-swagger": "^9.1.1", + "hoek": "^5.0.3", + "inert": "^5.1.0", + "joi": "^13.1.2", + "lodash": "^4.17.5", + "vision": "^5.3.2" + }, + "devDependencies": { + "autocannon": "^2.1.1", + "bloomrun": "^4.1.0", + "chalk": "2.3.2", + "code": "^5.2.0", + "coveralls": "^3.0.0", + "depcheck": "^0.6.9", + "jsonfile": "^4.0.0", + "lab": "^15.4.1", + "minimist": "1.2.0", + "napa": "^3.0.0", + "npx": "^10.0.1", + "pg": "^7.4.1", + "sinon": "^4.4.9", + "uuid": "^3.2.1" + } +} diff --git a/packages/hapi-auth-udaru/security/fixtures/injection-endpoints.json b/packages/hapi-auth-udaru/security/fixtures/injection-endpoints.json new file mode 100644 index 00000000..e2a5ff8c --- /dev/null +++ b/packages/hapi-auth-udaru/security/fixtures/injection-endpoints.json @@ -0,0 +1,50 @@ +{ + "dbms": "postgresql", + "level": 1, + "risk": 1, + "verbose": 0, + "timeout": 5, + "urls": [ + { + "url": "http://localhost:8080/authorization/organizations?page=1&limit=10", + "method": "GET", + "headers": "authorization: ROOTid\\norg: WONKA", + "params": "page,limit,org,authorization" + }, + { + "url": "http://localhost:8080/authorization/policies?page=1&limit=10", + "method": "GET", + "headers": "authorization: ROOTid\\norg: WONKA", + "params": "page,limit,org,authorization" + }, + { + "url": "http://localhost:8080/authorization/teams?page=1&limit=10", + "method": "GET", + "headers": "authorization: ROOTid\\norg: WONKA", + "params": "page,limit,org,authorization" + }, + { + "url": "http://localhost:8080/authorization/users?page=1&limit=10", + "method": "GET", + "headers": "authorization: ROOTid\\norg: WONKA", + "params": "page,limit,org,authorization" + }, + { + "url": "http://localhost:8080/authorization/users/CharlieId*/policies", + "method": "PUT", + "headers": "authorization: CharlieId\norg: WONKA", + "data": "{\"policies\":[\"policyId9\"]}" + }, + { + "url": "http://localhost:8080/authorization/users/CharlieId/policies", + "method": "PUT", + "headers": "authorization: CharlieId\norg: WONKA", + "data": "{\"policies\":[\"policyId9*\"]}" + }, + { + "url": "http://localhost:8080/authorization/access/ManyPoliciesId*/a*/a*", + "method": "GET", + "headers": "authorization: ROOTid\norg: WONKA" + } + ] +} diff --git a/packages/hapi-auth-udaru/security/runner.js b/packages/hapi-auth-udaru/security/runner.js new file mode 100644 index 00000000..2ede3e27 --- /dev/null +++ b/packages/hapi-auth-udaru/security/runner.js @@ -0,0 +1,138 @@ +'use strict' + +const jsonfile = require('jsonfile') +const spawn = require('child_process').spawn +const exec = require('child_process').exec +const path = require('path') +const source = path.join(__dirname, 'fixtures/injection-endpoints.json') +const chalk = require('chalk') +const hapi = spawn('node', [path.join(__dirname, '../start.js')]) + +const endpoints = jsonfile.readFileSync(source, { throws: false }) + +if (!endpoints) { + console.error('Invalid JSON file.') + process.exit(1) +} + +function findPython2 (pythonCommand, done) { + return new Promise((resolve, reject) => { + exec(`${pythonCommand} --version`, function (_err, stdout, stderr) { + if (stderr.indexOf('Python 2.') >= 0) { + console.log(chalk.green(`'${pythonCommand}' is a valid Python2 ✔️`)) + return resolve(pythonCommand) + } + + resolve(false) + }) + }) +} + +function executeMap (command, config, urlDescription) { + console.log('Python command that will be used:', command) + + return new Promise((resolve, reject) => { + const params = [ + `./node_modules/sqlmap/sqlmap.py`, + `--url=${urlDescription.url}`, + `--method=${urlDescription.method}`, + `--headers=${urlDescription.headers}`, + `--level=${config.level}`, + `--risk=${config.risk}`, + `--dbms=${config.dbms}`, + `--timeout=${config.timeout}`, + `-v`, `${config.verbose}`, + `--flush-session`, + `--batch` + ] + if (urlDescription.params) { + params.push(`-p`) + params.push(`${urlDescription.params}`) + } + if (urlDescription.data) { + params.push(`--data=${urlDescription.data}`) + } + + console.log(chalk.green('executing sqlmap with: ', (['' + command].concat(params)).join(' '))) + + const sql = spawn(command, params) + let vulnerabilities = false + + sql.stdout.on('data', (data) => { + if (data.length > 1) { + console.log(`sqlmap: ${data}`) + } + if (data.indexOf('identified the following injection') >= 0) { + vulnerabilities = true + } + }) + + sql.stderr.on('data', (data) => { + reject(data) + }) + + sql.on('error', (error) => { + console.error(chalk.red(error)) + reject(new Error('failed to start child process')) + }) + + sql.on('close', (code) => { + console.log(chalk.green(`child process exited with code ${code}\n`)) + resolve(vulnerabilities) + }) + }) +} + +async function runner () { + try { + const pythons = await Promise.all([ + findPython2('python2'), + findPython2('python') + ]) + + const python = pythons.find(f => f) + + hapi.stdout.once('data', async (data) => { + console.log(chalk.green(`hapi: ${data}`)) + let vulnerabilities + let endpointError + + for (const urlDescription of endpoints.urls) { + try { + const v = await executeMap(python, endpoints, urlDescription) + + if (v) { + vulnerabilities = v + break + } + } catch (err) { + endpointError = err + break + } + } + + if (endpointError) { + console.error(chalk.red(endpointError)) + } else if (vulnerabilities) { + console.error(chalk.red('[CRITICAL] FOUND injection vulnerabilities\n\n')) + } else { + console.log(chalk.green('no injection vulnerabilities found\n\n`')) + } + + hapi.kill() + return process.exit(endpointError || vulnerabilities ? 1 : 0) + }) + + hapi.stderr.on('data', (data) => { + console.error(chalk.red(`stderr: ${data}`)) + }) + + hapi.on('close', (code) => { + console.log(chalk.green(`child process exited with code ${code}`)) + }) + } catch (err) { + console.error(chalk.red(err)) + } +} + +runner().catch(console.error) diff --git a/packages/hapi-auth-udaru/start.js b/packages/hapi-auth-udaru/start.js new file mode 100644 index 00000000..cdc674a1 --- /dev/null +++ b/packages/hapi-auth-udaru/start.js @@ -0,0 +1,21 @@ +'use strict' + +const start = require('./lib/standalone/server') + +// if forked as child, send output message via ipc to parent +// otherwise output to console +function logMessage (message) { + if (!process.send) { + console.log(message) + } else { + process.send(message) + } +} + +start() + .then(server => { + logMessage('Server started on: ' + server.info.uri.toLowerCase()) + }) + .catch(err => { + logMessage(`Failed to start server: ${err.message}`) + }) diff --git a/packages/hapi-auth-udaru/test/authorization/authorization.test.js b/packages/hapi-auth-udaru/test/authorization/authorization.test.js new file mode 100644 index 00000000..cfa2ed90 --- /dev/null +++ b/packages/hapi-auth-udaru/test/authorization/authorization.test.js @@ -0,0 +1,155 @@ +const Lab = require('lab') +const lab = exports.lab = Lab.script() + +const server = require('../test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const { BuildFor, udaru } = require('../testBuilder') + +const organizationId = 'WONKA' +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: JSON.stringify({ + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }), + organizationId + } +} + +lab.experiment('Routes Authorizations', () => { + lab.experiment('authorization', () => { + lab.experiment('GET /authorization/access/{userId}/{action}/{resource*}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/access/Modifyid/action_a/resource_a', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:dummy'], + Resource: ['authorization/access'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/list/{userId}/{resource*}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/list/ModifyId/not/my/resource', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:actions'], + Resource: ['authorization/actions'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:dummy'], + Resource: ['authorization/actions'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:actions'], + Resource: ['authorization/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/list/{userId}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/list/ModifyId?resources=not/my/resource', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:resources:actions'], + Resource: ['authorization/actions/resources'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:dummy'], + Resource: ['authorization/actions'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:actions:resources'], + Resource: ['authorization/dummy'] + }]) + .shouldRespond(403) + }) + }) +}) diff --git a/packages/hapi-auth-udaru/test/authorization/organizations.test.js b/packages/hapi-auth-udaru/test/authorization/organizations.test.js new file mode 100644 index 00000000..6e8f1579 --- /dev/null +++ b/packages/hapi-auth-udaru/test/authorization/organizations.test.js @@ -0,0 +1,666 @@ +const Lab = require('lab') +const lab = exports.lab = Lab.script() + +const server = require('../test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const { BuildFor, udaru } = require('../testBuilder') + +const organizationId = 'WONKA' +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: JSON.stringify({ + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }), + organizationId + } +} + +lab.experiment('Routes Authorizations', () => { + lab.experiment('organizations', () => { + lab.experiment('GET ', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/organizations', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:list'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: ['*'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/organizations/{id}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/organizations/WONKA', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy on specific organization') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:read'], + Resource: ['/authorization/organization/WONKA'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with correct policy on all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:read'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: ['/authorization/organization/WONKA'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:read'], + Resource: ['/authorization/organization/OTHER-ORG'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + lab.afterEach(async () => { + try { + await Promise.all([ + udaru.organizations.delete('OTHERORG'), + udaru.organizations.delete('OTHER-ORG') + ]) + } catch (e) { + // This is needed to ignore the error (i.e. in case the organization wasn't properly created) + } + }) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/organizations', + payload: { + id: 'OTHERORG', + name: 'other org', + description: 'other org' + }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:create'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(201) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: ['*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:create'], + Resource: ['/authorization/organization/OTHER-ORG'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + }, + organizations: { + testedOrg: { id: 'OTHERORG', name: 'other org', description: 'other org' } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/organizations/{{testedOrg.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy on specific organization') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:delete'], + Resource: ['/authorization/organization/OTHERORG'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with correct policy on all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:delete'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: ['/authorization/organization/OTHERORG'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:delete'], + Resource: ['/authorization/organization/YET-ANOTHER-ORG'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT', () => { + const records = Factory(lab, { + + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + }, + organizations: { + testedOrg: { id: 'OTHERORG', name: 'other org', description: 'other org' } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/organizations/OTHERORG', + payload: { name: 'new name', description: 'new description' }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy on specific organization') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:update'], + Resource: ['/authorization/organization/OTHERORG'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with correct policy on all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:update'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: ['/authorization/organization/OTHERORG'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:update'], + Resource: ['/authorization/organization/YET-ANOTHER-ORG'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT organization policies', () => { + const otherOrgId = 'OTHERORGID' + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + organizations: { + testedOrg: { id: otherOrgId, name: 'other org', description: 'other org' } + }, + policies: { + testedPolicy: Policy(), + policyToAdd: { + id: 'policy-to-add', + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { policies: ['policy-to-add'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:add'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller with policy that has different organization scope') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:add'], + Resource: [`/authorization/organizations/${otherOrgId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with policy for organization wildcard') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:add'], + Resource: [`/authorization/organization/${organizationId}/*`] + }]) + .shouldRespond(403) + + endpoint.test('should authorize caller with policy for all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:add'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all organization actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:*'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:add'], + Resource: [`/authorization/organization/${organizationId}/*/dummy`] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST organization policies', () => { + const otherOrgId = 'OTHERORGID' + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + organizations: { + testedOrg: { id: otherOrgId, name: 'other org', description: 'other org' } + }, + policies: { + testedPolicy: Policy(), + policyToAdd: { + id: 'policy-to-add', + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { policies: ['policy-to-add'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:replace'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller with policy that has different organization scope') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:replace'], + Resource: [`/authorization/organizations/${otherOrgId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with policy for organization wildcard') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:replace'], + Resource: [`/authorization/organization/${organizationId}/*`] + }]) + .shouldRespond(403) + + endpoint.test('should authorize caller with policy for all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:replace'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all organization actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:*'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:replace'], + Resource: [`/authorization/organization/${organizationId}/*/dummy`] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE organization policies', () => { + const otherOrgId = 'OTHERORGID' + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + organizations: { + testedOrg: { id: otherOrgId, name: 'other org', description: 'other org', policies: ['policyToAdd'] } + }, + policies: { + testedPolicy: Policy(), + policyToAdd: { + id: 'org-policy', + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId: otherOrgId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller with policy that has different organization scope') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organizations/${otherOrgId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with policy for organization wildcard') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organization/${organizationId}/*`] + }]) + .shouldRespond(403) + + endpoint.test('should authorize caller with policy for all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all organization actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:*'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: [`/authorization/organization/${organizationId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organization/${organizationId}/*/dummy`] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE organization policy', () => { + const otherOrgId = 'OTHERORGID' + const policyId = 'policy-to-add' + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId: otherOrgId, policies: ['testedPolicy'] } + }, + organizations: { + testedOrg: { id: otherOrgId, name: 'other org', description: 'other org', policies: ['policyToAdd'] } + }, + policies: { + testedPolicy: { + id: policyId + '2', + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId: otherOrgId + }, + policyToAdd: { + id: policyId, + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId: otherOrgId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: `/authorization/organizations/${otherOrgId}/policies/${policyId}`, + payload: { }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organization/${otherOrgId}`] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller with policy that has different organization scope') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organizations/${organizationId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with policy for organization wildcard') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organization/${otherOrgId}/*`] + }]) + .shouldRespond(403) + + endpoint.test('should authorize caller with policy for all organizations') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: ['/authorization/organization/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all organization actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:*'], + Resource: [`/authorization/organization/${otherOrgId}`] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:dummy'], + Resource: [`/authorization/organization/${otherOrgId}`] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:organizations:policy:remove'], + Resource: [`/authorization/organization/${otherOrgId}/*/dummy`] + }]) + .shouldRespond(403) + }) + }) +}) diff --git a/packages/hapi-auth-udaru/test/authorization/policies.test.js b/packages/hapi-auth-udaru/test/authorization/policies.test.js new file mode 100644 index 00000000..fab2073f --- /dev/null +++ b/packages/hapi-auth-udaru/test/authorization/policies.test.js @@ -0,0 +1,588 @@ +const Lab = require('lab') +const lab = exports.lab = Lab.script() + +const server = require('../test-server') +const config = require('../../lib/config')() + +const Factory = require('@nearform/udaru-core/test/factory') +const { BuildFor, udaru } = require('../testBuilder') + +const organizationId = 'WONKA' +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: JSON.stringify({ + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }), + organizationId + } +} + +function SharedPolicy (Statement) { + return { + version: '2016-07-01', + name: 'Shared Test Policy', + statements: JSON.stringify({ + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }) + } +} + +lab.experiment('Routes Authorizations', () => { + lab.experiment('policies', () => { + lab.experiment('GET /authorization/policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/policies', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:list'], + Resource: ['/authorization/policy/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/policy/WONKA'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:list'], + Resource: ['/authorization/policy/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/policies/{id}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy(), + calledPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/policies/{{calledPolicy.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all policies') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:read'], + Resource: ['/authorization/policy/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:read'], + Resource: ['/authorization/policy/WONKA/{{calledPolicy.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/policy/WONKA/{{calledPolicy.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:read'], + Resource: ['/authorization/policy/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST /authorization/policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: `/authorization/policies?sig=${config.get('security.api.servicekeys.private')}`, + payload: { + id: 'added-policy', + version: 'anything', + name: 'Added Policy', + statements: { Statement: [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] } + }, + headers: { authorization: '{{caller.id}}' } + }) + + lab.afterEach(async () => { + try { + await udaru.policies.delete({id: 'added-policy', organizationId: 'WONKA'}) + } catch (e) { + // This is needed to ignore the error (i.e. in case the policy wasn't properly created) + } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:create'], + Resource: ['/authorization/policy/WONKA/*'] + }]) + .shouldRespond(201) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/policy/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:create'], + Resource: ['/authorization/policy/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/policies/{{id}}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy(), + calledPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: `/authorization/policies/{{calledPolicy.id}}?sig=${config.get('security.api.servicekeys.private')}`, + payload: { + version: 'anything', + name: 'Updated Policy', + statements: { Statement: [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] } + }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all policies') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:update'], + Resource: ['/authorization/policy/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for a specific policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:update'], + Resource: ['/authorization/policy/WONKA/{{calledPolicy.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/policy/WONKA/{{calledPolicy.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:update'], + Resource: ['/authorization/policy/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/policies/{{id}}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy(), + calledPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: `/authorization/policies/{{calledPolicy.id}}?sig=${config.get('security.api.servicekeys.private')}`, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all policies') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:delete'], + Resource: ['/authorization/policy/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for a specific policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:delete'], + Resource: ['/authorization/policy/WONKA/{{calledPolicy.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/policy/WONKA/{{calledPolicy.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:delete'], + Resource: ['/authorization/policy/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + }) + + lab.experiment('shared policies', () => { + lab.experiment('GET /authorization/shared-policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/shared-policies', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:list'], + Resource: ['/authorization/shared-policy/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/shared-policy'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:list'], + Resource: ['/authorization/shared-policy/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/shared-policies/{id}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + }, + sharedPolicies: { + calledPolicy: SharedPolicy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/shared-policies/{{calledPolicy.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all policies') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:read'], + Resource: ['/authorization/shared-policy/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:read'], + Resource: ['/authorization/shared-policy/{{calledPolicy.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/shared-policy/{{calledPolicy.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:read'], + Resource: ['/authorization/shared-policy/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST /authorization/shared-policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: `/authorization/shared-policies?sig=${config.get('security.api.servicekeys.private')}`, + payload: { + id: 'added-policy', + version: 'anything', + name: 'Added Policy', + statements: { Statement: [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] } + }, + headers: { authorization: '{{caller.id}}' } + }) + + lab.afterEach(async () => { + try { + await udaru.policies.deleteShared({id: 'added-policy'}) + } catch (e) { + // This is needed to ignore the error (i.e. in case the policy wasn't properly created) + } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:create'], + Resource: ['/authorization/shared-policy/*'] + }]) + .shouldRespond(201) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/shared-policy/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:create'], + Resource: ['/authorization/shared-policy/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/shared-policies/{{id}}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + }, + sharedPolicies: { + calledPolicy: SharedPolicy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: `/authorization/shared-policies/{{calledPolicy.id}}?sig=${config.get('security.api.servicekeys.private')}`, + payload: { + version: 'anything', + name: 'Updated Policy', + statements: { Statement: [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] } + }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all policies') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:update'], + Resource: ['/authorization/shared-policy/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for a specific policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:update'], + Resource: ['/authorization/shared-policy/{{calledPolicy.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/shared-policy/{{calledPolicy.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:update'], + Resource: ['/authorization/shared-policy/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/shared-policies/{{id}}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + }, + sharedPolicies: { + calledPolicy: SharedPolicy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: `/authorization/shared-policies/{{calledPolicy.id}}?sig=${config.get('security.api.servicekeys.private')}`, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all policies') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:delete'], + Resource: ['/authorization/shared-policy/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for a specific policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:delete'], + Resource: ['/authorization/shared-policy/{{calledPolicy.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:dummy'], + Resource: ['/authorization/shared-policy/{{calledPolicy.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:policies:delete'], + Resource: ['/authorization/shared-policy/dummy'] + }]) + .shouldRespond(403) + }) + }) +}) diff --git a/packages/hapi-auth-udaru/test/authorization/teams.test.js b/packages/hapi-auth-udaru/test/authorization/teams.test.js new file mode 100644 index 00000000..bb06a1a3 --- /dev/null +++ b/packages/hapi-auth-udaru/test/authorization/teams.test.js @@ -0,0 +1,915 @@ +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const server = require('../test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const { BuildFor, udaru } = require('../testBuilder') + +const organizationId = 'WONKA' +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: JSON.stringify({ + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }), + organizationId + } +} + +lab.experiment('Routes Authorizations', () => { + lab.experiment('teams', () => { + lab.experiment('GET /authorization/teams', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/teams', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:list'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:list'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/teams/{id}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/teams/{{calledTeam.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:read'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:read'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:read'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST /authorization/teams', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + lab.afterEach(async () => { + try { + await udaru.teams.delete({id: 'created_team', organizationId}) + } catch (e) { + // This is needed to ignore the error (i.e. in case the team wasn't properly created) + } + }) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: 'created_team', + name: 'called team', + description: 'called team' + }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with correct policy') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:create'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(201) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:create'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/teams/{id}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/teams/{{calledTeam.id}}', + payload: { + name: 'updated team', + description: 'updated team' + }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:update'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:update'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:update'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/teams/{id}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/teams/{{calledTeam.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:delete'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:delete'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:delete'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/teams/{id}/nest', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId }, + parentTeam: { name: 'parent team', description: 'parent team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/teams/{{calledTeam.id}}/nest', + payload: { parentId: '{{parentTeam.id}}' }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:manage'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:manage'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:manage'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/teams/{id}/unnest', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId }, + parentTeam: { name: 'parent team', description: 'parent team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/teams/{{calledTeam.id}}/unnest', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:manage'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:manage'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:manage'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/teams/{id}/policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy(), + addedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/teams/{{calledTeam.id}}/policies', + payload: { policies: ['{{addedPolicy.id}}'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:add'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:add'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:add'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST /authorization/teams/{id}/policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy(), + addedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/teams/{{calledTeam.id}}/policies', + payload: { policies: ['{{addedPolicy.id}}'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:replace'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:replace'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:replace'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/teams/{id}/policies', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/teams/{{calledTeam.id}}/policies', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:remove'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:remove'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:remove'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/teams/{id}/policies/{policyId}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId, policies: ['deletedPolicy'] } + }, + policies: { + testedPolicy: Policy(), + deletedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/teams/{{calledTeam.id}}/policies/{{deletedPolicy.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:remove'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:remove'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:policy:remove'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /authorization/teams/{id}/users', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + teams: { + calledTeam: { + name: 'called team', + description: 'called team', + organizationId, + users: ['called'] + } + }, + policies: { + testedPolicy: Policy(), + deletedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/teams/{{calledTeam.id}}/users?page=1&limit=1', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:read'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:read'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:read'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT /authorization/teams/{id}/users', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + member: { name: 'member', organizationId } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/teams/{{calledTeam.id}}/users', + payload: { users: ['{{member.id}}'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:add'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:add'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:add'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST /authorization/teams/{id}/users', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + member: { name: 'member', organizationId } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId } + }, + policies: { + testedPolicy: Policy(), + addedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/teams/{{calledTeam.id}}/users', + payload: { users: ['{{member.id}}'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:replace'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:replace'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:replace'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/teams/{id}/users', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + member: { name: 'member', organizationId } + }, + teams: { + calledTeam: { name: 'called team', description: 'called team', organizationId, users: ['member'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/teams/{{calledTeam.id}}/users', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:remove'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:remove'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:remove'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE /authorization/teams/{id}/users/{userId}', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + member: { name: 'member', organizationId } + }, + teams: { + calledTeam: { + name: 'called team', + description: 'called team', + organizationId, + users: ['member'] + } + }, + policies: { + testedPolicy: Policy(), + deletedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/teams/{{calledTeam.id}}/users/{{member.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize user with policy for all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:remove'], + Resource: ['/authorization/team/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize user with policy for specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:remove'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize user with incorrect policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:dummy'], + Resource: ['/authorization/team/WONKA/{{calledTeam.id}}'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize user with incorrect policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:teams:user:remove'], + Resource: ['/authorization/team/WONKA/dummy'] + }]) + .shouldRespond(403) + }) + }) +}) diff --git a/packages/hapi-auth-udaru/test/authorization/users.test.js b/packages/hapi-auth-udaru/test/authorization/users.test.js new file mode 100644 index 00000000..19214a52 --- /dev/null +++ b/packages/hapi-auth-udaru/test/authorization/users.test.js @@ -0,0 +1,874 @@ + +const Lab = require('lab') +const lab = exports.lab = Lab.script() + +const server = require('../test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const { BuildFor, udaru } = require('../testBuilder') + +const organizationId = 'WONKA' +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: { + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }, + organizationId + } +} + +lab.experiment('Routes Authorizations', () => { + lab.experiment('users', () => { + lab.experiment('GET /users/:id', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/users/{{called.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:read'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:read'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:read'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:read'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('GET /users', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/users', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy to list users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:list'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:list'], + Resource: ['dummy'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with authorization on a single team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:list'], + Resource: ['/authorization/user/WONKA/team/*'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST', () => { + const calledId = 'fake-user' + const userData = { + id: calledId, + name: 'Fake User' + } + + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/users', + payload: userData, + headers: { authorization: '{{caller.id}}' } + }) + + lab.afterEach(async () => { + try { + await udaru.users.delete({id: calledId, organizationId}) + } catch (e) { + // This is needed to ignore the error (i.e. in case the user wasn't properly created) + } + }) + + endpoint.test('should authorize caller with policy create users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:create'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(201) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:create'], + Resource: ['dummy'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with authorization on a team (create user on team doesn\'t make sense)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:create'], + Resource: ['/authorization/user/WONKA/team/*'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/users/{{called.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:delete'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:delete'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:delete'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:delete'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT', () => { + const userData = { + name: 'called user' + } + + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/users/{{called.id}}', + payload: userData, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:update'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:update'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:update'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:update'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('PUT user policies', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy(), + policyToAdd: { + id: 'policy-to-add', + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'PUT', + url: '/authorization/users/{{called.id}}/policies', + payload: { policies: ['policy-to-add'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:add'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:add'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:add'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:add'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST user policies', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy(), + policyToAdd: { + id: 'policy-to-add', + version: '2016-07-01', + name: 'Policy To Add', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/users/{{called.id}}/policies', + payload: { policies: ['policy-to-add'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:replace'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:replace'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:replace'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:replace'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE user policies', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId, policies: ['policyToDelete'] } + }, + policies: { + testedPolicy: Policy(), + policyToDelete: { + id: 'policy-to-delete', + version: '2016-07-01', + name: 'Policy To Delete', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/users/{{called.id}}/policies', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE single user policy', () => { + const records = Factory(lab, { + teams: { + calledTeam: { name: 'called team', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId, policies: ['policyToDelete'] } + }, + policies: { + testedPolicy: Policy(), + policyToDelete: { + id: 'policy-to-delete', + version: '2016-07-01', + name: 'Policy To Delete', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['an-action'], + Resource: ['a-resource'] + }] + }, + organizationId + } + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/users/{{called.id}}/policies/policy-to-add', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all users in specific team') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/{{calledTeam.id}}/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:*'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(204) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + }) + + lab.experiment('POST user teams', () => { + const records = Factory(lab, { + teams: { + calledTeam1: { name: 'called team 1', description: 'desc', organizationId, users: ['called'] }, + calledTeam2: { name: 'called team 2', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/users/{{called.id}}/teams', + payload: { teams: ['{{calledTeam1.id}}', '{{calledTeam2.id}}'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for all users in both teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:replace'], + Resource: [ + '/authorization/user/WONKA/{{calledTeam1.id}}/*', + '/authorization/user/WONKA/{{calledTeam2.id}}/*' + ] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users in all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:replace'], + Resource: ['/authorization/user/WONKA/*/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:*'], + Resource: ['/authorization/user/WONKA/*/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:replace'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy on one of the teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:replace'], + Resource: ['/authorization/user/WONKA/{{calledTeam1.id}}/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller with policy only on the specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:replace'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(403) + }) + + lab.experiment('DELETE user teams', () => { + const records = Factory(lab, { + teams: { + calledTeam1: { name: 'called team 1', description: 'desc', organizationId, users: ['called'] }, + calledTeam2: { name: 'called team 2', description: 'desc', organizationId, users: ['called'] } + }, + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] }, + called: { name: 'called', organizationId } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'DELETE', + url: '/authorization/users/{{called.id}}/teams', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint.test('should authorize caller with policy for all users in both teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:remove'], + Resource: [ + '/authorization/user/WONKA/{{calledTeam1.id}}/*', + '/authorization/user/WONKA/{{calledTeam2.id}}/*' + ] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all users in all teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:remove'], + Resource: ['/authorization/user/WONKA/*/*'] + }]) + .shouldRespond(200) + + endpoint.test('should authorize caller with policy for all user actions') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:*'], + Resource: ['/authorization/user/WONKA/*/*'] + }]) + .shouldRespond(200) + + endpoint.test('should not authorize caller without a correct policy (action)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:dummy'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy (resource)') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:policy:remove'], + Resource: ['/authorization/user/WONKA/*/dummy'] + }]) + .shouldRespond(403) + + endpoint.test('should not authorize caller without a correct policy on one of the teams') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:remove'], + Resource: ['/authorization/user/WONKA/{{calledTeam1.id}}/*'] + }]) + .shouldRespond(403) + + endpoint.test('should anot uthorize caller with policy only on the specific users') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:teams:remove'], + Resource: ['/authorization/user/WONKA/*/{{called.id}}'] + }]) + .shouldRespond(403) + }) + }) +}) diff --git a/packages/hapi-auth-udaru/test/edgeCases.test.js b/packages/hapi-auth-udaru/test/edgeCases.test.js new file mode 100644 index 00000000..adf356e2 --- /dev/null +++ b/packages/hapi-auth-udaru/test/edgeCases.test.js @@ -0,0 +1,233 @@ +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const Hapi = require('hapi') +const expect = require('code').expect +const sinon = require('sinon') + +const server = require('./test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const { BuildFor, udaru } = require('./testBuilder') +const config = require('../lib/config')() + +const organizationId = 'WONKA' +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: JSON.stringify({ + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + }), + organizationId + } +} + +lab.experiment('Edge cases', () => { + lab.experiment('configuration', () => { + lab.test('should expect a valid validation function', async () => { + const s = Hapi.Server() + await s.register({plugin: require('../lib/authentication'), options: {config}}) + + expect(() => s.auth.strategy('udaru', 'udaru', {validateFunc: 123})).to.throw(Error, 'options.validateFunc must be a valid function') + }) + }) + + lab.experiment('invalid routes', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/no/plugins', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint + .test('should not authorize user for routes defined in a invalid way') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + }]) + .shouldRespond(403) + }) + + lab.experiment('invalid validation resource', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/no/resource', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint + .test('should not authorize user for routes without a resource') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + }]) + .shouldRespond(500) + }) + + lab.experiment('invalid team validation resource', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + let stub + lab.beforeEach(async () => { + await endpoint.startServer() + + stub = sinon.stub(endpoint.serverInstance.udaruConfig, 'get') + stub.onFirstCall().callThrough() + stub.onSecondCall().returns({'team-resource': () => '*'}) + stub.onThirdCall().returns({}) + }) + + lab.afterEach(async () => { + stub.restore() + }) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/no/team-resource/{{caller.id}}', + payload: { policies: ['policy-to-add'] }, + headers: { authorization: '{{caller.id}}' } + }) + + endpoint + .test('should not authorize user for routes without a resource') + .withPolicy([{ + Effect: 'Allow', + Action: ['*'], + Resource: ['*'] + }]) + .shouldRespond(500) + }) + + lab.experiment('missing headers', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'POST', + url: '/authorization/policies', + headers: { authorization: '' } + }) + + endpoint + .test('should not authorize without an authorization header') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + }]) + .shouldRespond(401) + }) + + lab.experiment('resource not found', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + const endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/users/invalid', + headers: { authorization: '{{caller.id}}' } + }) + + endpoint + .test('should not authorize user for an invalid user resource') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + }]) + .shouldRespond(403) + }) + + lab.experiment('unhandled errors', () => { + const records = Factory(lab, { + users: { + caller: { name: 'caller', organizationId, policies: ['testedPolicy'] } + }, + policies: { + testedPolicy: Policy() + } + }, udaru) + + let stub + let endpoint = BuildFor(lab, records) + .server(server) + .endpoint({ + method: 'GET', + url: '/authorization/users/{{caller.id}}', + headers: { authorization: '{{caller.id}}' } + }) + + lab.beforeEach(async () => { + await endpoint.startServer() + + stub = sinon.stub(endpoint.serverInstance.udaru.users, 'read') + stub.callThrough() + stub.onCall(1).rejects(new Error('ERROR')) + }) + + lab.afterEach(() => { + stub.restore() + }) + + endpoint + .test('should not authorize user for a user resource when some other errors occured') + .withPolicy([{ + Effect: 'Allow', + Action: ['authorization:users:read'], + Resource: ['/authorization/user/WONKA/*'] + }]) + .shouldRespond(500) + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/authorization.test.js b/packages/hapi-auth-udaru/test/endToEnd/authorization.test.js new file mode 100644 index 00000000..fa69fc2c --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/authorization.test.js @@ -0,0 +1,391 @@ +'use strict' + +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const utils = require('@nearform/udaru-core/test/testUtils') +const serverFactory = require('../test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const udaru = require('@nearform/udaru-core')() + +lab.experiment('Authorization', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('check authorization should return access true for allowed', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/access/ROOTid/action_a/resource_a' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ access: true }) + }) + + lab.test('check authorization should return access false for denied', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/access/Modifyid/action_a/resource_a' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ access: false }) + }) + + lab.test('list authorizations should return actions allowed for the user', async () => { + const actionList = { + actions: [] + } + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/list/ModifyId/not/my/resource' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal(actionList) + }) + + lab.test('list authorizations should return actions allowed for the user', async () => { + const actionList = { + actions: ['Read'] + } + const options = utils.requestOptions({ + method: 'GET', + // TO BE DISCUSSED: We need double slashes "//" if we use a "/" at the beginning of a resource in the policies + // @see https://github.com/nearform/udaru/issues/198 + url: '/authorization/list/ManyPoliciesId//myapp/users/filippo' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal(actionList) + }) + + lab.test('list authorizations should return actions allowed for the user', async () => { + const actionList = [ + { + resource: '/myapp/users/filippo', + actions: ['Read'] + }, + { + resource: '/myapp/documents/no_access', + actions: [] + } + ] + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/list/ManyPoliciesId?resources=/myapp/users/filippo&resources=/myapp/documents/no_access' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal(actionList) + }) +}) + +lab.experiment('Authorization inherited org policies', () => { + const orgId1 = 'orgId1' + const orgId2 = 'orgId2' + const testUserId1 = 'testUserId1' + const testUserId2 = 'testUserId2' + const org1PolicyId = 'org1PolicyId' + + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + Factory(lab, { + organizations: { + org1: { + id: orgId1, + name: 'Test Organization', + description: 'Test Organization', + policies: ['testPolicy1', 'checkAccessPolicy1', 'contextTestPolicy', 'conditionTestPolicy', 'denyConditionTestPolicy'], + users: ['TestUser1'] + }, + org2: { + id: orgId2, + name: 'Test Organization', + description: 'Test Organization', + policies: ['checkAccessPolicy2'], + users: ['TestUser2'] + } + }, + users: { + TestUser1: { + id: testUserId1, + name: 'Test User1', + organizationId: orgId1 + }, + TestUser2: { + id: testUserId2, + name: 'Test User2', + organizationId: orgId2 + } + }, + policies: { + testPolicy1: { + id: org1PolicyId, + name: 'org1Policy', + organizationId: orgId1, + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['read'], + Resource: ['org:documents'] + } + ] + } + }, + checkAccessPolicy1: { + name: 'checkaccess', + organizationId: orgId1, + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + } + ] + } + }, + checkAccessPolicy2: { + name: 'checkaccess', + organizationId: orgId2, + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['authorization:authn:access'], + Resource: ['authorization/access'] + } + ] + } + }, + contextTestPolicy: { + name: 'contextTestPolicy', + organizationId: orgId1, + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['read'], + Resource: ['org:docs:$' + '{udaru:userId}'] + } + ] + } + }, + conditionTestPolicy: { + name: 'conditionTestPolicy', + organizationId: orgId1, + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['read'], + Resource: ['org:docs:$' + '{udaru:organizationId}'], + Condition: { + StringEquals: { 'request:source': 'server' }, + IpAddress: { 'request:sourceIp': '127.0.0.1' } + } + } + ] + } + }, + denyConditionTestPolicy: { + name: 'denyConditionTestPolicy', + organizationId: orgId1, + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['write'], + Resource: ['org:docs:$' + '{udaru:organizationId}'], + Condition: { + StringEquals: { 'request:source': 'api' } + } + } + ] + } + } + } + }, udaru) + + lab.test('User authorized against policies inherited from its own organization', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:documents`, + headers: { + authorization: testUserId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(true) + }) + + lab.test('User checks authorization for another org user', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:documents`, + headers: { + authorization: testUserId2 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(false) + }) + + lab.test('Non-existing user has no access to existing organization policies', async () => { + const userId = 'abcd1234' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:documents`, + headers: { + authorization: testUserId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(false) + }) + + lab.test('Root impersonates org in which checked authorization exists', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:documents`, + headers: { + authorization: 'ROOTid', + org: orgId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(true) + }) + + lab.test('User is granted access to resource based on udaru:userId context variable', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:docs:${userId}`, + headers: { + authorization: 'ROOTid', + org: orgId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(true) + }) + + lab.test('User is NOT granted access to other users resource based on udaru:userId context variable', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${testUserId1}/read/org:docs:${testUserId2}`, + headers: { + authorization: 'ROOTid', + org: orgId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(false) + }) + + lab.test('User is granted access to udaru:organizationId resource based on IP conditions', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:docs:${orgId1}`, + headers: { + authorization: 'ROOTid', + org: orgId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(true) + }) + + lab.test('User is denied write access to udaru:organization resourec based on request:source condition', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/write/org:docs:${orgId1}`, + headers: { + authorization: 'ROOTid', + org: orgId1 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(false) + }) + + lab.test('Root impersonates org in which checked authorization exists but provides valid other org data', async () => { + const userId = testUserId1 + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/access/${userId}/read/org:documents`, + headers: { + authorization: 'ROOTid', + org: orgId2 + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.access).to.equal(false) + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/fullOrgStructure.test.js b/packages/hapi-auth-udaru/test/endToEnd/fullOrgStructure.test.js new file mode 100644 index 00000000..3f55fb93 --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/fullOrgStructure.test.js @@ -0,0 +1,274 @@ +'use strict' + +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() + +const config = require('../../lib/config')() +const serverFactory = require('../test-server') +const Factory = require('@nearform/udaru-core/test/factory') +const utils = require('@nearform/udaru-core/test/testUtils') + +const udaru = require('@nearform/udaru-core')() +const Action = config.get('AuthConfig.Action') + +lab.experiment('SuperUsers with limited access across organizations', () => { + const defaultAdminPolicy = 'authorization.organizations.defaultPolicies.orgAdmin' + const rootOrgId = config.get('authorization.superUser.organization.id') + const orgId1 = 'orgId1' + const orgId2 = 'orgId2' + const orgId3 = 'orgId3' + + const teamId1 = 'teamId1' + const teamId2 = 'teamId2' + + const userId1 = 'userId1' + const userId2 = 'userId2' + const userId3 = 'userId3' + const userId4 = 'userId4' + + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + Factory(lab, { + organizations: { + // ROOT org is created by default in the test DB by the test suite + org1: { + id: orgId1, + name: 'org1', + description: 'org1' + }, + org2: { + id: orgId2, + name: 'org2', + description: 'org2' + }, + org3: { + id: orgId3, + name: 'org3', + description: 'org3' + } + }, + teams: { + team1: { + id: teamId1, + name: 'team1', + description: 'team1', + organizationId: rootOrgId, + users: ['user1', 'user2'], + policies: ['org1AdminPolicy', 'org2AdminPolicy', 'orgAuthPolicy', 'org1InternalPolicy', 'org2InternalPolicy'] + }, + team2: { + id: teamId2, + name: 'team2', + description: 'team2', + organizationId: rootOrgId, + users: ['user3'], + policies: ['org3AdminPolicy', 'orgAuthPolicy', 'org3InternalPolicy'] + } + }, + users: { + user1: { + id: userId1, + name: 'user1', + organizationId: rootOrgId + }, + user2: { + id: userId2, + name: 'user2', + organizationId: rootOrgId + }, + user3: { + id: userId3, + name: 'user3', + organizationId: rootOrgId + }, + user4: { + id: userId4, + name: 'user4', + organizationId: rootOrgId + } + }, + policies: { + org1AdminPolicy: { + name: 'org1AdminPolicy', + organizationId: rootOrgId, + statements: config.get(defaultAdminPolicy, {organizationId: orgId1}).statements + }, + org2AdminPolicy: { + name: 'org2AdminPolicy', + organizationId: rootOrgId, + statements: config.get(defaultAdminPolicy, {organizationId: orgId2}).statements + }, + org3AdminPolicy: { + name: 'org3AdminPolicy', + organizationId: rootOrgId, + statements: config.get(defaultAdminPolicy, {organizationId: orgId3}).statements + }, + orgAuthPolicy: { + name: 'orgAuthPolicy', + organizationId: rootOrgId, + statements: utils.AllowStatement([Action.CheckAccess], ['authorization/access']) + }, + org1InternalPolicy: { + name: 'org1InternalPolicy', + organizationId: rootOrgId, + statements: utils.AllowStatement(['org1:action:read'], ['org1:resource:res1']) + }, + org2InternalPolicy: { + name: 'org2InternalPolicy', + organizationId: rootOrgId, + statements: utils.AllowStatement(['org2:action:read'], ['org2:resource:res2']) + }, + org3InternalPolicy: { + name: 'org3InternalPolicy', + organizationId: rootOrgId, + statements: utils.AllowStatement(['org3:action:read'], ['org3::resource:res3']) + } + } + }, udaru) + + lab.experiment('Check limited super users organization management rights', () => { + lab.test('Get org on an org endpoint on which it has rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId1 + }, + method: 'GET', + url: `/authorization/organizations/${orgId1}` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result).to.exist() + expect(response.result.id).to.equal(orgId1) + }) + + lab.test('Get org on an org endpoint on which it has no rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId3 + }, + method: 'GET', + url: `/authorization/organizations/${orgId3}` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('SuperUser not in team has no rights', async () => { + const options = { + headers: { + authorization: userId4, + org: orgId1 + }, + method: 'GET', + url: `/authorization/organizations/${orgId1}` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('List teams and users on an org on which it has rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId1 + }, + method: 'GET', + url: '/authorization/teams' + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result).to.exist() + expect(response.result.data).to.exist() + }) + + lab.test('List teams and users on an org on which it has no rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId3 + }, + method: 'GET', + url: '/authorization/teams' + } + + let response = await server.inject(options) + expect(response.statusCode).to.equal(403) + + options.headers.authorization = userId4 + response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + }) + + lab.experiment('Limited SuperUser rights on accessing organization resources', () => { + lab.test('Access resource on which it has rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId1 + }, + method: 'GET', + url: `/authorization/access/${userId1}/org1:action:read/org1:resource:res1` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.access).to.equal(true) + }) + + lab.test('Do an invalid action on a resource on which it has rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId1 + }, + method: 'GET', + url: `/authorization/access/${userId1}/org1:action:dummy/org1:resource:res1` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.access).to.equal(false) + }) + + lab.test('Access resource on which it has no rights', async () => { + const options = { + headers: { + authorization: userId1, + org: orgId3 + }, + method: 'GET', + url: `/authorization/access/${userId1}/org3:action:read/org3:resource:res3` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.access).to.equal(false) + }) + + lab.test('Access resource by user not registered in teams on which it has no rights', async () => { + const options = { + headers: { + authorization: userId4, + org: orgId1 + }, + method: 'GET', + url: `/authorization/access/${userId4}/org1:action:read/org1:resource:res1` + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/monitor.test.js b/packages/hapi-auth-udaru/test/endToEnd/monitor.test.js new file mode 100644 index 00000000..cc19b501 --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/monitor.test.js @@ -0,0 +1,24 @@ +'use strict' + +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const serverFactory = require('../test-server') + +lab.experiment('monitor', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('calling ping should return 200 Ok', async () => { + const options = { + method: 'GET', + url: '/ping' + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/organizations.test.js b/packages/hapi-auth-udaru/test/endToEnd/organizations.test.js new file mode 100644 index 00000000..1460efd2 --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/organizations.test.js @@ -0,0 +1,711 @@ +'use strict' + +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const utils = require('@nearform/udaru-core/test/testUtils') +const uuid = require('uuid/v4') +const serverFactory = require('../test-server') +const udaru = require('@nearform/udaru-core')() + +const organizationId = 'SHIPLINE' +const statementsTest = { Statement: [{ Effect: 'Allow', Action: ['nfdocuments:Read'], Resource: ['nearform:documents:/public/*'] }] } +const testPolicy = { + id: uuid(), + version: '2016-07-01', + name: 'Test Policy Org', + organizationId: organizationId, + statements: statementsTest +} +const testPolicy2 = { + id: uuid(), + version: '2016-07-02', + name: 'Test Policy Org2', + organizationId: organizationId, + statements: statementsTest +} + +const metadata = {key1: 'val1', key2: 'val2'} + +lab.experiment('Organizations', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + + await udaru.policies.create(testPolicy) + await udaru.policies.create(testPolicy2) + }) + + lab.after(async () => { + try { + await Promise.all([ + udaru.organizations.deletePolicy({id: organizationId, policyId: testPolicy.id}), + udaru.organizations.deletePolicy({id: organizationId, policyId: testPolicy2.id}) + ]) + } catch (e) { + // This is needed to ignore the error (i.e. in case the organizations weren't properly created) + } + }) + + lab.afterEach(async () => { + try { + await Promise.all([ + udaru.organizations.delete('nearForm'), + udaru.organizations.delete('nearForm_Meta'), + udaru.organizations.delete('nearForm_Meta2') + ]) + } catch (e) { + // This is needed to ignore the error (i.e. in case the organizations weren't properly created) + } + }) + + lab.test('get organizations list has default pagination params', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result).to.exist() + expect(response.result.page).to.equal(1) + expect(response.result.total).greaterThan(1) + expect(response.result.limit).greaterThan(1) + }) + + lab.test('get organizations list', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations?limit=10&page=1' + }) + + const response = await server.inject(options) + const result = response.result + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(10) + expect(result.total).to.equal(6) + expect(result.data).to.equal([ + { + id: 'CONCH', + name: 'Conch Plc', + description: 'Global fuel distributors' + }, + { + id: 'OILCOEMEA', + name: 'Oilco EMEA', + description: 'Oilco EMEA Division' + }, + { + id: 'OILCOUSA', + name: 'Oilco USA', + description: 'Oilco EMEA Division' + }, + { + id: 'SHIPLINE', + name: 'Shipline', + description: 'World class shipping' + }, + { + id: 'ROOT', + name: 'Super Admin', + description: 'Super Admin organization' + }, + { + id: 'WONKA', + name: 'Wonka Inc', + description: 'Scrumpalicious Chocolate' + } + ]) + }) + lab.test('get organizations list: page1', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations?limit=3&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.data).to.equal([ + { + id: 'CONCH', + name: 'Conch Plc', + description: 'Global fuel distributors' + }, + { + id: 'OILCOEMEA', + name: 'Oilco EMEA', + description: 'Oilco EMEA Division' + }, + { + id: 'OILCOUSA', + name: 'Oilco USA', + description: 'Oilco EMEA Division' + } + ]) + }) + lab.test('get organizations list: page2', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations?limit=3&page=2' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.data).to.equal([ + { + id: 'SHIPLINE', + name: 'Shipline', + description: 'World class shipping' + }, + { + id: 'ROOT', + name: 'Super Admin', + description: 'Super Admin organization' + }, + { + id: 'WONKA', + name: 'Wonka Inc', + description: 'Scrumpalicious Chocolate' + } + ]) + }) + + lab.test('get single organization', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations/WONKA' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'WONKA', + name: 'Wonka Inc', + description: 'Scrumpalicious Chocolate', + policies: [] + }) + }) + + lab.test('get a single org with meta', async () => { + await udaru.organizations.create({id: 'nearForm_Meta2', name: 'nearForm Meta2', description: 'nearForm org with Meta2', metadata: metadata}) + + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations/nearForm_Meta2' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'nearForm_Meta2', + name: 'nearForm Meta2', + description: 'nearForm org with Meta2', + metadata: metadata, + policies: [] + }) + }) + + lab.test('get single organization with meta', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/organizations/CONCH' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'CONCH', + name: 'Conch Plc', + description: 'Global fuel distributors', + policies: [] + }) + }) + + lab.test('create organization should return 201 for success', async () => { + const organization = { + id: 'nearForm', + name: 'nearForm', + description: 'nearForm org' + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result).to.equal({ + organization: { + id: 'nearForm', + name: 'nearForm', + description: 'nearForm org', + policies: [] + }, + user: undefined + }) + + await udaru.organizations.delete('nearForm') + }) + + lab.test('create organization with metadata, return 201 for success', async () => { + const organization = { + id: 'nearForm_Meta', + name: 'nearForm_Meta', + description: 'nearForm org with meta', + metadata: metadata + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result).to.equal({ + organization: { + id: 'nearForm_Meta', + name: 'nearForm_Meta', + description: 'nearForm org with meta', + metadata: metadata, + policies: [] + }, + user: undefined + }) + + await udaru.organizations.delete('nearForm_Meta') + }) + + lab.test('create organization with no id', async () => { + const organization = { + name: 'nearForm', + description: 'nearForm org' + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result.organization + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.be.null() + expect(result.name).to.equal(organization.name) + expect(result.description).to.equal(organization.description) + + await udaru.organizations.delete(result.id) + }) + + lab.test('create organization with specified but undefined id', async () => { + const organization = { + id: undefined, + name: 'nearForm', + description: 'nearForm org' + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result.organization + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.be.null() + expect(result.name).to.equal(organization.name) + expect(result.description).to.equal(organization.description) + + await udaru.organizations.delete(result.id) + }) + + lab.test('create organization with null id', async () => { + const organization = { + id: null, + name: 'nearForm', + description: 'nearForm org' + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.id).to.not.exist() + }) + + lab.test('create organization with empty string id', async () => { + const organization = { + id: '', + name: 'nearForm', + description: 'nearForm org' + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.id).to.not.exist() + }) + + lab.test('create organization and an admin user should return 201 for success', async () => { + const organization = { + id: 'nearForm', + name: 'nearForm', + description: 'nearForm org', + user: { + id: 'exampleId', + name: 'example' + } + } + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations', + payload: organization + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result).to.equal({ + organization: { + id: 'nearForm', + name: 'nearForm', + description: 'nearForm org', + policies: [] + }, + user: { + id: 'exampleId', + name: 'example' + } + }) + + await udaru.organizations.delete('nearForm') + }) + + lab.test('delete organization should return 204 if success', async () => { + const res = await udaru.organizations.create({id: 'nearForm', name: 'nearForm', description: 'nearForm org'}) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/organizations/${res.organization.id}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(204) + expect(result).to.not.exist() + }) + + lab.test('update organization should return 200 for success', async () => { + await udaru.organizations.create({id: 'nearForm', name: 'nearForm', description: 'nearForm org'}) + + let orgUpdate = { + id: 'nearForm', + name: 'new name', + description: 'new desc' + } + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/organizations/${orgUpdate.id}`, + payload: { + name: orgUpdate.name, + description: orgUpdate.description + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ id: 'nearForm', name: 'new name', description: 'new desc', policies: [] }) + + await udaru.organizations.delete('nearForm') + }) + + lab.test('update organization with metadata should return 200 for success', async () => { + const res = await udaru.organizations.create({id: 'nearForm_Meta2', name: 'nearForm Meta2', description: 'nearForm org with Meta2'}) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/organizations/${res.organization.id}`, + payload: { + name: 'nearForm Meta2', + description: 'nearForm org with Meta2', + metadata: metadata + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ id: 'nearForm_Meta2', + name: 'nearForm Meta2', + description: 'nearForm org with Meta2', + metadata: metadata, + policies: [] + }) + }) + + lab.test('Policy instance addition and removal', async () => { + let options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/organizations/${organizationId}/policies`, + payload: {policies: []} + }) + + let response = await server.inject(options) + let result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(0) + + options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { + policies: [{ + id: testPolicy.id, + variables: {var1: 'value1'} + }] + } + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.contain([ + { id: testPolicy.id, name: testPolicy.name, version: testPolicy.version, variables: {var1: 'value1'} } + ]) + + const firstInstance = result.policies[0].instance + + options.payload = { + policies: [{ + id: testPolicy.id, + variables: {var2: 'value2'} + }, { + id: testPolicy.id, + variables: {var3: 'value3'} + }] + } + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(3) + expect(utils.PoliciesWithoutInstance(result.policies)).to.contain([ + { id: testPolicy.id, name: testPolicy.name, version: testPolicy.version, variables: {var3: 'value3'} } + ]) + + options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/organizations/${organizationId}/policies/${testPolicy.id}?instance=${firstInstance}` + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(204) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/organizations/${organizationId}` + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(2) + expect(utils.PoliciesWithoutInstance(result.policies)).to.not.contain([ + { id: testPolicy.id, name: testPolicy.name, version: testPolicy.version, variables: {var1: 'value1'} } + ]) + + options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/organizations/${organizationId}/policies/${testPolicy.id}` + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(204) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/organizations/${organizationId}` + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(0) + }) + + lab.test('add policies to an organization', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { + policies: [testPolicy.id] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies).to.exist() + expect(result.policies.length).to.equal(1) + expect(result.policies[0].id).to.equal(testPolicy.id) + }) + + lab.test('add policies with variables to an organization', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { + policies: [{ + id: testPolicy.id, + variables: {var1: 'value1'} + }] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies).to.exist() + // it's 2 because the previous tests insert one policy, this inserts the second + expect(result.policies.length).to.equal(2) + expect(utils.PoliciesWithoutInstance(result.policies)).to.include({ + id: testPolicy.id, + name: testPolicy.name, + version: testPolicy.version, + variables: {var1: 'value1'} + }) + }) + + lab.test('add policy with invalid ID to an organization', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/organizations/WONKA/policies', + payload: { + policies: ['InvalidPolicyID'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('replace the policies of an organization', async () => { + await udaru.organizations.addPolicies({id: organizationId, policies: [testPolicy.id]}) + + const options = utils.requestOptions({ + method: 'POST', + url: `/authorization/organizations/${organizationId}/policies`, + payload: { + policies: [testPolicy2.id] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies).to.exist() + expect(result.policies.length).to.equal(1) + expect(result.policies[0].id).to.equal(testPolicy2.id) + }) + + lab.test('add policy with invalid ID to an organization', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/organizations/WONKA/policies', + payload: { + policies: ['InvalidPolicyID'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('delete the policies of an organization', async () => { + await udaru.organizations.addPolicies({id: organizationId, policies: [testPolicy.id, testPolicy2.id]}) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/organizations/${organizationId}/policies` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + const res = await udaru.organizations.read(organizationId) + expect(res.policies.length).to.equal(0) + }) + + lab.test('delete the policy of an organization', async () => { + await udaru.organizations.addPolicies({id: organizationId, policies: [testPolicy.id, testPolicy2.id]}) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/organizations/${organizationId}/policies/${testPolicy2.id}` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + const res = await udaru.organizations.read(organizationId) + expect(res.policies.length).to.equal(1) + expect(res.policies[0].id).to.equal(testPolicy.id) + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/policies.test.js b/packages/hapi-auth-udaru/test/endToEnd/policies.test.js new file mode 100644 index 00000000..abf52714 --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/policies.test.js @@ -0,0 +1,636 @@ +'use strict' + +const _ = require('lodash') +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const utils = require('@nearform/udaru-core/test/testUtils') +const serverFactory = require('../test-server') +const udaru = require('@nearform/udaru-core')() + +const statements = { Statement: [{ Effect: 'Allow', Action: ['documents:Read'], Resource: ['wonka:documents:/public/*'] }] } +const policyCreateData = { + version: '2016-07-01', + name: 'Documents Admin', + statements, + organizationId: 'WONKA' +} + +lab.experiment('Policies - get/list', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('get policy list has default pagination params', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/policies' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.page).to.equal(1) + expect(response.result.limit).greaterThan(1) + }) + + lab.test('get policy list: limit', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/policies?limit=4&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).greaterThan(4) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(4) + expect(result.data.length).to.equal(4) + }) + + lab.test('get policy list', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/policies?limit=500&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).lessThan(result.limit) // Will fail if we need to increase limit + let accountantPolicy = _.find(result.data, {id: 'policyId2'}) + expect(accountantPolicy).to.equal({ + id: 'policyId2', + version: '0.1', + name: 'Accountant', + statements: { + Statement: [ + { + Effect: 'Allow', + Action: ['finance:ReadBalanceSheet'], + Resource: ['database:pg01:balancesheet'] + }, + { + Effect: 'Deny', + Action: ['finance:ImportBalanceSheet'], + Resource: ['database:pg01:balancesheet'] + }, + { + Effect: 'Deny', + Action: ['finance:ReadCompanies'], + Resource: ['database:pg01:companies'] + }, + { + Effect: 'Deny', + Action: ['finance:UpdateCompanies'], + Resource: ['database:pg01:companies'] + }, + { + Effect: 'Deny', + Action: ['finance:DeleteCompanies'], + Resource: ['database:pg01:companies'] + } + ] + } + }) + }) + + lab.test('get single policy', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/policies/${p.id}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal(p) + + await udaru.policies.delete({id: p.id, organizationId: 'WONKA'}) + }) +}) + +lab.experiment('Policies - create/update/delete (need service key)', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('create new policy without a service key should return 403 Forbidden', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=1234', + payload: { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('create new policy without valid data should return 400 Bad Request', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=123456789', + payload: { + version: '2016-07-01', + name: 'Documents Admin' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('create new policy with already present id should return 400 Bad Request', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=123456789', + payload: { + id: 'policyId1', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(409) + expect(response.result.message).to.equal('policy already exists') + }) + + lab.test('create new policy should return 201 and the created policy data', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=123456789', + payload: { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.name).to.equal('Documents Admin') + expect(result.statements).to.equal(statements) + + await udaru.policies.delete({id: result.id, organizationId: 'WONKA'}) + }) + + lab.test('create new policy with invalid effect data - should return a 400', async () => { + const badStatement = { Statement: [{ Effect: 'Groot', Action: ['documents:Read'], Resource: ['wonka:documents:/public/*'] }] } + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=123456789', + payload: { + id: 'badPolicy', + version: '2016-07-01', + name: 'Documents Admin', + badStatement + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('create new policy should allow empty string as id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=123456789', + payload: { + id: '', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.equal('') + expect(result.name).to.equal('Documents Admin') + + await udaru.policies.delete({id: result.id, organizationId: 'WONKA'}) + }) + + lab.test('create new policy specifying an id should return 201 and the created policy data', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/policies?sig=123456789', + payload: { + id: 'mySpecialPolicyId', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.equal('mySpecialPolicyId') + expect(result.name).to.equal('Documents Admin') + + await udaru.policies.delete({id: result.id, organizationId: 'WONKA'}) + }) + + lab.test('update new policy without a service key should return 403 Forbidden', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/policies/whatever?sig=123', + payload: { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('update policy without valid data should return 400 Bad Request', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/policies/whatever?sig=123456789', + payload: { + version: '2016-07-01', + name: 'Documents Admin' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('update new policy should return the updated policy data', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/policies/${p.id}?sig=123456789`, + payload: { + version: '1234', + name: 'new policy name', + statements: { + Statement: [ + { + Effect: 'Deny', + Action: ['documents:Read'], + Resource: ['wonka:documents:/public/*'] + } + ] + } + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.name).to.equal('new policy name') + expect(result.version).to.equal('1234') + expect(result.statements).to.equal({ Statement: [{ Action: ['documents:Read'], Effect: 'Deny', Resource: ['wonka:documents:/public/*'] }] }) + + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('delete policy without a service key should return 403 Forbidden', async () => { + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/policies/policyId1?sig=1234' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('delete policy should return 204', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/policies/${p.id}?sig=123456789` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + }) +}) + +lab.experiment('Shared Policies - create/update/delete (need service key)', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + const sharedPolicyCreateData = { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + + lab.test('create new shared policy without a service key should return 403 Forbidden', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/shared-policies?sig=1234', + payload: { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('create new shared policy without valid data should return 400 Bad Request', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/shared-policies?sig=123456789', + payload: { + version: '2016-07-01', + name: 'Documents Admin' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('create new shared policy with already present id should return 409 conflict', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/shared-policies?sig=123456789', + payload: { + id: 'policyId1', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(409) + expect(response.result.message).to.equal('policy already exists') + }) + + lab.test('create new shared policy should return 201 and the created policy data', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/shared-policies?sig=123456789', + payload: { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.name).to.equal('Documents Admin') + expect(result.statements).to.equal(statements) + + await udaru.policies.deleteShared({id: result.id}) + }) + + lab.test('create new shared policy should allow empty string as id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/shared-policies?sig=123456789', + payload: { + id: '', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.equal('') + expect(result.name).to.equal('Documents Admin') + + await udaru.policies.deleteShared({id: result.id}) + }) + + lab.test('create new shared policy specifying an id should return 201 and the created policy data', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/shared-policies?sig=123456789', + payload: { + id: 'mySpecialPolicyId', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.equal('mySpecialPolicyId') + expect(result.name).to.equal('Documents Admin') + + await udaru.policies.deleteShared({id: result.id}) + }) + + lab.test('update shared policy without a service key should return 403 Forbidden', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/shared-policies/whatever?sig=123', + payload: { + version: '2016-07-01', + name: 'Documents Admin', + statements + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('update shared policy without valid data should return 400 Bad Request', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/shared-policies/whatever?sig=123456789', + payload: { + version: '2016-07-01', + name: 'Documents Admin' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('update shared policy should return the updated policy data', async () => { + const p = await udaru.policies.createShared(sharedPolicyCreateData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/shared-policies/${p.id}?sig=123456789`, + payload: { + version: '1234', + name: 'new policy name', + statements: { + Statement: [ + { + Effect: 'Deny', + Action: ['documents:Read'], + Resource: ['wonka:documents:/public/*'] + } + ] + } + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.name).to.equal('new policy name') + expect(result.version).to.equal('1234') + expect(result.statements).to.equal({ Statement: [{ Action: ['documents:Read'], Effect: 'Deny', Resource: ['wonka:documents:/public/*'] }] }) + + await udaru.policies.deleteShared({id: p.id}) + }) + + lab.test('delete shared policy without a service key should return 403 Forbidden', async () => { + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/shared-policies/policyId1?sig=1234' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) + + lab.test('delete shared policy should return 204', async () => { + const p = await udaru.policies.createShared(sharedPolicyCreateData) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/shared-policies/${p.id}?sig=123456789` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + }) +}) + +lab.experiment('Shared policies - get/list', () => { + let server = null + + const sharedPolicyCreateData = { + id: 'sharedPolicyTestX', + version: '2016-07-01', + name: 'Documents Admin', + statements + } + + lab.before(async () => { + server = await serverFactory() + await udaru.policies.createShared(sharedPolicyCreateData) + }) + + lab.after(async () => { + try { + await udaru.policies.deleteShared({id: sharedPolicyCreateData.id}) + } catch (e) { + // This is needed to ignore the error (i.e. in case the policy wasn't properly created) + } + }) + + lab.test('get policy list has default pagination params', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/shared-policies' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.page).to.equal(1) + expect(response.result.limit).greaterThan(1) + }) + + lab.test('get policy list: limit', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/shared-policies?limit=1&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).greaterThan(1) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(1) + expect(result.data.length).to.equal(1) + }) + + lab.test('get policy list', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/shared-policies?limit=500&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).lessThan(result.limit) // Will fail if we need to increase limit + let accountantPolicy = _.find(result.data, {id: 'sharedPolicyId1'}) + expect(accountantPolicy).to.equal({ + id: 'sharedPolicyId1', + version: '0.1', + name: 'Shared policy from fixtures', + statements: { + Statement: [{ + Effect: 'Allow', + Action: ['Read'], + Resource: ['/myapp/documents/*'] + }] + } + }) + }) + + lab.test('get single policy', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/shared-policies/sharedPolicyTestX' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal(sharedPolicyCreateData) + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/teams.test.js b/packages/hapi-auth-udaru/test/endToEnd/teams.test.js new file mode 100644 index 00000000..a4dabeab --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/teams.test.js @@ -0,0 +1,1379 @@ +'use strict' + +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const utils = require('@nearform/udaru-core/test/testUtils') +const serverFactory = require('../test-server') +const udaru = require('@nearform/udaru-core')() + +const teamData = { + name: 'testTeam', + description: 'This is a test team', + parentId: null, + organizationId: 'WONKA' +} + +const metadata = { key1: 'val1', key2: 'val2' } +const teamDataMeta = { + name: 'testTeamMeta', + description: 'This is a test team with metadata', + parentId: null, + organizationId: 'WONKA', + metadata: metadata +} + +lab.experiment('Teams - search', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('should perform the search', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams/search?query=p' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.total).equal(2) + }) +}) + +lab.experiment('Teams - get/list', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('get team list: with pagination params', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.page).to.equal(1) + expect(response.result.limit).greaterThan(1) + }) + + lab.test('get teams list from organization with no team', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams', + headers: { + authorization: 'ROOTid' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(200) + expect(response.result.page).to.equal(1) + expect(response.result.limit).greaterThan(1) + expect(response.result.total).equal(0) + }) + + lab.test('get team list: page 1', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams?limit=3&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(3) + expect(result.total).to.equal(6) + expect(result.data).to.equal([ + { + id: '1', + name: 'Admins', + organizationId: 'WONKA', + description: 'Administrators of the Authorization System', + path: '1', + usersCount: 1 + }, + { + id: '3', + name: 'Authors', + organizationId: 'WONKA', + description: 'Content contributors', + path: '3', + usersCount: 1 + }, + { + id: '6', + name: 'Company Lawyer', + organizationId: 'WONKA', + description: 'Author of legal documents', + path: '6', + usersCount: 0 + } + ]) + }) + + lab.test('get team list: page 2', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams?limit=3&page=2' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(2) + expect(result.limit).to.equal(3) + expect(result.total).to.equal(6) + expect(result.data).to.equal([ + { + id: '4', + name: 'Managers', + organizationId: 'WONKA', + description: 'General Line Managers with confidential info', + path: '4', + usersCount: 1 + }, + { + id: '5', + name: 'Personnel Managers', + organizationId: 'WONKA', + description: 'Personnel Line Managers with confidential info', + path: '5', + usersCount: 1 + }, + { + id: '2', + name: 'Readers', + organizationId: 'WONKA', + description: 'General read-only access', + path: '2', + usersCount: 2 + } + ]) + }) + + lab.test('get team list', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams?page=1&limit=7' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.data).to.equal([ + { + id: '1', + name: 'Admins', + organizationId: 'WONKA', + description: 'Administrators of the Authorization System', + path: '1', + usersCount: 1 + }, + { + id: '3', + name: 'Authors', + organizationId: 'WONKA', + description: 'Content contributors', + path: '3', + usersCount: 1 + }, + { + id: '6', + name: 'Company Lawyer', + organizationId: 'WONKA', + description: 'Author of legal documents', + path: '6', + usersCount: 0 + }, + { + id: '4', + name: 'Managers', + organizationId: 'WONKA', + description: 'General Line Managers with confidential info', + path: '4', + usersCount: 1 + }, + { + id: '5', + name: 'Personnel Managers', + organizationId: 'WONKA', + description: 'Personnel Line Managers with confidential info', + path: '5', + usersCount: 1 + }, + { + id: '2', + name: 'Readers', + organizationId: 'WONKA', + description: 'General read-only access', + path: '2', + usersCount: 2 + } + ]) + }) + + lab.test('get single team', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${team.id}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.usersCount).to.exist() + expect(result.usersCount).to.equal(0) + expect(result.id).to.equal(team.id) + expect(result.name).to.equal(team.name) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('get single team with metadata', async () => { + const team = await udaru.teams.create(teamDataMeta) + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${team.id}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.usersCount).to.exist() + expect(result.usersCount).to.equal(0) + expect(result.id).to.equal(team.id) + expect(result.name).to.equal(team.name) + expect(result.metadata).to.equal(team.metadata) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('get users for a single team', async () => { + let team = await udaru.teams.create(teamData) + + const teamUsers = [ + { id: 'AugustusId', name: 'Augustus Gloop' }, + { id: 'CharlieId', name: 'Charlie Bucket' }, + { id: 'MikeId', name: 'Mike Teavee' }, + { id: 'VerucaId', name: 'Veruca Salt' }, + { id: 'WillyId', name: 'Willy Wonka' } + ] + const teamUsersIds = teamUsers.map((user) => user.id) + + team = await udaru.teams.addUsers({ id: team.id, organizationId: team.organizationId, users: teamUsersIds }) + expect(team.users).to.equal(teamUsers) + + let options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${team.id}/users?page=1&limit=10` + }) + + let response = await server.inject(options) + let result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(10) + expect(result.total).to.equal(5) + expect(result.data).to.equal(teamUsers) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${team.id}/users?page=2&limit=3` + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(2) + expect(result.limit).to.equal(3) + expect(result.total).to.equal(5) + expect(result.data).to.equal([ + { id: 'VerucaId', name: 'Veruca Salt' }, + { id: 'WillyId', name: 'Willy Wonka' } + ]) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('return 404 if team does not exist when requesting users', async () => { + const teamId = 'idontexist' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${teamId}/users` + }) + + const response = await server.inject(options) + const result = response.result + + expect(result.statusCode).to.equal(404) + expect(result.data).to.not.exist() + expect(result.total).to.not.exist() + expect(result.error).to.exist() + expect(result.message).to.exist() + expect(result.message.toLowerCase()).to.include(teamId).include('not').include('found') + }) +}) + +lab.experiment('Teams - create', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('Create with no id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + name: 'Team B', + description: 'This is Team B' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.be.null() + expect(result).to.contain({ + name: 'Team B', + organizationId: 'WONKA', + description: 'This is Team B', + users: [], + policies: [], + path: result.id + }) + + await udaru.teams.delete({ id: result.id, organizationId: result.organizationId }) + }) + + lab.test('Create with undefined id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: undefined, + name: 'Team B', + description: 'This is Team B' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.be.null() + expect(result).to.contain({ + name: 'Team B', + organizationId: 'WONKA', + description: 'This is Team B', + users: [], + policies: [], + path: result.id + }) + + await udaru.teams.delete({ id: result.id, organizationId: result.organizationId }) + }) + + lab.test('Create with specific id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: 'test_fixed_id', + name: 'Team B', + description: 'This is Team B' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result).to.contain({ + id: 'test_fixed_id', + path: 'test_fixed_id' + }) + + await udaru.teams.delete({ id: result.id, organizationId: result.organizationId }) + }) + + lab.test('create team with empty id string', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: '', + name: 'Team B', + description: 'This is Team B' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.id).to.not.exist() + }) + + lab.test('create team with null id string', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: null, + name: 'Team B', + description: 'This is Team B' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.id).to.not.exist() + }) + + lab.test('support handling of already present id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: '1', + name: 'Team already present', + description: 'This is already present' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(409) + expect(result.message).to.equal('Key (id)=(1) already exists.') + }) + + lab.test('Create a team with metadata', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: 'test_meta_id', + name: 'Team Meta', + description: 'This is Team Meta', + metadata: metadata + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result).to.contain({ + id: 'test_meta_id', + path: 'test_meta_id', + description: 'This is Team Meta', + metadata: metadata + }) + + await udaru.teams.delete({ id: result.id, organizationId: result.organizationId }) + }) + + lab.test('validates specific id format', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: { + id: 'invalid-id', + name: 'Team B', + description: 'This is Team B' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + expect(response.result.validation.keys).to.include('id') + }) + + lab.test('should return a 400 Bad Request when not providing name or description', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams', + payload: {} + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) +}) + +lab.experiment('Teams - update', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('update team validation nothing in payload', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/2', + payload: { + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('update only team name', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${team.id}`, + payload: { + name: 'Team C' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal(team.id) + expect(result.name).to.equal('Team C') + + await udaru.teams.delete({id: team.id, organizationId: team.organizationId}) + }) + + lab.test('update only team description', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${team.id}`, + payload: { + description: 'Team B is now Team C' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal(team.id) + expect(result.description).to.equal('Team B is now Team C') + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('update team', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${team.id}`, + payload: { + name: 'Team C', + description: 'Team B is now Team C' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal(team.id) + expect(result.name).to.equal('Team C') + expect(result.description).to.equal('Team B is now Team C') + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('update team with metadata', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${team.id}`, + payload: { + name: 'Team Meta', + description: 'Team B is now Team Meta', + metadata: metadata + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal(team.id) + expect(result.name).to.equal('Team Meta') + expect(result.description).to.equal('Team B is now Team Meta') + expect(result.metadata).to.equal(metadata) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) +}) + +lab.experiment('Teams - delete', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('delete team should return 204 for success', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/teams/${team.id}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(204) + expect(result).to.not.exist() + }) +}) + +lab.experiment('Teams - manage users', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('add users to a team', async () => { + const team = await udaru.teams.create(teamData) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${team.id}/users`, + payload: { + users: ['CharlieId', 'MikeId'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal(team.id) + expect(result.users).to.equal([ + { id: 'CharlieId', name: 'Charlie Bucket' }, + { id: 'MikeId', name: 'Mike Teavee' } + ]) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('replace users in a team', async () => { + let team = await udaru.teams.create(teamData) + team = await udaru.teams.addUsers({ id: team.id, organizationId: team.organizationId, users: ['CharlieId'] }) + + const options = utils.requestOptions({ + method: 'POST', + url: `/authorization/teams/${team.id}/users`, + payload: { + users: ['MikeId'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal(team.id) + expect(result.users).to.equal([ + { id: 'MikeId', name: 'Mike Teavee' } + ]) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('delete all team members', async () => { + let team = await udaru.teams.create(teamData) + team = await udaru.teams.addUsers({ id: team.id, organizationId: team.organizationId, users: ['CharlieId', 'MikeId'] }) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/teams/${team.id}/users` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('delete one team member', async () => { + let team = await udaru.teams.create(teamData) + team = await udaru.teams.addUsers({ id: team.id, organizationId: team.organizationId, users: ['CharlieId', 'MikeId'] }) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/teams/${team.id}/users/CharlieId` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + }) + + lab.test('default team admin should be able to assign users to own team', async () => { + const team = await udaru.teams.create({ + name: 'Team 5', + description: 'This is a test team', + parentId: null, + organizationId: 'WONKA', + user: { id: 'test-admin', name: 'Test Admin' } + }) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${team.id}/users`, + headers: { + authorization: 'test-admin' + }, + payload: { + users: ['CharlieId', 'MikeId'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: team.id, + name: 'Team 5', + description: 'This is a test team', + path: team.path, + organizationId: 'WONKA', + usersCount: 3, + users: [ + { id: 'CharlieId', name: 'Charlie Bucket' }, + { id: 'MikeId', name: 'Mike Teavee' }, + { id: 'test-admin', name: 'Test Admin' } + ], + policies: [] + }) + + await udaru.teams.delete({ id: team.id, organizationId: team.organizationId }) + await udaru.users.delete({ id: 'test-admin', organizationId: team.organizationId }) + }) + + lab.experiment('Teams - nest/un-nest', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('Nest team should update the team path', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/2/nest', + payload: { + parentId: '3' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.path).to.equal('3.2') + + await udaru.teams.move({ id: result.id, parentId: null, organizationId: result.organizationId }) + }) + + lab.test('Un-nest team should update the team path', async () => { + const res = await udaru.teams.move({ id: '2', parentId: '3', organizationId: 'WONKA' }) + + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${res.id}/unnest` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.path).to.equal('2') + }) + }) + + lab.experiment('Teams - manage policies', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('Add one policy to a team', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/1/policies', + payload: { + policies: ['policyId2'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.equal([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {} }, + { id: 'policyId1', name: 'Director', version: '0.1', variables: {} } + ]) + + await udaru.teams.replacePolicies({ id: result.id, policies: ['policyId1'], organizationId: result.organizationId }) + }) + }) + + lab.test('Add one policy with variables to a team', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/1/policies', + payload: { + policies: [{ + id: 'policyId2', + variables: { var1: 'value1' } + }] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.equal([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: { var1: 'value1' } }, + { id: 'policyId1', name: 'Director', version: '0.1', variables: {} } + ]) + + await udaru.teams.replacePolicies({ id: result.id, policies: ['policyId1'], organizationId: result.organizationId }) + }) + + lab.test('Policy instance addition and removal', async () => { + let options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/2/policies', + payload: { + policies: [{ + id: 'policyId2', + variables: {var1: 'value1'} + }] + } + }) + + let response = await server.inject(options) + let result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.equal([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {var1: 'value1'} } + ]) + + const firstInstance = result.policies[0].instance + + options.payload = { + policies: [{ + id: 'policyId2', + variables: {var2: 'value2'} + }, { + id: 'policyId2', + variables: {var3: 'value3'} + }] + } + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(3) + expect(utils.PoliciesWithoutInstance(result.policies)).to.contain([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {var3: 'value3'} } + ]) + + options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/teams/2/policies/policyId2?instance=${firstInstance}` + }) + + response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/2` + }) + + response = await server.inject(options) + result = response.result + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(2) + expect(utils.PoliciesWithoutInstance(result.policies)).to.not.contain([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {var1: 'value1'} } + ]) + + options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/teams/2/policies/policyId2` + }) + + response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/2` + }) + + response = await server.inject(options) + result = response.result + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(0) + + await udaru.teams.replacePolicies({ id: result.id, policies: ['policyId1'], organizationId: result.organizationId }) + }) + + lab.test('Add to one team a policy with invalid ID should return an error', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/1/policies', + payload: { + policies: ['InvalidID'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Add one policy from another org to a team should return an error', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/1/policies', + payload: { + policies: ['policyId9'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Add multiple policies to a team', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/1/policies', + payload: { + policies: ['policyId4', 'policyId5', 'policyId6'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.equal([ + { id: 'policyId5', name: 'DB Admin', version: '0.1', variables: {} }, + { id: 'policyId6', name: 'DB Only Read', version: '0.1', variables: {} }, + { id: 'policyId1', name: 'Director', version: '0.1', variables: {} }, + { id: 'policyId4', name: 'Finance Director', version: '0.1', variables: {} } + ]) + + await udaru.teams.replacePolicies({ id: result.id, policies: ['policyId1'], organizationId: result.organizationId }) + }) + + lab.test('Replace team policies', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams/1/policies', + payload: { + policies: ['policyId6'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.equal([{ + id: 'policyId6', + name: 'DB Only Read', + version: '0.1', + variables: {} + }]) + + await udaru.teams.replacePolicies({ id: result.id, policies: ['policyId1'], organizationId: result.organizationId }) + }) + + lab.test('Replace team policies with a policy with invalid ID should return an error', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams/1/policies', + payload: { + policies: ['InvalidID'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Replace team policies from another org should return an error', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams/1/policies', + payload: { + policies: ['policyId9'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Delete team policies', async () => { + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/teams/1/policies' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + await udaru.teams.replacePolicies({ id: '1', policies: ['policyId1'], organizationId: 'WONKA' }) + }) + + lab.test('Delete specific team policy', async () => { + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/teams/1/policies/policyId1' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + await udaru.teams.replacePolicies({ id: '1', policies: ['policyId1'], organizationId: 'WONKA' }) + }) +}) + +lab.experiment('Teams - checking org_id scoping', () => { + let teamId + let server = null + + lab.before(async () => { + server = await serverFactory() + + await udaru.organizations.create({ id: 'NEWORG', name: 'new org', description: 'new org' }) + const team = udaru.teams.create({ name: 'otherTeam', description: 'd', parentId: null, organizationId: 'NEWORG' }) + await udaru.users.create({ id: 'testUserId', name: 'testUser', organizationId: 'NEWORG' }) + teamId = team.id + }) + + lab.after(async () => { + udaru.organizations.delete('NEWORG') + }) + + lab.test('Adding a user with invalid ID should not be permitted', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/2/users', + payload: { + users: ['invalidUserId'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Adding a user from another organization should not be permitted', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/2/users', + payload: { + users: ['testUserId'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Adding multiple users from different organizations should not be permitted', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/teams/2/users', + payload: { + users: ['testUserId', 'MikeId'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Adding a user with invalid ID should not be permitted', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams/2/users', + payload: { + users: ['InvalidUserId'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Replacing users from another organization should not be permitted', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/teams/2/users', + payload: { + users: ['testUserId', 'MikeId'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('moving a team to another organization should not be permitted', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: `/authorization/teams/${teamId}/nest`, + payload: { + parentId: '1' + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('get error if team does not exist', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/IDONTEXIST/nested` + }) + + const response = await server.inject(options) + const result = response.result + + expect(result.statusCode).to.equal(404) + expect(result.error).to.exist() + expect(result.message).to.include('not').include('found') + }) + + lab.test('get nested team list with default paging', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams/3/nested' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.greaterThan(1) + expect(result.total).to.equal(1) + expect(result.data).to.equal([ + { + id: '6', + name: 'Company Lawyer', + description: 'Author of legal documents', + parentId: '3', + path: '6', + organizationId: 'WONKA', + usersCount: 0 + } + ]) + }) + + lab.test('get nested team list with paging', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams/3/nested?limit=1&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(1) + expect(result.total).to.equal(1) + expect(result.data).to.equal([ + { + id: '6', + name: 'Company Lawyer', + description: 'Author of legal documents', + parentId: '3', + path: '6', + organizationId: 'WONKA', + usersCount: 0 + } + ]) + }) + + lab.test('get nested team list with bad paging param', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams/3/nested?limit=1&page=0' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.message).to.exist() + expect(result.data).to.not.exist() + }) + + lab.test('get nested team list with bad limit param', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/teams/3/nested?limit=0&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.message).to.exist() + expect(result.data).to.not.exist() + }) +}) + +lab.experiment('Teams User Search', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('searching for a real user in an existing team', async () => { + const teamId = '4' + const query = 'Will' + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${teamId}/users/search?query=${query}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.data).to.exist() + expect(result.total).to.exist() + + expect(result.data.length).to.equal(1) + expect(result.total).to.equal(1) + }) + + lab.test('searching for a user that does not exist in an existing team', async () => { + const teamId = '4' + const query = 'IDONTEXIST' + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${teamId}/users/search?query=${query}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.data).to.exist() + expect(result.total).to.exist() + + expect(result.data.length).to.equal(0) + expect(result.total).to.equal(0) + }) + + lab.test('searching for a real user in a non-existing team', async () => { + const teamId = 'IDONTEXIST' + const query = 'Will' + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${teamId}/users/search?query=${query}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(404) + expect(result.data).to.not.exist() + expect(result.total).to.not.exist() + + expect(result.error).to.exist() + expect(result.message).to.include('not').include('found') + }) + + lab.test('missing query string', async () => { + const teamId = 'IDONTEXIST' + + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams/${teamId}/users/search?query=` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + + expect(result.error).to.exist() + expect(result.error.toLowerCase()).to.include('bad').include('request') + }) + + lab.test('missing team id param string', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/teams//users/search?query='query'` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(404) + + expect(result.error).to.exist() + expect(result.message.toLowerCase()).to.include('not').include('found') + }) +}) diff --git a/packages/hapi-auth-udaru/test/endToEnd/users.test.js b/packages/hapi-auth-udaru/test/endToEnd/users.test.js new file mode 100644 index 00000000..2b1a702d --- /dev/null +++ b/packages/hapi-auth-udaru/test/endToEnd/users.test.js @@ -0,0 +1,960 @@ +'use strict' + +const _ = require('lodash') +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const config = require('../../lib/config')() +const utils = require('@nearform/udaru-core/test/testUtils') +const serverFactory = require('../test-server') +const udaru = require('@nearform/udaru-core')() + +const defaultPageSize = config.get('authorization.defaultPageSize') +const statements = { Statement: [{ Effect: 'Allow', Action: ['documents:Read'], Resource: ['wonka:documents:/public/*'] }] } + +const policyCreateData = { + version: '2016-07-01', + name: 'Documents Admin', + statements, + organizationId: 'WONKA' +} + +lab.experiment('Users: read - delete - update', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('user list should have default pagination', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).greaterThan(1) + expect(result.total).to.be.at.least(7) + expect(result.data.length).to.equal(result.total) + }) + + lab.test('get user list', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?page=1&limit=3' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).to.equal(7) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(3) + expect(result.data.length).to.equal(3) + expect(result.data[0]).to.equal({ + id: 'AugustusId', + name: 'Augustus Gloop', + organizationId: 'WONKA' + }) + }) + + lab.test('no users list', async () => { + const options = { + headers: { + authorization: 'ROOTid', + org: 'OILCOEMEA' + }, + method: 'GET', + url: '/authorization/users' + } + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(defaultPageSize) + expect(result.total).to.equal(0) + expect(result.data.length).to.equal(0) + }) + + lab.test('get single user', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users/AugustusId' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'AugustusId', + name: 'Augustus Gloop', + organizationId: 'WONKA', + policies: [], + teams: [ + { + id: '1', + name: 'Admins' + } + ] + }) + }) + + lab.test('get single user with metadata', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users/MikeId' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'MikeId', + name: 'Mike Teavee', + organizationId: 'WONKA', + metadata: { key1: 'val1', key2: 'val2' }, + policies: [], + teams: [] + }) + }) + + lab.test('delete user should return 204 if success', async () => { + await udaru.users.create({ name: 'test', id: 'testId', organizationId: 'ROOT' }) + + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/users/testId', + headers: { + authorization: 'ROOTid' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(204) + expect(result).to.not.exist() + }) + + lab.test('update user should return 200 for success', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/ModifyId', + payload: { + name: 'Modify you' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'ModifyId', + name: 'Modify you', + organizationId: 'WONKA', + teams: [], + policies: [] + }) + + udaru.users.update({ name: 'Modify Me', id: 'ModifyId', organizationId: 'WONKA' }) + }) + + lab.test('update user with metadata field and 200ok response', async () => { + const metadata = { key1: 1, key2: 'y' } + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/ModifyId', + payload: { + name: 'Modify you', + metadata: metadata + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ + id: 'ModifyId', + name: 'Modify you', + organizationId: 'WONKA', + metadata: metadata, + teams: [], + policies: [] + }) + + await udaru.users.update({ name: 'Modify Me', id: 'ModifyId', organizationId: 'WONKA' }) + }) +}) + +lab.experiment('Users - create', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.after(async () => { + try { + await Promise.all([ + udaru.users.delete({ id: 'testId' }), + udaru.users.delete({ id: 'testId', organizationId: 'OILCOUSA' }) + ]) + } catch (e) { + // This is needed to ignore the error (i.e. in case the users weren't properly created) + } + }) + + lab.test('create user for a non existent organization', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + name: 'Salman', + id: 'testId' + }, + headers: { + authorization: 'ROOTid', + org: 'DOES_NOT_EXISTS' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result).to.equal({ + error: 'Bad Request', + message: `Organization 'DOES_NOT_EXISTS' does not exist`, + statusCode: 400 + }) + }) + + lab.test('create user for a specific organization being a SuperUser', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + name: 'Salman', + id: 'testId' + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.equal('testId') + expect(result.name).to.equal('Salman') + expect(result.organizationId).to.equal('OILCOUSA') + + await udaru.users.delete({ id: 'testId', organizationId: 'OILCOUSA' }) + }) + + lab.test('create user for a specific organization being a SuperUser with some metadata', async () => { + const metadata = { key1: 1, key2: 'y' } + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + name: 'Salman', + id: 'testId', + metadata: metadata + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.equal('testId') + expect(result.name).to.equal('Salman') + expect(result.organizationId).to.equal('OILCOUSA') + expect(result.metadata).to.equal(metadata) + + await udaru.users.delete({ id: 'testId', organizationId: 'OILCOUSA' }) + }) + + lab.test('create user for a specific organization being a SuperUser but without specifying the user id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + name: 'Salman' + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.be.null() + expect(result.name).to.equal('Salman') + expect(result.organizationId).to.equal('OILCOUSA') + + await udaru.users.delete({ id: result.id, organizationId: 'OILCOUSA' }) + }) + + lab.test('create user for a specific organization being a SuperUser with a specified undefined user id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + id: undefined, + name: 'Salman' + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.not.be.null() + expect(result.name).to.equal('Salman') + expect(result.organizationId).to.equal('OILCOUSA') + + await udaru.users.delete({ id: result.id, organizationId: 'OILCOUSA' }) + }) + + lab.test('create user for a specific organization being a SuperUser but with specifying empty string user id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + id: '', + name: 'Salman' + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.id).to.not.exist() + }) + + lab.test('create user for a specific organization being a SuperUser but with specifying null user id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + id: null, + name: 'Salman' + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.equal('Bad Request') + expect(result.id).to.not.exist() + }) + + lab.test('create user for a specific organization being a SuperUser with an already used id', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + id: 'ROOTid', + name: 'Salman' + }, + headers: { + authorization: 'ROOTid', + org: 'OILCOUSA' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(409) + expect(result.message).to.equal('Key (id)=(ROOTid) already exists.') + }) + + lab.test('create user for the admin organization', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: { + name: 'Salman', + id: 'U2FsbWFu' + }, + headers: { + authorization: 'ROOTid' + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(201) + expect(result.id).to.equal('U2FsbWFu') + expect(result.name).to.equal('Salman') + expect(result.organizationId).to.equal('ROOT') + + await udaru.users.delete({ id: result.id, organizationId: 'ROOT' }) + }) + + lab.test('create user should return 400 bad request if input validation fails', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users', + payload: {} + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result).to.include({ + statusCode: 400, + error: 'Bad Request' + }) + }) +}) + +lab.experiment('Users - manage policies', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('add policies to a user', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: [p.id] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies[0].id).to.equal(p.id) + + await udaru.users.deletePolicies({ id: 'ModifyId', organizationId: 'WONKA' }) + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('add policies with variables to a user', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: [{ + id: p.id, + variables: { var1: 'value1' } + }] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies[0].id).to.equal(p.id) + expect(result.policies[0].variables).to.equal({ var1: 'value1' }) + + await udaru.users.deletePolicies({ id: 'ModifyId', organizationId: 'WONKA' }) + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('Policy instance addition and removal', async () => { + let options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/VerucaId/policies', + payload: {policies: []} + }) + + let response = await server.inject(options) + let result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(0) + + options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/VerucaId/policies', + payload: { + policies: [{ + id: 'policyId2', + variables: {var1: 'value1'} + }] + } + }) + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(utils.PoliciesWithoutInstance(result.policies)).to.contain([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {var1: 'value1'} } + ]) + + const firstInstance = result.policies[0].instance + + options.payload = { + policies: [{ + id: 'policyId2', + variables: {var2: 'value2'} + }, { + id: 'policyId2', + variables: {var3: 'value3'} + }] + } + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(3) + expect(utils.PoliciesWithoutInstance(result.policies)).to.contain([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {var3: 'value3'} } + ]) + + options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/users/VerucaId/policies/policyId2?instance=${firstInstance}` + }) + + response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/VerucaId` + }) + + response = await server.inject(options) + result = response.result + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(2) + expect(utils.PoliciesWithoutInstance(result.policies)).to.not.contain([ + { id: 'policyId2', name: 'Accountant', version: '0.1', variables: {var1: 'value1'} } + ]) + + options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/users/VerucaId/policies/policyId2` + }) + + response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/VerucaId` + }) + + response = await server.inject(options) + result = response.result + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(0) + + await udaru.users.replacePolicies({ id: result.id, policies: ['policyId2'], organizationId: result.organizationId }) + }) + + lab.test('add policy with invalid ID to a user', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: ['InvalidPolicyID'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('replace policies with a policy with invalid ID should return an error', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: ['InvalidPolicyID'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('clear and replace policies for a user', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: [p.id] + } + }) + + let response = await server.inject(options) + let result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(1) + expect(result.policies[0].id).to.equal(p.id) + + const newP = await udaru.policies.create(policyCreateData) + + options.payload.policies = [newP.id] + + response = await server.inject(options) + result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.policies.length).to.equal(1) + expect(result.policies[0].id).to.equal(newP.id) + + await udaru.users.deletePolicies({ id: 'ModifyId', organizationId: 'WONKA' }) + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + await udaru.policies.delete({ id: newP.id, organizationId: 'WONKA' }) + }) + + lab.test('remove all user\'s policies', async () => { + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/users/ModifyId/policies' + }) + + const p = await udaru.policies.create(policyCreateData) + + await udaru.users.addPolicies({ id: 'ModifyId', organizationId: 'WONKA', policies: [p.id] }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + const user = await udaru.users.read({ id: 'ModifyId', organizationId: 'WONKA' }) + expect(user.policies).to.equal([]) + + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('remove one user\'s policies', async () => { + const p = await udaru.policies.create(policyCreateData) + + await udaru.users.addPolicies({ id: 'ModifyId', organizationId: 'WONKA', policies: [p.id] }) + + const options = utils.requestOptions({ + method: 'DELETE', + url: `/authorization/users/ModifyId/policies/${p.id}` + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(204) + + await udaru.users.read({ id: 'ModifyId', organizationId: 'WONKA' }) + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) +}) + +lab.experiment('Users - checking org_id scoping', () => { + let policyId + let server = null + + lab.before(async () => { + server = await serverFactory() + + await udaru.organizations.create({ id: 'NEWORG', name: 'new org', description: 'new org' }) + + const policyData = { + version: '1', + name: 'Documents Admin', + organizationId: 'NEWORG', + statements + } + + const policy = await udaru.policies.create(policyData) + + policyId = policy.id + }) + + lab.afterEach(async () => { + try { + await udaru.organizations.delete('NEWORG') + } catch (e) { + // This is needed to ignore the error (i.e. in case the organization wasn't properly created) + } + }) + + lab.test('add policies from a different organization should not be allowed', async () => { + const options = utils.requestOptions({ + method: 'PUT', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: [policyId] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('replace policies from a different organization should not be allowed', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/ModifyId/policies', + payload: { + policies: [policyId] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) +}) + +lab.experiment('Users - manage teams', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('get user teams', async () => { + const userId = 'VerucaId' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/${userId}/teams` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).to.equal(2) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(defaultPageSize) + expect(result.data.length).to.equal(2) + let expectedTeams = [ + 'Authors', + 'Readers' + ] + expect(_.map(result.data, 'name')).to.only.contain(expectedTeams) + }) + + lab.test('get user teams, invalid userId', async () => { + const userId = 'invalidid' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/${userId}/teams` + }) + + const response = await server.inject(options) + const result = response.result + + expect(result.statusCode).to.equal(404) + expect(result.data).to.not.exist() + expect(result.total).to.not.exist() + expect(result.error).to.exist() + expect(result.message).to.exist() + expect(result.message.toLowerCase()).to.include(userId).include('not').include('found') + }) + + lab.test('get user teams, user in no teams', async () => { + const userId = 'ModifyId' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/${userId}/teams` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).to.equal(0) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(defaultPageSize) + expect(result.data.length).to.equal(0) + }) + + lab.test('get user teams, paginated', async () => { + const userId = 'VerucaId' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/${userId}/teams?page=2&limit=1` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).to.equal(2) + expect(result.page).to.equal(2) + expect(result.limit).to.equal(1) + expect(result.data.length).to.equal(1) + let expectedTeams = [ + 'Readers' + ] + expect(_.map(result.data, 'name')).contains(expectedTeams) + }) + + lab.test('replace users teams', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/ModifyId/teams', + payload: { + teams: ['2', '3'] + } + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal('ModifyId') + expect(result.teams).to.equal([{ id: '3', name: 'Authors' }, { id: '2', name: 'Readers' }]) + + await udaru.users.deleteTeams({ id: result.id, organizationId: result.organizationId, teams: [] }) + }) + + lab.test('replace users teams for non-existent user', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/xyz/teams', + payload: { + teams: ['2', '3'] + } + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + expect(response.result.message).to.equal('User xyz not found') + }) + + lab.test('replace users teams for non-existent user (bad format)', async () => { + const options = utils.requestOptions({ + method: 'POST', + url: '/authorization/users/xyz/teams', + payload: ['1'] + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + expect(response.result.message).to.equal('No teams found in payload') + }) + + lab.test('Delete user from her teams', async () => { + udaru.users.replaceTeams({ id: 'ModifyId', organizationId: 'WONKA', teams: ['2', '3'] }) + + const options = utils.requestOptions({ + method: 'DELETE', + url: '/authorization/users/ModifyId/teams' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.id).to.equal('ModifyId') + expect(result.teams).to.equal([]) + }) +}) + +lab.experiment('Users - search for user', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test(`search with empty query`, async () => { + const query = '' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/search?query=${query}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(400) + expect(result.error).to.exist() + expect(result.validation).to.exist() + expect(result.error.toLowerCase()).to.include('bad').include('request') + }) + + lab.test(`search with query value 'm'`, async () => { + const query = 'm' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/search?query=${query}` + }) + + const response = await server.inject(options) + const result = response.result + const expectedUsers = [ + 'Many Polices', + 'Mike Teavee', + 'Modify Me' + ] + + expect(response.statusCode).to.equal(200) + expect(result.total).to.equal(3) + expect(result.data.length).to.equal(3) + + expect(_.map(result.data, 'name')).to.only.contain(expectedUsers) + expect(result.data.every(d => d.organizationId === 'WONKA')).to.be.true() + }) + + lab.test(`search with query value 'IDONTEXIST'`, async () => { + const query = 'IDONTEXIST' + const options = utils.requestOptions({ + method: 'GET', + url: `/authorization/users/search?query=${query}` + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.total).to.equal(0) + expect(result.data.length).to.equal(0) + }) +}) diff --git a/packages/hapi-auth-udaru/test/security/sqlinjection.test.js b/packages/hapi-auth-udaru/test/security/sqlinjection.test.js new file mode 100644 index 00000000..3564c835 --- /dev/null +++ b/packages/hapi-auth-udaru/test/security/sqlinjection.test.js @@ -0,0 +1,207 @@ +'use strict' + +const expect = require('code').expect +const Lab = require('lab') +const lab = exports.lab = Lab.script() +const utils = require('@nearform/udaru-core/test/testUtils') +const udaru = require('@nearform/udaru-core')() +const serverFactory = require('../test-server') + +const statements = { Statement: [{ Effect: 'Allow', Action: ['*'], Resource: ['*'] }] } +const policyCreateData = { + version: '2016-07-01', + name: 'Super Admin', + statements, + organizationId: 'WONKA' +} + +lab.experiment('get users SQL injection tests', () => { + let server = null + + lab.before(async () => { + server = await serverFactory() + }) + + lab.test('initial reference team list control test', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?limit=3&page=1' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(3) + expect(result.total).to.equal(7) + expect(result.data.length).to.equal(3) + }) + + lab.test('Try to inject the limit from paging', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?limit=3%20OR%201=1&page=1' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Try to inject the limit from paging with offset commenting', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?limit=3%20OR%201=1%3B--%20-&page=1' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Try to inject the page from paging functionality', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?limit=3&page=1%20OR%201' + }) + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('Try to inject the org id', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?limit=3&page=1' + }) + options.headers.org = '\'WONKA\' OR 1=1' + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result.page).to.equal(1) + expect(result.limit).to.equal(3) + expect(result.total).to.equal(0) + expect(result.data.length).to.equal(0) + }) + + lab.test('Try to use a long org name', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/users?limit=3&page=1' + }) + + let org = 'abcdefghijk' + for (var i = 0; i < 10; i++) { + org += org + } + options.headers.org = org + + const response = await server.inject(options) + expect(response.statusCode).to.equal(400) + }) + + lab.test('inject admin policy to a user through authorization field', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = { + headers: { + authorization: '\'ManyPoliciesId\' OR 1=1', + org: 'WONKA' + }, + method: 'PUT', + url: '/authorization/users/ManyPoliciesId/policies', + payload: { + policies: [p.id] + } + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(401) + + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('inject org from the adding admin policy to a user endpoint', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = { + headers: { + authorization: 'ManyPoliciesId', + org: '\'WONKA\' OR 1=1' + }, + method: 'PUT', + url: '/authorization/users/ManyPoliciesId/policies', + payload: { + policies: [p.id] + } + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('inject the url from the adding admin policy to a user endpoint', async () => { + const p = await udaru.policies.create(policyCreateData) + + const options = { + headers: { + authorization: 'ManyPoliciesId', + org: 'WONKA' + }, + method: 'PUT', + url: '/authorization/users/*/policies', + payload: { + policies: [p.id] + } + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + + await udaru.policies.delete({ id: p.id, organizationId: 'WONKA' }) + }) + + lab.test('control test - check authorization should return access false for denied', async () => { + const options = utils.requestOptions({ + method: 'GET', + url: '/authorization/access/Modifyid/action_a/resource_a' + }) + + const response = await server.inject(options) + const result = response.result + + expect(response.statusCode).to.equal(200) + expect(result).to.equal({ access: false }) + }) + + lab.test('Test inject header authorization field access route', async () => { + const options = { + headers: { + authorization: '\'ManyPoliciesId\' OR 1=1', + org: 'WONKA' + }, + method: 'GET', + url: '/authorization/access/Modifyid/action_a/resource_a' + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(401) + }) + + lab.test('Test inject header org field authorization access', async () => { + const options = { + headers: { + authorization: 'ManyPoliciesId', + org: '\'WONKA\' or 1=1' + }, + method: 'GET', + url: '/authorization/access/Modifyid/action_a/resource_a' + } + + const response = await server.inject(options) + expect(response.statusCode).to.equal(403) + }) +}) diff --git a/packages/hapi-auth-udaru/test/test-server.js b/packages/hapi-auth-udaru/test/test-server.js new file mode 100644 index 00000000..cf5b4d50 --- /dev/null +++ b/packages/hapi-auth-udaru/test/test-server.js @@ -0,0 +1,72 @@ +const config = require('../lib/config')() +const Action = config.get('AuthConfig.Action') +const Hapi = require('hapi') + +let server = null + +module.exports = async function () { + if (server) return server + + server = Hapi.Server({ + port: Number(config.get('hapi.port')), + host: config.get('hapi.host'), + routes: { + cors: { + additionalHeaders: ['org'] + }, + validate: { // This is to propagate validation keys in Hapi v17 - https://github.com/hapijs/hapi/issues/3706#issuecomment-349765943 + async failAction (request, h, err) { + throw err + } + } + } + }) + + await server.register({plugin: require('..'), options: {config}}) + + server.route({ + method: 'GET', + path: '/no/plugins', + async handler (request) { + return true + }, + config: { + plugins: {} + } + }) + + server.route({ + method: 'GET', + path: '/no/resource', + async handler (request) { + return true + }, + config: { + plugins: { + auth: { + action: Action.CheckAccess + } + } + } + }) + + server.route({ + method: 'POST', + path: '/no/team-resource/{id}', + async handler (request) { + return true + }, + config: { + plugins: { + auth: { + action: Action.DeleteUserTeams, + getParams: (request) => ({ userId: request.params.id }) + } + } + } + }) + + await server.start() + + return server +} diff --git a/packages/hapi-auth-udaru/test/testBuilder.js b/packages/hapi-auth-udaru/test/testBuilder.js new file mode 100644 index 00000000..3004a585 --- /dev/null +++ b/packages/hapi-auth-udaru/test/testBuilder.js @@ -0,0 +1,133 @@ +'use strict' + +const _ = require('lodash') +const { expect } = require('code') +const udaru = require('@nearform/udaru-core')() +const utils = require('@nearform/udaru-core/test/testUtils') + +function Policy (Statement) { + return { + version: '2016-07-01', + name: 'Test Policy', + statements: { + Statement: Statement || [{ + Effect: 'Allow', + Action: ['dummy'], + Resource: ['dummy'] + }] + } + } +} + +function BuildFor (lab, records) { + return new TestBuilder(lab, records) +} + +class TestBuilder { + constructor (lab, records) { + this.lab = lab + this.records = records + } + + endpoint (endpointData) { + this.endpointData = endpointData + return this + } + + server (serverInstance) { + this.serverInstance = serverInstance + return this + } + + test (description) { + const test = new CustomTest(this) + test.test(description) + return test + } + + async startServer () { + if (typeof this.serverInstance !== 'function') return + this.serverInstance = await this.serverInstance() + } +} + +function interpolate (value, data) { + function interpolator (value) { + return interpolate(value, data) + } + + if (_.isArray(value)) { + return _.map(value, interpolator) + } + + if (_.isObject(value)) { + return _.mapValues(value, interpolator) + } + + if (!_.isString(value)) { + return value + } + + return value.replace(/\{\{(.+?)\}\}/, (match, key) => { + return _.get(data, key, match) + }) +} + +class CustomTest { + constructor (builder) { + this.builder = builder + } + + test (description) { + this.description = description + return this + } + + withPolicy (statement) { + this.statement = statement + return this + } + + endpoint (endpointData) { + this.endpointData = endpointData + return this + } + + shouldRespond (statusCode) { + this.statusCode = statusCode + this.build() + return this + } + + skip () { + this._skip = true + return this + } + + build () { + let test = this.builder.lab.test + if (this._skip) { + test = this.builder.lab.test.skip + } + + test(this.description, async () => { + await this.builder.startServer() + + const { records, serverInstance, endpointData: parentEndpointData } = this.builder + const { statusCode, endpointData: childEndpointData, statement } = this + const endpointData = childEndpointData || parentEndpointData + const testedPolicy = records.testedPolicy + + const policyData = Policy(interpolate(statement, records)) + policyData.id = testedPolicy.id + policyData.organizationId = testedPolicy.organizationId || 'WONKA' + + await udaru.policies.update(policyData) + const options = utils.requestOptions(interpolate(endpointData, records)) + const response = await serverInstance.inject(options) + expect(response.statusCode).to.equal(statusCode) + }) + } +} + +module.exports = { BuildFor, udaru } diff --git a/packages/udaru-core/lib/ops/organizationOps.js b/packages/udaru-core/lib/ops/organizationOps.js index 5deadaa9..60c38731 100644 --- a/packages/udaru-core/lib/ops/organizationOps.js +++ b/packages/udaru-core/lib/ops/organizationOps.js @@ -445,14 +445,14 @@ function buildOrganizationOps (db, config) { * @param {Function} cb */ addOrganizationPolicies: function addOrganizationPolicies (params, cb) { - let promise = null - if (typeof cb !== 'function') [promise, cb] = asyncify() - const { id, policies } = params if (policies.length <= 0) { return organizationOps.readById(id, cb) } + let promise = null + if (typeof cb !== 'function') [promise, cb] = asyncify() + const tasks = [ (job, next) => { Joi.validate({ id, policies }, validationRules.addOrganizationPolicies, (err) => { diff --git a/packages/udaru-hapi-plugin/security/authorization.js b/packages/udaru-hapi-plugin/security/authorization.js index 30a941ea..b9a0f44f 100644 --- a/packages/udaru-hapi-plugin/security/authorization.js +++ b/packages/udaru-hapi-plugin/security/authorization.js @@ -64,7 +64,7 @@ function buildAuthorization (config) { const resourceBuilder = server.udaruConfig.get('AuthConfig.resources')[resourceType] if (!resourceBuilder) { - return reply(Boom.badImplentation('Resource builder not found')) + return reply(Boom.badImplementation('Resource builder not found')) } const tasks = teams.map(function (team) {