diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a55920..3903a0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,6 @@ jobs: strategy: matrix: node-version: - - 10.x - 12.x - 14.x steps: @@ -70,10 +69,6 @@ jobs: node-version: "${{ matrix.node-version }}" - name: "Unit Tests with Node.js ${{ matrix.node-version }}" run: | - docker network create --driver=bridge my-network - docker run -d -h mysql --net=my-network -p 3306:3306 --name mysql -v $(pwd)/test/mysql-data:/docker-entrypoint-initdb.d/:ro -e MYSQL_ROOT_PASSWORD=test mysql:5.7 - docker run -d --net=my-network -p 3000:3000 --name keyrock -e IDM_DB_USER=root -e IDM_DB_PASS=test -e IDM_DB_HOST=mysql -e IDM_DB_PORT=3306 fiware/idm:8.0.0 - npm install npm test @@ -89,10 +84,6 @@ jobs: with: node-version: 12.x - run: | - docker network create --driver=bridge my-network - docker run -d -h mysql --net=my-network -p 3306:3306 --name mysql -v $(pwd)/test/mysql-data:/docker-entrypoint-initdb.d/:ro -e MYSQL_ROOT_PASSWORD=test mysql:5.7 - docker run -d --net=my-network -p 3000:3000 --name keyrock -e IDM_DB_USER=root -e IDM_DB_PASS=test -e IDM_DB_HOST=mysql -e IDM_DB_PORT=3306 fiware/idm:8.0.0 - npm install npm run test:coverage - name: Push to Coveralls diff --git a/Dockerfile b/Dockerfile index 5bb006e..c356dfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,9 +84,11 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \ # PEP_PROXY_TOKEN_SECRET # PEP_PROXY_AUTH_ENABLED # PEP_PROXY_PDP -# PEP_PROXY_AZF_PROTOCOL -# PEP_PROXY_AZF_HOST -# PEP_PROXY_AZF_PORT +# PEP_PROXY_PDP_PROTOCOL +# PEP_PROXY_PDP_HOST +# PEP_PROXY_PDP_PORT +# PEP_PROXY_PDP_PATH +# PEP_PROXY_TENANT_HEADER # PEP_PROXY_AZF_CUSTOM_POLICY # PEP_PROXY_PUBLIC_PATHS # PEP_PROXY_CORS_ORIGIN @@ -97,3 +99,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \ # PEP_PROXY_CORS_MAX_AGE # PEP_PROXY_AUTH_FOR_NGINX # PEP_PROXY_MAGIC_KEY +# PEP_PROXY_ERROR_TEMPLATE +# PEP_PROXY_ERROR_CONTENT_TYPE diff --git a/app.js b/app.js new file mode 100644 index 0000000..2032848 --- /dev/null +++ b/app.js @@ -0,0 +1,114 @@ +#!/usr/bin/env node +const cors = require('cors'); +const config_service = require('./lib/config_service'); + +const fs = require('fs'); +const https = require('https'); +const errorhandler = require('errorhandler'); + +const logger = require('morgan'); +const debug = require('debug')('pep-proxy:app'); +const express = require('express'); + +process.on('uncaughtException', function (err) { + debug('Caught exception: ' + err); +}); +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +/** + * Start the express server to listen to all requests. Whitelisted public paths are + * proxied directly, all other requests are restricted access and must either: + * + * - hold a bearer token from an authenticated user + * - hold a bearer token and the user must be authorized to perform the action + * + * @param an auth token representing the PEP + * @param the configuration to use within the app + * + * @return a running express server + */ +exports.start_server = function (token, config) { + config_service.set_config(config, true); + const Root = require('./controllers/root'); + const Payload = require('./lib/payload_analyse'); + const Authorize = require('./lib/authorization_functions'); + const app = express(); + let server; + + // Set logs in development + if (config.debug) { + app.use(logger('dev')); + } + + app.use(function (req, res, next) { + const bodyChunks = []; + req.on('data', function (chunk) { + bodyChunks.push(chunk); + }); + + req.on('end', function () { + if (bodyChunks.length > 0) { + req.body = Buffer.concat(bodyChunks); + } + next(); + }); + }); + + app.disable('x-powered-by'); + app.use(errorhandler({ log: debug })); + app.use(cors(config.cors)); + + let port = config.pep_port || 80; + if (config.https.enabled) { + port = config.https.port || 443; + } + app.set('port', port); + app.set('pepToken', token); + app.set('trust proxy', '127.0.0.1'); + + // The auth mode (authorize or authenticate only) and PDP to adjudicate + // are set in the config. + debug( + 'Starting PEP proxy on port ' + + port + + (config.authorization.enabled + ? '. PDP authorization via ' + config.authorization.pdp + : '. User authentication via IDM') + ); + + for (const p in config.public_paths) { + debug('Public paths', config.public_paths[p]); + app.all(config.public_paths[p], Root.open_access); + } + + if (Authorize.checkPayload()) { + // Oddity for Subscriptions + app.post('/*/subscriptions', Payload.subscription, Root.restricted_access); + app.patch('/*/subscriptions/*', Payload.subscription, Root.restricted_access); + // Oddity for NGSI-v2 + app.all('/*/op/*', Payload.v2batch, Root.restricted_access); + app.use(Payload.query); + app.use(Payload.body); + app.all('/*/entities/:id', Payload.params, Root.restricted_access); + app.all('/*/entities/:id/attrs', Payload.params, Root.restricted_access); + app.all('/*/entities/:id/attrs/:attr', Payload.params, Root.restricted_access); + } + + app.all('/*', Root.restricted_access); + + if (config.https.enabled === true) { + const options = { + key: fs.readFileSync(config.https.key_file), + cert: fs.readFileSync(config.https.cert_file) + }; + + server = https + .createServer(options, function (req, res) { + app.handle(req, res); + }) + .listen(app.get('port')); + } else { + server = app.listen(app.get('port')); + } + return server; +}; diff --git a/bin/healthcheck.js b/bin/healthcheck.js index 5d20833..fb0ec59 100644 --- a/bin/healthcheck.js +++ b/bin/healthcheck.js @@ -1,5 +1,12 @@ #!/usr/bin/env node +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + const http = require('http'); const config = require('../config'); const http_code = process.env.HEALTHCHECK_CODE || 200; @@ -8,19 +15,17 @@ function to_array(env, default_value) { return env !== undefined ? env.split(',') : default_value; } -const public_paths = to_array(process.env.PEP_PROXY_PUBLIC_PATHS, [ - '/iot/about', -]); +const public_paths = to_array(process.env.PEP_PROXY_PUBLIC_PATHS, ['/iot/about']); const options = { host: 'localhost', port: process.env.PEP_PROXY_PORT || config.port, timeout: 2000, method: 'GET', - path: public_paths[0] || '/', + path: public_paths[0] || '/' }; -const request = http.request(options, result => { +const request = http.request(options, (result) => { // eslint-disable-next-line no-console console.info(`Performed health check, result ${result.statusCode}`); if (result.statusCode === http_code) { @@ -30,11 +35,9 @@ const request = http.request(options, result => { } }); -request.on('error', err => { +request.on('error', (err) => { // eslint-disable-next-line no-console - console.error( - `An error occurred while performing health check, error: ${err}` - ); + console.error(`An error occurred while performing health check, error: ${err}`); process.exit(1); }); diff --git a/bin/www b/bin/www index d0ac0c2..1b9f70c 100644 --- a/bin/www +++ b/bin/www @@ -1,111 +1,88 @@ -const config_service = require('../lib/config_service.js'); -config_service.set_config(require('../config.js'), true); -const config = config_service.get_config(); -const cors = require('cors'); +#!/usr/bin/env node + +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ -const fs = require('fs'); -const https = require('https'); -const Root = require('../controllers/root').Root; -const IDM = require('../lib/idm.js').IDM; +const config_service = require('../lib/config_service'); +config_service.set_config(require('../config'), true); +const config = config_service.get_config(); +const IDM = require('../lib/pdp/keyrock'); +const app = require('../app'); +const Authorize = require('../lib/authorization_functions'); const errorhandler = require('errorhandler'); config.azf = config.azf || {}; config.https = config.https || {}; -const logger = require('morgan'); -const debug = require('debug')('pep-proxy:app'); -const express = require('express'); +const debug = require('debug')('pep-proxy:www'); + const os = require('os'); const cluster = require('cluster'); -const clusterWorkerSize = os.cpus().length; +const clusterWorkerSize = (config.cluster.type === 'manual') ? config.cluster.number : os.cpus().length; process.on('uncaughtException', function (err) { debug('Caught exception: ' + err); }); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -let port = config.pep_port || 80; -if (config.https.enabled) { - port = config.https.port || 443; -} - -function start_server() { - const app = express(); - - // Set logs in development - if (config.debug) { - app.use(logger('dev')); - } - - app.use(function (req, res, next) { - const bodyChunks = []; - req.on('data', function (chunk) { - bodyChunks.push(chunk); - }); - - req.on('end', function () { - if (bodyChunks.length > 0) { - req.body = Buffer.concat(bodyChunks); - } - next(); - }); - }); - - app.disable('x-powered-by'); - app.use(errorhandler({ log: debug })); - app.use(cors(config.cors)); - app.set('port', port); - - for (const p in config.public_paths) { - debug('Public paths', config.public_paths[p]); - app.all(config.public_paths[p], Root.public); +function logConfig (idm_config) { + if (idm_config) { + debug('IDM authorization configuration: ' + config.authorization.pdp); + debug(' + Authzforce enabled: ' + idm_config.authzforce); + switch (idm_config.level) { + case 'payload': + debug(' + Authorization rules allowed: HTTP Verb+Resource and Payload'); + break; + case 'advanced': + debug(' + Authorization rules allowed: HTTP Verb+Resource and Advanced'); + break; + default: + debug(' + Authorization rules allowed: HTTP Verb+Resource'); + break; + } } +}; - app.all('/*', Root.pep); - - if (config.https.enabled === true) { - const options = { - key: fs.readFileSync(config.https.key_file), - cert: fs.readFileSync(config.https.cert_file) - }; - - https - .createServer(options, function (req, res) { - app.handle(req, res); - }) - .listen(app.get('port')); - } else { - app.listen(app.get('port')); - } -} +/** + * Check that the IDM is responding and the PEP is recognized within the IDM + * @return an auth token representing the PEP itself to be used in subsequent requests + */ function connect() { let retry = 20; return new Promise((resolve, reject) => { - const connect_with_retry = () => { - IDM.authenticate( - (token) => { - debug('Success authenticating PEP proxy.'); - resolve(token); - }, - (status, e) => { - debug('Error in IDM communication', e); - retry--; - if (retry === 0) { - reject(e); - } else { - debug('retry after 5 seconds.'); - //eslint-disable-next-line snakecase/snakecase - setTimeout(connect_with_retry, 5000); - } + const connect_with_retry = async () => { + try { + await IDM.checkConnectivity(); + debug('IDM is now available - requesting PEP authentication'); + + IDM.authenticatePEP() + .then((response) => { + logConfig(response.config); + return resolve(response.pepToken); + }) + .catch((error) => { + return reject('IDM rejected PEP authentication: ' + error.message); + }); + } catch (e) { + debug(e.message); + retry--; + if (retry === 0) { + return reject('IDM is not available. Giving up after 20 attempts'); } - ); + debug('retry after 5 seconds.'); + //eslint-disable-next-line snakecase/snakecase + setTimeout(connect_with_retry, 5000); + } }; connect_with_retry(); }); } -debug('Starting PEP proxy in port ' + port + '. IdM authentication ...'); connect().then( (token) => { debug('Success authenticating PEP proxy. Proxy Auth-token: ', token); @@ -115,14 +92,14 @@ connect().then( cluster.fork(); } } else { - start_server(); + app.start_server(token, config); } } else { - start_server(); + app.start_server(token,config); } }, (err) => { - debug('Error found after [%d] attempts: %s', 20, err); + debug(err); process.exit(1); } ); diff --git a/config.js b/config.js index 5c6fc7e..6d7e444 100644 --- a/config.js +++ b/config.js @@ -1,42 +1,43 @@ +#!/usr/bin/env node const config = {}; // Used only if https is disabled -config.pep_port = 80; +config.pep_port = 3003; // Set this var to undefined if you don't want the server to listen on HTTPS config.https = { enabled: false, cert_file: 'cert/cert.crt', key_file: 'cert/key.key', - port: 443, + port: 443 }; config.idm = { host: 'localhost', - port: 3005, - ssl: false, + port: 3000, + ssl: false }; config.app = { - host: 'www.fiware.org', - port: '80', - ssl: false, // Use true if the app server listens in https + host: 'localhost', + port: '3002', + ssl: false // Use true if the app server listens in https }; config.organizations = { enabled: false, - header: 'fiware-service', + header: 'fiware-service' }; // Credentials obtained when registering PEP Proxy in app_id in Account Portal config.pep = { - app_id: '', - username: '', - password: '', + app_id: 'c365f878-a348-4584-8ac4-7e940697e1b6', + username: 'pep_proxy_7f270eda-17ed-4a49-b9fc-2f6f68490782', + password: 'pep_proxy_6a079689-6d4d-466c-bfd9-d2a0af4c6196', token: { - secret: '', // Secret must be configured in order validate a jwt + secret: '' // Secret must be configured in order validate a jwt }, - trusted_apps: [], + trusted_apps: [] }; // in seconds @@ -45,30 +46,39 @@ config.cache_time = 300; // if enabled PEP checks permissions in two ways: // - With IdM: only allow basic authorization // - With Authzforce: allow basic and advanced authorization. -// For advanced authorization, you can use custom policy checks by including programatic scripts +// For advanced authorization, you can use custom policy checks by including programatic scripts // in policies folder. An script template is included there // -// This is only compatible with oauth2 tokens engine +// This is only compatible with oauth2 tokens engine config.authorization = { - enabled: false, - pdp: 'idm', // idm|authzforce - azf: { + enabled: true, + pdp: 'idm', // idm|iShare|xacml|authzforce + header: undefined, // NGSILD-Tenant|fiware-service + location: { protocol: 'http', host: 'localhost', port: 8080, - custom_policy: undefined, // use undefined to default policy checks (HTTP verb + path). + path: '' }, + azf: { + custom_policy: undefined // use undefined to default policy checks (HTTP verb + path). + } }; config.cors = { - origin: "*", - methods: "GET,HEAD,PUT,PATCH,POST,DELETE", + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', preflightContinue: false, optionsSuccessStatus: 204, credentials: true }; +config.cluster = { + type: 'manual', // manual|allCPUCores + number: 1 +}; + // list of paths that will not check authentication/authorization // example: ['/public/*', '/static/css/'] config.public_paths = []; @@ -76,4 +86,11 @@ config.public_paths = []; config.magic_key = undefined; config.auth_for_nginx = false; +config.error_template = `{ + "type": "{{type}}", + "title": "{{title}}", + "detail": "{{message}}" + }`; +config.error_content_type = 'application/json'; + module.exports = config; diff --git a/config.js.template b/config.js.template index 2f5b979..da63f40 100644 --- a/config.js.template +++ b/config.js.template @@ -1,3 +1,4 @@ +#!/usr/bin/env node const config = {}; // Used only if https is disabled @@ -52,11 +53,15 @@ config.cache_time = 300; config.authorization = { enabled: false, - pdp: 'idm', // idm|authzforce - azf: { + pdp: 'idm', // idm|iShare|xacml|authzforce + header: undefined, // NGSILD-Tenant|fiware-service + location: { protocol: 'http', host: 'localhost', port: 8080, + path: '' + }, + azf: { custom_policy: undefined, // use undefined to default policy checks (HTTP verb + path). }, }; @@ -69,6 +74,11 @@ config.cors = { credentials: true }; +config.cluster = { + type: 'manual', // manual|allCPUCores + number: 1 +}; + // list of paths that will not check authentication/authorization // example: ['/public/*', '/static/css/'] config.public_paths = []; @@ -76,4 +86,11 @@ config.public_paths = []; config.magic_key = undefined; config.auth_for_nginx = false; +config.error_template = `{ + "type": "{{type}}", + "title": "{{title}}", + "detail": "{{message}}" + }`; +config.error_content_type = "application/json"; + module.exports = config; diff --git a/controllers/root.js b/controllers/root.js index fe129e9..81f7369 100644 --- a/controllers/root.js +++ b/controllers/root.js @@ -1,244 +1,146 @@ -const config_service = require('../lib/config_service.js'); -const config = config_service.get_config(); -const proxy = require('../lib/HTTPClient.js'); -const IDM = require('../lib/idm.js').IDM; -const AZF = require('../lib/azf.js').AZF; +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('../lib/config_service'); +let config; +const IDM = require('../lib/pdp/keyrock'); const jsonwebtoken = require('jsonwebtoken'); - +const access = require('../lib/access_functions'); +const PDP = require('../lib/authorization_functions'); const debug = require('debug')('pep-proxy:root'); -const Root = (function() { - //{token: {userInfo: {}, date: Date, verb1: [res1, res2, ..], verb2: [res3, res4, ...]}} - const tokensCache = {}; - - const pep = function(req, res) { - const tokenHeader = req.headers.authorization; - let authToken = tokenHeader - ? tokenHeader.split('Bearer ')[1] - : req.headers['x-auth-token']; - - if (authToken === undefined && req.headers.authorization !== undefined) { - const headerAuth = req.headers.authorization.split(' ')[1]; - authToken = new Buffer(headerAuth, 'base64').toString(); - } - - const organizationToken = req.headers[config.organizations.header] - ? req.headers[config.organizations.header] - : null; - - if (authToken === undefined) { - debug('Auth-token not found in request header'); - const authHeader = 'IDM uri = ' + config.idm_host; - res.set('WWW-Authenticate', authHeader); - res.status(401).send('Auth-token not found in request header'); - } else { - if (config.magic_key && config.magic_key === authToken) { - const options = { - host: config.app.host, - port: config.app.port, - path: req.url, - method: req.method, - headers: proxy.getClientIp(req, req.headers), - }; - const protocol = config.app.ssl ? 'https' : 'http'; - proxy.sendData(protocol, options, req.body, res); - return; - } - - let action; - let resource; - let authzforce; - - if (config.authorization.enabled) { - if (config.authorization.pdp === 'authzforce') { - authzforce = true; - } else { - action = req.method; - resource = req.path; - } - } - - if (config.pep.token.secret) { - jsonwebtoken.verify(authToken, config.pep.token.secret, function( - err, - userInfo - ) { - if (err) { - if (err.name === 'TokenExpiredError') { - res.status(401).send('Invalid token: jwt token has expired'); - } else { - debug('Error in JWT ', err.message); - debug('Or JWT secret bad configured'); - debug('Validate Token with Keyrock'); - checkToken( - req, - res, - authToken, - null, - action, - resource, - authzforce, - organizationToken - ); - } - } else if (config.authorization.enabled) { - if (config.authorization.pdp === 'authzforce') { - authorizeAzf(req, res, authToken, userInfo); - } else if (config.authorization.pdp === 'idm') { - checkToken( - req, - res, - authToken, - userInfo.exp, - action, - resource, - authzforce, - organizationToken - ); - } else { - res.status(401).send('User access-token not authorized'); - } - } else { - setHeaders(req, userInfo); - redirRequest(req, res, userInfo); - } - }); +/** + * Authenticate the JWT token and then authorize the action if necessary. + * + * @param req - the incoming request + * @param res - the response to return + * @param tokens - a collection of auth tokens to use for this verification + */ +function validateAccessJWT(req, res, tokens) { + return jsonwebtoken.verify(tokens.authToken, config.pep.token.secret, function (err, userInfo) { + if (err) { + if (err.name === 'TokenExpiredError') { + return access.deny(res, 'Invalid token: jwt token has expired', 'urn:dx:as:ExpiredAuthenticationToken'); } else { - checkToken( - req, - res, - authToken, - null, - action, - resource, - authzforce, - organizationToken - ); + debug('Error in JWT ', err.message); + debug('Or JWT secret misconfigured'); + debug('Validate Token with Keyrock'); + // Fallback to AuthToken access validation + return validateAccessIDM(req, res, tokens); } } - }; - - const checkToken = function( - req, - res, - authToken, - jwtExpiration, - action, - resource, - authzforce, - organizationToken - ) { - IDM.checkToken( - authToken, - jwtExpiration, - action, - resource, - authzforce, - organizationToken, - function(userInfo) { - setHeaders(req, userInfo); - if (config.authorization.enabled) { - if (config.authorization.pdp === 'authzforce') { - authorizeAzf(req, res, authToken, userInfo); - } else if (userInfo.authorization_decision === 'Permit') { - redirRequest(req, res, userInfo); - } else { - res.status(401).send('User access-token not authorized'); - } - } else { - redirRequest(req, res, userInfo); - } - }, - function(status, e) { - if (status === 404 || status === 401) { - debug(e); - res.status(401).send(e); - } else { - debug('Error in IDM communication ', e); - res.status(503).send('Error in IDM communication'); - } - }, - tokensCache - ); - }; - - const setHeaders = function(req, userInfo) { - // Set headers with user information - req.headers['X-Nick-Name'] = userInfo.id ? userInfo.id : ''; - req.headers['X-Display-Name'] = userInfo.displayName - ? userInfo.displayName - : ''; - req.headers['X-Roles'] = userInfo.roles - ? JSON.stringify(userInfo.roles) - : []; - req.headers['X-Organizations'] = userInfo.organizations - ? JSON.stringify(userInfo.organizations) - : []; - req.headers['X-Eidas-Profile'] = userInfo.eidas_profile - ? JSON.stringify(userInfo.eidas_profile) - : {}; - req.headers['X-App-Id'] = userInfo.app_id; - }; - - const authorizeAzf = function(req, res, authToken, userInfo) { - // Check decision through authzforce - AZF.checkPermissions( - authToken, - userInfo, - req, - function() { - redirRequest(req, res, userInfo); - }, - function(status, e) { - if (status === 401) { - debug('User access-token not authorized: ', e); - res.status(401).send('User token not authorized'); - } else if (status === 404) { - debug('Domain not found: ', e); - res.status(404).send(e); - } else { - debug('Error in AZF communication ', e); - res.status(503).send('Error in AZF communication'); - } - }, - tokensCache - ); - }; - - const publicFunc = function(req, res) { - redirRequest(req, res); - }; - - const redirRequest = ('auth_for_nginx' in config && config.auth_for_nginx) - ? - // eslint-disable-next-line no-unused-vars - function(req, res, userInfo) { - debug('Access-token OK. Response 204'); - res.sendStatus(204); - } - : function(req, res, userInfo) { - if (userInfo) { - debug('Access-token OK. Redirecting to app...'); - } else { - debug('Public path. Redirecting to app...'); - } - - const protocol = config.app.ssl ? 'https' : 'http'; - - const options = { - host: config.app.host, - port: config.app.port, - path: req.url, - method: req.method, - headers: proxy.getClientIp(req, req.headers), - }; - proxy.sendData(protocol, options, req.body, res); - }; - - return { - pep, - public: publicFunc, - }; -})(); + req.user = userInfo; + if (!config.authorization.enabled) { + // JWT Authentication Access granted + setHeaders(req); + return access.permit(req, res); + } -exports.Root = Root; + if (PDP.validateJWT()) { + // JWT Authorization by PDP + return PDP.authorize(req, res, tokens.authToken); + } else { + // JWT Authorization by IDM, the user will already exist. + tokens.jwtExpiry = userInfo.exp; + return validateAccessIDM(req, res, tokens); + } + }); +} + +/** + * Authenticate the user token via Keyrock and then authorize the action if necessry. + * + * @param req - the incoming request + * @param res - the response to return + * @param tokens - a collection of auth tokens to use for this verification + */ +async function validateAccessIDM(req, res, tokens) { + const tenant_header = config.authorization.header ? req.get(config.authorization.header) : undefined; + + try { + req.user = await IDM.authenticateUser(tokens, req.method, req.path, tenant_header); + setHeaders(req); + if (config.authorization.enabled) { + return PDP.authorize(req, res, tokens.authToken); + } else { + // Authentication only. + return access.permit(req, res); + } + } catch (e) { + debug(e); + if (e.type) { + return access.deny(res, e.message, e.type); + } else { + return access.internalError(res, e, 'IDM'); + } + } +} + +/** + * Set headers with user information + * @param req - the incoming request + */ +function setHeaders(req) { + const user = req.user || {}; + req.headers['X-Nick-Name'] = user.id ? user.id : ''; + req.headers['X-Display-Name'] = user.displayName ? user.displayName : ''; + req.headers['X-Roles'] = user.roles ? JSON.stringify(user.roles) : []; + req.headers['X-Organizations'] = user.organizations ? JSON.stringify(user.organizations) : []; + req.headers['X-Eidas-Profile'] = user.eidas_profile ? JSON.stringify(user.eidas_profile) : {}; + req.headers['X-App-Id'] = user.app_id; +} + +/** + * Extract the bearer token for the user, organization and the PEP itself + * + * @param req - the incoming request + */ +function getTokens(req) { + const tokenHeader = req.get('authorization'); + const pepToken = req.app.get('pepToken'); + const authOrgToken = config.organizations.header ? req.get(config.organizations.header) : undefined; + let authToken = tokenHeader ? tokenHeader.split('Bearer ')[1] : req.get('x-auth-token'); + + if (authToken === undefined && req.headers.authorization !== undefined) { + const headerAuth = req.headers.authorization.split(' ')[1]; + authToken = Buffer.from(headerAuth, 'base64').toString(); + } + + return { authToken, authOrgToken, pepToken }; +} + +/** + * For most requests, check the headers and permit or deny access. + * + * @param req - the incoming request + * @param res - the response to return + */ +exports.restricted_access = function (req, res) { + config = config_service.get_config(); + const tokens = getTokens(req, res); + + if (tokens.authToken === undefined) { + debug('Auth-token not found in request header'); + res.set('WWW-Authenticate', 'IDM uri = ' + config.idm_host); + access.deny(res, 'Auth-token not found in request header', 'urn:dx:as:MissingAuthenticationToken'); + return; + } + if (config.magic_key && config.magic_key === tokens.authToken) { + access.permit(req, res); + return; + } + if (config.pep.token.secret) { + validateAccessJWT(req, res, tokens); + } else { + validateAccessIDM(req, res, tokens); + } +}; + +/** + * Allow access to whitelisted resources + */ +exports.open_access = access.permit; diff --git a/doc/admin_guide.md b/doc/admin_guide.md index c492394..4aeaad8 100644 --- a/doc/admin_guide.md +++ b/doc/admin_guide.md @@ -76,19 +76,31 @@ you have to first register an application. The steps can be found [here](https://fiware-idm.readthedocs.io/en/latest/user_and_programmers_guide/application_guide/index.html#register-pep-proxy-and-iot-agents). You can also configure Pep Proxy to validate authorization in your application -([levels 2 and 3 of authorization](user_guide.md#level-2-basic-authorization)). If enabled PEP checks permissions in two -ways: +([levels 2 and 3 of authorization](user_guide.md#level-2-basic-authorization)). If enabled PEP checks permissions in +multiple ways: - With [Keyrock Identity Manager](https://github.com/Fiware/catalogue/tree/master/security#keyrock): only allow basic authorization +- With [Keyrock Identity Manager](https://github.com/Fiware/catalogue/tree/master/security#keyrock): payload attribute + level authorization requests in iShare format. +- With [Keyrock Identity Manager](https://github.com/Fiware/catalogue/tree/master/security#keyrock): payload attribute + level authorization requests in + [XACML 3.0 JSON](https://docs.oasis-open.org/xacml/xacml-json-http/v1.0/xacml-json-http-v1.0.html) format. +- With [Keyrock Identity Manager](https://github.com/Fiware/catalogue/tree/master/security#keyrock): payload attribute + level authorization requests in [Open Policy Agent](https://www.openpolicyagent.org/) format. - With [Authzforce Authorization PDP](https://github.com/Fiware/catalogue/tree/master/security#authzforce): allow basic and advanced authorization. For advanced authorization, you can use custom policy checks by including programatic scripts in policies folder. An script template is included there. +The `config.authorization.header` can be passed to Keyrock IDM to reduce permissions to a single tenant and if used +should correspond to the tenant header (`NGSILD-Tenant` or `fiware-service`) when authorizing a multi-tenant system such +as FIWARE + ```javascript config.authorization = { enabled: false, - pdp: 'idm', // idm|authzforce + pdp: 'idm', // idm|iShare|xacml|authzforce|opa + header: undefined, azf: { protocol: 'http', host: 'localhost', @@ -283,13 +295,18 @@ These are the parameters that can be configured in the global section: - **pep_port**: Port to use if HTTPS is disabled - **https**: HTTPS configuration. Disable or leave undefined if you are testing without an HTTPS certificate +- **error_template**: A [Handlebars](https://handlebarsjs.com/) template defining the format of an error message + payload +- **error_content_type**: The content-type header of the error message ```json { "enabled": false, "cert_file": "cert/cert.crt", "key_file": "cert/key.key", - "port": 443 + "port": 443, + "error_template" : "{\"type\": \"{{type}}\", \"title\": \"{{title}}\", \"detail\": \"{{message}}\"}", + "error_content_type" "application/json" } ``` @@ -415,39 +432,45 @@ with container-based technologies, like Docker, Heroku, etc... The following table shows the accepted environment variables, as well as the configuration parameter the variable overrides. -| Environment variable | Configuration attribute | -| :------------------------------------ | :-------------------------------- | -| PEP_PROXY_PORT | `pep_port` | -| PEP_PROXY_HTTPS_ENABLED | `https` | -| PEP_PROXY_HTTPS_PORT | `https.port` | -| PEP_PROXY_IDM_HOST | `idm.host` | -| PEP_PROXY_IDM_PORT | `idm.port` | -| PEP_PROXY_IDM_SSL_ENABLED | `idm.ssl` | -| PEP_PROXY_APP_HOST | `app.host` | -| PEP_PROXY_APP_PORT | `app.port` | -| PEP_PROXY_APP_SSL_ENABLED | `app.ssl` | -| PEP_PROXY_ORG_ENABLED | `organizations.enabled` | -| PEP_PROXY_ORG_HEADER | `organizations.header` | -| PEP_PROXY_APP_ID | `pep.app_id` | -| PEP_PROXY_USERNAME | `pep.username` | -| PEP_PROXY_PASSWORD | `pep.password` | -| PEP_PROXY_TOKEN_SECRET | `pep.token` | -| PEP_PROXY_TRUSTED_APPS | `pep.trusted_apps` | -| PEP_PROXY_AUTH_ENABLED | `authorization.enabled` | -| PEP_PROXY_PDP | `authorization.pdp` | -| PEP_PROXY_AZF_PROTOCOL | `authorization.azf.protocol` | -| PEP_PROXY_AZF_HOST | `authorization.azf.host` | -| PEP_PROXY_AZF_PORT | `authorization.azf.port` | -| PEP_PROXY_AZF_CUSTOM_POLICY | `authorization.azf.custom_policy` | -| PEP_PROXY_PUBLIC_PATHS | `public_path` | -| PEP_PROXY_CORS_ORIGIN | `cors.origin` | -| PEP_PROXY_CORS_METHODS | `cors.methods` | -| PEP_PROXY_CORS_OPTIONS_SUCCESS_STATUS | `cors.optionsSuccessStatus` | -| PEP_PROXY_CORS_ALLOWED_HEADERS | `cors.allowedHeaders` | -| PEP_PROXY_CORS_CREDENTIALS | `cors.credentials` | -| PEP_PROXY_CORS_MAX_AGE | `cors.maxAge` | -| PEP_PROXY_AUTH_FOR_NGINX | `config.auth_for_nginx` | +| Environment variable | Configuration attribute | Notes | | +| :------------------------------------ | :-------------------------------- | ------------------------------------------- | --- | +| PEP_PROXY_PORT | `pep_port` | | +| PEP_PROXY_HTTPS_ENABLED | `https` | | +| PEP_PROXY_HTTPS_PORT | `https.port` | | +| PEP_PROXY_IDM_HOST | `idm.host` | | +| PEP_PROXY_IDM_PORT | `idm.port` | | +| PEP_PROXY_IDM_SSL_ENABLED | `idm.ssl` | | +| PEP_PROXY_APP_HOST | `app.host` | | +| PEP_PROXY_APP_PORT | `app.port` | | +| PEP_PROXY_APP_SSL_ENABLED | `app.ssl` | | +| PEP_PROXY_ORG_ENABLED | `organizations.enabled` | | +| PEP_PROXY_ORG_HEADER | `organizations.header` | | +| PEP_PROXY_APP_ID | `pep.app_id` | | +| PEP_PROXY_USERNAME | `pep.username` | | +| PEP_PROXY_PASSWORD | `pep.password` | | +| PEP_PROXY_TOKEN_SECRET | `pep.token` | | +| PEP_PROXY_TRUSTED_APPS | `pep.trusted_apps` | | +| PEP_PROXY_AUTH_ENABLED | `authorization.enabled` | | +| PEP_PROXY_PDP | `authorization.pdp` | | +| PEP_PROXY_PDP_PROTOCOL | `authorization.pdp.protocol` | | +| PEP_PROXY_PDP_HOST | `authorization.pdp.host` | | +| PEP_PROXY_PDP_PORT | `authorization.pdp.port` | | +| PEP_PROXY_PDP_PATH | `authorization.pdp.path` | | +| PEP_PROXY_TENANT_HEADER | `authorization.header` | | +| PEP_PROXY_AZF_PROTOCOL | `authorization.azf.protocol` | **deprecated** use `PEP_PROXY_PDP_PROTOCOL` | +| PEP_PROXY_AZF_HOST | `authorization.azf.host` | **deprecated** use `PEP_PROXY_PDP_HOST` | +| PEP_PROXY_AZF_PORT | `authorization.azf.port` | **deprecated** use `PEP_PROXY_PDP_PORT` | +| PEP_PROXY_AZF_CUSTOM_POLICY | `authorization.azf.custom_policy` | | +| PEP_PROXY_PUBLIC_PATHS | `public_path` | | +| PEP_PROXY_CORS_ORIGIN | `cors.origin` | | +| PEP_PROXY_CORS_METHODS | `cors.methods` | | +| PEP_PROXY_CORS_OPTIONS_SUCCESS_STATUS | `cors.optionsSuccessStatus` | | +| PEP_PROXY_CORS_ALLOWED_HEADERS | `cors.allowedHeaders` | | +| PEP_PROXY_CORS_CREDENTIALS | `cors.credentials` | | +| PEP_PROXY_CORS_MAX_AGE | `cors.maxAge` | | +| PEP_PROXY_AUTH_FOR_NGINX | `config.auth_for_nginx` | | | PEP_PROXY_MAGIC_KEY | `config.magic_key` | +| PEP_PROXY_ERROR_TEMPLATE | `config.error_template` | Note: diff --git a/extras/docker/Dockerfile b/extras/docker/Dockerfile index 7e7b10a..4154cd1 100644 --- a/extras/docker/Dockerfile +++ b/extras/docker/Dockerfile @@ -216,6 +216,7 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \ # PEP_PROXY_TOKEN_SECRET # PEP_PROXY_AUTH_ENABLED # PEP_PROXY_PDP +# PEP_PROXY_TENANT_HEADER # PEP_PROXY_AZF_PROTOCOL # PEP_PROXY_AZF_HOST # PEP_PROXY_AZF_PORT diff --git a/extras/docker/README.md b/extras/docker/README.md index d1e9eb1..ecdf6df 100644 --- a/extras/docker/README.md +++ b/extras/docker/README.md @@ -142,10 +142,15 @@ Currently, the following `--build-arg` parameters are supported: - `PEP_PROXY_USERNAME` - default value is left blank and must be overridden - `PEP_PROXY_PASSWORD` - default value is left blank and must be overridden - `PEP_PROXY_AUTH_ENABLED` - default value is `false` -- `PEP_PROXY_PDP` - default value is `idm` can be set tp `authzforce` -- `PEP_PROXY_AZF_PROTOCOL` - default value is `http` -- `PEP_PROXY_AZF_HOST` - default value is `localhost` -- `PEP_PROXY_AZF_PORT` - default value is `8080` +- `PEP_PROXY_PDP` - default value is `idm` can be set to `authzforce`, `iShare` or `xacml` +- `PEP_PROXY_PDP_PROTOCOL` - default value is `http` +- `PEP_PROXY_PDP_HOST` - default value is `localhost` +- `PEP_PROXY_PDP_PORT` - default value is `8080` +- `PEP_PROXY_PDP_PATH` - default value is blank +- `PEP_PROXY_TENANT_HEADER` - default value is left blank. Typically set to `NGSILD-Tenant` or `fiware-service`. +- `PEP_PROXY_AZF_PROTOCOL` - _deprecated_ use `PEP_PROXY_PDP_PROTOCOL` +- `PEP_PROXY_AZF_HOST` - _deprecated_ use `PEP_PROXY_PDP_HOST` +- `PEP_PROXY_AZF_PORT` - _deprecated_ use `PEP_PROXY_PDP_PORT` - `PEP_PROXY_AZF_CUSTOM_POLICY` - default value is `undefined` which impliesthe usage of default policy checks (HTTP verb + path). - `PEP_PROXY_PUBLIC_PATHS` - default value is `[]` - Use `,` to split paths - example: @@ -160,3 +165,5 @@ Currently, the following `--build-arg` parameters are supported: - `PEP_PROXY_CORS_MAX_AGE` - The `Access-Control-Max-Age` header is not sent by default. set to `true` to enable it. - `PEP_PROXY_MAGIC_KEY` - default value is `undefined` - should be overridden - `PEP_PROXY_AUTH_FOR_NGINX` - default value is `false` +- `PEP_PROXY_ERROR_TEMPLATE` - default value is an NGSI error payload. +- `PEP_PROXY_ERROR_CONTENT_TYPE` - default value is `application/json` diff --git a/lib/HTTPClient.js b/lib/HTTPClient.js deleted file mode 100644 index d3ba23f..0000000 --- a/lib/HTTPClient.js +++ /dev/null @@ -1,117 +0,0 @@ -const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; - -const debug = require('debug')('pep-proxy:HTTP-Client'); - -exports.getClientIp = function(req, headers) { - const ipAddress = req.connection.remoteAddress; - - let forwardedIpsStr = req.header('x-forwarded-for'); - - if (forwardedIpsStr) { - // 'x-forwarded-for' header may return multiple IP addresses in - // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the - // the first one - forwardedIpsStr += "," + ipAddress; - } else { - forwardedIpsStr = String(ipAddress); - } - - headers['x-forwarded-for'] = forwardedIpsStr; - - return headers; -}; - - -exports.sendData = function(protocol, options, data, res, callBackOK, callbackError) { - options.headers = options.headers || {}; - - callbackError = callbackError || function(status, resp) { - debug("Error: ", status, resp); - res.statusCode = status; - res.send(resp); - }; - callBackOK = callBackOK || function(status, resp, headers) { - res.statusCode = status; - for (const idx in headers) { - res.setHeader(idx, headers[idx]); - } - debug("Response: ", status); - debug(" Body: ", resp); - res.send(resp); - }; - - const url = protocol + "://" + options.host + ":" + options.port + options.path; - const xhr = new XMLHttpRequest(); - xhr.open(options.method, url, true); - if (options.headers["content-type"]) { - xhr.setRequestHeader("Content-Type", options.headers["content-type"]); - } - for (const headerIdx in options.headers) { - switch (headerIdx) { - // Unsafe headers - case "host": - break; - case "connection": - break; - case "referer": - break; -// case "accept-encoding": -// case "accept-charset": -// case "cookie": - case "content-type": - break; - case "origin": - break; - default: - xhr.setRequestHeader(headerIdx, options.headers[headerIdx]); - break; - } - } - - xhr.onerror = function() { - // DO NOTHING? - } - xhr.onreadystatechange = function () { - - // This resolves an error with Zombie.js - if (flag) { - return; - } - - if (xhr.readyState === 4) { - flag = true; - - if (xhr.status !== 0 && xhr.status < 400) { - const allHeaders = xhr.getAllResponseHeaders().split('\r\n'); - const headers = {}; - for (const h in allHeaders) { - headers[allHeaders[h].split(': ')[0]] = allHeaders[h].split(': ')[1]; - } - callBackOK(xhr.status, xhr.responseText, headers); - } else { - callbackError(xhr.status, xhr.responseText); - } - } - }; - - let flag = false; - debug("Sending ", options.method, " to: " + url); - debug(" Headers: ", options.headers); - //debug(" Body: ", data); - if (data !== undefined) { - try { - xhr.send(data); - } catch (e) { - - callbackError(e.message); - - } - } else { - try { - xhr.send(); - } catch (e) { - callbackError(e.message); - - } - } -} \ No newline at end of file diff --git a/lib/access_functions.js b/lib/access_functions.js new file mode 100644 index 0000000..132d899 --- /dev/null +++ b/lib/access_functions.js @@ -0,0 +1,152 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('./config_service'); +const config = config_service.get_config(); +const debug = require('debug')('pep-proxy:access'); +const got = require('got'); +const StatusCodes = require('http-status-codes').StatusCodes; +const getReasonPhrase = require('http-status-codes').getReasonPhrase; + +const PROXY_URL = (config.app.ssl ? 'https://' : 'http://') + config.app.host + ':' + config.app.port; +const template = require('handlebars').compile( + config.error_template || + `{ + "type": "{{type}}", + "title": "{{title}}", + "detail": "{{message}}" + }` +); + +const error_content_type = config.error_content_type || 'application/json'; + +/** + * Add the client IP of the proxy client to the list of X-forwarded-for headers. + * + * @param req - the incoming request + * @return a string representation of the X-forwarded-for header + */ +function getClientIp(req) { + let ip = req.ip; + if (ip.substr(0, 7) === '::ffff:') { + ip = ip.substr(7); + } + let forwardedIpsStr = req.header('x-forwarded-for'); + + if (forwardedIpsStr) { + // 'x-forwarded-for' header may return multiple IP addresses in + // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the + // the first one + forwardedIpsStr += ',' + ip; + } else { + forwardedIpsStr = String(ip); + } + + return forwardedIpsStr; +} + +/** + * Based on the PDP decision, decide whether to forward the request + * or return "Access Denied" response + * + * @param req - the incoming request + * @param res - the response to return + * @param decision - the PDP decision permir/deny + */ +exports.adjudicate = function (req, res, decision) { + if (decision) { + permit(req, res); + } else { + deny(res, 'User access-token not authorized', 'urn:dx:as:InvalidRole'); + } +}; + +/** + * Return an "Access Denied" response + * + * @param res - the response to return + * @param message - the error message to display + * @param type - the error type + */ +function deny(res, message, type) { + debug('Denied. ' + type); + res.setHeader('Content-Type', error_content_type); + res.status(StatusCodes.UNAUTHORIZED).send( + template({ + type, + title: getReasonPhrase(StatusCodes.UNAUTHORIZED), + message + }) + ); +} + +/** + * Return an "Internal Error" response. These should not occur + * during standard operation + * + * @param res - the response to return + * @param e - the error that occurred + * @param component - the component that caused the error + */ +function internalError(res, e, component) { + const message = e ? e.message : undefined; + debug(`Error in ${component} communication `, message ? message : e); + res.setHeader('Content-Type', error_content_type); + res.status(StatusCodes.INTERNAL_SERVER_ERROR).send( + template({ + type: 'urn:dx:as:InternalServerError', + title: getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + message + }) + ); +} + +/** + * "Access Permitted" forwarding when using the PEP with NGINX + * + * @param req - the incoming request + * @param res - the response to return + */ +function nginxResponse(req, res) { + debug('Permitted. Response 204'); + res.sendStatus(StatusCodes.NO_CONTENT); +} + +/** + * "Access Permitted" forwarding. Forward the proxied request and + * return the response. + * + * @param req - the incoming request + * @param res - the response to return + */ +function pepResponse(req, res) { + const headers = req.headers; + headers['x-forwarded-for'] = getClientIp(req); + + got(PROXY_URL + req.url, { + method: req.method, + headers, + body: req.body, + allowGetBody: true, + throwHttpErrors: false, + retry: 0 + }) + .then((response) => { + debug(req.user ? 'Permitted.' : 'Public path.'); + res.statusCode = response.statusCode; + res.headers = response.headers; + return response.body ? res.send(response.body) : res.send(); + }) + .catch((error) => { + return internalError(res, error, 'Proxy'); + }); +} + +const permit = 'auth_for_nginx' in config && config.auth_for_nginx ? nginxResponse : pepResponse; +exports.permit = permit; +exports.deny = deny; +exports.internalError = internalError; diff --git a/lib/authorization_functions.js b/lib/authorization_functions.js new file mode 100644 index 0000000..35cfcef --- /dev/null +++ b/lib/authorization_functions.js @@ -0,0 +1,154 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('./config_service'); +const AZF = require('./pdp/authzforce'); +const OPA = require('./pdp/openPolicyAgent'); +const IDM = require('./pdp/keyrock'); +const XACML = require('./pdp/xacml'); +const ISHARE = require('./pdp/iShare'); +const access = require('./access_functions'); +const debug = require('debug')('pep-proxy:authorize'); + +function getRoles(user) { + const roles = []; + for (const orgIdx in user.organizations) { + const org = user.organizations[orgIdx]; + for (const roleIdx in org.roles) { + const role = org.roles[roleIdx]; + if (roles.indexOf(role.id) === -1) { + roles.push(role.id); + } + } + } + + for (const roleIdx in user.roles) { + const role = user.roles[roleIdx]; + if (roles.indexOf(role) === -1) { + roles.push(role.id); + } + } + + return roles; +} + +function getData(req, res) { + const user = req.user; + const authorization = config_service.get_config().authorization; + return { + roles: getRoles(user), + appId: user.app_id, + azfDomain: user.app_azf_domain, + action: req.method, + resource: req.path, + payloadEntityIds: res.locals.ids, + payloadIdPatterns: res.locals.IdPatterns, + payloadAttrs: res.locals.attrs, + payloadTypes: res.locals.types, + tenant_header: authorization.header ? req.get(authorization.header) : undefined + }; +} + +function idmAuthorize(req, res) { + const decision = IDM.checkPolicies(req.user); + access.adjudicate(req, res, decision); +} +function xacmlAuthorize(req, res) { + // Check decision through XACML Endpoint + const authToken = req.app.get('pepToken'); + XACML.checkPolicies(authToken, getData(req, res)) + .then((decision) => { + access.adjudicate(req, res, decision); + }) + .catch((e) => { + access.internalError(res, e, 'XACML'); + }); +} + +function openPolicyAgentAuthorize(req, res) { + // Check decision through Open Policy Agent Endpoint + const authToken = req.app.get('pepToken'); + OPA.checkPolicies(authToken, getData(req, res)) + .then((decision) => { + access.adjudicate(req, res, decision); + }) + .catch((e) => { + access.internalError(res, e, 'Open Policy Agent'); + }); +} + +function iShareAuthorize(req, res) { + // Check decision through iShare Endpoint + const user = req.user || {}; + const authToken = req.app.get('pepToken'); + const decision = ISHARE.checkPolicies( + authToken, + getData(req, res), + user.delegationEvidence, + user.authorzationRegistry + ); + access.adjudicate(req, res, decision); +} +function authzforceAuthorize(req, res, authToken) { + // Check decision through authzforce + AZF.checkPolicies(authToken, getData(req, res), req) + .then((decision) => { + access.adjudicate(req, res, decision); + }) + .catch((e) => { + if (e.status === 404) { + debug('Domain not found: ', e); + access.deny(res, 'Domain not Found', 'urn:dx:as:UnauthorizedEndpoint'); + } else { + access.internalError(res, e, 'Authzforce'); + } + }); +} + +const authorize = { + idm: idmAuthorize, + xacml: xacmlAuthorize, + ishare: iShareAuthorize, + opa: openPolicyAgentAuthorize, + azf: authzforceAuthorize +}; + +const payload_enabled = { + azf: AZF.payload_enabled, + idm: IDM.payload_enabled, + xacml: XACML.payload_enabled, + ishare: ISHARE.payload_enabled, + opa: OPA.payload_enabled +}; + +const jwt_enabled = { + azf: AZF.jwt_enabled, + idm: IDM.jwt_enabled, + xacml: XACML.jwt_enabled, + ishare: ISHARE.jwt_enabled, + opa: OPA.jwt_enabled +}; + +/** + * Can the PDP check payloads? + */ +exports.checkPayload = function () { + const authorization = config_service.get_config().authorization; + return payload_enabled[authorization.pdp]; +}; + +/** + * Can the PDP validate from a JWT? + */ +exports.validateJWT = function () { + const authorization = config_service.get_config().authorization; + return jwt_enabled[authorization.pdp]; +}; + +exports.authorize = function (req, res, authToken) { + return authorize[config_service.get_config().authorization.pdp](req, res, authToken); +}; diff --git a/lib/azf.js b/lib/azf.js deleted file mode 100644 index 1ee3514..0000000 --- a/lib/azf.js +++ /dev/null @@ -1,304 +0,0 @@ -const config_service = require('./config_service.js'); -const config = config_service.get_config(); -const proxy = require('./HTTPClient.js'); -const xml2json = require('xml2json'); -const escapeXML = require('escape-html'); -const xml2js = require('xml2js'); - -const debug = require('debug')('pep-proxy:AZF-Client'); - -const AZF = (function() { - const checkConn = function(callback, callbackError) { - const options = { - host: config.azf.host, - port: config.azf.port, - path: '/', - method: 'GET', - }; - const protocol = config.azf.ssl ? 'https' : 'http'; - proxy.sendData( - protocol, - options, - undefined, - undefined, - callback, - callbackError - ); - }; - - const checkPermissions = function( - authToken, - userInfo, - req, - callback, - callbackError, - cache - ) { - const roles = getRoles(userInfo); - const appId = userInfo.app_id; - const azfDomain = userInfo.app_azf_domain; - - let xml; - const action = req.method; - const resource = req.path; - - if (config.authorization.azf.custom_policy) { - debug('Checking custom policy with AZF...'); - xml = require('./../policies/' + config.azf.custom_policy).getPolicy( - roles, - req, - appId - ); - } else { - if ( - cache[authToken] && - cache[authToken][action] && - cache[authToken][action].indexOf(resource) !== -1 - ) { - debug('Permission in cache...'); - - callback(); - return; - } - debug('Checking auth with AZF...'); - xml = getRESTPolicy(roles, action, resource, appId); - } - - if (!azfDomain) { - callbackError(404, 'AZF domain not created for application ' + appId); - } else { - sendData( - xml, - authToken, - azfDomain, - function() { - // only caching basic authorization policies (verb + path) - if (!config.authorization.azf.custom_policy && cache[authToken]) { - if (!cache[authToken][action]) { - cache[authToken][action] = []; - cache[authToken][action].push(resource); - } else if ( - cache[authToken][action] && - cache[authToken][action].indexOf(resource) === -1 - ) { - cache[authToken][action].push(resource); - } - } - - callback(); - }, - callbackError - ); - } - }; - - const getRoles = function(userInfo) { - const roles = []; - for (const orgIdx in userInfo.organizations) { - const org = userInfo.organizations[orgIdx]; - for (const roleIdx in org.roles) { - const role = org.roles[roleIdx]; - if (roles.indexOf(role.id) === -1) { - roles.push(role.id); - } - } - } - - for (const roleIdx in userInfo.roles) { - const role = userInfo.roles[roleIdx]; - if (roles.indexOf(role) === -1) { - roles.push(role.id); - } - } - - return roles; - }; - - const getRESTPolicy = function(roles, action, resource, appId) { - debug( - 'Checking authorization to roles', - roles, - 'to do ', - action, - ' on ', - resource, - 'and app ', - appId - ); - - const XACMLPolicy = { - Request: { - xmlns: 'urn:oasis:names:tc:xacml:3.0:core:schema:wd-17', - CombinedDecision: 'false', - ReturnPolicyIdList: 'false', - Attributes: [ - { - Category: - 'urn:oasis:names:tc:xacml:1.0:subject-category:access-subject', - Attribute: [ - // ????? - // { - // "AttributeId":"urn:oasis:names:tc:xacml:1.0:subject:subject-id", - // "IncludeInResult": "false", - // "AttributeValue":{ - // "DataType":"http://www.w3.org/2001/XMLSchema#string", - // "$t":"joe" - // } - // }, - // Include the role Attribute if and only if the user has at least one role, since the XACML schema requires at least one AttributeValue in a element - //{ - // "AttributeId":"urn:oasis:names:tc:xacml:2.0:subject:role", - // "IncludeInResult": "false", - // "AttributeValue": [ - // One per role - // { - // "DataType":"http://www.w3.org/2001/XMLSchema#string", - // "$t":"Manager" - // } - // ] - //} - ], - }, - { - Category: - 'urn:oasis:names:tc:xacml:3.0:attribute-category:resource', - Attribute: [ - { - AttributeId: - 'urn:oasis:names:tc:xacml:1.0:resource:resource-id', - IncludeInResult: 'false', - AttributeValue: { - DataType: 'http://www.w3.org/2001/XMLSchema#string', - $t: appId, - }, - }, - { - AttributeId: 'urn:thales:xacml:2.0:resource:sub-resource-id', - IncludeInResult: 'false', - AttributeValue: { - DataType: 'http://www.w3.org/2001/XMLSchema#string', - $t: escapeXML(resource), - }, - }, - ], - }, - { - Category: 'urn:oasis:names:tc:xacml:3.0:attribute-category:action', - Attribute: { - AttributeId: 'urn:oasis:names:tc:xacml:1.0:action:action-id', - IncludeInResult: 'false', - AttributeValue: { - DataType: 'http://www.w3.org/2001/XMLSchema#string', - $t: action, - }, - }, - }, - { - Category: - 'urn:oasis:names:tc:xacml:3.0:attribute-category:environment', - }, - ], - }, - }; - - // create Attribute only roles is not empty because XACML schema requires that an Attribute has at least one AttributeValue - if (roles.length > 0) { - XACMLPolicy.Request.Attributes[0].Attribute[0] = { - AttributeId: 'urn:oasis:names:tc:xacml:2.0:subject:role', - IncludeInResult: 'false', - AttributeValue: [ - // One per role - // { - // "DataType":"http://www.w3.org/2001/XMLSchema#string", - // "$t":"Manager" - // } - ], - }; - - for (const i in roles) { - XACMLPolicy.Request.Attributes[0].Attribute[0].AttributeValue[i] = { - //"AttributeId":"urn:oasis:names:tc:xacml:2.0:subject:role", - //"IncludeInResult": "false", - //"AttributeValue":{ - DataType: 'http://www.w3.org/2001/XMLSchema#string', - $t: roles[i], - //} - }; - } - } - - const xml = - '' + - xml2json.toXml(XACMLPolicy); - - debug('XML: ', xml); - return xml; - }; - - const sendData = function(xml, authToken, azfDomain, success, error) { - const path = '/authzforce-ce/domains/' + azfDomain + '/pdp'; - - const options = { - host: config.authorization.azf.host, - port: config.authorization.azf.port, - path, - method: 'POST', - headers: { - 'X-Auth-Token': authToken, - Accept: 'application/xml', - 'Content-Type': 'application/xml', - }, - }; - - const protocol = config.authorization.azf.ssl ? 'https' : 'http'; - - proxy.sendData( - protocol, - options, - xml, - undefined, - function(status, resp) { - debug('AZF response status: ', status); - debug('AZF response: ', resp); - let decision; - // xml2json keeps namespace prefixes in json keys, which is not right because prefixes are not supposed to be fixed; only the namespace URIs they refer to - // After parsing to JSON, we need to extract the Decision element in XACML namespace.. - // But there does not seem to be any good npm packge supporting namespace-aware XPath or equivalent evaluation on JSON. - // (xml2js-xpath will probably support namespaces in the next release: https://github.com/dsummersl/node-xml2js-xpath/issues/5 ) - // The easy way to go (but with inconvenients) is to get rid of prefixes.One way to refixes is to use npm package 'xml2js' with stripPrefix option. - xml2js.parseString( - resp, - { tagNameProcessors: [xml2js.processors.stripPrefix] }, - function(err, jsonRes) { - debug('AZF response parsing result (JSON): ', jsonRes); - debug( - "AZF response parsing error ('null' means no error): ", - err - ); - // xml2js puts child nodes in array by default, except on the root node (option 'explicitArray') - decision = jsonRes.Response.Result[0].Decision[0]; - } - ); - - decision = String(decision); - - debug('Decision: ', decision); - if (decision.includes('Permit')) { - success(); - } else { - error( - 401, - 'User not authorized in AZF for the given action and resource' - ); - } - }, - error - ); - }; - - return { - checkPermissions, - checkConn, - }; -})(); -exports.AZF = AZF; diff --git a/lib/cache.js b/lib/cache.js new file mode 100644 index 0000000..ed79aea --- /dev/null +++ b/lib/cache.js @@ -0,0 +1,88 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const debug = require('debug')('pep-proxy:cache'); +const config_service = require('./config_service'); +const NodeCache = require('node-cache'); +const cache = new NodeCache({ + stdTTL: config_service.get_config().cache_time, + checkperiod: config_service.get_config().cache_time +}); + +/** + * Add a user into the cache + */ +function storeUser(token, user) { + cache.set(token, { date: new Date(), info: user }); +} + +/** + * Add an action+resource+token combo into the cache + */ +function storeAction(token, action, resource) { + const user = cache.get(token); + if (user) { + if (!user[action]) { + user[action] = []; + user[action].push(resource); + } else if (user[action] && user[action].indexOf(resource) === -1) { + user[action].push(resource); + } + + cache.set(token, user); + } +} + +/** + * Check if a user is found in the cache + * @return a user if a token for the given user is found and the token + * has not expired + */ +function checkTokenCache(token, jwtExpiration, action, resource) { + const user = cache.get(token); + const config = config_service.get_config(); + if (!user) { + return undefined; + } + debug('Token found, checking timestamp...'); + debug(token); + const currentTime = new Date().getTime(); + const tokenTime = jwtExpiration ? jwtExpiration * 1000 : user.date.getTime(); + + if (currentTime - tokenTime > config.cache_time * 1000) { + debug('Token in cache expired'); + cache.del(token); + return undefined; + } + + if (config.authorization.pdp === 'idm') { + if (tokenPermission(token, action, resource)) { + debug('Action-level permission in cache...'); + } else { + return undefined; + } + } + return user.info; +} + +/** + * Check if an action+resource is found in the cache + * @return true if a token for the given action and resource is found + */ +function tokenPermission(token, action, resource) { + const user = cache.get(token); + return user && user[action] && user[action].indexOf(resource) !== -1; +} + +exports.flush = function () { + cache.flushAll(); +}; + +exports.storeUser = storeUser; +exports.storeAction = storeAction; +exports.checkCache = checkTokenCache; +exports.tokenPermission = tokenPermission; diff --git a/lib/config_service.js b/lib/config_service.js index ea97fac..56ceb2c 100644 --- a/lib/config_service.js +++ b/lib/config_service.js @@ -1,4 +1,11 @@ -const debug = require('debug')('pep-proxy:Server'); +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const debug = require('debug')('pep-proxy:config'); const path = require('path'); const fs = require('fs'); @@ -9,7 +16,7 @@ const secrets = {}; if (fs.existsSync(SECRETS_DIR)) { const files = fs.readdirSync(SECRETS_DIR); // eslint-disable-next-line no-unused-vars - files.forEach(function (file, index) { + files.forEach((file, index) => { const fullPath = path.join(SECRETS_DIR, file); const key = file; try { @@ -65,6 +72,11 @@ function process_environment_variables(verbose) { 'PEP_TRUSTED_APPS', // Deprecated 'PEP_PROXY_AUTH_ENABLED', 'PEP_PROXY_PDP', + 'PEP_PROXY_PDP_PROTOCOL', + 'PEP_PROXY_PDP_HOST', + 'PEP_PROXY_PDP_PORT', + 'PEP_PROXY_PDP_PATH', + 'PEP_PROXY_TENANT_HEADER', 'PEP_PROXY_AZF_PROTOCOL', 'PEP_PROXY_AZF_HOST', 'PEP_PROXY_AZF_PORT', @@ -78,7 +90,11 @@ function process_environment_variables(verbose) { 'PEP_PROXY_CORS_MAX_AGE', 'PEP_PROXY_AUTH_FOR_NGINX', 'PEP_PROXY_MAGIC_KEY', - 'PEP_PROXY_DEBUG' + 'PEP_PROXY_DEBUG', + 'PEP_PROXY_ERROR_TEMPLATE', + 'PEP_PROXY_ERROR_CONTENT_TYPE', + 'PEP_PROXY_CLUSTER_TYPE', + 'PEP_PROXY_CLUSTER_NUMBER' ]; const protected_variables = [ @@ -89,7 +105,7 @@ function process_environment_variables(verbose) { 'PEP_PASSWORD', // Deprecated 'PEP_TOKEN_SECRET', // Deprecated 'PEP_TRUSTED_APPS' // Deprecated - ]; + ]; // Substitute Docker Secret Variables where set. protected_variables.forEach((key) => { @@ -185,7 +201,7 @@ function process_environment_variables(verbose) { config.pep.trusted_apps = to_array(process.env.PEP_TRUSTED_APPS, []); } - // if enabled PEP checks permissions in two ways: + // if enabled PEP checks permissions in four ways: // - With IdM: only allow basic authorization // - With Authzforce: allow basic and advanced authorization. // For advanced authorization, you can use custom policy checks by including programatic scripts @@ -197,21 +213,53 @@ function process_environment_variables(verbose) { if (process.env.PEP_PROXY_AUTH_ENABLED) { config.authorization.enabled = to_boolean(process.env.PEP_PROXY_AUTH_ENABLED, false); } - if (process.env.PEP_PROXY_PDP) { - config.authorization.pdp = process.env.PEP_PROXY_PDP; - } - config.authorization.azf = config.authorization.azf || {}; - if (process.env.PEP_PROXY_AZF_PROTOCOL) { - config.authorization.azf.protocol = process.env.PEP_PROXY_AZF_PROTOCOL; - } - if (process.env.PEP_PROXY_AZF_HOST) { - config.authorization.azf.host = process.env.PEP_PROXY_AZF_HOST; - } - if (process.env.PEP_PROXY_AZF_PORT) { - config.authorization.azf.port = process.env.PEP_PROXY_AZF_PORT; + if (config.authorization.enabled) { + if (process.env.PEP_PROXY_PDP) { + config.authorization.pdp = process.env.PEP_PROXY_PDP; + } + let pdp = config.authorization.pdp.toLowerCase(); + if (pdp === 'authzforce') { + pdp = 'azf'; + } + config.authorization.pdp = pdp; + config.authorization.azf = config.authorization.azf || {}; + config.authorization[pdp] = config.authorization.location || {}; + + if (process.env.PEP_PROXY_PDP_PROTOCOL) { + config.authorization[pdp].protocol = process.env.PEP_PROXY_PDP_PROTOCOL; + } + if (process.env.PEP_PROXY_PDP_HOST) { + config.authorization[pdp].host = process.env.PEP_PROXY_PDP_HOST; + } + if (process.env.PEP_PROXY_PDP_PORT) { + config.authorization[pdp].port = process.env.PEP_PROXY_PDP_PORT; + } + if (process.env.PEP_PROXY_PDP_PATH) { + config.authorization[pdp].path = process.env.PEP_PROXY_PDP_PATH; + } + + // Deprecated - use common PDP attributes + if (process.env.PEP_PROXY_AZF_PROTOCOL) { + config.authorization.azf.protocol = process.env.PEP_PROXY_AZF_PROTOCOL; + } + // Deprecated - use common PDP attributes + if (process.env.PEP_PROXY_AZF_HOST) { + config.authorization.azf.host = process.env.PEP_PROXY_AZF_HOST; + } + // Deprecated - use common PDP attributes + if (process.env.PEP_PROXY_AZF_PORT) { + config.authorization.azf.port = process.env.PEP_PROXY_AZF_PORT; + } + if (process.env.PEP_PROXY_AZF_CUSTOM_POLICY) { + config.authorization.azf.custom_policy = process.env.PEP_PROXY_AZF_CUSTOM_POLICY; + } + } else { + // Authorization is disabled - remove auth config. + config.authorization = {}; } - if (process.env.PEP_PROXY_AZF_CUSTOM_POLICY) { - config.authorization.azf.custom_policy = process.env.PEP_PROXY_AZF_CUSTOM_POLICY; + + if (process.env.PEP_PROXY_TENANT_HEADER) { + config.authorization.header = process.env.PEP_PROXY_TENANT_HEADER; } if (process.env.PEP_PROXY_PUBLIC_PATHS) { @@ -256,6 +304,18 @@ function process_environment_variables(verbose) { if (process.env.PEP_PROXY_DEBUG) { config.debug = to_boolean(process.env.PEP_PROXY_DEBUG, true); } + if (process.env.PEP_PROXY_ERROR_TEMPLATE) { + config.error_template = process.env.PEP_PROXY_ERROR_TEMPLATE; + } + if (process.env.PEP_PROXY_ERROR_CONTENT_TYPE) { + config.error_content_type = process.env.PEP_PROXY_ERROR_CONTENT_TYPE; + } + if (process.env.PEP_PROXY_CLUSTER_TYPE) { + config.type = process.env.PEP_PROXY_CLUSTER_TYPE; + } + if (process.env.PEP_PROXY_CLUSTER_NUMBER) { + config.number = process.env.PEP_PROXY_CLUSTER_NUMBER; + } } function set_config(new_config, verbose = false) { diff --git a/lib/idm.js b/lib/idm.js deleted file mode 100644 index dd831d5..0000000 --- a/lib/idm.js +++ /dev/null @@ -1,255 +0,0 @@ -const config_service = require('../lib/config_service.js'); -const config = config_service.get_config(); -const proxy = require('./HTTPClient.js'); -const isHex = require('is-hex'); -const debug = require('debug')('pep-proxy:IDM-Client'); - -const IDM = (function() { - let myToken; - - const checkConn = function(callback, callbackError) { - const options = { - host: config.idm.host, - port: config.idm.port, - path: '/version', - method: 'GET', - }; - const protocol = config.idm.ssl ? 'https' : 'http'; - - proxy.sendData( - protocol, - options, - undefined, - undefined, - callback, - callbackError - ); - }; - - const authenticate = function(callback, callbackError) { - const options = { - host: config.idm.host, - port: config.idm.port, - path: '/v3/auth/tokens', - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }; - const protocol = config.idm.ssl ? 'https' : 'http'; - const body = { - name: config.pep.username, - password: config.pep.password, - }; - proxy.sendData( - protocol, - options, - JSON.stringify(body), - undefined, - function(status, resp, headers) { - const response = JSON.parse(resp); - if (response.idm_authorization_config) { - debug('IDM authorization configuration:'); - debug( - ' + Authzforce enabled: ' + - response.idm_authorization_config.authzforce - ); - const rules = - response.idm_authorization_config.level === 'advanced' - ? 'HTTP Verb+Resource and Advanced' - : 'HTTP Verb+Resource'; - debug(' + Authorization rules allowed: ' + rules); - } - - myToken = headers['x-subject-token']; - callback(myToken); - }, - callbackError - ); - }; - - const checkToken = function( - token, - jwtExpiration, - action, - resource, - authzforce, - organizationToken, - callback, - callbackError, - cache - ) { - let path = '/user?access_token=' + encodeURIComponent(token); - - if (action && resource) { - path = path + '&action=' + action; - path = path + '&resource=' + resource; - path = path + '&app_id=' + config.pep.app_id; - } else if (authzforce) { - path = path + '&authzforce=' + authzforce; - } - else { - path = path + '&app_id=' + config.pep.app_id; - } - - const options = { - host: config.idm.host, - port: config.idm.port, - path, - method: 'GET', - headers: { 'X-Auth-Token': myToken, Accept: 'application/json' }, - }; - - const protocol = config.idm.ssl ? 'https' : 'http'; - - checkTokenCache(token, jwtExpiration, action, resource, cache, function( - cachedUserInfo - ) { - if (cachedUserInfo) { - callback(cachedUserInfo); - } else { - debug('Checking token with IDM...'); - proxy.sendData( - protocol, - options, - undefined, - undefined, - function(status, resp) { - const userInfo = JSON.parse(resp); - const organizations = userInfo.organizations - ? userInfo.organizations.map(elem => elem.id) - : []; - - - if (!checkApplication(userInfo.app_id, userInfo.trusted_apps)) { - debug('User not authorized in application', config.pep.app_id); - callbackError( - 401, - 'User not authorized in application', - config.pep.app_id - ); - } else if (!checkOrganizations(organizations, organizationToken)) { - debug('User not belongs to organization', organizationToken); - callbackError( - 401, - 'User not belongs to organization', - organizationToken - ); - } else { - storeToken(token, userInfo, action, resource, cache); - callback(userInfo); - } - }, - function(status, e) { - debug('Error in IDM communication ', e); - callbackError(status, e ? JSON.parse(e) : undefined); - } - ); - } - }); - }; - - const checkTokenCache = function( - token, - jwtExpiration, - action, - resource, - cache, - callback - ) { - if (cache[token]) { - debug('Token in cache, checking timestamp...'); - debug(token); - const currentTime = new Date().getTime(); - const tokenTime = - token.length <= 40 && isHex(token) - ? jwtExpiration * 1000 - : cache[token].date.getTime(); - - if (currentTime - tokenTime < config.cache_time * 1000) { - if ( - config.authorization.enabled && - config.authorization.pdp === 'idm' - ) { - if ( - cache[token] && - cache[token][action] && - cache[token][action].indexOf(resource) !== -1 - ) { - debug('Permission in cache...'); - - callback(cache[token].userInfo); - } else { - callback(); - } - } else { - callback(cache[token].userInfo); - } - } else { - debug('Token in cache expired'); - delete cache[token]; - callback(); - } - } else { - callback(); - } - }; - - const storeToken = function(token, userInfo, action, resource, cache) { - cache[token] = {}; - cache[token].date = new Date(); - cache[token].userInfo = userInfo; - - if (config.authorization.enabled) { - if ( - config.authorization.pdp === 'idm' && - userInfo.authorization_decision === 'Permit' - ) { - if (!cache[token][action]) { - cache[token][action] = []; - cache[token][action].push(resource); - } else if ( - cache[token][action] && - cache[token][action].indexOf(resource) === -1 - ) { - cache[token][action].push(resource); - } - } - } - }; - - const checkApplication = function(appId, trusted_apps) { - debug('Token created in application: ', appId); - debug('PEP Proxy application: ', config.pep.app_id); - debug('PEP Proxy trusted_apps: ', config.pep.trusted_apps); - - if ( - appId === config.pep.app_id || - trusted_apps.indexOf(appId) !==-1 || - config.pep.trusted_apps.indexOf(appId) !== -1 - ) { - return true; - } - return false; - }; - - const checkOrganizations = function(organizations, organizationToken) { - if (!config.organizations.enabled) { - return true; - } - debug('User belongs to: ', organizations); - debug('Token is in the scope of: ', organizationToken); - - if (organizations.includes(organizationToken)) { - return true; - } - return false; - }; - - return { - checkConn, - authenticate, - checkToken, - checkTokenCache, - storeToken, - }; -})(); -exports.IDM = IDM; diff --git a/lib/payload_analyse.js b/lib/payload_analyse.js new file mode 100644 index 0000000..3e40666 --- /dev/null +++ b/lib/payload_analyse.js @@ -0,0 +1,187 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const _ = require('underscore'); +const debug = require('debug')('pep-proxy:payload'); + +function getAttrs(obj, ids, types, attrs) { + if (Array.isArray(obj)) { + obj.forEach((element) => { + getAttrs(element, ids, types, attrs); + }); + } else { + const keys = _.without(_.keys(obj), 'value', 'type', 'id', 'observedAt', 'metadata', 'unitCode'); + + keys.forEach((key) => { + if (Array.isArray(obj[key])) { + getAttrs(obj[key], ids, types, attrs); + } else { + attrs.push(key); + } + }); + + if (obj.id) { + ids.push(obj.id); + } + if (obj.type) { + types.push(obj.type); + } + } +} + +/** + * Check the payload body for attributes, types and ids + */ +exports.v2batch = function (req, res, next) { + debug('v2batch'); + + const ids = []; + const attrs = []; + const types = []; + + if (req.body) { + const body = JSON.parse(req.body.toString()); + const entities = body.entities || []; + getAttrs(entities, ids, types, attrs); + res.locals.ids = _.uniq(ids); + res.locals.attrs = _.uniq(attrs); + res.locals.types = _.uniq(types); + } + next(); +}; + +/** + * Check the payload body for attributes, types and ids + */ +exports.subscription = function (req, res, next) { + debug('subscription'); + + function getSubIdPatterns(entityInfo) { + const IdPatterns = []; + + entityInfo.forEach((entity) => { + if (entity.idPattern) { + IdPatterns.push(entity.idPattern); + } + }); + return _.isEmpty(IdPatterns) ? undefined : IdPatterns; + } + + function getSubIds(entityInfo) { + const ids = []; + entityInfo.forEach((entity) => { + if (entity.id) { + ids.push(entity.id); + } + }); + return _.isEmpty(ids) ? undefined : ids; + } + + function getSubAttrs(notificationAttrs, watchedAttrs) { + let attrs = []; + if (notificationAttrs) { + attrs = _.union(attrs, notificationAttrs); + } + if (watchedAttrs) { + attrs = _.union(attrs, watchedAttrs); + } + return _.isEmpty(attrs) ? undefined : attrs; + } + + function getSubTypes(entityInfo) { + const types = []; + entityInfo.forEach((entity) => { + if (entity.type) { + types.push(entity.type); + } + }); + + return _.isEmpty(types) ? undefined : types; + } + + if (req.body) { + const body = JSON.parse(req.body.toString()); + const entityInfo = body.entities || []; + const notification = body.notification || {}; + + res.locals.IdPatterns = getSubIdPatterns(entityInfo); + res.locals.ids = getSubIds(entityInfo); + res.locals.attrs = getSubAttrs(notification.attributes, body.watchedAttributes); + res.locals.types = getSubTypes(entityInfo); + } + next(); +}; + +/** + * Check the payload body for attributes, types and ids + */ +exports.body = function (req, res, next) { + debug('body'); + const ids = []; + const attrs = []; + const types = []; + + if (req.body) { + getAttrs(JSON.parse(req.body.toString()), ids, types, attrs); + res.locals.ids = _.uniq(ids); + res.locals.attrs = _.uniq(attrs); + res.locals.types = _.uniq(types); + } + next(); +}; + +/** + * Check the URL path for attributes and ids + */ +exports.params = function (req, res, next) { + debug('params'); + if (req.params) { + if (req.params.id) { + res.locals.ids = res.locals.ids || []; + if (!res.locals.ids.includes(req.params.id)) { + res.locals.ids.push(req.params.id); + } + } + + if (req.params.attr) { + res.locals.attr = res.locals.attr || []; + if (!res.locals.attr.includes(req.params.attr)) { + res.locals.ids.push(req.params.attr); + } + } + } + next(); +}; + +/** + * Check the query string for attributes, types and ids + */ +exports.query = function (req, res, next) { + debug('query'); + if (req.query) { + if (req.query.ids) { + res.locals.ids = res.locals.ids || []; + const ids = req.query.ids.split(','); + ids.forEach((id) => { + if (!res.locals.ids.includes(id)) { + res.locals.ids.push(id); + } + }); + } + + if (req.query.type) { + res.locals.types = res.locals.types || []; + const types = req.query.type.split(','); + types.forEach((type) => { + if (!res.locals.types.includes(type)) { + res.locals.types.push(type); + } + }); + } + } + next(); +}; diff --git a/lib/pdp/authzforce.js b/lib/pdp/authzforce.js new file mode 100644 index 0000000..cc36a88 --- /dev/null +++ b/lib/pdp/authzforce.js @@ -0,0 +1,225 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('../config_service'); +const xml2json = require('xml2json'); +const escapeXML = require('escape-html'); +const xml2js = require('xml2js'); +const got = require('got'); + +const debug = require('debug')('pep-proxy:AZF-Client'); +const cache = require('../cache'); + +/** + * Can the Authzforce PDP check payloads? + * + * Not yet, but the Authzforce policies could be reconfigured to check payloads at + * some point + */ +exports.payload_enabled = false; +/** + * Can the Authzforce PDP authorize via a JWT? + */ +exports.jwt_enabled = true; + +function getUrl() { + const pdp = config_service.get_config().authorization.azf; + return (pdp.ssl ? 'https://' : 'http://') + pdp.host + ':' + pdp.port; +} + +/** + * Check that Authzforce is available and responding to requests + */ +exports.checkConnectivity = function () { + return got('', { prefixUrl: getUrl() }); +}; + +/** + * Make request to the Authzforce PDP and interpret the result + * + * @param authToken - the authToken for Authzforce + * @param data - A bag of data holding the action, resources, payload etc. + * this will be used to make the decision + * @param req - the incoming request for custom policies + * + * @return permit/deny + */ +exports.checkPolicies = function (authToken, data, req) { + const azf = config_service.get_config().authorization.azf; + + return new Promise((resolve, reject) => { + let xml; + const action = data.action; + const resource = data.resource; + const roles = data.roles; + const appId = data.appId; + + if (!data.azfDomain) { + return reject({ status: 404, message: 'AZF domain not created for application ' + appId }); + } + if (cache.tokenPermission(authToken, action, resource)) { + debug('Permission in cache...'); + return resolve(true); + } + + if (azf.custom_policy) { + debug('Checking custom policy with AZF...'); + xml = require('./../policies/' + azf.custom_policy).getPolicy(roles, req, appId); + } else { + xml = getRESTPolicy(roles, action, resource, appId); + } + return got + .post('authzforce-ce/domains/' + data.azfDomain + '/pdp', { + prefixUrl: getUrl(), + headers: { + 'X-Auth-Token': authToken, + Accept: 'application/xml', + 'Content-Type': 'application/xml' + }, + body: xml + }) + .then((response) => { + debug('AZF response status: ', response.statusCode); + debug(response.body); + + // xml2json keeps namespace prefixes in json keys, which is not right because prefixes are not supposed to be fixed; only the namespace URIs they refer to + // After parsing to JSON, we need to extract the Decision element in XACML namespace.. + // But there does not seem to be any good npm packge supporting namespace-aware XPath or equivalent evaluation on JSON. + // (xml2js-xpath will probably support namespaces in the next release: https://github.com/dsummersl/node-xml2js-xpath/issues/5 ) + // The easy way to go (but with inconvenients) is to get rid of prefixes.One way to refixes is to use npm package 'xml2js' with stripPrefix option. + xml2js.parseString(response.body, { tagNameProcessors: [xml2js.processors.stripPrefix] }, (err, json) => { + if (err) { + return reject(err); + } + // xml2js puts child nodes in array by default, except on the root node (option 'explicitArray') + const decision = json.Response.Result[0].Decision[0].includes('Permit'); + if (decision && !azf.custom_policy) { + cache.storeAction(authToken, action, resource); + } + return resolve(decision); + }); + }) + .catch((error) => { + if (error instanceof got.HTTPError) { + return resolve(false); + } + debug('Error in Authzforce communication ', error); + return reject(error); + }); + }); +}; + +/** + * Create a payload for making an XACML request to Authzforce + * based on the action,resource,tenant and attributes + * @return XML payload + */ +const getRESTPolicy = function (roles, action, resource, appId) { + debug('Checking authorization to roles', roles, 'to do ', action, ' on ', resource, 'and app ', appId); + + const XACMLPolicy = { + Request: { + xmlns: 'urn:oasis:names:tc:xacml:3.0:core:schema:wd-17', + CombinedDecision: 'false', + ReturnPolicyIdList: 'false', + Attributes: [ + { + Category: 'urn:oasis:names:tc:xacml:1.0:subject-category:access-subject', + Attribute: [ + // ????? + // { + // "AttributeId":"urn:oasis:names:tc:xacml:1.0:subject:subject-id", + // "IncludeInResult": "false", + // "AttributeValue":{ + // "DataType":"http://www.w3.org/2001/XMLSchema#string", + // "$t":"joe" + // } + // }, + // Include the role Attribute if and only if the user has at least one role, since the XACML schema requires at least one AttributeValue in a element + //{ + // "AttributeId":"urn:oasis:names:tc:xacml:2.0:subject:role", + // "IncludeInResult": "false", + // "AttributeValue": [ + // One per role + // { + // "DataType":"http://www.w3.org/2001/XMLSchema#string", + // "$t":"Manager" + // } + // ] + //} + ] + }, + { + Category: 'urn:oasis:names:tc:xacml:3.0:attribute-category:resource', + Attribute: [ + { + AttributeId: 'urn:oasis:names:tc:xacml:1.0:resource:resource-id', + IncludeInResult: 'false', + AttributeValue: { + DataType: 'http://www.w3.org/2001/XMLSchema#string', + $t: appId + } + }, + { + AttributeId: 'urn:thales:xacml:2.0:resource:sub-resource-id', + IncludeInResult: 'false', + AttributeValue: { + DataType: 'http://www.w3.org/2001/XMLSchema#string', + $t: escapeXML(resource) + } + } + ] + }, + { + Category: 'urn:oasis:names:tc:xacml:3.0:attribute-category:action', + Attribute: { + AttributeId: 'urn:oasis:names:tc:xacml:1.0:action:action-id', + IncludeInResult: 'false', + AttributeValue: { + DataType: 'http://www.w3.org/2001/XMLSchema#string', + $t: action + } + } + }, + { + Category: 'urn:oasis:names:tc:xacml:3.0:attribute-category:environment' + } + ] + } + }; + + // create Attribute only roles is not empty because XACML schema requires that an Attribute has at least one AttributeValue + if (roles.length > 0) { + XACMLPolicy.Request.Attributes[0].Attribute[0] = { + AttributeId: 'urn:oasis:names:tc:xacml:2.0:subject:role', + IncludeInResult: 'false', + AttributeValue: [ + // One per role + // { + // "DataType":"http://www.w3.org/2001/XMLSchema#string", + // "$t":"Manager" + // } + ] + }; + + for (const i in roles) { + XACMLPolicy.Request.Attributes[0].Attribute[0].AttributeValue[i] = { + //"AttributeId":"urn:oasis:names:tc:xacml:2.0:subject:role", + //"IncludeInResult": "false", + //"AttributeValue":{ + DataType: 'http://www.w3.org/2001/XMLSchema#string', + $t: roles[i] + //} + }; + } + } + + const xml = '' + xml2json.toXml(XACMLPolicy); + + debug(xml); + return xml; +}; diff --git a/lib/pdp/iShare.js b/lib/pdp/iShare.js new file mode 100644 index 0000000..1799ce0 --- /dev/null +++ b/lib/pdp/iShare.js @@ -0,0 +1,114 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const debug = require('debug')('pep-proxy:iShare-Client'); + +/** + * Can the iSHARE PDP check payloads? + */ +exports.payload_enabled = true; +/** + * Can the iSHARE PDP authorize via JWT? + */ +exports.jwt_enabled = true; + +/** + * Apply the attached policy to the payload & interpret the result + * + * @param req - the incoming request + * @param data - A bag of data holding the action, resources, payload etc. + * this will be used to make the decision + * + * @return permit/deny + */ + +/* eslint-disable-next-line no-unused-vars */ +exports.checkPolicies = function (token, data, ishare_policy, ishare_authregistry) { + debug('Checking policy'); + + const unix_timestamp = Math.floor(new Date().getTime() / 1000); + if (!ishare_policy || !ishare_policy.policySets) { + debug('No iSHARE Policy found'); + // TODO use the auth registry to retrieve a policy. + return false; + } else if (ishare_policy.notBefore > unix_timestamp) { + debug('Attached iSHARE Policy not yet valid'); + return false; + } else if (ishare_policy.notOnOrAfter <= unix_timestamp) { + debug('Attached iSHARE Policy expired'); + return false; + } else if (!validPayload(data, ishare_policy.policySets)) { + debug('Attached iSHARE Policy disallows the request'); + return false; + } + return true; +}; + +// TODO: This currently does not deal with exception rules +function validPayload(data, policy_sets) { + // If no type found in the payload, set it to check for null + data.payloadTypes = data.payloadTypes || [null]; + const result = data.payloadTypes.every((type) => { + return policy_sets.some((policy_set) => { + // Policies are permissive, at least one policy + // from the collection of policy sets must fire. + return policy_set.policies.some((policy) => { + const ruleEffect = policy.rules[0].effect === 'Permit'; + let fireRule = true; + + // The action of the request must be found in the + // array of actions found in the policy + if (policy.target.actions) { + fireRule = policy.target.actions.some((action) => { + return action.toUpperCase() === data.action; + }); + } + + // If a type is defined in a policy, the payload must have a type + // and that type must equal the policy type directly + if (fireRule && policy.target.resource.type) { + fireRule = !!type && type === policy.target.resource.type; + } + + // If ids are defined in the policy, and ids are found in the payload + // the payload ids must match a regex + if (fireRule && policy.target.resource.identifiers && data.payloadEntityIds) { + fireRule = data.payloadEntityIds.every((id) => { + return policy.target.resource.identifiers.some((identifier) => { + const regex = new RegExp(identifier, 'i'); + return regex.test(id); + }); + }); + } + + // If ids are defined in the policy, and idPatterns are found in the payload + // the payload idPatterns must equal the policy id directly + if (fireRule && policy.target.resource.identifiers && data.payloadIdPatterns) { + fireRule = data.payloadIdPatterns.every((id) => { + return policy.target.resource.identifiers.some((identifier) => { + return identifier === id; + }); + }); + } + + // If attributes are defined in the policy, and attributes are found in the payload + // the payload attributes must match a regex + if (fireRule && policy.target.resource.attributes && data.payloadEntityAttrs) { + fireRule = data.payloadEntityAttrs.every((attr) => { + return policy.target.resource.attributes.some((attribute) => { + const regex = new RegExp(attribute, 'i'); + return regex.test(attr); + }); + }); + } + + return fireRule ? ruleEffect : !ruleEffect; + }); + }); + }); + return result; +} diff --git a/lib/pdp/keyrock.js b/lib/pdp/keyrock.js new file mode 100644 index 0000000..07da7b3 --- /dev/null +++ b/lib/pdp/keyrock.js @@ -0,0 +1,186 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('../config_service'); +const debug = require('debug')('pep-proxy:IDM-Client'); +const cache = require('../cache'); +const got = require('got'); + +/** + * Can the Keyrock PDP check payloads? + */ +exports.payload_enabled = false; +/** + * Can the Keyrock PDP check JWT? + */ +exports.jwt_enabled = false; + +function getUrl() { + const idm = config_service.get_config().idm; + return (idm.ssl ? 'https://' : 'http://') + idm.host + ':' + idm.port; +} +/** + * The Permit/Deny decision will be found within the user data. + * + * @param user - the authorized User + * + * @return permit/deny + */ +exports.checkPolicies = function (user) { + return user.authorization_decision === 'Permit'; +}; + +/** + * When making a request to Keyrock ensure that the correct data is + * returned. When using Keyrock as a PDP, also include the action and + * resource. When using Authzforce as the PDP, ensure the domain is + * returned for further request. + * + * @param token - the PEP bearer token + * @param action - the action the user is requesting + * @param resource - the resource the user is requesting + * @param tenant - the tenant or service path the user is requesting + * + * @return a URL to make the request to Keyrock + */ +function getPath(token, action, resource, tenant) { + const authorization = config_service.get_config().authorization; + const policy_decision_point = authorization.enabled ? authorization.pdp : undefined; + let path = + 'user?access_token=' + encodeURIComponent(token.authToken) + '&app_id=' + config_service.get_config().pep.app_id; + + if (policy_decision_point === 'idm') { + // Using Keyrock as combined IDM and PDP - get a permit/deny adjudication directly + path = path + '&action=' + action; + path = path + '&resource=' + resource; + if (tenant) { + path = path + '&authorization_service_header=' + tenant; + } + } + if (policy_decision_point === 'azf') { + // Using Authzforce as a PDP - get the location of the Authzforce PDP Domain + path = path + '&authzforce=true'; + } + + return path; +} + +/** + * Check that the appId aligns with the PEP or its trusted apps. + * @returns true if the application exists + */ +function checkApplication(appId, trusted_apps) { + const pep = config_service.get_config().pep; + return appId === pep.app_id || trusted_apps.indexOf(appId) !== -1 || pep.trusted_apps.indexOf(appId) !== -1; +} + +/** + * Check that a user has valid organizations + * @returns true if the organizations are valid + */ +function checkOrganizations(organizations, organizationToken) { + if (!config_service.get_config().organizations.enabled) { + return true; + } + debug('User belongs to: ', organizations); + debug('Token is in the scope of: ', organizationToken); + + return organizations.includes(organizationToken); +} + +/** + * Check that Keyrock is responding to requests + */ +exports.checkConnectivity = function () { + return got('version', { prefixUrl: getUrl() }); +}; + +/** + * Contact Keyrock and check the PEP Token is valid. + * @return the token and configuration for the PEP + */ +exports.authenticatePEP = function () { + const pep = config_service.get_config().pep; + return got + .post('v3/auth/tokens', { + prefixUrl: getUrl(), + json: { + name: pep.username, + password: pep.password + } + }) + .then((response) => { + const body = JSON.parse(response.body) || {}; + return { config: body.idm_authorization_config, pepToken: response.headers['x-subject-token'] }; + }) + .catch((error) => { + throw error; + }); +}; + +/** + * Connect with Keyrock and see if the token is valid for the application + * When using Keyrock as a PDP this also returns a permit/deny response. + * + * @param token - tokens to use in this request + * @param action - the action the user is requesting + * @param resource - the resource the user is requesting + * @param tenant - the tenant or service path the user is requesting + * + * @return the user if found + */ +exports.authenticateUser = function (token, action, resource, tenant) { + const authorization = config_service.get_config().authorization; + return new Promise((resolve, reject) => { + const authToken = token.authToken; + const authOrgToken = token.authOrgToken; + debug('Authenticating user'); + const cachedUser = cache.checkCache(authToken, token.jwtExpiry, action, resource); + if (cachedUser) { + return resolve(cachedUser); + } + + return got(getPath(token, action, resource, tenant), { + prefixUrl: getUrl(), + headers: { + 'X-Auth-Token': token.pepToken, + Accept: 'application/json' + } + }) + .then((response) => { + const user = JSON.parse(response.body) || {}; + const organizations = user.organizations ? user.organizations.map((elem) => elem.id) : []; + if (!checkApplication(user.app_id, user.trusted_apps)) { + debug('User not authorized in application', config_service.get_config().pep.app_id); + return reject({ + type: 'urn:dx:as:InvalidRole', + message: 'User not have the required role in the application' + }); + } else if (!checkOrganizations(organizations, authOrgToken)) { + debug('User does not belong to the organization', authOrgToken); + return reject({ type: 'urn:dx:as:InvalidRole', message: 'User does not belong to the organization' }); + } + // Keyrock is in use as an IDM - store the User + cache.storeUser(authToken, user); + if (authorization.pdp === 'idm') { + // Keyrock is also in use as a PDP - store the permissions. + cache.storeAction(authToken, action, resource); + } + return resolve(user); + }) + .catch((error) => { + if (error instanceof got.HTTPError) { + return reject({ + type: 'urn:dx:as:InvalidAuthenticationToken', + message: 'User not authorized in the application' + }); + } + debug('Error in IDM communication ', error); + return reject({ message: error.message }); + }); + }); +}; diff --git a/lib/pdp/openPolicyAgent.js b/lib/pdp/openPolicyAgent.js new file mode 100644 index 0000000..d843292 --- /dev/null +++ b/lib/pdp/openPolicyAgent.js @@ -0,0 +1,85 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('../config_service'); +const debug = require('debug')('pep-proxy:OPA-Client'); +const got = require('got'); + +/** + * Can the Open Policy PDP check payloads? + */ +exports.payload_enabled = true; +/** + * Can the Open Policy PDP check JWT? + */ +exports.jwt_enabled = false; + +function getUrl() { + const pdp = config_service.get_config().authorization.opa; + return (pdp.ssl ? 'https://' : 'http://') + pdp.host + ':' + pdp.port + pdp.path; +} + +/** + * Make request to the Open Policy Agent endpoint and interpret the result + * + * @param token - authorization token. + * @param data - A bag of data holding the action, resources, payload etc. + * this will be used to make the decision + * + * + * @return permit/deny + */ +exports.checkPolicies = function (token, data) { + return new Promise((resolve, reject) => { + return got + .post(getUrl(), { + headers: { + 'X-Auth-Token': token + }, + json: getPolicy(data) + }) + .json() + .then((result) => { + debug(result); + return resolve(result.allow); + }) + .catch((error) => { + if (error instanceof got.HTTPError) { + return resolve(false); + } + debug('Error in OPA communication ', error); + return reject(error); + }); + }); +}; + +/** + * Create a payload for making an Open Policy Agent request + * based on the action,resource,tenant and attributes + * @return OPA payload + */ +function getPolicy(data) { + const action = data.action; + const resource = data.resource; + const roles = data.roles; + const appId = data.appId; + debug('Checking authorization to roles', roles, 'to do ', action, ' on ', resource, 'and app ', appId); + const json = { + appId, + resource, + roles, + action, + tenant: data.tenant_header, + ids: data.payloadEntityIds, + idPatterns: data.payloadIdPatterns, + attrs: data.payloadAttrs, + types: data.payloadTypes + }; + + debug('Open Policy Agent request: ', JSON.stringify(json)); + return json; +} diff --git a/lib/pdp/xacml.js b/lib/pdp/xacml.js new file mode 100644 index 0000000..df4c03c --- /dev/null +++ b/lib/pdp/xacml.js @@ -0,0 +1,119 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('../config_service'); +const debug = require('debug')('pep-proxy:XACML-Client'); +const got = require('got'); + +/** + * Can the XACML PDP check payloads? + */ +exports.payload_enabled = true; +/** + * Can the XACML PDP check JWT? + */ +exports.jwt_enabled = false; + +function getUrl() { + const pdp = config_service.get_config().authorization.xacml; + return (pdp.ssl ? 'https://' : 'http://') + pdp.host + ':' + pdp.port + pdp.path; +} + +/** + * Make request to the XACML JSON endpoint and interpret the result + * + * @param token - authorization token. + * @param data - A bag of data holding the action, resources, payload etc. + * this will be used to make the decision + * + * + * @return permit/deny + */ +exports.checkPolicies = function (token, data) { + return new Promise((resolve, reject) => { + return got + .post(getUrl(), { + headers: { + 'X-Auth-Token': token + }, + json: getPolicy(data) + }) + .json() + .then((result) => { + debug(JSON.stringify(result)); + return resolve(result.Response[0].Decision === 'Permit'); + }) + .catch((error) => { + if (error instanceof got.HTTPError) { + return resolve(false); + } + debug('Error in XACML communication ', error); + return reject(error); + }); + }); +}; + +/** + * Add an attribute to the XACML payload + * @return Object + */ +function attribute(id, value) { + return { + AttributeId: id, + Value: value + }; +} + +/** + * Create a payload for making an XACML JSON request + * based on the action,resource,tenant and attributes + * @return XACML payload + */ +function getPolicy(data) { + const action = data.action; + const resource = data.resource; + const roles = data.roles; + const appId = data.appId; + const tenant = data.tenant_header; + debug('Checking authorization to roles', roles, 'to do ', action, ' on ', resource, 'and app ', appId); + + const resourceInfo = [ + attribute('urn:thales:xacml:2.0:resource:sub-resource-id', resource), + attribute('urn:oasis:names:tc:xacml:1.0:resource:resource-id', appId) + ]; + if (tenant) { + resourceInfo.push(attribute('urn:ngsi-ld:resource:tenant', tenant)); + } + if (data.payloadTypes) { + resourceInfo.push(attribute('urn:ngsi-ld:resource:types', data.payloadTypes)); + } + if (data.payloadAttrs) { + resourceInfo.push(attribute('urn:ngsi-ld:resource:attrs', data.payloadAttrs)); + } + if (data.payloadEntityIds) { + resourceInfo.push(attribute('urn:ngsi-ld:resource:ids', data.payloadEntityIds)); + } + if (data.payloadIdPatterns) { + resourceInfo.push(attribute('urn:ngsi-ld:resource:id-patterns', data.payloadIdPatterns)); + } + const json = { + Request: { + AccessSubject: { + Attribute: [attribute('urn:oasis:names:tc:xacml:2.0:subject:role', roles)] + }, + Action: { + Attribute: [attribute('urn:oasis:names:tc:xacml:1.0:action:action-id', action)] + }, + Resource: { + Attribute: resourceInfo + } + } + }; + + debug('XACML request: ', JSON.stringify(json)); + return json; +} diff --git a/package-lock.json b/package-lock.json index 469df99..32b08f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -344,6 +344,11 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@sindresorhus/is": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", + "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==" + }, "@sinonjs/commons": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", @@ -389,6 +394,14 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, "@textlint/ast-node-types": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-4.3.4.tgz", @@ -1039,12 +1052,49 @@ "integrity": "sha512-KmU+kGi7vG5toUhNdLHHPxyVN1mNGcjMVe1tNDEXT1wU/3oqA96bunElrROWHYw5iNt1QVRaIAtNeMVyzyAdVA==", "dev": true }, + "@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "@types/keyv": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz", + "integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/unist": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", @@ -1362,6 +1412,25 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, "caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -1651,6 +1720,19 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "co": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", @@ -1842,6 +1924,21 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -1886,6 +1983,11 @@ "strip-bom": "^4.0.0" } }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1969,7 +2071,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -2669,7 +2770,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, "requires": { "pump": "^3.0.0" } @@ -2721,6 +2821,24 @@ "type-fest": "^0.8.1" } }, + "got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -2733,6 +2851,25 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -2818,6 +2955,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -2841,6 +2983,20 @@ "sshpk": "^1.7.0" } }, + "http-status-codes": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.1.4.tgz", + "integrity": "sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg==" + }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -3364,6 +3520,11 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -3494,6 +3655,14 @@ "safe-buffer": "^5.0.1" } }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "requires": { + "json-buffer": "3.0.1" + } + }, "lcov-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", @@ -3820,6 +3989,11 @@ "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", "dev": true }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3994,6 +4168,11 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -4006,8 +4185,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "misspellings": { "version": "1.1.0", @@ -4197,6 +4375,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "nise": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", @@ -4262,6 +4445,14 @@ } } }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-expat": { "version": "2.3.18", "resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.18.tgz", @@ -4304,6 +4495,11 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -4537,7 +4733,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -4571,6 +4766,11 @@ "word-wrap": "^1.2.3" } }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -4893,7 +5093,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -4909,6 +5108,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5525,12 +5729,25 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz", + "integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA==" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -6570,6 +6787,17 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.1.tgz", + "integrity": "sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g==", + "optional": true + }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -7041,6 +7269,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "workerpool": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", @@ -7120,8 +7353,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", @@ -7195,11 +7427,6 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 31f728c..3265ab7 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,25 @@ "description": "PEP oauth2 authentication proxy for FIWARE GE services", "author": "GING DIT UPM", "dependencies": { - "debug": "~4.1.1", "cors": "^2.8.5", + "debug": "~4.1.1", "errorhandler": "1.x", "escape-html": "1.0.3", "express": "4.x", + "got": "^11.8.2", + "handlebars": "^4.7.7", + "http-status-codes": "^2.1.4", "is-hex": "^1.1.3", "jsonwebtoken": "^8.5.1", "morgan": "^1.10.0", + "node-cache": "^5.1.2", + "underscore": "1.12.1", "xml2js": "0.4.23", - "xml2json": "0.12.0", - "xmlhttprequest": "1.8.0" + "xml2json": "0.12.0" }, "devDependencies": { - "coveralls": "^3.1.0", "chai": "4.2.0", + "coveralls": "^3.1.0", "eslint": "^7.7.0", "eslint-config-tamia": "^7.2.5", "eslint-plugin-prettier": "^3.1.4", @@ -33,7 +37,7 @@ "prettier": "^2.0.5", "remark-cli": "^8.0.1", "remark-preset-lint-recommended": "^4.0.1", - "should": "13.2.3", + "should": "^13.2.3", "sinon": "9.0.3", "sinon-chai": "3.5.0", "textlint": "^11.7.6", @@ -43,10 +47,17 @@ "textlint-rule-terminology": "^2.1.4", "textlint-rule-write-good": "^1.6.2" }, + "engines": { + "node": ">=12" + }, "repository": { "type": "git", "url": "https://github.com/ging/fiware-pep-proxy" }, + "homepage": "https://fiware-pep-proxy.readthedocs.io/en/latest/", + "bugs": { + "url": "https://github.com/ging/fiware-pep-proxy/issues" + }, "contributors": [ { "name": "Alvaro Alonso", @@ -61,14 +72,13 @@ "start": "node ./bin/www", "debug": "DEBUG=pep-proxy:* node ./bin/www", "healthcheck": "node ./bin/healthcheck.js", - "pretest": "npm run lint", "lint": "eslint . --cache --fix", "lint:text": "textlint '*.md' 'doc/*.md' 'doc/**/*.md' 'extras/docker/*.md'", "lint:md": "remark -f 'README.md' 'roadmap.md' 'doc' 'extras/docker'", - "prettier": "prettier --config .prettierrc.json --write **/*.js *.js", + "prettier": "prettier --config .prettierrc.json --write **/**/*.js **/*.js *.js", "prettier:text": "prettier --parser markdown 'README.md' 'doc/**/*.md' 'extras/docker/*.md' --tab-width 4 --print-width 120 --write --prose-wrap always", "clean": "rm -rf package-lock.json && rm -rf node_modules && rm -rf coverage", - "test": "nyc --reporter=text mocha -- --recursive 'test/**/*.js' --reporter spec --timeout 3000 --ui bdd --exit --color true", + "test": "nyc --reporter=html mocha -- --recursive 'test/**/*.js' --reporter spec --timeout 3000 --ui bdd --exit --color true", "test:coverage": "nyc --reporter=lcov mocha -- --recursive 'test/**/*.js' --timeout 3000 --ui bdd --exit --color true", "test:coveralls": "npm run test:coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" }, diff --git a/policies/custom_policy.js.template b/policies/custom_policy.js.template index a04c36e..6372779 100644 --- a/policies/custom_policy.js.template +++ b/policies/custom_policy.js.template @@ -1,3 +1,4 @@ +#!/usr/bin/env node /* Params roles: Array with the list of roles the user has inside app_id diff --git a/sanity/test.js b/sanity/test.js deleted file mode 100644 index ac11b86..0000000 --- a/sanity/test.js +++ /dev/null @@ -1,79 +0,0 @@ -//const should = require('should'); -//const mocha = require('mocha'); - -const config = require('../test/config_test.js'); -const IDM = require('../lib/idm.js').IDM; -const AZF = require('../lib/azf.js').AZF; - -const debug = require('debug')('pep-proxy:test'); -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - -describe('Sanity Checks for Wilma PEP Proxy - Identity Manager Checks', function() { - describe('Testing Keystone configuration', function() { - it('should have PEP user configured', function(done) { - if (config.pep.username !== undefined && config.pep.username !== '') { - if (config.pep.password !== undefined && config.pep.password !== '') { - done(); - } - } - }); - }); - - describe('Testing connection with Keystone', function() { - it('should have connectivity with Keystone', function(done) { - IDM.checkConn( - function(status) { - if (status === 200) { - done(); - } - }, - function(status, e) { - debug('Error in keystone communication', e); - } - ); - }); - - it('should authenticate with Keystone', function(done) { - IDM.authenticate( - () => { - done(); - }, - () => {} - ); - }); - }); -}); - -describe('Sanity Checks for Wilma PEP Proxy - AuthZForce Checks', function() { - if ( - config.authorization.enabled && - config.authorization.pdp === 'authzforce' - ) { - describe('Testing configuration', function() { - it('should have AZF server configured', function(done) { - if (config.azf.host !== undefined && config.azf.host !== '') { - if (config.azf.port !== undefined && config.azf.port !== '') { - done(); - } - } - }); - }); - - describe('Testing connection with AZF', function() { - it('should have connectivity with AZF', function(done) { - AZF.checkConn( - function() {}, - function(status) { - if (status === 401) { - done(); - } - } - ); - }); - }); - } else { - it('AZF not enabled', function(done) { - done(); - }); - } -}); diff --git a/test/config_test.js b/test/config_test.js deleted file mode 100644 index 105487c..0000000 --- a/test/config_test.js +++ /dev/null @@ -1,78 +0,0 @@ -const config = {}; - -function toBoolean(env, defaultValue) { - return env !== undefined ? env.toLowerCase() === 'true' : defaultValue; -} - -function to_array(env, default_value) { - return env !== undefined ? env.split(',') : default_value; -} - -// Used only if https is disabled -config.pep_port = process.env.PEP_PROXY_PORT || 80; - -// Set this var to undefined if you don't want the server to listen on HTTPS -config.https = { - enabled: toBoolean(process.env.PEP_PROXY_HTTPS_ENABLED, false), - cert_file: 'cert/cert.crt', - key_file: 'cert/key.key', - port: process.env.PEP_PROXY_HTTPS_PORT || 443, -}; - -config.idm = { - host: process.env.PEP_PROXY_IDM_HOST || 'localhost', - port: process.env.PEP_PROXY_IDM_PORT || 4000, - ssl: toBoolean(process.env.PEP_PROXY_IDM_SSL_ENABLED, false), -}; - -config.app = { - host: process.env.PEP_PROXY_APP_HOST || 'www.fiware.org', - port: process.env.PEP_PROXY_APP_PORT || '80', - ssl: toBoolean(process.env.PEP_PROXY_APP_SSL_ENABLED, false), // Use true if the app server listens in https -}; - -config.organizations = { - enabled: toBoolean(process.env.PEP_PROXY_ORG_ENABLED, false), - header: process.env.PEP_PROXY_ORG_HEADER || 'fiware-service', -}; - -// Credentials obtained when registering PEP Proxy in app_id in Account Portal -config.pep = { - app_id: process.env.PEP_PROXY_APP_ID || '', - username: process.env.PEP_PROXY_USERNAME || '', - password: process.env.PEP_PASSWORD || '', - token: { - secret: process.env.PEP_TOKEN_SECRET || '', // Secret must be configured in order validate a jwt - }, - trusted_apps: [], -}; - -// in seconds -config.cache_time = 300; - -// if enabled PEP checks permissions in two ways: -// - With IdM: only allow basic authorization -// - With Authzforce: allow basic and advanced authorization. -// For advanced authorization, you can use custom policy checks by including programatic scripts -// in policies folder. An script template is included there -// -// This is only compatible with oauth2 tokens engine - -config.authorization = { - enabled: toBoolean(process.env.PEP_PROXY_AUTH_ENABLED, false), - pdp: process.env.PEP_PROXY_PDP || 'idm', // idm|authzforce - azf: { - protocol: process.env.PEP_PROXY_AZF_PROTOCOL || 'http', - host: process.env.PEP_PROXY_AZF_HOST || 'localhost', - port: process.env.PEP_PROXY_AZF_PORT || 8080, - custom_policy: process.env.PEP_PROXY_AZF_CUSTOM_POLICY || undefined, // use undefined to default policy checks (HTTP verb + path). - }, -}; - -// list of paths that will not check authentication/authorization -// example: ['/public/*', '/static/css/'] -config.public_paths = to_array(process.env.PEP_PROXY_PUBLIC_PATHS, []); - -config.magic_key = process.env.PEP_PROXY_MAGIC_KEY || undefined; - -module.exports = config; diff --git a/test/mysql-data/backup.sql b/test/mysql-data/backup.sql deleted file mode 100644 index dcb6b8f..0000000 --- a/test/mysql-data/backup.sql +++ /dev/null @@ -1,766 +0,0 @@ --- MySQL dump 10.13 Distrib 5.7.22, for Linux (x86_64) --- --- Host: localhost Database: idm --- ------------------------------------------------------ --- Server version 5.7.22 - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `SequelizeMeta` --- - -CREATE DATABASE IF NOT EXISTS idm; -USE idm - -DROP TABLE IF EXISTS `SequelizeMeta`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `SequelizeMeta` ( - `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, - PRIMARY KEY (`name`), - UNIQUE KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `SequelizeMeta` --- - -LOCK TABLES `SequelizeMeta` WRITE; -/*!40000 ALTER TABLE `SequelizeMeta` DISABLE KEYS */; -INSERT INTO `SequelizeMeta` VALUES ('201802190000-CreateUserTable.js'),('201802190003-CreateUserRegistrationProfileTable.js'),('201802190005-CreateOrganizationTable.js'),('201802190008-CreateOAuthClientTable.js'),('201802190009-CreateUserAuthorizedApplicationTable.js'),('201802190010-CreateRoleTable.js'),('201802190015-CreatePermissionTable.js'),('201802190020-CreateRoleAssignmentTable.js'),('201802190025-CreateRolePermissionTable.js'),('201802190030-CreateUserOrganizationTable.js'),('201802190035-CreateIotTable.js'),('201802190040-CreatePepProxyTable.js'),('201802190045-CreateAuthZForceTable.js'),('201802190050-CreateAuthTokenTable.js'),('201802190060-CreateOAuthAuthorizationCodeTable.js'),('201802190065-CreateOAuthAccessTokenTable.js'),('201802190070-CreateOAuthRefreshTokenTable.js'),('201802190075-CreateOAuthScopeTable.js'),('20180405125424-CreateUserTourAttribute.js'),('20180612134640-CreateEidasTable.js'); -/*!40000 ALTER TABLE `SequelizeMeta` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `auth_token` --- - -DROP TABLE IF EXISTS `auth_token`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `auth_token` ( - `access_token` varchar(255) NOT NULL, - `expires` datetime DEFAULT NULL, - `valid` tinyint(1) DEFAULT NULL, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `pep_proxy_id` varchar(255) DEFAULT NULL, - PRIMARY KEY (`access_token`), - UNIQUE KEY `access_token` (`access_token`), - KEY `user_id` (`user_id`), - KEY `pep_proxy_id` (`pep_proxy_id`), - CONSTRAINT `auth_token_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, - CONSTRAINT `auth_token_ibfk_2` FOREIGN KEY (`pep_proxy_id`) REFERENCES `pep_proxy` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `auth_token` --- - -LOCK TABLES `auth_token` WRITE; -/*!40000 ALTER TABLE `auth_token` DISABLE KEYS */; -INSERT INTO `auth_token` VALUES -('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa','2036-07-30 12:04:45',1,'aaaaaaaa-good-0000-0000-000000000000',NULL), -('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb','2036-07-30 12:38:13',1,'bbbbbbbb-good-0000-0000-000000000000',NULL), -('cccccccc-cccc-cccc-cccc-cccccccccccc','2036-07-31 09:36:13',1,'cccccccc-good-0000-0000-000000000000',NULL), -('51f2e380-c959-4dee-a0af-380f730137c3','2036-07-30 13:02:37',1,'admin',NULL); -/*!40000 ALTER TABLE `auth_token` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `authzforce` --- - -DROP TABLE IF EXISTS `authzforce`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `authzforce` ( - `az_domain` varchar(255) NOT NULL, - `policy` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `version` int(11) DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`az_domain`), - KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `authzforce_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `authzforce` --- - -LOCK TABLES `authzforce` WRITE; -/*!40000 ALTER TABLE `authzforce` DISABLE KEYS */; -/*!40000 ALTER TABLE `authzforce` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `eidas_credentials` --- - -DROP TABLE IF EXISTS `eidas_credentials`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `eidas_credentials` ( - `id` char(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `support_contact_person_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `support_contact_person_surname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `support_contact_person_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `support_contact_person_telephone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `support_contact_person_company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `technical_contact_person_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `technical_contact_person_surname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `technical_contact_person_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `technical_contact_person_telephone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `technical_contact_person_company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `organization_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `organization_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `organization_nif` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `sp_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `attributes_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - PRIMARY KEY (`id`), - UNIQUE KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `eidas_credentials_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `eidas_credentials` --- - -LOCK TABLES `eidas_credentials` WRITE; -/*!40000 ALTER TABLE `eidas_credentials` DISABLE KEYS */; -/*!40000 ALTER TABLE `eidas_credentials` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `iot` --- - -DROP TABLE IF EXISTS `iot`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `iot` ( - `id` varchar(255) NOT NULL, - `password` varchar(40) DEFAULT NULL, - `salt` varchar(40) DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `iot_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `iot` --- - -LOCK TABLES `iot` WRITE; -/*!40000 ALTER TABLE `iot` DISABLE KEYS */; -INSERT INTO `iot` VALUES ('iot_sensor_00000000-0000-0000-0000-000000000000','e9f7c64ec2895eec281f8fd36e588d1bc762bcca',NULL,'tutorial-dckr-site-0000-xpresswebapp'); -/*!40000 ALTER TABLE `iot` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `oauth_access_token` --- - -DROP TABLE IF EXISTS `oauth_access_token`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `oauth_access_token` ( - `access_token` varchar(255) NOT NULL, - `expires` datetime DEFAULT NULL, - `scope` varchar(255) DEFAULT NULL, - `refresh_token` varchar(255) DEFAULT NULL, - `valid` tinyint(1) DEFAULT NULL, - `extra` json DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `iot_id` varchar(255) DEFAULT NULL, - `authorization_code` varchar(255) DEFAULT NULL, - PRIMARY KEY (`access_token`), - UNIQUE KEY `access_token` (`access_token`), - KEY `oauth_client_id` (`oauth_client_id`), - KEY `user_id` (`user_id`), - KEY `iot_id` (`iot_id`), - CONSTRAINT `oauth_access_token_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE, - CONSTRAINT `oauth_access_token_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, - CONSTRAINT `oauth_access_token_ibfk_3` FOREIGN KEY (`iot_id`) REFERENCES `iot` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `oauth_access_token` --- - -LOCK TABLES `oauth_access_token` WRITE; -/*!40000 ALTER TABLE `oauth_access_token` DISABLE KEYS */; -INSERT INTO `oauth_access_token` VALUES -('15682667caa4bb5ac15056fee3836b2980288bf2','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'8ca60ce9-32f9-42d6-a013-a19b3af0c13d','admin',NULL,NULL), -('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa','alice',NULL,NULL), -('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb','bob',NULL,NULL), -('cccccccccccccccccccccccccccccccccccccccc','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'cccccccc-cccc-cccc-cccc-cccccccccccc','charlie',NULL,NULL), -('d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'d1d1d1d1-dddd-dddd-dddd-d1d1d1d1d1d1','detective1',NULL,NULL), -('d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'d2d2d2d2-dddd-dddd-dddd-d2d2d2d2d2d2','detective2',NULL,NULL), -('m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'m1m1m1m1-mmmm-mmmm-mmmm-m1m1m1m1m1m1','manager1',NULL,NULL), -('m2m2m2m2m2m2m2m2m2m2m2m2m2m2m2m2m2m2m2m2','2016-07-30 12:14:21',NULL,NULL,NULL,NULL,'m2m2m2m2-mmmm-mmmm-mmmm-m2m2m2m2m2m2','manager2',NULL,NULL); - -/*!40000 ALTER TABLE `oauth_access_token` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `oauth_authorization_code` --- - -DROP TABLE IF EXISTS `oauth_authorization_code`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `oauth_authorization_code` ( - `authorization_code` varchar(256) NOT NULL, - `expires` datetime DEFAULT NULL, - `redirect_uri` varchar(2000) DEFAULT NULL, - `scope` varchar(255) DEFAULT NULL, - `valid` tinyint(1) DEFAULT NULL, - `extra` json DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`authorization_code`), - UNIQUE KEY `authorization_code` (`authorization_code`), - KEY `oauth_client_id` (`oauth_client_id`), - KEY `user_id` (`user_id`), - CONSTRAINT `oauth_authorization_code_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE, - CONSTRAINT `oauth_authorization_code_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `oauth_authorization_code` --- - -LOCK TABLES `oauth_authorization_code` WRITE; -/*!40000 ALTER TABLE `oauth_authorization_code` DISABLE KEYS */; -/*!40000 ALTER TABLE `oauth_authorization_code` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `oauth_client` --- - -DROP TABLE IF EXISTS `oauth_client`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `oauth_client` ( - `id` char(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `secret` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `url` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `redirect_uri` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `image` varchar(255) DEFAULT 'default', - `grant_type` varchar(255) DEFAULT NULL, - `response_type` varchar(255) DEFAULT NULL, - `client_type` varchar(15) DEFAULT NULL, - `scope` varchar(80) DEFAULT NULL, - `extra` json DEFAULT NULL, - `token_types` varchar(2000) DEFAULT 'bearer', - `jwt_secret` varchar(2000) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `oauth_client` --- - -LOCK TABLES `oauth_client` WRITE; -/*!40000 ALTER TABLE `oauth_client` DISABLE KEYS */; -INSERT INTO `oauth_client` VALUES -('tutorial-dckr-site-0000-xpresswebapp','FIWARE Tutorial', - 'FIWARE Application protected by OAuth2 and Keyrock','tutorial-dckr-site-0000-clientsecret', - 'http://localhost:3000','http://localhost:3000/login','default', - 'authorization_code,implicit,password,client_credentials,refresh_token','code',NULL,NULL,NULL,'bearer', NULL), -('tutorial-lcal-host-0000-xpresswebapp','localhost App', - 'Localhost Callback protected by OAuth2 and Keyrock','tutorial-lcal-host-0000-clientsecret', - 'http://localhost:3000','http://localhost:3000/login','default', - 'authorization_code,implicit,password,client_credentials,refresh_token','code',NULL,NULL,NULL,'bearer', NULL); - -/*!40000 ALTER TABLE `oauth_client` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `oauth_refresh_token` --- - -DROP TABLE IF EXISTS `oauth_refresh_token`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `oauth_refresh_token` ( - `refresh_token` varchar(256) NOT NULL, - `expires` datetime DEFAULT NULL, - `scope` varchar(255) DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `iot_id` varchar(255) DEFAULT NULL, - `valid` tinyint(1) DEFAULT NULL, - `authorization_code` varchar(255) DEFAULT NULL, - PRIMARY KEY (`refresh_token`), - UNIQUE KEY `refresh_token` (`refresh_token`), - KEY `oauth_client_id` (`oauth_client_id`), - KEY `user_id` (`user_id`), - KEY `iot_id` (`iot_id`), - CONSTRAINT `oauth_refresh_token_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE, - CONSTRAINT `oauth_refresh_token_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, - CONSTRAINT `oauth_refresh_token_ibfk_3` FOREIGN KEY (`iot_id`) REFERENCES `iot` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `oauth_refresh_token` --- - -LOCK TABLES `oauth_refresh_token` WRITE; -/*!40000 ALTER TABLE `oauth_refresh_token` DISABLE KEYS */; -INSERT INTO `oauth_refresh_token` VALUES ('4eb1f99f80f37c81a8ef85d92eae836919887e1e','2018-08-13 11:14:21',NULL,'8ca60ce9-32f9-42d6-a013-a19b3af0c13d','admin',NULL, NULL,NULL); -/*!40000 ALTER TABLE `oauth_refresh_token` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `oauth_scope` --- - -DROP TABLE IF EXISTS `oauth_scope`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `oauth_scope` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `scope` varchar(255) DEFAULT NULL, - `is_default` tinyint(1) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `oauth_scope` --- - -LOCK TABLES `oauth_scope` WRITE; -/*!40000 ALTER TABLE `oauth_scope` DISABLE KEYS */; -/*!40000 ALTER TABLE `oauth_scope` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `organization` --- - -DROP TABLE IF EXISTS `organization`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `organization` ( - `id` char(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `website` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `image` varchar(255) DEFAULT 'default', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `organization` --- - -LOCK TABLES `organization` WRITE; -/*!40000 ALTER TABLE `organization` DISABLE KEYS */; -INSERT INTO `organization` VALUES -('security-team-0000-0000-000000000000','Security','Security Group for Store Detectives',NULL,'default'), -('managers-team-0000-0000-000000000000','Management','Management Group for Store Managers',NULL,'default'); -/*!40000 ALTER TABLE `organization` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `pep_proxy` --- - -DROP TABLE IF EXISTS `pep_proxy`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `pep_proxy` ( - `id` varchar(255) NOT NULL, - `password` varchar(40) DEFAULT NULL, - `salt` varchar(40) DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `pep_proxy_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `pep_proxy` --- - -LOCK TABLES `pep_proxy` WRITE; -/*!40000 ALTER TABLE `pep_proxy` DISABLE KEYS */; -INSERT INTO `pep_proxy` VALUES ('pep_proxy_00000000-0000-0000-0000-000000000000','e9f7c64ec2895eec281f8fd36e588d1bc762bcca',NULL,'tutorial-dckr-site-0000-xpresswebapp'); -/*!40000 ALTER TABLE `pep_proxy` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `permission` --- - -DROP TABLE IF EXISTS `permission`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `permission` ( - `id` char(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `is_internal` tinyint(1) DEFAULT '0', - `action` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `resource` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `xml` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `permission_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `permission` --- - -LOCK TABLES `permission` WRITE; -/*!40000 ALTER TABLE `permission` DISABLE KEYS */; -INSERT INTO `permission` VALUES -('1','Get and assign all internal application roles',NULL,1,NULL,NULL,NULL,'idm_admin_app'), -('2','Manage the application',NULL,1,NULL,NULL,NULL,'idm_admin_app'), -('3','Manage roles',NULL,1,NULL,NULL,NULL,'idm_admin_app'),('4','Manage authorizations',NULL,1,NULL,NULL,NULL,'idm_admin_app'), -('5','Get and assign all public application roles',NULL,1,NULL,NULL,NULL,'idm_admin_app'), -('6','Get and assign only public owned roles',NULL,1,NULL,NULL,NULL,'idm_admin_app'), -('increase-stck-0000-0000-000000000000','Order Stock','Increase Stock Count',0,'GET','/app/order-stock',NULL,'tutorial-dckr-site-0000-xpresswebapp'), -('entrance-open-0000-0000-000000000000','Unlock','Unlock main entrance',0,'POST','/door/unlock',NULL,'tutorial-dckr-site-0000-xpresswebapp'), -('alrmbell-ring-0000-0000-000000000000','Ring Alarm Bell',NULL,0,'POST','/bell/ring',NULL,'tutorial-dckr-site-0000-xpresswebapp'), -('pricechg-stck-0000-0000-000000000000','Access Price Changes',NULL,0,'GET','/app/price-change',NULL,'tutorial-dckr-site-0000-xpresswebapp'); -/*!40000 ALTER TABLE `permission` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `role` --- - -DROP TABLE IF EXISTS `role`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `role` ( - `id` char(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `is_internal` tinyint(1) DEFAULT '0', - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `role_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `role` --- - -LOCK TABLES `role` WRITE; -/*!40000 ALTER TABLE `role` DISABLE KEYS */; -INSERT INTO `role` VALUES -('security-role-0000-0000-000000000000','Security Team',0,'tutorial-dckr-site-0000-xpresswebapp'), -('managers-role-0000-0000-000000000000','Management',0,'tutorial-dckr-site-0000-xpresswebapp'), -('provider','Provider',1,'idm_admin_app'),('purchaser','Purchaser',1,'idm_admin_app'); -/*!40000 ALTER TABLE `role` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `role_assignment` --- - -DROP TABLE IF EXISTS `role_assignment`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `role_assignment` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `role_organization` varchar(255) DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `role_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `organization_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `oauth_client_id` (`oauth_client_id`), - KEY `role_id` (`role_id`), - KEY `organization_id` (`organization_id`), - KEY `user_id` (`user_id`), - CONSTRAINT `role_assignment_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE, - CONSTRAINT `role_assignment_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE, - CONSTRAINT `role_assignment_ibfk_3` FOREIGN KEY (`organization_id`) REFERENCES `organization` (`id`) ON DELETE CASCADE, - CONSTRAINT `role_assignment_ibfk_4` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `role_assignment` --- - -LOCK TABLES `role_assignment` WRITE; -/*!40000 ALTER TABLE `role_assignment` DISABLE KEYS */; -INSERT INTO `role_assignment` VALUES -(1,NULL,'8ca60ce9-32f9-42d6-a013-a19b3af0c13d','provider',NULL,'96154659-cb3b-4d2d-afef-18d6aec0518e'), -(2,'member','8ca60ce9-32f9-42d6-a013-a19b3af0c13d','provider','74f5299e-3247-468c-affb-957cda03f0c4',NULL), -(3,NULL,'222eda27-958b-4f0c-a5cb-e4114fb170c3','provider',NULL,'admin'), -(4,NULL,'222eda27-958b-4f0c-a5cb-e4114fb170c3','provider',NULL,'96154659-cb3b-4d2d-afef-18d6aec0518e'), -(5,NULL,'tutorial-dckr-site-0000-xpresswebapp','provider',NULL,'aaaaaaaa-good-0000-0000-000000000000'), -(6,NULL,'tutorial-lcal-host-0000-xpresswebapp','provider',NULL,'aaaaaaaa-good-0000-0000-000000000000'), -(10,NULL,'tutorial-dckr-site-0000-xpresswebapp','security-role-0000-0000-000000000000',NULL,'cccccccc-good-0000-0000-000000000000'), -(11,'member','tutorial-dckr-site-0000-xpresswebapp','security-role-0000-0000-000000000000','security-team-0000-0000-000000000000',NULL), -(12,NULL,'tutorial-dckr-site-0000-xpresswebapp','managers-role-0000-0000-000000000000',NULL,'bbbbbbbb-good-0000-0000-000000000000'), -(13,'member','tutorial-dckr-site-0000-xpresswebapp','managers-role-0000-0000-000000000000','managers-team-0000-0000-000000000000',NULL); - -/*!40000 ALTER TABLE `role_assignment` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `role_permission` --- - -DROP TABLE IF EXISTS `role_permission`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `role_permission` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `role_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `permission_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `role_id` (`role_id`), - KEY `permission_id` (`permission_id`), - CONSTRAINT `role_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE, - CONSTRAINT `role_permission_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `role_permission` --- - -LOCK TABLES `role_permission` WRITE; -/*!40000 ALTER TABLE `role_permission` DISABLE KEYS */; -INSERT INTO `role_permission` VALUES -(1,'provider','1'),(2,'provider','2'),(3,'provider','3'),(4,'provider','4'),(5,'provider','5'),(6,'provider','6'), -(7,'purchaser','5'), -(8,'security-role-0000-0000-000000000000','alrmbell-ring-0000-0000-000000000000'), -(9,'security-role-0000-0000-000000000000','entrance-open-0000-0000-000000000000'), -(10,'managers-role-0000-0000-000000000000','alrmbell-ring-0000-0000-000000000000'), -(11,'managers-role-0000-0000-000000000000','increase-stck-0000-0000-000000000000'), -(12,'managers-role-0000-0000-000000000000','pricechg-stck-0000-0000-000000000000'); - - - - -/*!40000 ALTER TABLE `role_permission` ENABLE KEYS */; -UNLOCK TABLES; - - --- --- Table structure for table `trusted_application` --- - -DROP TABLE IF EXISTS `trusted_application`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `trusted_application` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `trusted_oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `oauth_client_id` (`oauth_client_id`), - KEY `trusted_oauth_client_id` (`trusted_oauth_client_id`), - CONSTRAINT `trusted_application_ibfk_1` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE, - CONSTRAINT `trusted_application_ibfk_2` FOREIGN KEY (`trusted_oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `trusted_application` --- - -LOCK TABLES `trusted_application` WRITE; -/*!40000 ALTER TABLE `trusted_application` DISABLE KEYS */; -/*!40000 ALTER TABLE `trusted_application` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user` --- - -DROP TABLE IF EXISTS `user`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `user` ( - `id` char(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `website` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `image` varchar(255) DEFAULT 'default', - `gravatar` tinyint(1) DEFAULT '0', - `email` varchar(255) DEFAULT NULL, - `password` varchar(40) DEFAULT NULL, - `salt` varchar(40) DEFAULT NULL, - `date_password` datetime DEFAULT NULL, - `enabled` tinyint(1) DEFAULT '0', - `admin` tinyint(1) DEFAULT '0', - `extra` varchar(255) DEFAULT NULL, - `scope` varchar(80) DEFAULT NULL, - `starters_tour_ended` tinyint(1) DEFAULT '0', - `eidas_id` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `email` (`email`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user` --- - -LOCK TABLES `user` WRITE; -/*!40000 ALTER TABLE `user` DISABLE KEYS */; -INSERT INTO `user` VALUES -('aaaaaaaa-good-0000-0000-000000000000','alice', 'Alice is the admin',NULL,'default',0,'alice-the-admin@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,1,NULL,NULL,0,NULL), -('bbbbbbbb-good-0000-0000-000000000000','bob','Bob is the regional manager',NULL,'default',0,'bob-the-manager@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('cccccccc-good-0000-0000-000000000000','charlie','Charlie is head of security',NULL,'default',0,'charlie-security@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('manager1-good-0000-0000-000000000000','manager1','Manager works for Bob',NULL,'default',0,'manager1@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('manager2-good-0000-0000-000000000000','manager2','Manager works for Bob',NULL,'default',0,'manager2@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('detective1-good-0000-0000-000000000000','detective1','Detective works for Charlie',NULL,'default',0,'detective1@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('detective2-good-0000-0000-000000000000','detective2','Detective works for Charlie',NULL,'default',0,'detective2@test.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('eve-evil-0000-0000-000000000000','eve','Eve the Eavesdropper',NULL,'default',0,'eve@example.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('mallory-evil-0000-0000-000000000000','mallory','Mallory the malicious attacker',NULL,'default',0,'mallory@example.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL), -('rob-evil-0000-0000-000000000000','rob','Rob the Robber' ,NULL,'default',0,'rob@example.com','89e48c55e4e4b3b86141fb15f5e6abf70f8c32c0', 'fbba54b6750b16e8', '2018-07-30 11:41:14',1,0,NULL,NULL,0,NULL);/*!40000 ALTER TABLE `user` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user_authorized_application` --- - -DROP TABLE IF EXISTS `user_authorized_application`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `user_authorized_application` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `oauth_client_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - KEY `oauth_client_id` (`oauth_client_id`), - CONSTRAINT `user_authorized_application_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, - CONSTRAINT `user_authorized_application_ibfk_2` FOREIGN KEY (`oauth_client_id`) REFERENCES `oauth_client` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user_authorized_application` --- - -LOCK TABLES `user_authorized_application` WRITE; -/*!40000 ALTER TABLE `user_authorized_application` DISABLE KEYS */; -INSERT INTO `user_authorized_application` VALUES (1,'admin','8ca60ce9-32f9-42d6-a013-a19b3af0c13d'); -/*!40000 ALTER TABLE `user_authorized_application` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user_organization` --- - -DROP TABLE IF EXISTS `user_organization`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `user_organization` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `role` varchar(10) DEFAULT NULL, - `user_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - `organization_id` char(36) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - KEY `organization_id` (`organization_id`), - CONSTRAINT `user_organization_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, - CONSTRAINT `user_organization_ibfk_2` FOREIGN KEY (`organization_id`) REFERENCES `organization` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user_organization` --- - -LOCK TABLES `user_organization` WRITE; -/*!40000 ALTER TABLE `user_organization` DISABLE KEYS */; -INSERT INTO `user_organization` VALUES -(2,'owner', 'aaaaaaaa-good-0000-0000-000000000000','security-team-0000-0000-000000000000'), -(3,'owner', 'aaaaaaaa-good-0000-0000-000000000000','managers-team-0000-0000-000000000000'), -(4,'owner', 'bbbbbbbb-good-0000-0000-000000000000','managers-team-0000-0000-000000000000'), -(5,'member','manager1-good-0000-0000-000000000000','managers-team-0000-0000-000000000000'), -(6,'member','manager2-good-0000-0000-000000000000','managers-team-0000-0000-000000000000'), -(7,'owner', 'cccccccc-good-0000-0000-000000000000','security-team-0000-0000-000000000000'), -(8,'member','detective1-good-0000-0000-000000000000','security-team-0000-0000-000000000000'), -(9,'member','detective2-good-0000-0000-000000000000','security-team-0000-0000-000000000000'); -/*!40000 ALTER TABLE `user_organization` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user_registration_profile` --- - -DROP TABLE IF EXISTS `user_registration_profile`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `user_registration_profile` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `activation_key` varchar(255) DEFAULT NULL, - `activation_expires` datetime DEFAULT NULL, - `reset_key` varchar(255) DEFAULT NULL, - `reset_expires` datetime DEFAULT NULL, - `verification_key` varchar(255) DEFAULT NULL, - `verification_expires` datetime DEFAULT NULL, - `user_email` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `user_email` (`user_email`), - CONSTRAINT `user_registration_profile_ibfk_1` FOREIGN KEY (`user_email`) REFERENCES `user` (`email`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user_registration_profile` --- - -LOCK TABLES `user_registration_profile` WRITE; -/*!40000 ALTER TABLE `user_registration_profile` DISABLE KEYS */; -INSERT INTO `user_registration_profile` VALUES (1,'b26roiin0r','2018-07-31 10:03:53',NULL,NULL,NULL,NULL,'eve@test.com'); -/*!40000 ALTER TABLE `user_registration_profile` ENABLE KEYS */; -UNLOCK TABLES; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2018-08-10 9:03:58 diff --git a/test/unit/add-test.js b/test/unit/add-test.js deleted file mode 100644 index 3820b94..0000000 --- a/test/unit/add-test.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2019 - Universidad Politécnica de Madrid. - * - * This file is part of Keyrock - * - */ - -//var iotaOPCUA= require('../../../../index.js'), -/* -const config = require('./config-test.js'); -const nock = require('nock'); -const async = require('async'); -const request = require('request'); -*/ - -describe('Attribute alias', function() { - beforeEach(function(done) { - // Set up - done(); - }); - - afterEach(function(done) { - // Clean Up - done(); - }); - - describe('When a new multiple measure arrives with a timestamp in an attribute alias', function() { - beforeEach(function() { - // Set Up - }); - it('should send its value to the Context Broker', function(done) { - // Run test - done(); - }); - }); -}); diff --git a/test/unit/authentication-test.js b/test/unit/authentication-test.js new file mode 100644 index 0000000..6d1cbfa --- /dev/null +++ b/test/unit/authentication-test.js @@ -0,0 +1,263 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of Keyrock + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; +const utils = require('./utils'); + +const shortToken = '111111111'; +const longToken = '11111111111111111111111111111111111111111111111111111111111111'; + +const no_token = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false +}; + +const auth_token = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': shortToken } +}; + +const bearer_token = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { authorization: 'Bearer: ' + Buffer.from(shortToken, 'utf-8').toString('base64') } +}; + +const bearer_token_long = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { authorization: 'Bearer: ' + Buffer.from(longToken, 'utf-8').toString('base64') } +}; + +const magic_key = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '999999999' } +}; + +const config = { + magic_key: '999999999', + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [] + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 1, + public_paths: ['/public'], + authorization: { + enabled: false, + pdp: 'idm', // idm|iShare|xacml|authzforce|opa|azf + header: undefined, // NGSILD-Tenant|fiware-service + location: { + protocol: 'http', + host: 'localhost', + port: 8080, + custom_policy: undefined // use undefined to default policy checks (HTTP verb + path). + } + } +}; + +const keyrock_user_response = { + app_id: 'application_id', + trusted_apps: [], + id: 'username', + displayName: 'Some User' +}; + +describe('Authentication: Keyrock IDM', () => { + let pep; + let contextBrokerMock; + let idmMock; + + beforeEach((done) => { + const app = require('../../app'); + pep = app.start_server('12345', config); + cache.flush(); + nock.cleanAll(); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + done(); + }); + + describe('When a URL is requested and no token is present', () => { + beforeEach(() => { + // Set Up + }); + it('should deny access', (done) => { + got.get('restricted_path', no_token).then((response) => { + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a public path is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/public').reply(StatusCodes.OK, {}); + }); + it('should allow access', (done) => { + got.get('public', no_token).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested and the token matches the magic key', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + }); + it('should allow access', (done) => { + got.get('restricted', magic_key).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested for a legitimate user with an x-auth-token', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id') + .reply(StatusCodes.OK, keyrock_user_response); + }); + it('should authenticate the user and allow access', (done) => { + got.get('restricted', auth_token).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested for a legitimate user with a bearer token', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id') + .reply(StatusCodes.OK, keyrock_user_response); + }); + it('should authenticate the user and allow access', (done) => { + got.get('restricted', bearer_token).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested for a forbidden user', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id') + .reply(StatusCodes.UNAUTHORIZED); + }); + it('should authenticate the user and deny access', (done) => { + got.get('restricted', auth_token).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a non-existant restricted path is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(404); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id') + .reply(StatusCodes.OK, keyrock_user_response); + }); + it('should authenticate the user and proxy the error', (done) => { + got.get('restricted', auth_token).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(response.statusCode, 404); + done(); + }); + }); + }); + + describe('When the same restricted path is requested multiple times', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').times(2).reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id') + .reply(StatusCodes.OK, keyrock_user_response); + }); + it('should access the user from cache', (done) => { + got + .get('restricted', auth_token) + .then((firstResponse) => { + should.equal(firstResponse.statusCode, StatusCodes.OK); + return got.get('restricted', auth_token); + }) + .then((secondResponse) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(secondResponse.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When the same restricted path is requested multiple times with a bearer token', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').times(3).reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + longToken + '&app_id=application_id') + .times(2) + .reply(StatusCodes.OK, keyrock_user_response); + }); + it('should access the user from cache', (done) => { + got + .get('restricted', bearer_token_long) + .then((firstResponse) => { + should.equal(firstResponse.statusCode, StatusCodes.OK); + return got.get('restricted', bearer_token_long); + }) + .then(async function (secondResponse) { + should.equal(secondResponse.statusCode, StatusCodes.OK); + await utils.sleep(2000); + return got.get('restricted', bearer_token_long); + }) + .then((thirdResponse) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(thirdResponse.statusCode, StatusCodes.OK); + done(); + }); + }); + }); +}); diff --git a/test/unit/authzforce-pdp-test.js b/test/unit/authzforce-pdp-test.js new file mode 100644 index 0000000..c98e9e5 --- /dev/null +++ b/test/unit/authzforce-pdp-test.js @@ -0,0 +1,174 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; + +const request_with_header = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111' } +}; + +const keyrock_user_with_azf = { + app_id: 'application_id', + trusted_apps: [], + app_azf_domain: 'authzforce', + roles: [ + { + id: 'managers-role-0000-0000-000000000000', + name: 'Management' + } + ], + organizations: [ + { + roles: [ + { + id: 'my-organization-0000-0000-000000000000', + name: 'Organization' + } + ] + } + ] +}; + +const authzforce_permit_response = ` + + + Permit + +`; + +const authzforce_deny_response = ` + + + Deny + +`; + +const config = { + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [] + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: [], + authorization: { + enabled: true, + pdp: 'authzforce', // idm|iShare|xacml|authzforce|opa|azf + header: undefined, // NGSILD-Tenant|fiware-service + location: { + protocol: 'http', + host: 'authzforce.com', + port: 8080, + custom_policy: undefined // use undefined to default policy checks (HTTP verb + path). + } + } +}; + +describe('Authorization: Authzforce PDP', () => { + let pep; + let contextBrokerMock; + let idmMock; + let authzforceMock; + + beforeEach((done) => { + const app = require('../../app'); + pep = app.start_server('12345', config); + cache.flush(); + nock.cleanAll(); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=111111111&app_id=application_id&authzforce=true') + .reply(StatusCodes.OK, keyrock_user_with_azf); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + done(); + }); + + describe('When a restricted path is requested for a legitimate user', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + authzforceMock = nock('http://authzforce.com:8080') + .post('/authzforce-ce/domains/authzforce/pdp') + .reply(StatusCodes.OK, authzforce_permit_response); + }); + + it('should allow access', (done) => { + got.get('restricted', request_with_header).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + authzforceMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested for a forbidden user', () => { + beforeEach(() => { + authzforceMock = nock('http://authzforce.com:8080') + .post('/authzforce-ce/domains/authzforce/pdp') + .reply(StatusCodes.OK, authzforce_deny_response); + }); + + it('should deny access when denied', (done) => { + got.get('restricted', request_with_header).then((response) => { + idmMock.done(); + authzforceMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When no AZF domain is returned', () => { + beforeEach(() => { + nock.cleanAll(); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=111111111&app_id=application_id&authzforce=true') + .reply(StatusCodes.OK, { + app_id: 'application_id', + trusted_apps: [] + }); + }); + it('should deny access', (done) => { + got.get('restricted', request_with_header).then((response) => { + idmMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); +}); diff --git a/test/unit/connection-test.js b/test/unit/connection-test.js new file mode 100644 index 0000000..48f8fb9 --- /dev/null +++ b/test/unit/connection-test.js @@ -0,0 +1,129 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const config_service = require('../../lib/config_service'); +const should = require('should'); +const nock = require('nock'); +const IDM = require('../../lib/pdp/keyrock'); +const Authzforce = require('../../lib/pdp/authzforce'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; + +const config = { + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [], + username: 'user', + password: 'password' + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: [], + authorization: { + enabled: true, + pdp: 'azf', + location: { + protocol: 'http', + host: 'authzforce.com', + port: 8080, + custom_policy: undefined // use undefined to default policy checks (HTTP verb + path). + } + } +}; + +describe('Connection Tests', () => { + let idmMock; + let authzforceMock; + + beforeEach((done) => { + config_service.set_config(config, true); + cache.flush(); + nock.cleanAll(); + done(); + }); + + afterEach((done) => { + done(); + }); + + describe('When connecting to Authzforce and it is present', () => { + beforeEach(() => { + authzforceMock = nock('http://authzforce.com:8080').get('/').reply(StatusCodes.OK, {}); + }); + it('should not error', (done) => { + Authzforce.checkConnectivity() + .then(() => { + authzforceMock.done(); + done(); + }) + .catch((err) => { + should.fail("error was thrown when it shouldn't have been", err); + }); + }); + }); + + describe('When connecting to Keyrock and it is present', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000').get('/version').reply(StatusCodes.OK, {}); + }); + it('should not error', (done) => { + IDM.checkConnectivity() + .then(() => { + idmMock.done(); + done(); + }) + .catch((err) => { + should.fail("error was thrown when it shouldn't have been", err); + }); + }); + }); + + describe('When authenticating the PEP with Keyrock', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000').post('/v3/auth/tokens').reply(StatusCodes.OK, {}); + }); + it('should not error', (done) => { + IDM.authenticatePEP() + .then(() => { + idmMock.done(); + done(); + }) + .catch((err) => { + should.fail("error was thrown when it shouldn't have been", err); + }); + }); + }); + + describe('When authenticating a misconfigured PEP with Keyrock', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000').post('/v3/auth/tokens').reply(StatusCodes.UNAUTHORIZED); + }); + it('should error', (done) => { + IDM.authenticatePEP() + .then(() => { + should.fail('no error was thrown when it should have been'); + }) + .catch(() => { + idmMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ishare-test.js b/test/unit/ishare-test.js new file mode 100644 index 0000000..bb716fa --- /dev/null +++ b/test/unit/ishare-test.js @@ -0,0 +1,285 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of Keyrock + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const jwt = require('jsonwebtoken'); +const StatusCodes = require('http-status-codes').StatusCodes; + +const ngsiPayload = [ + { + id: 'urn:ngsi-ld:TemperatureSensor:002', + type: 'TemperatureSensor', + category: { + type: 'Property', + value: 'sensor' + }, + temperature: { + type: 'Property', + value: 21, + unitCode: 'CEL' + } + }, + { + id: 'urn:ngsi-ld:TemperatureSensor:003', + type: 'TemperatureSensor', + category: { + type: 'Property', + value: 'sensor' + }, + temperature: { + type: 'Property', + value: 27, + unitCode: 'CEL' + } + } +]; + +const ngsi_subscription = { + description: 'Notify me of low feedstock on Farm:001', + type: 'Subscription', + entities: [ + { type: 'TemperatureSensor' }, + { id: 'urn:ngsi-ld:TemperatureSensor001' }, + { idPattern: 'urn:ngsi-ld:.*' } + ], + watchedAttributes: ['temperature'], + q: 'temperature>0.6;temperature<0.8;controlledAsset==urn:ngsi-ld:Building:farm001', + notification: { + attributes: ['temperature', 'controlledAsset'], + format: 'keyValues', + endpoint: { + uri: 'http://tutorial:3000/subscription/low-stock-farm001', + accept: 'application/json' + } + }, + '@context': 'http://context/ngsi-context.jsonld' +}; + +const token = jwt.sign( + { + app_id: 'application_id', + trusted_apps: [], + id: 'username', + displayName: 'Some User', + delegationEvidence: { + notBefore: Math.floor(new Date().getTime() / 1000) - 2000, + notOnOrAfter: Math.floor(new Date().getTime() / 1000) + 2000, + policyIssuer: 'EU.EORI.NLPACKETDEL', + target: { + accessSubject: 'EU.EORI.NLNOCHEAPER' + }, + policySets: [ + { + maxDelegationDepth: 1, + target: { + environment: { + licenses: ['ISHARE.0001'] + } + }, + policies: [ + { + target: { + resource: { + type: 'TemperatureSensor', + identifiers: ['urn:ngsi-ld:.*'], + attributes: ['.*'] + }, + actions: ['GET', 'PATCH', 'POST'] + }, + rules: [ + { + effect: 'Permit' + } + ] + }, + { + target: { + resource: { + type: 'SoilSensor', + identifiers: ['.*'], + attributes: ['.*'] + }, + actions: ['GET'] + }, + rules: [ + { + effect: 'Permit' + } + ] + } + ] + } + ] + } + }, + 'shhhhh' +); + +const request_with_jwt = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': token }, + retry: 0 +}; + +const request_with_jwt_and_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': token }, + json: ngsiPayload, + retry: 0 +}; + +const request_with_jwt_and_subscription_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': token }, + json: ngsi_subscription, + retry: 0 +}; + +const config = { + magic_key: '999999999', + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [], + token: { secret: 'shhhhh' } + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: ['/public'], + authorization: { + enabled: true, + pdp: 'ishare', // idm|iShare|xacml|authzforce|opa|azf + header: 'fiware-service', + location: { + protocol: 'http', + host: 'ishare.org', + port: 8080 + } + } +}; + +describe('Authorization: iSHARE PDP', () => { + let pep; + let contextBrokerMock; + + beforeEach((done) => { + const app = require('../../app'); + pep = app.start_server('12345', config); + nock.cleanAll(); + cache.flush(); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + done(); + }); + + describe('When a restricted URL matches the JWT policy and is legitimate', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .get('/path/entities/urn:ngsi-ld:SoilSensor:1111?type=SoilSensor') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access', (done) => { + got.get('path/entities/urn:ngsi-ld:SoilSensor:1111?type=SoilSensor', request_with_jwt).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted URL does not match the attached JWT policy', () => { + it('should deny access', (done) => { + got.get('path/entities/urn:ngsi-ld:Tractor:1111?type=Tractor', request_with_jwt).then((response) => { + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + xdescribe('When a JWT policy is not recognized by the iSHARE delegate', () => { + beforeEach(() => { + //iShareMock = nock('http://ishare.com:8080').post('/delegate').reply(StatusCodes.OK, ishare_policy_not_recognized); + }); + + it('should deny access', (done) => { + got.get('path/entities/urn:ngsi-ld:SoilSensor:1111', request_with_jwt).then((response) => { + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a restricted URL with a string is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .get('/path/entities/?ids=urn:ngsi-ld:SoilSensor:1111&type=SoilSensor') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access based on the JWT policy and entities', (done) => { + got.get('path/entities/?ids=urn:ngsi-ld:SoilSensor:1111&type=SoilSensor', request_with_jwt).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted URL with a payload body is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .patch('/path/entityOperations/upsert') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access based on the JWT policy and entities', (done) => { + got.patch('path/entityOperations/upsert', request_with_jwt_and_body).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted subscription URL with a payload body is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .post('/path/ngsi-ld/v1/subscriptions') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access based on entities', (done) => { + got.post('path/ngsi-ld/v1/subscriptions', request_with_jwt_and_subscription_body).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); +}); diff --git a/test/unit/jwt-authentication-test.js b/test/unit/jwt-authentication-test.js new file mode 100644 index 0000000..8bc597a --- /dev/null +++ b/test/unit/jwt-authentication-test.js @@ -0,0 +1,184 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of Keyrock + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; +const utils = require('./utils'); + +const token = utils.createJWT(); + +const invalid_token = utils.createJWT('wrong_secret'); + +const expired_token = utils.createJWT( + 'shhhhh', + { + app_id: 'application_id', + trusted_apps: [], + id: 'username', + displayName: 'Some User' + }, + { expiresIn: '1ms' } +); + +const jwt = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': token } +}; + +const invalid_jwt = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': invalid_token }, + retry: 0 +}; + +const missing_jwt = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false +}; + +const request_with_expired_jwt = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': expired_token } +}; + +const config = { + magic_key: '999999999', + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [], + token: { secret: 'shhhhh' } + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: ['/public'], + authorization: { + enabled: false, + pdp: 'idm', // idm|iShare|xacml|authzforce|opa|azf + header: undefined, // NGSILD-Tenant|fiware-service + location: { + protocol: 'http', + host: 'localhost', + port: 8080, + custom_policy: undefined // use undefined to default policy checks (HTTP verb + path). + } + } +}; + +describe('Authentication: JWT Token', () => { + let pep; + let contextBrokerMock; + let idmMock; + + beforeEach((done) => { + const app = require('../../app'); + pep = app.start_server('12345', config); + nock.cleanAll(); + cache.flush(); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + config.pep.secret = undefined; + done(); + }); + + describe('When a URL is requested and no JWT token is present', () => { + it('should deny access', (done) => { + got.get('restricted_path', missing_jwt).then((response) => { + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a public path is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/public').reply(StatusCodes.OK, {}); + }); + it('should allow access', (done) => { + got.get('public', jwt).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested with a legitimate JWT', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + }); + it('should authenticate the user and allow access', (done) => { + got.get('restricted', jwt).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested with an expired JWT', () => { + beforeEach(async () => { + await utils.sleep(100); + }); + it('should deny access', (done) => { + got.get('restricted', request_with_expired_jwt).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a restricted path is requested for an unrecognized JWT', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + invalid_token + '&app_id=application_id') + .reply(StatusCodes.UNAUTHORIZED); + }); + it('should fallback to Keyrock and deny access', (done) => { + got.get('restricted', invalid_jwt).then((response) => { + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + idmMock.done(); + done(); + }); + }); + }); + + describe('When a non-existant restricted path is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(404); + }); + it('should authenticate the user and proxy the error', (done) => { + got.get('restricted', jwt).then((response) => { + contextBrokerMock.done(); + should.equal(response.statusCode, 404); + done(); + }); + }); + }); +}); diff --git a/test/unit/keyrock-pdp-test.js b/test/unit/keyrock-pdp-test.js new file mode 100644 index 0000000..8e85899 --- /dev/null +++ b/test/unit/keyrock-pdp-test.js @@ -0,0 +1,290 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; +const shortToken = '111111111'; +const longToken = '11111111111111111111111111111111111111111111111111111111111111'; +const utils = require('./utils'); + +const auth_token = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': shortToken } +}; + +const auth_token_and_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': shortToken }, + body: 'HELLO' +}; + +const bearer_token_long = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { authorization: 'Bearer: ' + Buffer.from(longToken, 'utf-8').toString('base64') } +}; + +const jwt = utils.createJWT( + 'shhhhh', + { + app_id: 'application_id', + trusted_apps: [], + id: 'username', + displayName: 'Some User' + }, + { expiresIn: '20000ms' } +); + +const bearer_jwt_token = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { authorization: 'Bearer: ' + Buffer.from(jwt, 'utf-8').toString('base64') } +}; + +const keyrock_deny_response = { + app_id: 'application_id', + trusted_apps: [], + authorization_decision: 'Deny' +}; + +const keyrock_permit_response = { + app_id: 'application_id', + trusted_apps: [], + authorization_decision: 'Permit' +}; + +const config = { + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [] + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 1, + public_paths: [], + authorization: { + enabled: true, + pdp: 'idm' // idm|iShare|xacml|authzforce|opa|azf + } +}; + +describe('Authorization: Keyrock PDP', () => { + let pep; + let contextBrokerMock; + let idmMock; + + beforeEach((done) => { + nock.cleanAll(); + const app = require('../../app'); + pep = app.start_server('12345', config); + cache.flush(); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + done(); + }); + + describe('When a restricted path is requested for a legitimate user', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id&action=GET&resource=/restricted') + .reply(StatusCodes.OK, keyrock_permit_response); + }); + it('should allow access', (done) => { + got.get('restricted', auth_token).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted path is requested and the app-id is not found', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id&action=GET&resource=/restricted') + .reply(StatusCodes.OK, { + app_id: '', + trusted_apps: [], + authorization_decision: 'Permit' + }); + }); + it('should deny access', (done) => { + got.get('restricted', auth_token).then((response) => { + idmMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a restricted path is requested for a forbidden user', () => { + beforeEach(() => { + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id&action=GET&resource=/restricted') + .reply(StatusCodes.OK, keyrock_deny_response); + }); + it('should deny access', (done) => { + got.get('restricted', auth_token).then((response) => { + idmMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When the same action on a restricted path multiple times', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').times(2).reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id&action=GET&resource=/restricted') + .reply(StatusCodes.OK, keyrock_permit_response); + }); + it('should access the user action from cache', (done) => { + got + .get('restricted', auth_token) + .then((firstResponse) => { + should.equal(firstResponse.statusCode, StatusCodes.OK); + return got.get('restricted', auth_token); + }) + .then((secondResponse) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(secondResponse.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When the same action on a restricted path multiple times with a bearer token', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').times(3).reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + longToken + '&app_id=application_id&action=GET&resource=/restricted') + .times(2) + .reply(StatusCodes.OK, keyrock_permit_response); + }); + it('should access the user action from cache', (done) => { + got + .get('restricted', bearer_token_long) + .then((firstResponse) => { + should.equal(firstResponse.statusCode, StatusCodes.OK); + return got.get('restricted', bearer_token_long); + }) + .then(async function (secondResponse) { + should.equal(secondResponse.statusCode, StatusCodes.OK); + await utils.sleep(2000); + return got.get('restricted', bearer_token_long); + }) + .then((thirdResponse) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(thirdResponse.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When the same user request two different actions on a restricted path', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').reply(StatusCodes.OK, {}); + contextBrokerMock.post('/restricted').reply(204); + + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + shortToken + '&app_id=application_id&action=GET&resource=/restricted') + .reply(StatusCodes.OK, keyrock_permit_response); + idmMock + .get('/user?access_token=' + shortToken + '&app_id=application_id&action=POST&resource=/restricted') + .reply(StatusCodes.OK, keyrock_permit_response); + }); + it('should not access the user from cache', (done) => { + got + .get('restricted', auth_token) + .then((firstResponse) => { + should.equal(firstResponse.statusCode, StatusCodes.OK); + return got.post('restricted', auth_token_and_body); + }) + .then((secondResponse) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(secondResponse.statusCode, 204); + done(); + }); + }); + }); +}); + +describe('Authorization: Keyrock PDP', () => { + let pep; + let contextBrokerMock; + let idmMock; + + beforeEach((done) => { + nock.cleanAll(); + const app = require('../../app'); + config.pep.token.secret = 'shhhhh'; + pep = app.start_server('12345', config); + cache.flush(); + done(); + }); + + afterEach((done) => { + delete config.pep.token.secret; + pep.close(config.pep_port); + done(); + }); + + describe('When the same action on a restricted path multiple times with a bearer jwt', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026').get('/restricted').times(3).reply(StatusCodes.OK, {}); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=' + jwt + '&app_id=application_id&action=GET&resource=/restricted') + .times(1) + .reply(StatusCodes.OK, keyrock_permit_response); + }); + it('should access the user action from cache', (done) => { + got + .get('restricted', bearer_jwt_token) + .then((firstResponse) => { + should.equal(firstResponse.statusCode, StatusCodes.OK); + return got.get('restricted', bearer_jwt_token); + }) + .then(async function (secondResponse) { + should.equal(secondResponse.statusCode, StatusCodes.OK); + await utils.sleep(2000); + return got.get('restricted', bearer_jwt_token); + }) + .then((thirdResponse) => { + contextBrokerMock.done(); + idmMock.done(); + should.equal(thirdResponse.statusCode, StatusCodes.OK); + done(); + }); + }); + }); +}); diff --git a/test/unit/opa-pdp-test.js b/test/unit/opa-pdp-test.js new file mode 100644 index 0000000..f1758e6 --- /dev/null +++ b/test/unit/opa-pdp-test.js @@ -0,0 +1,299 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; + +const ngsiPayload = [ + { + id: 'urn:ngsi-ld:TemperatureSensor:002', + type: 'TemperatureSensor', + category: { + type: 'Property', + value: 'sensor' + }, + temperature: { + type: 'Property', + value: 21, + unitCode: 'CEL' + } + }, + { + id: 'urn:ngsi-ld:TemperatureSensor:003', + type: 'TemperatureSensor', + category: { + type: 'Property', + value: 'sensor' + }, + temperature: { + type: 'Property', + value: 27, + unitCode: 'CEL' + } + } +]; + +const ngsi_subscription = { + description: 'Notify me of low feedstock on Farm:001', + type: 'Subscription', + entities: [ + { type: 'FillingLevelSensor' }, + { id: 'urn:ngsi-ld:FillingLevelSensor001' }, + { idPattern: 'urn:ngsi-ld:*' } + ], + watchedAttributes: ['filling'], + q: 'filling>0.6;filling<0.8;controlledAsset==urn:ngsi-ld:Building:farm001', + notification: { + attributes: ['filling', 'controlledAsset'], + format: 'keyValues', + endpoint: { + uri: 'http://tutorial:3000/subscription/low-stock-farm001', + accept: 'application/json' + } + }, + '@context': 'http://context/ngsi-context.jsonld' +}; + +const ngsi_v2_batch_operation = { + actionType: 'append_strict', + entities: [ + { + id: 'urn:ngsi-ld:TemperatureSensor:004', + type: 'TemperatureSensor', + category: { type: 'Text', value: 'sensor' }, + temperature: { type: 'Integer', value: 25, metadata: { unitCode: { type: 'Text', value: 'CEL' } } } + }, + { + id: 'urn:ngsi-ld:TemperatureSensor:005', + type: 'TemperatureSensor', + category: { type: 'Text', value: 'sensor' }, + temperature: { type: 'Integer', value: 30, metadata: { unitCode: { type: 'Text', value: 'CEL' } } } + } + ] +}; + +const keyrock_user_response = { + app_id: 'application_id', + trusted_apps: [], + roles: [ + { + id: 'managers-role-0000-0000-000000000000', + name: 'Management' + } + ] +}; + +const request_with_headers = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111', 'fiware-service': 'smart-gondor', 'x-forwarded-for': 'example.com' } +}; +const request_with_headers_and_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111', 'fiware-service': 'smart-gondor' }, + json: ngsiPayload +}; +const request_with_headers_and_subscription_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111', 'fiware-service': 'smart-gondor' }, + json: ngsi_subscription +}; +const request_with_headers_and_v2_batch_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111', 'fiware-service': 'smart-gondor' }, + json: ngsi_v2_batch_operation +}; + +const open_policy_agent_permit_response = { + allow: true +}; +const open_policy_agent_deny_response = { + allow: false +}; + +const config = { + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [] + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: [], + authorization: { + enabled: true, + pdp: 'opa', // idm|iShare|xacml|authzforce|opa|azf + header: 'fiware-service', + location: { + protocol: 'http', + host: 'openpolicyagent.com', + port: 8080, + path: '/query' + } + } +}; + +describe('Authorization: Open Policy Agent PDP', () => { + let pep; + let contextBrokerMock; + let idmMock; + let openPolicyAgentMock; + + beforeEach((done) => { + const app = require('../../app'); + pep = app.start_server('12345', config); + cache.flush(); + nock.cleanAll(); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=111111111&app_id=application_id') + .reply(StatusCodes.OK, keyrock_user_response); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + done(); + }); + + describe('When a restricted URL is requested by a legitimate user', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .get('/path/entities/urn:ngsi-ld:entity:1111') + .reply(StatusCodes.OK, {}); + openPolicyAgentMock = nock('http://openpolicyagent.com:8080') + .post('/query') + .reply(StatusCodes.OK, open_policy_agent_permit_response); + }); + + it('should allow access', (done) => { + got.get('path/entities/urn:ngsi-ld:entity:1111', request_with_headers).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + openPolicyAgentMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted URL is requested by a forbidden user', () => { + beforeEach(() => { + openPolicyAgentMock = nock('http://openpolicyagent.com:8080') + .post('/query') + .reply(StatusCodes.OK, open_policy_agent_deny_response); + }); + + it('should deny access', (done) => { + got.get('path/entities/urn:ngsi-ld:entity:1111', request_with_headers).then((response) => { + idmMock.done(); + openPolicyAgentMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a restricted URL with a query string is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .get('/path/entities/?ids=urn:ngsi-ld:entity:1111&type=entity') + .reply(StatusCodes.OK, {}); + openPolicyAgentMock = nock('http://openpolicyagent.com:8080') + .post('/query') + .reply(StatusCodes.OK, open_policy_agent_permit_response); + }); + + it('should allow access based on entities', (done) => { + got.get('path/entities/?ids=urn:ngsi-ld:entity:1111&type=entity', request_with_headers).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + openPolicyAgentMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted URL with a payload body is requested', () => { + beforeEach(() => { + openPolicyAgentMock = nock('http://openpolicyagent.com:8080') + .post('/query') + .reply(StatusCodes.OK, open_policy_agent_permit_response); + contextBrokerMock = nock('http://fiware.org:1026') + .patch('/path/entityOperations/upsert') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access based on entities', (done) => { + got.patch('path/entityOperations/upsert', request_with_headers_and_body).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + openPolicyAgentMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted subscription URL with a payload body is requested', () => { + beforeEach(() => { + openPolicyAgentMock = nock('http://openpolicyagent.com:8080') + .post('/query') + .reply(StatusCodes.OK, open_policy_agent_permit_response); + contextBrokerMock = nock('http://fiware.org:1026') + .post('/path/ngsi-ld/v1/subscriptions') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access based on entities', (done) => { + got.post('path/ngsi-ld/v1/subscriptions', request_with_headers_and_subscription_body).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + openPolicyAgentMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted NGSI-v2 batch operation with a payload body is requested', () => { + beforeEach(() => { + openPolicyAgentMock = nock('http://openpolicyagent.com:8080') + .post('/query') + .reply(StatusCodes.OK, open_policy_agent_permit_response); + contextBrokerMock = nock('http://fiware.org:1026').post('/path/v2/op/update').reply(StatusCodes.OK, {}); + }); + + it('should allow access based on entities', (done) => { + got.post('path/v2/op/update', request_with_headers_and_v2_batch_body).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + openPolicyAgentMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); +}); diff --git a/test/unit/sanity-test.js b/test/unit/sanity-test.js deleted file mode 100644 index ab87c22..0000000 --- a/test/unit/sanity-test.js +++ /dev/null @@ -1,90 +0,0 @@ -//const should = require('should'); -//const mocha = require('mocha'); - -process.env.PEP_PROXY_IDM_PORT = 3000; -process.env.PEP_PROXY_IDM_HOST = 'localhost'; -process.env.PEP_PROXY_IDM_SSL_ENABLED = false; -process.env.PEP_PROXY_USERNAME = - 'pep_proxy_00000000-0000-0000-0000-000000000000'; -process.env.PEP_PASSWORD = 'test'; - -const config_service = require('../../lib/config_service.js'); -config_service.set_config(require('../config_test.js')); -const config = config_service.get_config(); -const IDM = require('./../../lib/idm.js').IDM; -const AZF = require('./../../lib/azf.js').AZF; - -const debug = require('debug')('pep-proxy:sanity-test'); -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - -describe('Sanity Checks for Wilma PEP Proxy - Identity Manager Checks', function() { - describe('Testing Keyrock configuration', function() { - it('should have PEP user configured', function(done) { - if (config.pep.username !== undefined && config.pep.username !== '') { - if (config.pep.password !== undefined && config.pep.password !== '') { - done(); - } - } - }); - }); - - describe('Testing connection with Keyrock', function() { - it('should have connectivity with Keyrock', function(done) { - IDM.checkConn( - function(status) { - if (status === 200) { - done(); - } - }, - function(status, e) { - debug('Error in Keyrock communication', e); - } - ); - }); - - it('should authenticate with Keyrock', function(done) { - IDM.authenticate( - () => { - done(); - }, - function(status, e) { - debug('Error in Keyrock communication', e); - } - ); - }); - }); -}); - -describe('Sanity Checks for Wilma PEP Proxy - AuthZForce Checks', function() { - if ( - config.authorization.enabled && - config.authorization.pdp === 'authzforce' - ) { - describe('Testing configuration', function() { - it('should have AZF server configured', function(done) { - if (config.azf.host !== undefined && config.azf.host !== '') { - if (config.azf.port !== undefined && config.azf.port !== '') { - done(); - } - } - }); - }); - - describe('Testing connection with AZF', function() { - it('should have connectivity with AZF', function(done) { - AZF.checkConn( - function() {}, - function(status) { - if (status === 401) { - done(); - } - } - ); - }); - }); - } else { - it('AZF not enabled', function(done) { - done(); - }); - } -}); diff --git a/test/unit/start-up-test.js b/test/unit/start-up-test.js new file mode 100644 index 0000000..b7af3a7 --- /dev/null +++ b/test/unit/start-up-test.js @@ -0,0 +1,180 @@ +const config_service = require('../../lib/config_service'); +const should = require('should'); + +const config = { + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [] + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: [], + authorization: { + enabled: false, + pdp: 'authzforce' // idm|iShare|xacml|authzforce|opa|azf + } +}; + +describe('When the PEP Proxy is started with environment variables', () => { + beforeEach(() => { + process.env.PEP_PROXY_PORT = 8080; + process.env.PEP_PROXY_HTTPS_ENABLED = 'true'; + process.env.PEP_PROXY_HTTPS_PORT = 443; + process.env.PEP_PROXY_IDM_HOST = 'idm_host'; + process.env.PEP_PROXY_IDM_PORT = 3000; + process.env.PEP_PROXY_IDM_SSL_ENABLED = 'true'; + process.env.PEP_PROXY_APP_HOST = 'app_host'; + process.env.PEP_PROXY_APP_PORT = '1026'; + process.env.PEP_PROXY_APP_SSL_ENABLED = 'true'; + process.env.PEP_PROXY_ORG_ENABLED = 'true'; + process.env.PEP_PROXY_ORG_HEADER = 'organization'; + process.env.PEP_PROXY_APP_ID = '9999999111'; + process.env.PEP_PROXY_USERNAME = 'user'; + process.env.PEP_PASSWORD = 'password'; + process.env.PEP_TOKEN_SECRET = 'secret-token'; + process.env.PEP_TRUSTED_APPS = ''; + + process.env.PEP_PROXY_PUBLIC_PATHS = 'a,b,c'; + process.env.PEP_PROXY_AUTH_FOR_NGINX = 'false'; + process.env.PEP_PROXY_MAGIC_KEY = '54321'; + process.env.PEP_PROXY_DEBUG = 'PEP-Proxy:*'; + process.env.PEP_PROXY_ERROR_TEMPLATE = '{{message}}'; + process.env.PEP_PROXY_ERROR_CONTENT_TYPE = 'text/html'; + }); + + afterEach(() => { + delete process.env.IOTA_CB_HOST; + delete process.env.PEP_PROXY_PORT; + delete process.env.PEP_PROXY_HTTPS_ENABLED; + delete process.env.PEP_PROXY_HTTPS_PORT; + delete process.env.PEP_PROXY_IDM_HOST; + delete process.env.PEP_PROXY_IDM_PORT; + delete process.env.PEP_PROXY_IDM_SSL_ENABLED; + delete process.env.PEP_PROXY_APP_HOST; + delete process.env.PEP_PROXY_APP_PORT; + delete process.env.PEP_PROXY_APP_SSL_ENABLED; + delete process.env.PEP_PROXY_ORG_ENABLED; + delete process.env.PEP_PROXY_ORG_HEADER; + delete process.env.PEP_PROXY_APP_ID; + delete process.env.PEP_PROXY_USERNAME; + delete process.env.PEP_PASSWORD; + delete process.env.PEP_TOKEN_SECRET; + delete process.env.PEP_TRUSTED_APPS; + + delete process.env.PEP_PROXY_PUBLIC_PATHS; + delete process.env.PEP_PROXY_AUTH_FOR_NGINX; + delete process.env.PEP_PROXY_MAGIC_KEY; + delete process.env.PEP_PROXY_DEBUG; + delete process.env.PEP_PROXY_ERROR_TEMPLATE; + delete process.env.PEP_PROXY_ERROR_CONTENT_TYPE; + }); + + it('should amend the configuration', (done) => { + config_service.set_config(config, true); + const pep_config = config_service.get_config(); + + should.equal(pep_config.pep_port, '8080'); + should.equal(pep_config.pep.app_id, '9999999111'); + should.equal(pep_config.pep.username, 'user'); + should.equal(pep_config.pep.password, 'password'); + should.equal(pep_config.pep.token.secret, 'secret-token'); + + should.equal(pep_config.idm.host, 'idm_host'); + should.equal(pep_config.idm.port, '3000'); + should.equal(pep_config.idm.ssl, true); + + should.equal(pep_config.app.host, 'app_host'); + should.equal(pep_config.app.port, '1026'); + should.equal(pep_config.app.ssl, true); + + should.equal(pep_config.organizations.enabled, true); + should.equal(pep_config.organizations.header, 'organization'); + done(); + }); +}); + +describe('When any PDP is configured with environment variables', () => { + beforeEach(() => { + process.env.PEP_PROXY_AUTH_ENABLED = 'true'; + process.env.PEP_PROXY_PDP = 'opa'; + process.env.PEP_PROXY_PDP_PROTOCOL = 'https'; + process.env.PEP_PROXY_PDP_HOST = 'pdp-host'; + process.env.PEP_PROXY_PDP_PORT = 443; + }); + + afterEach(() => { + delete process.env.PEP_PROXY_PDP; + delete process.env.PEP_PROXY_PDP_PROTOCOL; + delete process.env.PEP_PROXY_PDP_HOST; + delete process.env.PEP_PROXY_PDP_PORT; + }); + + it('should amend the PDP configuration', (done) => { + config_service.set_config(config, true); + const authorization = config_service.get_config().authorization; + const pdp = config_service.get_config().authorization.opa; + + should.equal(authorization.enabled, true); + should.equal(authorization.pdp, 'opa'); + should.equal(pdp.protocol, 'https'); + should.equal(pdp.host, 'pdp-host'); + should.equal(pdp.port, '443'); + done(); + }); +}); + +describe('When the Authzforce PDP is started with environment variables', () => { + beforeEach(() => { + process.env.PEP_PROXY_AZF_PROTOCOL = 'http'; + process.env.PEP_PROXY_AZF_HOST = 'authzforce.com'; + process.env.PEP_PROXY_AZF_PORT = 9090; + process.env.PEP_PROXY_AZF_CUSTOM_POLICY = 'policy'; + }); + + afterEach(() => { + delete process.env.PEP_PROXY_AZF_PROTOCOL; + delete process.env.PEP_PROXY_AZF_HOST; + delete process.env.PEP_PROXY_AZF_PORT; + delete process.env.PEP_PROXY_AZF_CUSTOM_POLICY; + }); + + it('should amend the PDP configuration', (done) => { + config_service.set_config(config, true); + const azf = config_service.get_config().authorization.azf; + + should.equal(azf.protocol, 'http'); + should.equal(azf.host, 'authzforce.com'); + should.equal(azf.port, '9090'); + should.equal(azf.custom_policy, 'policy'); + done(); + }); +}); + +describe('When authorization is disabled with environment variables', () => { + beforeEach(() => { + process.env.PEP_PROXY_AUTH_ENABLED = 'false'; + }); + + afterEach(() => { + delete process.env.PEP_PROXY_AUTH_ENABLED; + }); + it('should remove the authorization config', (done) => { + config_service.set_config(config, true); + const authorization = config_service.get_config().authorization; + authorization.should.be.empty(); + done(); + }); +}); diff --git a/test/unit/utils.js b/test/unit/utils.js new file mode 100644 index 0000000..53f0f65 --- /dev/null +++ b/test/unit/utils.js @@ -0,0 +1,23 @@ +const jwt = require('jsonwebtoken'); + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function createJWT( + key = 'shhhhh', + data = { + app_id: 'application_id', + trusted_apps: [], + id: 'username', + displayName: 'Some User' + }, + expiry = undefined +) { + return jwt.sign(data, key, expiry); +} + +exports.sleep = sleep; +exports.createJWT = createJWT; diff --git a/test/unit/xacml-pdp-test.js b/test/unit/xacml-pdp-test.js new file mode 100644 index 0000000..9f1c6c3 --- /dev/null +++ b/test/unit/xacml-pdp-test.js @@ -0,0 +1,220 @@ +/* + * Copyright 2021 - Universidad Politécnica de Madrid. + * + * This file is part of PEP-Proxy + * + */ + +const got = require('got'); +const should = require('should'); +const nock = require('nock'); +const cache = require('../../lib/cache'); +const StatusCodes = require('http-status-codes').StatusCodes; + +const ngsiPayload = [ + { + id: 'urn:ngsi-ld:TemperatureSensor:002', + type: 'TemperatureSensor', + category: { + type: 'Property', + value: 'sensor' + }, + temperature: { + type: 'Property', + value: 21, + unitCode: 'CEL' + } + }, + { + id: 'urn:ngsi-ld:TemperatureSensor:003', + type: 'TemperatureSensor', + category: { + type: 'Property', + value: 'sensor' + }, + temperature: { + type: 'Property', + value: 27, + unitCode: 'CEL' + } + } +]; +const keyrock_user_response = { + app_id: 'application_id', + trusted_apps: [], + roles: [ + { + id: 'managers-role-0000-0000-000000000000', + name: 'Management' + } + ] +}; + +const request_with_headers = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111', 'fiware-service': 'smart-gondor', 'x-forwarded-for': 'example.com' } +}; +const request_with_headers_and_body = { + prefixUrl: 'http:/localhost:1026', + throwHttpErrors: false, + headers: { 'x-auth-token': '111111111', 'fiware-service': 'smart-gondor' }, + json: ngsiPayload +}; + +const xacml_permit_response = { + Response: [ + { + Decision: 'Permit', + Status: { + StatusCode: { + Value: 'urn:oasis:names:tc:xacml:1.0:status:ok', + StatusCode: { Value: 'urn:oasis:names:tc:xacml:1.0:status:ok' } + } + } + } + ] +}; + +const xacml_deny_response = { + Response: [ + { + Decision: 'Deny', + Status: { + StatusCode: { + Value: 'urn:oasis:names:tc:xacml:1.0:status:ok', + StatusCode: { Value: 'urn:oasis:names:tc:xacml:1.0:status:ok' } + } + } + } + ] +}; + +const config = { + pep_port: 1026, + pep: { + app_id: 'application_id', + trusted_apps: [] + }, + idm: { + host: 'keyrock.com', + port: '3000', + ssl: false + }, + app: { + host: 'fiware.org', + port: '1026', + ssl: false // Use true if the app server listens in https + }, + organizations: { + enabled: false + }, + cache_time: 300, + public_paths: [], + authorization: { + enabled: true, + pdp: 'xacml', // idm|iShare|xacml|authzforce|opa|azf + header: 'fiware-service', + location: { + protocol: 'http', + host: 'xacml.com', + port: 8080, + path: '/xacml' + } + } +}; + +describe('Authorization: XACML PDP', () => { + let pep; + let contextBrokerMock; + let idmMock; + let xacmlMock; + + beforeEach((done) => { + const app = require('../../app'); + pep = app.start_server('12345', config); + cache.flush(); + nock.cleanAll(); + idmMock = nock('http://keyrock.com:3000') + .get('/user?access_token=111111111&app_id=application_id') + .reply(StatusCodes.OK, keyrock_user_response); + done(); + }); + + afterEach((done) => { + pep.close(config.pep_port); + done(); + }); + + describe('When a restricted URL is requested by a legitimate user', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .get('/path/entities/urn:ngsi-ld:entity:1111') + .reply(StatusCodes.OK, {}); + xacmlMock = nock('http://xacml.com:8080').post('/xacml').reply(StatusCodes.OK, xacml_permit_response); + }); + + it('should allow access', (done) => { + got.get('path/entities/urn:ngsi-ld:entity:1111', request_with_headers).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + xacmlMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted URL is requested by a forbidden user', () => { + beforeEach(() => { + xacmlMock = nock('http://xacml.com:8080').post('/xacml').reply(StatusCodes.OK, xacml_deny_response); + }); + + it('should deny access', (done) => { + got.get('path/entities/urn:ngsi-ld:entity:1111', request_with_headers).then((response) => { + idmMock.done(); + xacmlMock.done(); + should.equal(response.statusCode, StatusCodes.UNAUTHORIZED); + done(); + }); + }); + }); + + describe('When a restricted URL with a query string is requested', () => { + beforeEach(() => { + contextBrokerMock = nock('http://fiware.org:1026') + .get('/path/entities/?ids=urn:ngsi-ld:entity:1111&type=entity') + .reply(StatusCodes.OK, {}); + xacmlMock = nock('http://xacml.com:8080').post('/xacml').reply(StatusCodes.OK, xacml_permit_response); + }); + + it('should allow access based on entities', (done) => { + got.get('path/entities/?ids=urn:ngsi-ld:entity:1111&type=entity', request_with_headers).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + xacmlMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); + + describe('When a restricted URL with a payload body is requested', () => { + beforeEach(() => { + xacmlMock = nock('http://xacml.com:8080').post('/xacml').reply(StatusCodes.OK, xacml_permit_response); + contextBrokerMock = nock('http://fiware.org:1026') + .patch('/path/entityOperations/upsert') + .reply(StatusCodes.OK, {}); + }); + + it('should allow access based on entities', (done) => { + got.patch('path/entityOperations/upsert', request_with_headers_and_body).then((response) => { + contextBrokerMock.done(); + idmMock.done(); + xacmlMock.done(); + should.equal(response.statusCode, StatusCodes.OK); + done(); + }); + }); + }); +}); diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index 73ff319..0000000 --- a/test/utils.js +++ /dev/null @@ -1,19 +0,0 @@ -const fs = require('fs'); - -function readExampleFile(name, raw) { - const text = fs.readFileSync(name, 'UTF8'); - - if (raw) { - return text; - } - return JSON.parse(text); -} - -function delay(ms) { - return function(callback) { - setTimeout(callback, ms); - }; -} - -exports.readExampleFile = readExampleFile; -exports.delay = delay;