From c14d7c3ca6ad735e4578c9a899deb31e107a3b61 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Wed, 24 May 2023 10:40:16 +0200 Subject: [PATCH 01/16] Remove old code base --- activation/createPolicy.js | 322 -------- activation/token.js | 91 --- config.js | 82 -- package-lock.json | 1545 ------------------------------------ package.json | 25 - server.js | 104 --- util/database.js | 111 --- util/utils.js | 14 - 8 files changed, 2294 deletions(-) delete mode 100644 activation/createPolicy.js delete mode 100644 activation/token.js delete mode 100644 config.js delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 server.js delete mode 100644 util/database.js delete mode 100644 util/utils.js diff --git a/activation/createPolicy.js b/activation/createPolicy.js deleted file mode 100644 index f404e1b..0000000 --- a/activation/createPolicy.js +++ /dev/null @@ -1,322 +0,0 @@ -var debug = require('debug')('as:createPolicy'); -const https = require('https'); -const fetch = require('node-fetch'); -const moment = require('moment'); -const uuid = require('uuid'); -const jose = require('node-jose'); -var jwt = require('jsonwebtoken'); - -var database = require('../util/database.js'); -const config = require('../config.js'); -const error = require('../util/utils.js').error; - -const httpsAgent = new https.Agent({ - rejectUnauthorized: config.ar_ssl, -}); - -// Creates JWT for obtaining token at AR -// -async function createJwt(chain) { - const now = moment(); - const iat = now.unix(); - const exp = now.add(30, 'seconds').unix(); - const payload = { - jti: uuid.v4(), - iss: config.id, - sub: config.id, - aud: [ - config.ar_id, - config.ar_token - ], - iat, - nbf: iat, - exp - }; - const key = await jose.JWK.asKey(config.key, "pem"); - return await jose.JWS.createSign({ - algorithm: 'RS256', - format: 'compact', - fields: { - typ: "JWT", - x5c: chain - } - }, key).update(JSON.stringify(payload)).final(); -} - -// Get token from AR -// -async function getToken(chain) { - debug('Obtaining token from AR'); - let result = { - access_token: null, - err: null - }; - const jwtoken = await createJwt(chain); - let access_token = null; - try { - const tparams = new URLSearchParams(); - tparams.append('grant_type', 'client_credentials'); - tparams.append('scope', 'iSHARE'); - tparams.append('client_id', config.id); - tparams.append('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'); - tparams.append('client_assertion', jwtoken); - const options = { - method: 'POST', - body: tparams, - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - } - if(config.ar_token.toLowerCase().startsWith("https://")) { - options.agent = httpsAgent; - } - const ar_response = await fetch(config.ar_token, options); - const res_body = await ar_response.json(); - if (ar_response.status != 200) { - const err_body = await ar_response.text(); - result.err = "Error when retrieving token at AR: " + err_body; - return result; - } - if ( !res_body.access_token) { - debug('access_token not found in response: %o', res_body); - result.err = "Received invalid response from AR: " + JSON.stringify(res_body); - return result; - } - result.access_token = res_body.access_token; - return result; - } catch (e) { - console.error(e); - let msg = "General error when obtaining token from AR"; - if (e.response) { - msg = msg += ": " + e.response.text(); - } - debug(msg); - result.err = msg; - return result; - } -} - -// Build delegation payload -// -async function getDelegationEvidence(eori) { - let payload = { - delegationRequest: { - policyIssuer: config.id, - target: { - accessSubject: eori - }, - policySets: [ - { - policies: [ - { - target: { - resource: { - type: "delegationEvidence", - identifiers: [ - "*" - ], - attributes: [ - "*" - ] - }, - actions: [ - "POST" - ] - }, - rules: [ - { - "effect": "Permit" - } - ] - } - ] - } - ] - } - }; - return payload; -} - -// Check for delegation evidence if sender is allowed to create policies -// -async function checkCreateDelegationEvidence(eori, access_token) { - debug('Check at AR if sender is allowed to create policies'); - const payload = await getDelegationEvidence(eori); - debug('Required delegationEvidence: %j', payload); - const options = { - method: "POST", - body: JSON.stringify(payload), - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + access_token - } - }; - if(config.ar_delegation.toLowerCase().startsWith("https://")) { - options.agent = httpsAgent; - } - let evidence = null; - try { - debug('Sending delegationRequest to AR'); - const ar_response = await fetch(config.ar_delegation, options); - if (ar_response.status == 404) { - debug('Received 404 NotFound error'); - return "Policy not found at AR, Creating policies not permitted"; - } - if (ar_response.status != 200) { - const err_body = await ar_response.text(); - debug('Wrong status code in response: %o', err_body); - return "Error when retrieving policy from AR: " + err_body; - } - const res_body = await ar_response.json(); - if ( !res_body.delegation_token) { - debug('No delegation_token found in response: %o', res_body); - return "Received invalid response from AR: " + JSON.stringify(res_body); - } - let decoded_delegation = jwt.decode(res_body.delegation_token); - debug('Check for Permit rule in delegationEvidence: %j', decoded_delegation); - if (decoded_delegation.delegationEvidence) { - let delev = decoded_delegation.delegationEvidence; - let psets = delev.policySets; - if (psets && psets.length > 0) { - let pset = psets[0]; - if (pset && pset.policies && pset.policies.length > 0) { - let p = pset.policies[0]; - if (p && p.target && p.target.resource && p.target.resource.type && - p.target.resource.type == "delegationEvidence") { - if (p.rules && p.rules.length > 0) { - let r = p.rules[0]; - if (r && r.effect && r.effect == "Permit") { - return null; - } - } - } - } - } - } - return "Creating policies not permitted"; - } catch (e) { - console.error(e); - let msg = "General error when obtaining delegation evidence from AR"; - if (e.response) { - msg = msg += ": " + e.response.text(); - } - return msg; - } - - return "Checking for delegation evidence to create policies failed!"; -} - -// Create requested policy at AR -// -async function createPolicy(token, payload) { - debug('Creating new policy at AR'); - let result = { - policy_token: null, - err: null - }; - const options = { - method: "POST", - body: JSON.stringify(payload), - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + token, - "Accept": "application/json" - } - }; - if(config.ar_policy.toLowerCase().startsWith("https://")) { - options.agent = httpsAgent; - } - try { - debug('Sending request to AR /policy endpoint with policy: %j', payload); - const ar_response = await fetch(config.ar_policy, options); - if (ar_response.status != 200) { - const err_body = await ar_response.text(); - result.err = "Error when creating policy at AR: " + err_body; - return result; - } - const res_body = await ar_response.json(); - if (!res_body.policy_token) { - // Response is not specified, can be empty - debug('No policy token in response: %o', res_body); - return result; - } - result.policy_token = res_body.policy_token; - return result; - } catch (e) { - console.error(e); - let msg = "General error when creating policy at AR"; - if (e.response) { - msg = msg += ": " + e.response.text(); - } - debug(msg); - result.err = msg; - return result; - } -} - -// Perform createPolicy request -// -async function performCreatePolicy(req, res, db, chain) { - // Get Autorization header - debug('Extracting authorization header'); - if ( !req.header('Authorization')) { - debug("Missing Authorization header"); - error(400, "Missing Authorization header", res); - return null; - } - const auth = req.header('Authorization'); - let token = null; - if (auth.startsWith("Bearer ")){ - token = auth.split(" ")[1]; - } - if (!token) { - debug('No authorization header found: %o', auth); - error(400, "Missing Authorization header Bearer token", res); - return null; - } - - // Get DB entry for token - debug('Retrieving token from DB'); - const db_token = await database.getByToken(token, db); - if (!db_token.token) { - let msg = "No valid token supplied"; - if (db_token.err) { - msg += ": " + db_token.err; - } - debug(msg); - error(400, msg, res); - return null; - } - - // Get token from AR - const tresult = await getToken(chain); - if (tresult.err) { - let msg = "Retrieving token failed: " + tresult.err; - debug(msg); - error(400, msg, res); - return null; - } - const access_token = tresult.access_token; - - // Check for policy at AR, if sender is allowed to create delegation evidence - const err = await checkCreateDelegationEvidence(db_token.token.eori, access_token); - if (err) { - let msg = db_token.token.eori + " was not issued required policy: " + err; - debug(msg); - error(400, msg, res); - return null; - } - - // Create requested policy at AR - const presult = await createPolicy(access_token, req.body); - if (presult.err) { - let msg = "Creating policy failed: " + presult.err; - debug(msg); - error(400, msg, res); - return null; - } - - return presult; -} - -module.exports = { - performCreatePolicy: performCreatePolicy -}; diff --git a/activation/token.js b/activation/token.js deleted file mode 100644 index 4d54004..0000000 --- a/activation/token.js +++ /dev/null @@ -1,91 +0,0 @@ -var debug = require('debug')('as:token'); -const https = require('https'); -const fetch = require('node-fetch'); - -var database = require('../util/database.js'); -const config = require('../config.js'); -const error = require('../util/utils.js').error; - -const httpsAgent = new https.Agent({ - rejectUnauthorized: config.ar_ssl, -}); - -// Forward token request to AR -// -async function forward_token(req, res) { - debug('Forward request to /token endpoint of AR'); - if ( !req.body.client_id) { - debug("Missing parameter client_id"); - error(400, "Missing parameter client_id", res); - return null; - } - let eori = req.body.client_id; - - // Proxy request to AR - let token = {}; - try { - const tparams = new URLSearchParams(req.body); - const options = { - method: 'POST', - body: tparams, - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - } - if(config.ar_token.toLowerCase().startsWith("https://")) { - options.agent = httpsAgent; - } - const ar_response = await fetch(config.ar_token, options); - const res_body = await ar_response.json(); - if (ar_response.status != 200) { - debug('Wrong status code in response: %o', res_body); - res.status(ar_response.status).send(res_body); - return null; - } - if ( !res_body.access_token || !res_body.expires_in) { - debug('Invalid response: %o', res_body); - error(400, "Received invalid response from AR: " + JSON.stringify(res_body), res); - return null; - } - token = { - eori: eori, - access_token: res_body.access_token, - expires: Date.now() + (1000*res_body.expires_in) - }; - debug('Received response: %o', res_body); - return { - token: token, - response: res_body - }; - } catch (e) { - console.error(e); - let msg = e; - if (e.response) { - msg = e.response.text(); - } - error(500, "Error when forwarding request to AR: " + msg, res); - return null; - } -} - -// Perform token request -// -async function performToken(req, res, db) { - - // Forward token request to AR - const token = await forward_token(req, res); - if ( !token ) { - return null; - } - - // DB entry insert - const ins_err = await database.insertToken(token.token, db); - if (ins_err) { - error(500, "Could not insert token into DB: " + ins_err, res); - return null; - } - - return token; -} - -module.exports = { - performToken: performToken -}; diff --git a/config.js b/config.js deleted file mode 100644 index 8a6f135..0000000 --- a/config.js +++ /dev/null @@ -1,82 +0,0 @@ -var debug = require('debug')('as:config'); -const fs = require('fs'); -const yaml = require('js-yaml'); - -var user_cfg = {} -try { - let config_file = './config/as.yml'; - console.log("Loading config: ", config_file); - let fileContents = fs.readFileSync(config_file, 'utf8'); - user_cfg = yaml.load(fileContents); -} catch (e) { - console.error("Error loading config/as.yml: ", e); - process.exit(1) -} - -let config = {}; - -// Default values -config.key = ""; -config.crt = ""; -config.id = "EU.EORI.NLPACKETDEL"; -config.port = 7000; -config.url = "http://localhost:7000"; -config.db_source = ":memory:"; //"db.sqlite"; -config.ar_token = "http://localhost/connect/token"; -config.ar_policy = "http://localhost/policy"; -config.ar_delegation = "http://localhost/delegation"; -config.ar_id = "EU.EORI.NL000000004"; -config.ar_ssl = false; - -// Client data -if (user_cfg.client) { - if (user_cfg.client.id) { - config.id = user_cfg.client.id; - } - - // Private key - config.key = user_cfg.client.key; - if (!!process.env.AS_CLIENT_KEY) { - config.key = process.env.AS_CLIENT_KEY; - } - - // Certificate chain - config.crt = user_cfg.client.crt; - if (!!process.env.AS_CLIENT_CRT) { - config.crt = process.env.AS_CLIENT_CRT; - } -} - -// Database -if (user_cfg.db) { - if (user_cfg.db.source) { - config.db_source = user_cfg.db.source; - } -} - -// Authorisation registry -if (user_cfg.ar) { - if (user_cfg.ar.token) { - config.ar_token = user_cfg.ar.token; - } - if (user_cfg.ar.policy) { - config.ar_policy = user_cfg.ar.policy; - } - if (user_cfg.ar.id) { - config.ar_id = user_cfg.ar.id; - } - if (user_cfg.ar.delegation) { - config.ar_delegation = user_cfg.ar.delegation - } - if (user_cfg.ar.rejectUnauthorized) { - config.ar_ssl = user_cfg.ar.rejectUnauthorized; - } -} - -// Debug output of config -if (process.env.AS_MAX_HEADER_SIZE) { - debug('Max HTTP header size set to: %s', process.env.AS_MAX_HEADER_SIZE); -} -debug('Loaded config: %O', config); - -module.exports = config; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 81e3ed3..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1545 +0,0 @@ -{ - "name": "activation-service", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "optional": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "optional": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "optional": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "optional": true - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "optional": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "optional": true - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "optional": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "optional": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "optional": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "optional": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "optional": true - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "optional": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "optional": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "optional": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "optional": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "optional": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "optional": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "optional": true - }, - "js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "requires": { - "argparse": "^2.0.1" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "optional": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "optional": true - }, - "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" - }, - "mime-types": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", - "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", - "requires": { - "mime-db": "1.46.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "needle": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", - "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "node-addon-api": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", - "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, - "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "optional": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - } - }, - "node-jose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.0.0.tgz", - "integrity": "sha512-j8zoFze1gijl8+DK/dSXXqX7+o2lMYv1XS+ptnXgGV/eloQaqq1YjNtieepbKs9jBS4WTnMOqyKSaQuunJzx0A==", - "requires": { - "base64url": "^3.0.1", - "buffer": "^5.5.0", - "es6-promise": "^4.2.8", - "lodash": "^4.17.15", - "long": "^4.0.0", - "node-forge": "^0.10.0", - "pako": "^1.0.11", - "process": "^0.11.10", - "uuid": "^3.3.3" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "optional": true, - "requires": { - "abbrev": "1" - } - }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "optional": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "optional": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "optional": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "optional": true - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "optional": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true - } - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "sqlite": { - "version": "4.0.19", - "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.0.19.tgz", - "integrity": "sha512-UiHAgJI4NzbnBXdD3r8EHOpwCOiv8VxcC6dnOoEYPEde8bw6t2HRqUcvGppF3uOV/SrqyVHicMl5Cjmuaalnlw==" - }, - "sqlite3": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz", - "integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==", - "requires": { - "node-addon-api": "^3.0.0", - "node-gyp": "3.x", - "node-pre-gyp": "^0.11.0" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "optional": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "optional": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "optional": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "optional": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 7c510ec..0000000 --- a/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "activation-service", - "version": "0.1.0", - "description": "Service allowing to activate services and create policies in an iSHARE authorisation registry during the acquisition step.", - "main": "server.js", - "scripts": { - "start": "node --max-http-header-size=${AS_MAX_HEADER_SIZE:-8192} ./server.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Dr. Dennis Wendland", - "license": "MIT", - "dependencies": { - "body-parser": "^1.19.0", - "debug": "^4.3.1", - "express": "^4.17.1", - "js-yaml": "^4.0.0", - "jsonwebtoken": "^8.5.1", - "moment": "^2.29.1", - "node-fetch": "^2.6.1", - "node-jose": "^2.0.0", - "sqlite": "^4.0.19", - "sqlite3": "^5.0.2", - "uuid": "^8.3.2" - } -} diff --git a/server.js b/server.js deleted file mode 100644 index 904a208..0000000 --- a/server.js +++ /dev/null @@ -1,104 +0,0 @@ -var debug = require('debug')('as:server'); -const moment = require('moment'); -const uuid = require('uuid'); -const fetch = require('node-fetch'); -const https = require('https'); -const jose = require('node-jose'); -var jwt = require('jsonwebtoken'); -var bodyParser = require('body-parser'); -const express = require('express'); -const app = express(); - -var database = require('./util/database.js'); -const config = require('./config.js'); -const error = require('./util/utils.js').error; -const performToken = require('./activation/token.js').performToken; -const performCreatePolicy = require('./activation/createPolicy.js').performCreatePolicy; - -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); -const httpsAgent = new https.Agent({ - rejectUnauthorized: config.ar_ssl, -}); -let db = null; -let chain = []; - -// Init server -// -async function init() { - debug('Initialising server...'); - // Prepare DB - db = await database.openDB(); - - // Prepare CRT - const crt_regex = /^-----BEGIN CERTIFICATE-----\n([\s\S]+?)\n-----END CERTIFICATE-----$/gm; - let m; - while ((m = crt_regex.exec(config.crt)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (m.index === crt_regex.lastIndex) { - crt_regex.lastIndex++; - } - chain.push(m[1].replace(/\n/g, "")); - } -} - -// /token -// Proxy request to /token endpoint of AR -// and store returned token -app.post('/token', async (req, res) => { - debug('Received request at /token endpoint'); - const token = await performToken(req, res, db); - - // Return AR response - if (token) { - debug('Received access_token with response: %o', token.response); - debug('=============='); - res.send(token.response); - } -}); - - -// /createpolicy -// Create policy at AR -// Perform additional activation steps if needed -app.post('/createpolicy', async (req, res) => { - debug('Received request at /createpolicy endpoint'); - // Create requested policy at AR - const presult = await performCreatePolicy(req, res, db, chain); - - // ********************** - // Other activation steps (e.g. starting computation nodes) - // could be added here! - // ********************** - - // Return result - if (presult) { - if (presult.policy_token) { - debug('Successfully created new policy at AR. Received policy_token: %o', presult.policy_token); - res.send({ - policy_token: presult.policy_token - }); - } else { - debug('Successfully created new policy at AR'); - res.sendStatus(200); - } - debug('=============='); - } -}); - -// /health -// Healthcheck endpoint -app.get('/health', (req, res) => { - res.send({ - uptime: process.uptime(), - message: 'OK', - timestamp: Date.now() - }); -}) - -// Start server -// -const server = app.listen(config.port, () => { - console.log(`Express running → PORT ${server.address().port}`); - init(); -}); diff --git a/util/database.js b/util/database.js deleted file mode 100644 index 4a3e9ab..0000000 --- a/util/database.js +++ /dev/null @@ -1,111 +0,0 @@ -var debug = require('debug')('as:database'); -var sqlite3 = require('sqlite3').verbose(); -const { open } = require('sqlite'); -const config = require('../config.js'); - -const DBSOURCE = config.db_source; - -// Open DB -async function openDB() { - console.log("Connecting to SQLite Database:", config.db_source); - let db = await open({ - filename: config.db_source, - driver: sqlite3.Database - }); - - try { - console.log("Setup database..."); - await db.run(`CREATE TABLE token ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - eori text NOT NULL UNIQUE, - access_token text NOT NULL UNIQUE, - expires int NOT NULL - )`); - console.log("Created new database"); - } catch (err) { - console.log("Loaded existing database"); - let clean_err = await clean(db); - if (clean_err) { - console.log("Error cleaning tokens: ", clean_err); - throw clean_err; - } - } - return db; -} - -async function insertToken(token, db) { - var sql = 'INSERT OR REPLACE INTO token (eori, access_token, expires) VALUES (?,?,?)'; - var params = [ token.eori, token.access_token, token.expires]; - try { - debug('Inserting new token for %o', token.eori); - const result = await db.run(sql, params); - return null; - } catch (err) { - console.error(err); - return err; - } - -} - -async function getByEORI(eori, db) { - let result = { - token: null, - err: null - }; - let clean_err = await clean(db); - if (clean_err) { - result.err = clean_err; - return result; - } - var sql = 'SELECT eori, access_token, expires FROM token WHERE eori = ?'; - try { - debug('Getting token DB entry by EORI: %o', eori); - const res = await db.get(sql, eori); - result.token = res; - } catch (err) { - result.err = err; - } - return result; -} - -async function getByToken(token, db) { - let result = { - token: null, - err: null - }; - let clean_err = await clean(db); - if (clean_err) { - result.err = clean_err; - return result; - } - var sql = 'SELECT eori, access_token, expires FROM token WHERE access_token = ?'; - try { - debug('Getting token DB entry by token'); - const res = await db.get(sql, token); - result.token = res; - } catch (err) { - result.err = err; - } - return result; -} - -async function clean(db) { - let cur_date = Date.now(); - try { - const result = await db.run('DELETE FROM token WHERE expires < ?', - cur_date); - debug('Removed expired tokens from DB: %o', result); - return null; - } catch (err) { - console.error("err:",err); - return err; - } -} - -module.exports = { - openDB: openDB, - clean: clean, - insertToken: insertToken, - getByEORI: getByEORI, - getByToken: getByToken -}; diff --git a/util/utils.js b/util/utils.js deleted file mode 100644 index 846e438..0000000 --- a/util/utils.js +++ /dev/null @@ -1,14 +0,0 @@ - - -// Return error response -// -function error(code, msg, res) { - res.status(code).send({ - status: "ERROR", - msg: msg - }); -} - -module.exports = { - error: error -}; From 62166640611466349ebac59081de7be7c5507e9e Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Thu, 25 May 2023 10:43:12 +0200 Subject: [PATCH 02/16] Implementing token endpoint with Python --- .github/workflows/prerelease.yml | 25 +++- .github/workflows/test.yml | 27 ++++ Dockerfile | 24 ++-- api/__init__.py | 1 + api/app.py | 37 ++++++ api/errors.py | 22 ++++ api/models/token.py | 11 ++ api/token.py | 49 +++++++ api/util/db_handler.py | 67 ++++++++++ api/util/token_handler.py | 65 ++++++++++ bin/run.sh | 17 +++ config/as.yml | 24 ++-- pytest.ini | 8 ++ requirements.txt | 10 ++ tests/__init__.py | 0 tests/config/as.yml | 190 ++++++++++++++++++++++++++++ tests/pytest/__init__.py | 0 tests/pytest/test_token_handler.py | 106 ++++++++++++++++ tests/pytest/util/config_handler.py | 13 ++ wsgi.py | 70 ++++++++++ 20 files changed, 740 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 api/__init__.py create mode 100644 api/app.py create mode 100644 api/errors.py create mode 100644 api/models/token.py create mode 100644 api/token.py create mode 100644 api/util/db_handler.py create mode 100644 api/util/token_handler.py create mode 100755 bin/run.sh create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/config/as.yml create mode 100644 tests/pytest/__init__.py create mode 100644 tests/pytest/test_token_handler.py create mode 100644 tests/pytest/util/config_handler.py create mode 100644 wsgi.py diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index dd19991..803582d 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -14,9 +14,32 @@ env: jobs: - prerelease: + test: + name: Tests runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run pytest + run: | + pytest + + prerelease: + runs-on: ubuntu-latest + needs: ["test"] + steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ff431bc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Test + +on: + push + +jobs: + + test: + name: Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run pytest + run: | + pytest diff --git a/Dockerfile b/Dockerfile index 5e1cc28..bd2a67c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,14 @@ -FROM node:10 +FROM python:3.7-alpine -# User -RUN groupadd --gid 5000 aservice \ - && useradd --home-dir /home/aservice --create-home --uid 5000 \ - --gid 5000 --shell /bin/sh --skel /dev/null aservice -COPY . /home/aservice -USER aservice -WORKDIR /home/aservice +ENV AS_PORT=8080 -# npm -RUN npm install +RUN apk update && \ + apk add gcc build-base libc-dev libffi-dev openssl-dev bash curl -# Start -EXPOSE 7000 -CMD [ "npm", "start" ] +WORKDIR /var/aservice +COPY ./ ./ +RUN pip install --no-cache-dir -r requirements.txt + +HEALTHCHECK CMD curl --fail http://localhost:${AS_PORT}/health || exit 1 +EXPOSE $AS_PORT +CMD [ "./bin/run.sh" ] diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..c07c459 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1 @@ +from .app import app diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000..52c6b9a --- /dev/null +++ b/api/app.py @@ -0,0 +1,37 @@ +from flask import Flask, Response, jsonify, request + +from .errors import errors +#from .versions import versions +#from .trusted_list import trusted_list +#from .parties import parties +from .token import token_endpoint +#from .trusted_issuer import trusted_issuer + +app = Flask(__name__) + +# Register error handler +app.register_blueprint(errors) + +# Register routes +#app.register_blueprint(versions) +#app.register_blueprint(trusted_list) +#app.register_blueprint(parties) +app.register_blueprint(token_endpoint) +#app.register_blueprint(trusted_issuer) + +# Register health endpoint +@app.route("/health") +def health(): + + # TEST + #from api.models.token import Token + #token = Token( + # eori="EU.EORI.DEABC", + # access_token="dgagaggdgagg", + # expires=3600) + #from flask import current_app + #db = current_app.config['db'] + #db.session.add(token) # Adds new User record to database + #db.session.commit() # Commits all changes + + return Response("OK", status=200) diff --git a/api/errors.py b/api/errors.py new file mode 100644 index 0000000..486ed37 --- /dev/null +++ b/api/errors.py @@ -0,0 +1,22 @@ +from flask import Blueprint, Response, jsonify +from werkzeug.exceptions import HTTPException, InternalServerError + +# Blueprint +errors = Blueprint("errors", __name__) + +# Handler for HTTP exceptions +@errors.app_errorhandler(HTTPException) +def http_error(error): + return { + 'code': error.code, + 'message': error.name, + 'description': error.description, + }, error.code + +# Handler for any other kind of exceptions +@errors.app_errorhandler(Exception) +def server_error(error): + return { + 'code': 500, + 'message': 'Internal server error: {}'.format(error), + }, 500 diff --git a/api/models/token.py b/api/models/token.py new file mode 100644 index 0000000..c9312f8 --- /dev/null +++ b/api/models/token.py @@ -0,0 +1,11 @@ +from flask import current_app +db = current_app.config['db'] + +class Token(db.Model): + id = db.Column(db.Integer, primary_key=True) + eori = db.Column(db.String(150), nullable=False) + access_token = db.Column(db.Text, nullable=False, unique=True) + expires = db.Column(db.Integer, nullable=False) + + def __repr__(self): + return 'eori={}, access_token={}, expires={}'.format(self.eori, self.access_token[:20]+"...", self.expires) diff --git a/api/token.py b/api/token.py new file mode 100644 index 0000000..100e33d --- /dev/null +++ b/api/token.py @@ -0,0 +1,49 @@ +from flask import Blueprint, Response, current_app, abort, request +from api.util.token_handler import forward_token +import time + +# Blueprint +token_endpoint = Blueprint("token_endpoint", __name__) + +# POST /token +@token_endpoint.route("/token", methods = ['POST']) +def index(): + current_app.logger.debug('Received request at /token endpoint') + + # Load config + conf = current_app.config['as'] + + # Forward token + response = forward_token(request, current_app, abort) + auth_data = response.json() + if (not 'access_token' in auth_data) or (not 'expires_in' in auth_data): + app.logger.debug("Invalid response from AR: {}".format(auth_data)) + abort(400, description="Received invalid response from AR") + return None + + # Build Token object and return + from api.models.token import Token + client_id = request.form.get('client_id') + ar_token = Token( + eori=client_id, + access_token=auth_data['access_token'], + expires=int(time.time() * 1000) + (1000*auth_data['expires_in'])) + current_app.logger.debug('Received access token with data: {}'.format(ar_token)) + + # Insert token + from api.util.db_handler import insert_token + insert_error = insert_token(ar_token, current_app) + if insert_error: + current_app.logger.debug("Error when inserting token into DB: {}".format(insert_error)) + abort(500, description="Internal server error (DB access)") + + # TEST + #from api.util.db_handler import get_token_by_eori + #t = get_token_by_eori("EU.EORI.DEMARKETPLACE", current_app) + #current_app.logger.info("Get by EORI: {}".format(t)) + + return auth_data, 200 + + + + diff --git a/api/util/db_handler.py b/api/util/db_handler.py new file mode 100644 index 0000000..49e5f6a --- /dev/null +++ b/api/util/db_handler.py @@ -0,0 +1,67 @@ +from api.models.token import Token +import time + +# Insert token +def insert_token(token, app): + app.logger.debug("Inserting token: {}".format(token)) + db = app.config['db'] + try: + db.session.add(token) + db.session.commit() + return None + except Exception as error: + db.session.rollback() + app.logger.error("Error inserting token: {}".format(error)) + return "Error inserting token" + +# Get token by EORI +def get_token_by_eori(eori, app): + app.logger.debug("Get token by EORI: {}".format(eori)) + db = app.config['db'] + + # First clean entries + clean_err = clean_token(app) + if clean_err: + return None + + # Perform query + try: + token = Token.query.filter_by(eori=eori).first() + return token + except Exception as error: + db.session.rollback() + app.logger.error("Error retrieving token: {}".format(error)) + return None + +# Get token by access_token +def get_token_by_token(token, app): + app.logger.debug("Get token by access_token: {}".format(token[:50])) + db = app.config['db'] + + # First clean entries + clean_err = clean_token(app) + if clean_err: + return None + + # Perform query + try: + r_token = Token.query.filter_by(access_token=token).first() + return r_token + except Exception as error: + db.session.rollback() + app.logger.error("Error retrieving token: {}".format(error)) + return None + +# Clean expired tokens +def clean_token(app): + app.logger.debug("Removing expired tokens...") + db = app.config['db'] + try: + deleted = Token.query.filter(Token.expires -# Configuration of express web server -express: - # Port (Internal port of the express web server) - port: 7000 - -# Configuration of SQLite database +# Configuration of database db: - # Source (":memory:" or file e.g. "db.sqlite" for persistence) - source: ":memory:" + # Use sqlite file database + useFile: "as.db" + # Use URI to external DB (e.g., MySQL, PostgreSQL) + #useURI: "" + # Enable tracking of modifications + modTracking: false + # Enable SQL logging to stderr + echo: true -# Configuration of authorisation registry +# Configuration of iSHARE authorisation registry ar: # Endpoint for token request token: "https://ar.packetdelivery.net/connect/token" @@ -29,5 +30,4 @@ ar: delegation: "https://ar.packetdelivery.net/delegation" # EORI of AR id: "EU.EORI.NLPACKETDEL" - # Disable SSL verification - rejectUnauthorized: false + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5e0a637 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +minversion = 7.1.2 +log_cli = True +addopts = --testdox +testpaths = tests/pytest +markers = + ok: marks tests for successful behaviour + failure: marks failure tests diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..82d1dec --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +Flask==2.2.0 +Flask-SQLAlchemy==3.0.3 +gunicorn==20.1.0 +requests==2.31.0 +PyJWT==2.4.0 +PyYAML==6.0 +Werkzeug==2.2.1 +pytest==7.1.2 +pytest-testdox==3.0.1 +pytest-mock==3.10.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/config/as.yml b/tests/config/as.yml new file mode 100644 index 0000000..2fe9349 --- /dev/null +++ b/tests/config/as.yml @@ -0,0 +1,190 @@ +# Configuration for activation-service application + +# Client configuration +client: + # Client ID + id: "EU.EORI.DEPROVIDER" + # Client key (PEM private key) + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIJKQIBAAKCAgEA48v3//08EA1QzSje+AYjRN1MwoJOlwrk8xOgLz9MZmSYtPv2 + cPbhRuaguznho3FvIG4kx2rbu6ohAVM7l1QC7X1dtUqWfVGd35dyL/4+bGbfSSqi + bg/UlEDe3DkleKyTd2mGnIChUYc2HLImmXVq6nS+P+BBfFahPUHIoWTG1jUU6ZYr + uNA1r6/z3SG+/Q5xVu6VHtmrqyCPtc+bJC136p975oH0ty/OtwRBL+ediQ1f6lih + I11cYAchGX7+49y0oPuUnNw7jOSVEXrVvc5BaE9KkhjoG3P3ojRWhDq3m9vfmb1C + gMefpssC0ls/5yNHE1b4QAzVKDaHn0aH5vrV0zbYn0onyjnQMauf1tM4BqZxl16T + t6zyTkWf6logEOBhxOUHMVrV5IeuD6ULLRZzqJmPvfOrP02tN+VbmLmEIe3mN0xX + bDhUL+/UDEwHV9RFjMxd+uSKCVFzLgkcp1XFIOM6+A+VAtfMci4ETA8G7i54Suz1 + w/ARFU6F2sJGNESn7dUVY3mV7yI0YS8hjsTp31+0stQfcb2Xx8WHivnnLZ/tRCpT + 57CC8XLbzx6knExeO/7bIPfYIerEHQY0iVxEfyEIEqLMlidUqm/TN5yhf4N2FiUE + jmwzx1yW30B0DynFqo0OuGOd3/o2+0sdNvuC1sV+8Q6N3VLVP0qBL8u6dfECAwEA + AQKCAgBuYQtOk1pjPNCGWOORswer+r+TEKkcLz85Oa1It5dBlkUYjW6g1j/apy4D + Csz28aQzRquzpWfLy2gqyDX74c15XmHl0rqRBtdE3JGMB9oflMllHq+OPUV4gOZ8 + N4ScsKLUHeIIO5vvcWEbDof9nBOuf9sgAH46zY9bq2CYM8jVSutTNF0DNICPHOdB + o3R0EmeBBCJlzHuOdDyukGZQZWfR3G2hCB+YFCZKMFmV6NlA66YqW7/Y7wgvz8SO + IidIKk2sI7ujNmP0pV5GFgsz6Zlv/dvYZxExHERF6K5zbDBD6YqzaC2tUQ1fOMep + PmX8je+Gw4GPJ6ixz5pv5zE7DxhEUcXhLD4kImhP5P5QEoBg7ORecVhBEWhBzGLD + 89hGNJqXqgvRzh++FaoxuDWriPe8Sd4eXQIePv+gz+qtnCpV9hfOd7+LPurxZH8o + Ji8Cwrx6KsvMGbXd+aZQX+eybSKqupbYJmRwIThkFZMUvfnowe2p45fWfK3F1zER + B3jJhQ/2OipG1N5ouNCZGhiGSBvUiC5kVkxPWXKXoqpWq5cYpPPTfqbgnmstWRuo + 3Gf2wmvSKkGkLMnTMnOPg1mbUmSZW6JKt6KyigOSxSrpJpo85MslbgQ+/ra7PN92 + 7TLDGHYnSLWhB/AUYvUCDjT1k59cKqexqGSDU4nLDr0eP50fAQKCAQEA//yuqlNs + tphRiGW16e7zvGdM0H5RzU2LTAAGxJCw/ohIDaKUpou187vK/rzf7yls+qCiPKfD + oq+iuFqbutafYLv9dIYt4yIUIcRMMmIlru2mLgCm/uNmAyey/cgkrPllAYJBM5bW + T+bLchnTM2oefmnRVdHoW4kv913uoBfC3dFNRkFLaUZE+nUuW0tjdPo0QauRyM7s + 4Qp7RhPD0Fy7EARyMh4QOmTunEzPQO51lHwybiwa5loVd8KzoZNyZa7Y99TLr03+ + I5Bu77B8JsyMKHQ1GXBnkLjzUm30TT3I9iQDlT5yyqZsb8IeNpGG+lnXmfsKUUs1 + AxnjSElVvsXR6QKCAQEA487rzXaG2OyYWQcTIr50F9Gj/cL1QfNZ6bDIegUvN1Oy + ho6IcCyL2phGXxylERQU+jPjCJ/AqkQbIPryK2c6o8FLGSGburAnvYwV1MZeiMu4 + g0tgK6xQ0yVAwhEhRSWzdJa8wBhvGu8e/szyzaDy7wujiDqCg084NF8X4weDo1HY + KjehBNJYKQasFR8S9iWLznYl8KRYUNjf2fNU9s3GZqnGoZxr8XWXv00a87qTXh8z + ++ccpzGgQxkg+kX5O4bh6SFtHV7YqXYHpKgdYPlDdkl6r/ynCwdOzYxNGeS41481 + qkmAmEfOMReJLs0+wJiqed+z60o8zaSa6oQa9KW2yQKCAQEA7hz71+Gr8Rk0VhzO + HyEvRWQNVq75pm/oD7TClttEWC8qXsyJW3X/tQ1M4LGN16h//42l+6fN2aloQfW+ + gAgdzlxvgZFCY9GSSmqOuhsDlHjoqEfhDp01id/GpjwiqWd+pe2kerlm9oHnYmZV + R7EBVnNVzm9npKWyoXWVfwM7Nxv2tlAMNouvpA2WJkO06t3F+AQAonqgayBV1LST + 42AufNK16pp+W+MA9RCZhHuLkagJPOP3zaej6neIodZGhgEjPzyIjrOMn0Y9euV+ + RpTkFskpj1U4cK0pYNZ4ddTv9s0/K/cfzhMKMNavRfEOThd97nROf5nmYNEW0mms + wby+6QKCAQAz/3QlVsua0g5IK+w7PvuiwDBNaOWwYwp/4+EusOZBG7KhMgGEkTZu + 89kENraaulm7boKA4m4Irzj+AymprIte+2zX1KsGJtU7V7FX+ttzIAUCuv69mTxt + pZAte9l7PgrDLvDwa+NYY4JQqJk3RGiPFboDC0/lBv0OPzJlmL58Vd8ga8guIJEM + 9D/tJkWet508yA0K64hZE9esmPUozHlfz5rOIrUkzTGQ8dmYDls63aZw9iS9KP+q + sR6s4bHs23HPU9jjHlYYAB/ofpMOInc6lbUSXHoIc2eSVVb39RQX59FZCCP5HlK7 + M+MVMrYDIhWvcddQeoZ8bfkKVJMOipC5AoIBAQDWpSN/7bqxZAavYd9ar2pRez9l + 7Wvwnx10zuvmHEcOPwZqk0p8UGj9ROevbTDaguZ/3Y2hXxWx2Uwk7FiOKq35z7Zp + oG9PmrliC8nJet71po5+ccrE+U3KWsqlNqSAilYAfOMzihK0DLYfMLjY6hX+65SP + c/pq02T+5nXgXFIcAQsKi11iNY3N4nr46GwG9rO3nzmeDUeIgDJgoUU9eROsRKDW + h/ietpIKHxRHBPrPihLOsZV8umfxL4Xo0Y0RhYgjG3N3jLoLwgCHaOy6HagTWcxL + Tx8bGBGgpxpxd1YWxZld5qKMa/x7FLJcRn6Xxk1L6gOgYhMIQiCpbgOD8/Qh + -----END RSA PRIVATE KEY----- + # Client certificate (PEM certificate chain) + crt: | + -----BEGIN CERTIFICATE----- + MIIGWTCCBEGgAwIBAgIJALTnY4ETe5dPMA0GCSqGSIb3DQEBCwUAMGoxCzAJBgNV + BAYTAkRFMQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAoTBkZJV0FSRTEWMBQGA1UE + AxQNRklXQVJFLUNBX1RMUzEhMB8GCSqGSIb3DQEJARYScm9vdC1jYUBmaXdhcmUu + b3JnMB4XDTIzMDEzMTEyMjg1MFoXDTI4MDEzMDEyMjg1MFowgaUxCzAJBgNVBAYT + AkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEZMBcGA1UECgwQ + U2VydmljZSBQcm92aWRlcjEZMBcGA1UEAwwQU2VydmljZS1Qcm92aWRlcjEhMB8G + CSqGSIb3DQEJARYSYWRtaW5AcHJvdmlkZXIuY29tMRswGQYDVQQFExJFVS5FT1JJ + LkRFUFJPVklERVIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDjy/f/ + /TwQDVDNKN74BiNE3UzCgk6XCuTzE6AvP0xmZJi0+/Zw9uFG5qC7OeGjcW8gbiTH + atu7qiEBUzuXVALtfV21SpZ9UZ3fl3Iv/j5sZt9JKqJuD9SUQN7cOSV4rJN3aYac + gKFRhzYcsiaZdWrqdL4/4EF8VqE9QcihZMbWNRTpliu40DWvr/PdIb79DnFW7pUe + 2aurII+1z5skLXfqn3vmgfS3L863BEEv552JDV/qWKEjXVxgByEZfv7j3LSg+5Sc + 3DuM5JURetW9zkFoT0qSGOgbc/eiNFaEOreb29+ZvUKAx5+mywLSWz/nI0cTVvhA + DNUoNoefRofm+tXTNtifSifKOdAxq5/W0zgGpnGXXpO3rPJORZ/qWiAQ4GHE5Qcx + WtXkh64PpQstFnOomY+986s/Ta035VuYuYQh7eY3TFdsOFQv79QMTAdX1EWMzF36 + 5IoJUXMuCRynVcUg4zr4D5UC18xyLgRMDwbuLnhK7PXD8BEVToXawkY0RKft1RVj + eZXvIjRhLyGOxOnfX7Sy1B9xvZfHxYeK+ectn+1EKlPnsILxctvPHqScTF47/tsg + 99gh6sQdBjSJXER/IQgSosyWJ1Sqb9M3nKF/g3YWJQSObDPHXJbfQHQPKcWqjQ64 + Y53f+jb7Sx02+4LWxX7xDo3dUtU/SoEvy7p18QIDAQABo4HFMIHCMAkGA1UdEwQC + MAAwEQYJYIZIAYb4QgEBBAQDAgWgMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdl + bmVyYXRlZCBDbGllbnQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFEprqbFVWgRy56dH + SQnOXSM+OymvMB8GA1UdIwQYMBaAFCQf4QbGYYhkHdTU0TZScDAF22yDMA4GA1Ud + DwEB/wQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwDQYJKoZI + hvcNAQELBQADggIBAFRorLe2/c3cv8wVlNR6ARhlkJ3FiTbzRtnp1CJgzQXiyifv + /Ss6rTodYNfQ5KRZsU+JzPEvNbgMIdMRiHPClU9EmRZZUCxW0jlxHaJE1LFavbD/ + NXen0xMssgZSlOcS79jvdWRL/cwattd0QiPaPiF7F9L8y0deJHxsyMRmu1emLTJt + yOgorvrOYsU2NAjkYAkVgzbBp1VdBAm3VyGvnzIyA7Bownv3ZvqM8mKSNADQZ6cy + UfQmbVt5IquNpJn9dPT4DoTecWQeUYoDXRE/r5PKyvMpXeeJ60l5rFgV3dI6RYmw + y4xCq7eUxfFO1zGCiKYuVHuEmcqaQm4wtCzHMdVp+8D8Bmv8usouwJQkHfj9x4L0 + AQkV0+rHiTZmUxpbqpwUE0ezxaMZ3q9HhmA1tiKL1crbid0hBIWKguOj/kBAX6mx + Dk0GnC01ykI2hUcpnznxVWt82kppJbPOPuXTGRvq0QeVhJULNpTDWg23IUVu230h + A4xctgqrG8Y8V3Zt9pOXWVG2OC63S/QOPMbJVJiH/aMbZ/UiLm9FWDJ5KxsvG9t8 + QXVjlUE2Whj0cbseqQfEryj4qo3DKKo8B3oXdNLs6dN3j2jTOLmQp4Ys0OaIGT0E + SsuDrHI4SKKSGqzGG4zkhcvw3T3FLjAmjCptFpMmLMoftGD/7+hmKb6Mvf3F + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIFwjCCA6qgAwIBAgIBATANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJERTEP + MA0GA1UECBMGQmVybGluMQ8wDQYDVQQHEwZCZXJsaW4xDzANBgNVBAoTBkZJV0FS + RTESMBAGA1UEAxMJRklXQVJFLUNBMSEwHwYJKoZIhvcNAQkBFhJyb290LWNhQGZp + d2FyZS5vcmcwHhcNMjIwOTIxMTIwNDUzWhcNMjkxMjIzMTIwNDUzWjBqMQswCQYD + VQQGEwJERTEPMA0GA1UECBMGQmVybGluMQ8wDQYDVQQKEwZGSVdBUkUxFjAUBgNV + BAMUDUZJV0FSRS1DQV9UTFMxITAfBgkqhkiG9w0BCQEWEnJvb3QtY2FAZml3YXJl + Lm9yZzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANjtrG/NDN2PtA7l + 1fU9U42abgOyMryMRoT3DB/SlOnGcg8JeWEyha4t8znZMduwP7lMUq281rS3pEZf + R3lUBfXhC5cBUjQDLKlYdVWTVJTTqhhMLwyNrerd4RIVkx5vMgEfObFkJOjCxDR2 + +bFEHLyXwUaFFz8EAUj8kLsphHIuXv3NLSWiKWH2xYVOx1DKxwAZ+dqTueGyeOvi + QvO5r78uOG9NjwXw46boREKNSleDXpQMEG94gzgZygjdQ1tR3a/0K8QMlgBI12NO + pa6XL+an5l5iSzB/oYzZguAX0HGumMUvr4zhKiPzgiKyRrFf1mOjEUpgUV9rDH35 + MNv5cjgjFR1CdvPgveZc7zjZWT4786U8NZ6Wa/jRfiPUISykUps3zfsOq2jWNqSz + u+LSGcoXRcaL0bIjuScQkVaH3BFua4SuU75bifMaZqol/xVA4CONqXkFkihyuqw2 + eu86045w+XiPpZ1NYa+wdE9pR6RBNgb9vG8Po+43TLe0phxbB8eO5Xokpq2Qe9Hx + NbrF6sI03n/9VQXmIRcaLvtQ+k2a0sPcmMi0s1b5GCqmf13OlWRxTqPWLuKy8i8c + yiq2q/XVNpZ5UdIf47rkkI8PGLKe6Nzh/uhd0ZCn0t5Hu8nRS7hi/XCfJ+he6QRg + KCGSXITG0IwjA50gyaFo6c/9VRePAgMBAAGjZjBkMB0GA1UdDgQWBBQkH+EGxmGI + ZB3U1NE2UnAwBdtsgzAfBgNVHSMEGDAWgBR/813uZ43zu/7iXBl2WsWTuSlmDjAS + BgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF + AAOCAgEAVgpKfvpk9uyb16VIWs3uksYzgwJyjGqpJ0j9FmTrg4HyrzfXLKlDX09W + 6RaEZ8dVGPugNsLTA/WXWsejWjaj8Ygk8sy5OLQ/qVO9MFEK5muco2O4Au8GjrOM + x+Y65DMUByHPHPhUhBapUzSz4ho4rpqabHYc0FIq5tmSMweNUG9rcPRMx9//anOc + 4fkw9Rkvl0oMXHBqDeluLVgfN6xuJX8pqZidgvj6P3Zg7dJqj+1RSNuVdfWwDTug + 25p/VAZqKeFY1UrdbRmREQ22tjgw9eH0+8fv4hK5gWfR8U1qNOIBwRAQasUcOs4T + tot/QakAb/aP753p2wg2prm/pByB2S3uTcuayj6Z8OUYKQwnF6pLa9HwKOObTD6K + kRP2OlcPQwwNXzZiHs3eiVgAAoyCSc1I1PIKA64iAJXHsFsCHJzBBStrZLdL1Xcx + GeurZHg7nulEcMZ32JEShxqPsH/YqorO2RzP4XhsUMh7mEHCLMCZHphzjip5kaoi + y1Q7+Q1/P0kf7yUTCLudp+20Vm5kFcy9zLLggl7EUkru6djv5q2cbPSz0J/NnPjR + GOFMlCrJ/DjWNJN0Ss/k3rCuhQbzZGyNw1vQCc602VNpJT5N7N9H7tKaZlE89G1k + oU/RLlGTDpD/RxBX2s9oFiO4yIX5+R2nfKW36uC9f4hiYT6sRvE= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIGUTCCBDmgAwIBAgIJAOA3HGewsq2PMA0GCSqGSIb3DQEBCwUAMHcxCzAJBgNV + BAYTAkRFMQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAcTBkJlcmxpbjEPMA0GA1UE + ChMGRklXQVJFMRIwEAYDVQQDEwlGSVdBUkUtQ0ExITAfBgkqhkiG9w0BCQEWEnJv + b3QtY2FAZml3YXJlLm9yZzAeFw0yMjA5MjExMTU5MjNaFw0zMjA5MTgxMTU5MjNa + MHcxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAcTBkJlcmxp + bjEPMA0GA1UEChMGRklXQVJFMRIwEAYDVQQDEwlGSVdBUkUtQ0ExITAfBgkqhkiG + 9w0BCQEWEnJvb3QtY2FAZml3YXJlLm9yZzCCAiIwDQYJKoZIhvcNAQEBBQADggIP + ADCCAgoCggIBAJvAjZJxXckYB11eSRHpQKSoHwXaBmp4S6Tn5JZ6mUy4Z7c0oINg + mcajIXZwXEU3W+rxd40OMVB2CcoYScxsTF7nrN1LIuSnYYL02yq3pqWe1JpZlAA5 + qakI1x6kx3duj8YocmdZlLDSZt699lN6+7rMdahm0Vcy9Ir7sE23oNMDgzMhTCkq + OzDfElDYRnpuH6eU3tcToAqkBAQ0qVcfmudsTuGpZ4JrrWgacaB9Ef0hENM5IAK3 + 7/rpfYDiKr+5j2VMfuUJ2WlhkiHUNt1Y9UfTrW6qLvnXjM8LjhHrzA2nB6zt9nVW + iq4K36Ci/nsEAgiJj31mvhufmqBU2Q4SXOWVf73v7QM1ObHps5XuCav1Nh/gKcRe + Qj1a/nFz+IhENVHlmdPUI2f2/5j/R11oaKeOcLIOeMdt9xzAtCaBYwlhwmgQF2/l + 8dbOugcE2PsWS01rPT735hQ+iJNNWM2791ufhPC/dEQpTP36L4JxUeQgUJfmDZCX + NEZOFEek8gwmJTySIbnE43oHWMixHtAsUFDa+TYDZtkGWGsQFTsRfh79DTcniJcu + Gm9rYsYCo54870rkzJwDc73LBIL+5mnUUd1utoI94x72FO1bm4nIKCbSUylrwCaX + 4F5K8bSyfZFUVhW1u/nVo7k5cbwviESYnfFrlO9kZIdbCkLIm5vp0RMzAgMBAAGj + gd8wgdwwHQYDVR0OBBYEFH/zXe5njfO7/uJcGXZaxZO5KWYOMIGpBgNVHSMEgaEw + gZ6AFH/zXe5njfO7/uJcGXZaxZO5KWYOoXukeTB3MQswCQYDVQQGEwJERTEPMA0G + A1UECBMGQmVybGluMQ8wDQYDVQQHEwZCZXJsaW4xDzANBgNVBAoTBkZJV0FSRTES + MBAGA1UEAxMJRklXQVJFLUNBMSEwHwYJKoZIhvcNAQkBFhJyb290LWNhQGZpd2Fy + ZS5vcmeCCQDgNxxnsLKtjzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA + A4ICAQCQiR/V4p9VDv2/61apgsUWxyRXa4HXPpuxsTWL/8Z2ztgHiHClmHt48q59 + rn3gGbrXj1sMapCawfN7n7gCJAzCOcwZrNdSiHjBkEOgBSTKoiTfMCcrhLaQP6z6 + 6pY+ZJlL5BbhkMd95gAua0VL7W3zt4nDvYB1trzoDlIZPuAYVrAm7xB4CuZVdJDW + 7QpNykH6Er58FiRFOMHP+KvPJm3nZVq6hcQ78XQf6Dco55PsZspXmBLMlTZmKKr2 + rpHHdg9ewayQqkCYiRm+Yr6G1tfkKCehKYdz3ORWVfJ+NrwOEQbRfPuDG2YdQo4+ + R7sXeYMJIEzHHXvO59yiZqPuKok839d7kkrZSqkEZXQSvSolXhqNWH3p7IuOX1/P + ph1jPFZT+RrORbInUwoScPWsK8yh+mYo9h/+QbA/vZeve0/ExrFCUj2BwHa5scnm + 1uororcEuRkFJjDMX4tJOGv9t/C3+kORSKJSgmPmSZ2XM/jLCIhfDWxlpwFNSJJQ + F69uygJdFZNlo1jl6fG5lrwWNhArnI49WLQrEmjwG/1zUQz5EiDt8GTKFGNBJNxV + kb2CZS4H8+GB2UK1nsk+Fv9Joc4CFp6LOvXSJ9m8O/9GIfqOwbl12ldN9/9Oyia5 + gty2Gjmr0kunFqqelUzoqMc7Jh/8EPPjVpK3gJUdZM4JlSHZjA== + -----END CERTIFICATE----- + + +# Configuration of SQLite database +db: + # Use sqlite file database + useFile: "as.db" + # Use URI to external DB (e.g., MySQL, PostgreSQL) + #useURI: "" + # Enable tracking of modifications + modTracking: false + # Enable SQL logging to stderr + echo: false + +# Configuration of authorisation registry +ar: + # Endpoint for token request + token: "https://ar.provider.com/token" + # Endpoint for create policy requests + policy: "https://ar.provider.com/ar/policy" + # Endpoint for delegation requests + delegation: "https://ar.provider.com/ar/delegation" + # EORI of AR + id: "EU.EORI.DEPROVIDER" + diff --git a/tests/pytest/__init__.py b/tests/pytest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/test_token_handler.py b/tests/pytest/test_token_handler.py new file mode 100644 index 0000000..5089d3e --- /dev/null +++ b/tests/pytest/test_token_handler.py @@ -0,0 +1,106 @@ +import pytest +#import flask +from api import app +from tests.pytest.util.config_handler import load_config +from api.util.token_handler import forward_token + +# Get AS config +as_config = load_config("tests/config/as.yml", app) +app.config['as'] = as_config + +# Dummy access token +ACCESS_TOKEN = 'gfgarhgrfha' + +# Form parameters +REQ_FORM = { + 'client_id': 'EU.EORI.DEMARKETPLACE', + 'grant_type': 'client_credentials', + 'scope': 'iSHARE', + 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion': 'dfggrghaerhahahhahp' +} + +@pytest.fixture +def mock_request_ok(mocker): + def form_get(attr): + if attr == "client_id": return REQ_FORM['client_id'] + elif attr == "grant_type": return REQ_FORM['grant_type'] + elif attr == "scope": return REQ_FORM['scope'] + elif attr == "client_assertion_type": return REQ_FORM['client_assertion_type'] + elif attr == "client_assertion": return REQ_FORM['client_assertion'] + request = mocker.Mock() + request.form.get.side_effect = form_get + return request + +@pytest.fixture +def mock_request_missing_attr(mocker): + def form_get(attr): + raise Exception("Missing attribute") + request = mocker.Mock() + request.form.get.side_effect = form_get + return request + +@pytest.fixture +def mock_request_missing_client_id(mocker): + def form_get(attr): + if attr == "client_id": raise Exception("Missing client_id") + elif attr == "grant_type": return REQ_FORM['grant_type'] + elif attr == "scope": return REQ_FORM['scope'] + elif attr == "client_assertion_type": return REQ_FORM['client_assertion_type'] + elif attr == "client_assertion": return REQ_FORM['client_assertion'] + request = mocker.Mock() + request.form.get.side_effect = form_get + return request + +@pytest.fixture +def mock_proxy_request_ok(mocker): + ar_response = { + 'access_token': ACCESS_TOKEN, + 'expires_in': 3600 + } + return mocker.patch('api.util.token_handler.proxy_request', return_value=ar_response) + +# Test: Successful token +@pytest.mark.ok +@pytest.mark.it('should successfully forward token request') +def test_forward_token_ok(mocker, mock_request_ok, mock_proxy_request_ok): + + # Mock abort function + abort = mocker.Mock() + + # Call function + response = forward_token(mock_request_ok, app, abort) + + # Asserts + mock_proxy_request_ok.assert_called_once() + assert 'access_token' in response, 'Response should contain access_token' + assert response['access_token'] == ACCESS_TOKEN + assert response['expires_in'] == 3600 + +# Test: Missing attr +@pytest.mark.failure +@pytest.mark.it('should fail due to missing attr') +def test_forward_token_missing_attr(mocker, mock_request_missing_attr, mock_proxy_request_ok): + + # Mock abort function + abort = mocker.Mock() + + # Call function + response = forward_token(mock_request_missing_attr, app, abort) + + # Asserts + assert response == None, "should return empty response" + +# Test: Missing client_id +@pytest.mark.failure +@pytest.mark.it('should fail due to missing client_id') +def test_forward_token_missing_client_id(mocker, mock_request_missing_client_id, mock_proxy_request_ok): + + # Mock abort function + abort = mocker.Mock() + + # Call function + response = forward_token(mock_request_missing_client_id, app, abort) + + # Asserts + assert response == None, "should return empty response" diff --git a/tests/pytest/util/config_handler.py b/tests/pytest/util/config_handler.py new file mode 100644 index 0000000..ec77cdc --- /dev/null +++ b/tests/pytest/util/config_handler.py @@ -0,0 +1,13 @@ +import yaml, sys + +def load_config(filename, app): + try: + with open(filename, "r") as stream: + conf = yaml.safe_load(stream) + return conf + except yaml.YAMLError as exc: + app.logger.error('Error loading YAML: {}'.format(exc)) + sys.exit(4) + except FileNotFoundError as fnfe: + app.logger.error('Could not load config file: {}'.format(fnfe)) + sys.exit(4) diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..458b58a --- /dev/null +++ b/wsgi.py @@ -0,0 +1,70 @@ +from api import app +from flask_sqlalchemy import SQLAlchemy +import logging, os +import yaml, sys + +# Port +port = int(os.environ.get("AS_PORT", 8080)) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=port) +else: + # Running inside gunicorn, set logger + gunicorn_logger = logging.getLogger('gunicorn.error') + app.logger.handlers = gunicorn_logger.handlers + app.logger.setLevel(gunicorn_logger.level) + app.logger.info("Setting gunicorn logger...") + +# Load config +app.logger.info("Loading config from " + "config/as.yml") +try: + with open("config/as.yml", "r") as stream: + conf = yaml.safe_load(stream) + app.logger.debug("... config loaded") + app.config['as'] = conf +except yaml.YAMLError as exc: + app.logger.error('Error loading YAML: {}'.format(exc)) + sys.exit(4) +except FileNotFoundError as fnfe: + app.logger.error('Could not load config file: {}'.format(fnfe)) + sys.exit(4) + +# Create database +app.logger.info("Creating database...") +conf = app.config['as'] +if 'db' not in conf: + app.logger.error('No database configuration in config file') + sys.exit(4) +db_conf = conf['db'] + +if os.environ.get('AS_DATABASE_URI'): + app.logger.info("... taking URI from ENV...") + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('AS_DATABASE_URI') +elif 'useFile' in db_conf and db_conf['useFile'] and len(db_conf['useFile']) > 0: + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']) + app.logger.info("... using file-based SQLite (" + dbpath + ") ...") + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath +elif 'useURI' in db_conf and db_conf['useURI'] and len(db_conf['useURI']) > 0: + app.logger.info("... using specified URI '" + db_conf['useURI'] + "' ...") + app.config['SQLALCHEMY_DATABASE_URI'] = db_conf['useURI'] + +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +if db_conf['modTracking']: + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True + +app.config['SQLALCHEMY_ECHO'] = False +if db_conf['echo']: + app.config['SQLALCHEMY_ECHO'] = True + +db = SQLAlchemy(app) +app.config['db'] = db +app.logger.info("... database created!") + +with app.app_context(): + app.logger.info("Creating database tables...") + from api.models import token + db.drop_all() # TODO: Make configurable + db.create_all() + app.logger.info("... database tables created") + From e4a2df7439c3d2ae023f5a115f13f4e65642d9e4 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Thu, 25 May 2023 12:56:44 +0200 Subject: [PATCH 03/16] Add more tests for token endpoint --- api/token.py | 9 +-- api/util/token_handler.py | 17 +++++- requirements.txt | 1 + tests/pytest/test_token.py | 95 ++++++++++++++++++++++++++++++ tests/pytest/test_token_handler.py | 4 +- 5 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 tests/pytest/test_token.py diff --git a/api/token.py b/api/token.py index 100e33d..d6443a8 100644 --- a/api/token.py +++ b/api/token.py @@ -12,7 +12,7 @@ def index(): # Load config conf = current_app.config['as'] - + # Forward token response = forward_token(request, current_app, abort) auth_data = response.json() @@ -21,7 +21,7 @@ def index(): abort(400, description="Received invalid response from AR") return None - # Build Token object and return + # Build Token object from api.models.token import Token client_id = request.form.get('client_id') ar_token = Token( @@ -41,9 +41,6 @@ def index(): #from api.util.db_handler import get_token_by_eori #t = get_token_by_eori("EU.EORI.DEMARKETPLACE", current_app) #current_app.logger.info("Get by EORI: {}".format(t)) - - return auth_data, 200 - - + return auth_data, 200 diff --git a/api/util/token_handler.py b/api/util/token_handler.py index 51c04a8..7893741 100644 --- a/api/util/token_handler.py +++ b/api/util/token_handler.py @@ -7,6 +7,7 @@ def proxy_request(url, form_data, headers, app): app.logger.debug("Proxy request to AR...") app.logger.debug("...sending request to {}".format(url)) app.logger.debug("...form parameters: {}".format(form_data)) + response = requests.post(url, data=form_data, headers=headers) try: response.raise_for_status() @@ -20,7 +21,7 @@ def proxy_request(url, form_data, headers, app): abort(400, description=message) return None app.logger.debug('...received response') - + # Return response return response @@ -30,7 +31,7 @@ def forward_token(request, app, abort): # Load config conf = app.config['as'] - + # Check for form parameters client_id = "" grant_type = "" @@ -39,15 +40,25 @@ def forward_token(request, app, abort): client_assertion = "" try: client_id = request.form.get('client_id') + if not client_id: + raise Exception("Missing client_id") grant_type = request.form.get('grant_type') + if not grant_type: + raise Exception("Missing grant_type") scope = request.form.get('scope') + if not scope: + raise Exception("Missing scope") client_assertion_type = request.form.get('client_assertion_type') + if not client_assertion_type: + raise Exception("Missing client_assertion_type") client_assertion = request.form.get('client_assertion') + if not client_assertion: + raise Exception("Missing client_assertion") except Exception as ex: app.logger.debug('Missing form parameters: {}'.format(ex)) abort(400) return None - + # Proxy request to AR /token endpoint url = conf['ar']['token'] form_data = { diff --git a/requirements.txt b/requirements.txt index 82d1dec..ed6e7bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ Werkzeug==2.2.1 pytest==7.1.2 pytest-testdox==3.0.1 pytest-mock==3.10.0 +requests-mock==1.10.0 diff --git a/tests/pytest/test_token.py b/tests/pytest/test_token.py new file mode 100644 index 0000000..b512d6f --- /dev/null +++ b/tests/pytest/test_token.py @@ -0,0 +1,95 @@ +import pytest +import os +from api import app +from flask_sqlalchemy import SQLAlchemy + +from tests.pytest.util.config_handler import load_config + +# Get AS config +as_config = load_config("tests/config/as.yml", app) +app.config['as'] = as_config + +# Database +db_conf = as_config['db'] +basedir = os.path.abspath(os.path.dirname(__file__)) +dbpath = os.path.join(basedir, db_conf['useFile']) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath +db = SQLAlchemy(app) +app.config['db'] = db + +# Token endpoint +TOKEN_ENDPOINT = as_config['ar']['token'] + +# Dummy access token +ACCESS_TOKEN = 'gfgarhgrfha' + +# Client EORI +CLIENT_EORI = 'EU.EORI.DEMARKETPLACE' + +# Form parameters +REQ_FORM = { + 'client_id': CLIENT_EORI, + 'grant_type': 'client_credentials', + 'scope': 'iSHARE', + 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion': 'dfggrghaerhahahhahp' +} + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + +@pytest.fixture +def clean_db(): + with app.app_context(): + from api.models import token + db.drop_all() + db.create_all() + +@pytest.fixture +def mock_post_token_ok(requests_mock): + return requests_mock.post(TOKEN_ENDPOINT, + json={ + 'access_token': ACCESS_TOKEN, + 'expires_in': 3600, + 'token_type': "Bearer" + }) + +# Test: Successful token +@pytest.mark.ok +@pytest.mark.it('should successfully obtain access token') +def test_token_ok(client, mock_post_token_ok, clean_db): + + # Invoke request + response = client.post(TOKEN_ENDPOINT, data=REQ_FORM) + + # Asserts on response + assert mock_post_token_ok.called + assert mock_post_token_ok.call_count == 1 + assert 'access_token' in response.json, 'Response should contain access_token' + assert response.json['access_token'] == ACCESS_TOKEN, "should have correct access token" + assert response.json['expires_in'] == 3600, "should have correct expiration period" + + # Get and assert DB entry + with app.app_context(): + from api.models.token import Token + db_token = Token.query.filter_by(eori=CLIENT_EORI).first() + print(db_token) + assert db_token.eori == CLIENT_EORI, "DB entry should have correct EORI" + assert db_token.access_token == ACCESS_TOKEN, "DB entry should have correct access token" + +# Test: Failure missing client_id +@pytest.mark.failure +@pytest.mark.it('should fail due to missing client_id') +def test_token_ok(client, mock_post_token_ok, clean_db): + + # Remove client_id + form = dict(REQ_FORM) + form.pop('client_id', None) + + # Invoke request + response = client.post(TOKEN_ENDPOINT, data=form) + + # Asserts on response + assert response.status_code == 400, "should return code 400" diff --git a/tests/pytest/test_token_handler.py b/tests/pytest/test_token_handler.py index 5089d3e..27dc761 100644 --- a/tests/pytest/test_token_handler.py +++ b/tests/pytest/test_token_handler.py @@ -1,5 +1,4 @@ import pytest -#import flask from api import app from tests.pytest.util.config_handler import load_config from api.util.token_handler import forward_token @@ -56,7 +55,8 @@ def form_get(attr): def mock_proxy_request_ok(mocker): ar_response = { 'access_token': ACCESS_TOKEN, - 'expires_in': 3600 + 'expires_in': 3600, + 'token_type': "Bearer" } return mocker.patch('api.util.token_handler.proxy_request', return_value=ar_response) From 4bf9b69e71330ec40a33d38d5c0cbf450267752c Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Thu, 25 May 2023 17:59:20 +0200 Subject: [PATCH 04/16] Add /createpolicy endpoint --- api/app.py | 22 ++-------------------- api/token.py | 5 ----- api/util/db_handler.py | 4 ++-- requirements.txt | 1 + 4 files changed, 5 insertions(+), 27 deletions(-) diff --git a/api/app.py b/api/app.py index 52c6b9a..bb0534b 100644 --- a/api/app.py +++ b/api/app.py @@ -1,11 +1,8 @@ from flask import Flask, Response, jsonify, request from .errors import errors -#from .versions import versions -#from .trusted_list import trusted_list -#from .parties import parties from .token import token_endpoint -#from .trusted_issuer import trusted_issuer +from .createpolicy import createpolicy_endpoint app = Flask(__name__) @@ -13,25 +10,10 @@ app.register_blueprint(errors) # Register routes -#app.register_blueprint(versions) -#app.register_blueprint(trusted_list) -#app.register_blueprint(parties) +app.register_blueprint(createpolicy_endpoint) app.register_blueprint(token_endpoint) -#app.register_blueprint(trusted_issuer) # Register health endpoint @app.route("/health") def health(): - - # TEST - #from api.models.token import Token - #token = Token( - # eori="EU.EORI.DEABC", - # access_token="dgagaggdgagg", - # expires=3600) - #from flask import current_app - #db = current_app.config['db'] - #db.session.add(token) # Adds new User record to database - #db.session.commit() # Commits all changes - return Response("OK", status=200) diff --git a/api/token.py b/api/token.py index d6443a8..3d566a2 100644 --- a/api/token.py +++ b/api/token.py @@ -37,10 +37,5 @@ def index(): current_app.logger.debug("Error when inserting token into DB: {}".format(insert_error)) abort(500, description="Internal server error (DB access)") - # TEST - #from api.util.db_handler import get_token_by_eori - #t = get_token_by_eori("EU.EORI.DEMARKETPLACE", current_app) - #current_app.logger.info("Get by EORI: {}".format(t)) - return auth_data, 200 diff --git a/api/util/db_handler.py b/api/util/db_handler.py index 49e5f6a..a5ecdca 100644 --- a/api/util/db_handler.py +++ b/api/util/db_handler.py @@ -6,7 +6,7 @@ def insert_token(token, app): app.logger.debug("Inserting token: {}".format(token)) db = app.config['db'] try: - db.session.add(token) + db.session.add(token) #TODO: check for existing token and delete it beforehand? db.session.commit() return None except Exception as error: @@ -35,7 +35,7 @@ def get_token_by_eori(eori, app): # Get token by access_token def get_token_by_token(token, app): - app.logger.debug("Get token by access_token: {}".format(token[:50])) + app.logger.debug("Get token by access_token: {}...".format(token[:50])) db = app.config['db'] # First clean entries diff --git a/requirements.txt b/requirements.txt index ed6e7bf..58f9d30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ Flask-SQLAlchemy==3.0.3 gunicorn==20.1.0 requests==2.31.0 PyJWT==2.4.0 +cryptography==37.0.4 PyYAML==6.0 Werkzeug==2.2.1 pytest==7.1.2 From b52f2f01b70e3baba9dcc78a4a549a9a5e34a708 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Thu, 25 May 2023 18:19:02 +0200 Subject: [PATCH 05/16] Adding missing files --- api/createpolicy.py | 84 +++++++ api/exceptions/create_policy_exception.py | 7 + api/util/createpolicy_handler.py | 260 ++++++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 api/createpolicy.py create mode 100644 api/exceptions/create_policy_exception.py create mode 100644 api/util/createpolicy_handler.py diff --git a/api/createpolicy.py b/api/createpolicy.py new file mode 100644 index 0000000..252cd58 --- /dev/null +++ b/api/createpolicy.py @@ -0,0 +1,84 @@ +from flask import Blueprint, Response, current_app, abort, request + +from api.util.createpolicy_handler import extract_access_token, get_ar_token, check_create_delegation_evidence, create_delegation_evidence + +from api.exceptions.create_policy_exception import CreatePolicyException + +# Blueprint +createpolicy_endpoint = Blueprint("createpolicy_endpoint", __name__) + +# POST /createpolicy +@createpolicy_endpoint.route("/createpolicy", methods = ['POST']) +def index(): + current_app.logger.debug('Received request at /createpolicy endpoint') + + # Load config + conf = current_app.config['as'] + + # Get access token from request header + token = None + try: + request_token = extract_access_token(request) + except CreatePolicyException as cpe: + current_app.logger.debug("Error when extracting access token from authorization header: {}. Returning status {}.".format(cpe.internal_message, cpe.status_code)) + abort(cpe.status_code, cpe.message) + except Exception as ex: + current_app.logger.error("Internal error: {}".format(ex)) + abort(500, "Internal error") + current_app.logger.debug("...received access token in incoming request: {}".format(request_token)) + + # Get token DB entry + current_app.logger.debug("Compare token at database...") + db_token = None + try: + from api.util.db_handler import get_token_by_token + db_token = get_token_by_token(request_token, current_app) + except Exception as exd: + current_app.logger.error("Internal error when accessing DB: {}".format(exd)) + abort(500, "Internal error") + if not db_token: + current_app.logger.debug("Token could not be found in DB, probably no valid token provided by request") + abort(400, "No valid token has been provided") + current_app.logger.debug("...provided token is valid") + + # Get token from AR + current_app.logger.debug("AS requests access_token at AR...") + try: + access_token = get_ar_token(conf) + except CreatePolicyException as cpet: + current_app.logger.error("Error when AS is obtaining access token from AR: {}. Returning status {}.".format(cpet.internal_message, cpet.status_code)) + abort(cpet.status_code, cpet.message) + except Exception as ext: + current_app.logger.error("Internal error: {}".format(ext)) + abort(500, "Internal error") + current_app.logger.debug("...received access token from AR: {}".format(access_token)) + + # Check delegationEvidence allowing to create policy + current_app.logger.debug('Check at AR if sender is allowed to create policies...') + try: + check_create_delegation_evidence(conf, db_token.eori, access_token) + except CreatePolicyException as cpede: + current_app.logger.debug("Necessary delegationEvidence could not be retrieved from AR: {}. Returning status: {}.".format(cpede.internal_message, cpede.status_code)) + abort(cped.status_code, cpede.message) + except Exception as exde: + current_app.logger.error("Internal error: {}".format(exde)) + abort(500, "Internal error") + current_app.logger.debug("... necessary delegationEvidence has been found, creating policies is allowed.") + + # Create policy + current_app.logger.debug("Create policy from sender at AR...") + try: + policy_response = create_delegation_evidence(conf, access_token, request) + if policy_response: + current_app.logger.debug("... policy created, AR returned policy_token: {}".format(policy_response)) + return policy_response, 200 + else: + current_app.logger.debug("... policy created, AR returned empty response") + return '', 200 + except CreatePolicyException as cpep: + current_app.logger.error("Error when AS is creating policy at AR: {}. Returning status {}.".format(cpep.internal_message, cpep.status_code)) + abort(cpep.status_code, cpep.message) + except Exception as exp: + current_app.logger.error("Internal error: {}".format(exp)) + abort(500, "Internal error") + diff --git a/api/exceptions/create_policy_exception.py b/api/exceptions/create_policy_exception.py new file mode 100644 index 0000000..005ad54 --- /dev/null +++ b/api/exceptions/create_policy_exception.py @@ -0,0 +1,7 @@ +class CreatePolicyException(Exception): + + def __init__(self, message, status_code, internal_msg): + super().__init__(message) + + self.status_code = status_code + self.internal_msg = internal_msg diff --git a/api/util/createpolicy_handler.py b/api/util/createpolicy_handler.py new file mode 100644 index 0000000..3c19524 --- /dev/null +++ b/api/util/createpolicy_handler.py @@ -0,0 +1,260 @@ +import requests +import time, os +import jwt +import uuid +from requests.exceptions import HTTPError + +from api.exceptions.create_policy_exception import CreatePolicyException + +# ENV for PRIVATE_KEY +ENV_PRIVATE_KEY = "AS_KEY" + +# ENV for certificate chain +ENV_CERTIFICATES = "AS_CERTS" + +# Obtain private key from yaml or ENV +def _get_private_key(config): + return os.environ.get(ENV_PRIVATE_KEY, config['key']) + +# Obtain certificate chains from yaml or ENV +def _get_certificates(config): + return os.environ.get(ENV_CERTIFICATES, config['crt']) + +# Create iSHARE JWT +def _build_token(params): + def getCAChain(cert): + + sp = cert.split('-----BEGIN CERTIFICATE-----\n') + sp = sp[1:] + + ca_chain = [] + for ca in sp: + ca_sp = ca.split('\n-----END CERTIFICATE-----') + ca_chain.append(ca_sp[0]) + + return ca_chain + + iat = int(str(time.time()).split('.')[0]) + exp = iat + 30 + + token = { + "jti": str(uuid.uuid4()), + "iss": params['client_id'], + "sub": params['client_id'], + "aud": params['ar_id'], + "iat": iat, + "nbf": iat, + "exp": exp + } + + return jwt.encode(token, params['key'], algorithm="RS256", headers={ + 'x5c': getCAChain(params['cert']) + }) + +# delegationEvidence template allowing to create policys +def _get_delegation_evidence_template(issuer, eori): + return { + 'delegationRequest': { + 'policyIssuer': issuer, + 'target': { + 'accessSubject': eori + }, + 'policySets': [ + { + 'policies': [ + { + 'target': { + 'resource': { + 'type': "delegationEvidence", + 'identifiers': ["*"], + 'attributes': ["*"] + }, + 'actions': ["POST"] + }, + 'rules': [ + { + 'effect': "Permit" + } + ] + } + ] + } + ] + } + } + +# Analyse request header and extract access token +def extract_access_token(request): + + # Get header + auth_header = request.headers.get('Authorization') + if not auth_header: + message = "Missing Authorization header" + raise CreatePolicyException(message, 400, message) + + # Split Bearer/token + if not auth_header.startswith("Bearer"): + message = "Invalid Authorization header" + raise CreatePolicyException(message, 400, message) + split_header = auth_header.split(" ") + if len(split_header) != 2: + message = "Invalid Authorization header" + raise CreatePolicyException(message, 400, message) + + # Token + token = split_header[1] + if not token or len(token) < 1: + message = "Invalid Authorization header" + raise CreatePolicyException(message, 400, message) + + return token + +# Get token from AR as AS +def get_ar_token(conf): + + # Generate iSHARE JWT + token = _build_token({ + 'client_id': conf['client']['id'], + 'ar_id': conf['ar']['id'], + 'key': _get_private_key(conf['client']), + 'cert': _get_certificates(conf['client']) + }) + + # Retrieve token from AR + url = conf['ar']['token'] + auth_params = { + 'grant_type': 'client_credentials', + 'scope': 'iSHARE', + 'client_id': conf['client']['id'], + 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion': token + } + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + response = requests.post(url, data=auth_params, headers=headers) + try: + response.raise_for_status() + except HTTPError as e: + message = "Error when AS is retrieving token from AR" + internal_message = "Error retrieving token from AR: {}".format(e) + if response.json(): + internal_message += ", received JSON response: {}".format(response.json()) + raise CreatePolicyException(message, 400, internal_message) + + auth_data = response.json() + if (not 'access_token' in auth_data) or (not 'expires_in' in auth_data): + message = "AS received invalid response from AR when obtaining token" + internal_message = "AS received invalid response from AR when obtaining token: {}".format(auth_data) + raise CreatePolicyException(message, 400, internal_message) + + return auth_data['access_token'] + +# Check if sender is allowed to create policies (delegationEvidence) +def check_create_delegation_evidence(conf, eori, access_token): + + # Get delegationEvidence template for query + payload = _get_delegation_evidence_template(conf['client']['id'], eori) + + # Send query to AR + url = conf['ar']['delegation'] + headers={ + 'Authorization': 'Bearer ' + access_token + } + + response = requests.post(url, json=payload, headers=headers) + try: + response.raise_for_status() + except HTTPError as e: + message = "Necessary delegationEvidence could not be retrieved from AR" + internal_message = "Error when querying delegationEvidence at AR: {}".format(e) + if response.json(): + internal_message += ", received JSON response: {}".format(response.json()) + raise CreatePolicyException(message, 400, internal_message) + + # Check response + query_data = response.json() + print(query_data) + if not query_data['delegation_token']: + message = "Ar was not providing valid response" + internal_message = message + ": {}".format(query_data) + raise CreatePolicyException(message, 400, internal_message) + + delegation_token = query_data['delegation_token'] + decoded_token = jwt.decode(delegation_token, options={"verify_signature": False}) + del_ev = decoded_token['delegationEvidence'] + message = "AR did not provide valid delegationEvidence to create policies." + if not del_ev: + raise CreatePolicyException(message, 400, "Missing 'delegationEvidence' object") + + psets = del_ev['policySets'] + if not psets or len(psets) < 1: + raise CreatePolicyException(message, 400, "Missing 'policySets'") + + pset = psets[0] + policies = pset['policies'] + if not policies or len(policies) < 1: + raise CreatePolicyException(message, 400, "Missing 'policies'") + + p = policies[0] + target = p['target'] + if not target: + raise CreatePolicyException(message, 400, "Missing 'target'") + + resource = target['resource'] + if not resource: + raise CreatePolicyException(message, 400, "Missing 'resource'") + + ptype = resource['type'] + if not ptype: + raise CreatePolicyException(message, 400, "Missing 'type'") + if ptype != "delegationEvidence": + raise CreatePolicyException(message, 400, "Wrong type: {} != delegationEvidence".format(ptype)) + + rules = p['rules'] + if not rules or len(rules) < 1: + raise CreatePolicyException(message, 400, "Missing 'rules'") + + r = rules[0] + effect = r['effect'] + if not effect: + raise CreatePolicyException(message, 400, "Missing 'effect'") + if effect != "Permit": + raise CreatePolicyException(message, 400, "Wrong effect: {} != Permit".format(effect)) + + return None + +# Create delegationEvidence at AR +def create_delegation_evidence(conf, access_token, request): + + # Get payload from request + payload = request.json + if not payload: + message = "Missing payload in /createpolicy request" + raise CreatePolicyException(message, 400, message) + + # Create policy at AR + url = conf['ar']['policy'] + headers={ + 'Authorization': 'Bearer ' + access_token + } + + response = requests.post(url, json=payload, headers=headers) + try: + response.raise_for_status() + except HTTPError as e: + message = "Policy could not be created at AR: {}".format(e) + if response.json(): + message += ", received JSON response: {}".format(response.json()) + raise CreatePolicyException(message, 400, message) + + # Check response + query_data = response.json() + if query_data: + if query_data['policy_token']: + return { + 'policy_token': query_data['policy_token'] + } + + return None From 66408abd2870e7e4d2e10191a49af632f6fdf7e3 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Fri, 26 May 2023 10:00:20 +0200 Subject: [PATCH 06/16] Improve error handling --- api/app.py | 7 ++ api/createpolicy.py | 40 ++++------- api/exceptions/create_policy_exception.py | 6 +- api/exceptions/database_exception.py | 12 ++++ api/exceptions/token_exception.py | 11 +++ api/token.py | 44 ++++++++---- api/util/createpolicy_handler.py | 40 +++++------ api/util/db_handler.py | 84 +++++++++++++---------- api/util/token_handler.py | 67 +++++++++--------- tests/pytest/test_token.py | 3 +- tests/pytest/test_token_handler.py | 21 +++--- 11 files changed, 189 insertions(+), 146 deletions(-) create mode 100644 api/exceptions/database_exception.py create mode 100644 api/exceptions/token_exception.py diff --git a/api/app.py b/api/app.py index bb0534b..5ba99d4 100644 --- a/api/app.py +++ b/api/app.py @@ -13,6 +13,13 @@ app.register_blueprint(createpolicy_endpoint) app.register_blueprint(token_endpoint) +# Default 500 error handler +@app.errorhandler(500) +def catch_server_errors(e): + app.logger.error("Internal error: {}".format(e)) + return "Internal server error", 500 + #abort(500, "Internal server error") + # Register health endpoint @app.route("/health") def health(): diff --git a/api/createpolicy.py b/api/createpolicy.py index 252cd58..3875b92 100644 --- a/api/createpolicy.py +++ b/api/createpolicy.py @@ -3,6 +3,7 @@ from api.util.createpolicy_handler import extract_access_token, get_ar_token, check_create_delegation_evidence, create_delegation_evidence from api.exceptions.create_policy_exception import CreatePolicyException +from api.exceptions.database_exception import DatabaseException # Blueprint createpolicy_endpoint = Blueprint("createpolicy_endpoint", __name__) @@ -20,11 +21,8 @@ def index(): try: request_token = extract_access_token(request) except CreatePolicyException as cpe: - current_app.logger.debug("Error when extracting access token from authorization header: {}. Returning status {}.".format(cpe.internal_message, cpe.status_code)) - abort(cpe.status_code, cpe.message) - except Exception as ex: - current_app.logger.error("Internal error: {}".format(ex)) - abort(500, "Internal error") + current_app.logger.debug("Error when extracting access token from authorization header: {}. Returning status {}.".format(cpe.internal_msg, cpe.status_code)) + abort(cpe.status_code, cpe.public_msg) current_app.logger.debug("...received access token in incoming request: {}".format(request_token)) # Get token DB entry @@ -32,10 +30,12 @@ def index(): db_token = None try: from api.util.db_handler import get_token_by_token - db_token = get_token_by_token(request_token, current_app) - except Exception as exd: - current_app.logger.error("Internal error when accessing DB: {}".format(exd)) - abort(500, "Internal error") + db_result = get_token_by_token(request_token, current_app.config['db']) + db_token = db_result['token'] + current_app.logger.debug("... token DB entries deleted: {}".format(db_result['deleted'])) + except DatabaseException as dex: + current_app.logger.error("Error when retreiving token from DB: {}. Returning status: {}".format(dex.internal_msg, dex.status_code)) + abort(dex.status_code, dex.public_msg) if not db_token: current_app.logger.debug("Token could not be found in DB, probably no valid token provided by request") abort(400, "No valid token has been provided") @@ -46,11 +46,8 @@ def index(): try: access_token = get_ar_token(conf) except CreatePolicyException as cpet: - current_app.logger.error("Error when AS is obtaining access token from AR: {}. Returning status {}.".format(cpet.internal_message, cpet.status_code)) - abort(cpet.status_code, cpet.message) - except Exception as ext: - current_app.logger.error("Internal error: {}".format(ext)) - abort(500, "Internal error") + current_app.logger.error("Error when AS is obtaining access token from AR: {}. Returning status {}.".format(cpet.internal_msg, cpet.status_code)) + abort(cpet.status_code, cpet.public_msg) current_app.logger.debug("...received access token from AR: {}".format(access_token)) # Check delegationEvidence allowing to create policy @@ -58,11 +55,8 @@ def index(): try: check_create_delegation_evidence(conf, db_token.eori, access_token) except CreatePolicyException as cpede: - current_app.logger.debug("Necessary delegationEvidence could not be retrieved from AR: {}. Returning status: {}.".format(cpede.internal_message, cpede.status_code)) - abort(cped.status_code, cpede.message) - except Exception as exde: - current_app.logger.error("Internal error: {}".format(exde)) - abort(500, "Internal error") + current_app.logger.debug("Necessary delegationEvidence could not be retrieved from AR: {}. Returning status: {}.".format(cpede.internal_msg, cpede.status_code)) + abort(cped.status_code, cpede.public_msg) current_app.logger.debug("... necessary delegationEvidence has been found, creating policies is allowed.") # Create policy @@ -76,9 +70,5 @@ def index(): current_app.logger.debug("... policy created, AR returned empty response") return '', 200 except CreatePolicyException as cpep: - current_app.logger.error("Error when AS is creating policy at AR: {}. Returning status {}.".format(cpep.internal_message, cpep.status_code)) - abort(cpep.status_code, cpep.message) - except Exception as exp: - current_app.logger.error("Internal error: {}".format(exp)) - abort(500, "Internal error") - + current_app.logger.error("Error when AS is creating policy at AR: {}. Returning status {}.".format(cpep.internal_msg, cpep.status_code)) + abort(cpep.status_code, cpep.public_msg) diff --git a/api/exceptions/create_policy_exception.py b/api/exceptions/create_policy_exception.py index 005ad54..49eac9f 100644 --- a/api/exceptions/create_policy_exception.py +++ b/api/exceptions/create_policy_exception.py @@ -1,7 +1,11 @@ class CreatePolicyException(Exception): - def __init__(self, message, status_code, internal_msg): + def __init__(self, message, internal_msg, status_code): super().__init__(message) self.status_code = status_code self.internal_msg = internal_msg + self.public_msg = message + + if not internal_msg: + self.internal_msg = message diff --git a/api/exceptions/database_exception.py b/api/exceptions/database_exception.py new file mode 100644 index 0000000..a470385 --- /dev/null +++ b/api/exceptions/database_exception.py @@ -0,0 +1,12 @@ +class DatabaseException(Exception): + + def __init__(self, message, internal_msg, status_code): + super().__init__(message) + + self.status_code = status_code + self.internal_msg = internal_msg + self.public_msg = message + + if not internal_msg: + self.internal_msg = message + diff --git a/api/exceptions/token_exception.py b/api/exceptions/token_exception.py new file mode 100644 index 0000000..2c8bfc3 --- /dev/null +++ b/api/exceptions/token_exception.py @@ -0,0 +1,11 @@ +class TokenException(Exception): + + def __init__(self, message, internal_msg, status_code): + super().__init__(message) + + self.status_code = status_code + self.internal_msg = internal_msg + self.public_msg = message + + if not internal_msg: + self.internal_msg = message diff --git a/api/token.py b/api/token.py index 3d566a2..ee8c910 100644 --- a/api/token.py +++ b/api/token.py @@ -2,6 +2,9 @@ from api.util.token_handler import forward_token import time +from api.exceptions.token_exception import TokenException +from api.exceptions.database_exception import DatabaseException + # Blueprint token_endpoint = Blueprint("token_endpoint", __name__) @@ -14,28 +17,39 @@ def index(): conf = current_app.config['as'] # Forward token - response = forward_token(request, current_app, abort) - auth_data = response.json() + auth_data = None + try: + response = forward_token(request, current_app) + auth_data = response.json() + except TokenException as tex: + current_app.logger.debug("Error when forwarding token request to AR: {}. Returning status {}.".format(tex.internal_msg, tex.status_code)) + abort(tex.status_code, tex.public_msg) if (not 'access_token' in auth_data) or (not 'expires_in' in auth_data): app.logger.debug("Invalid response from AR: {}".format(auth_data)) - abort(400, description="Received invalid response from AR") + abort(400, "Received invalid response from AR") return None # Build Token object - from api.models.token import Token - client_id = request.form.get('client_id') - ar_token = Token( - eori=client_id, - access_token=auth_data['access_token'], - expires=int(time.time() * 1000) + (1000*auth_data['expires_in'])) + try: + from api.models.token import Token + client_id = request.form.get('client_id') + ar_token = Token( + eori=client_id, + access_token=auth_data['access_token'], + expires=int(time.time() * 1000) + (1000*auth_data['expires_in'])) + except Exception as bex: + print(bex) + current_app.logger.error("Internal error when building Token object for DB insert: {}".format(ex)) + abort(500, "Internal error") current_app.logger.debug('Received access token with data: {}'.format(ar_token)) # Insert token - from api.util.db_handler import insert_token - insert_error = insert_token(ar_token, current_app) - if insert_error: - current_app.logger.debug("Error when inserting token into DB: {}".format(insert_error)) - abort(500, description="Internal server error (DB access)") - + try: + from api.util.db_handler import insert_token + insert_token(ar_token, current_app.config['db']) + except DatabaseException as dex: + current_app.logger.error("Error when inserting token into DB: {}. Returning status: {}".format(dex.internal_msg, dex.status_code)) + abort(dex.status_code, dex.public_msg) + return auth_data, 200 diff --git a/api/util/createpolicy_handler.py b/api/util/createpolicy_handler.py index 3c19524..17ec8a1 100644 --- a/api/util/createpolicy_handler.py +++ b/api/util/createpolicy_handler.py @@ -90,22 +90,22 @@ def extract_access_token(request): auth_header = request.headers.get('Authorization') if not auth_header: message = "Missing Authorization header" - raise CreatePolicyException(message, 400, message) + raise CreatePolicyException(message, None, 400) # Split Bearer/token if not auth_header.startswith("Bearer"): message = "Invalid Authorization header" - raise CreatePolicyException(message, 400, message) + raise CreatePolicyException(message, None, 400) split_header = auth_header.split(" ") if len(split_header) != 2: message = "Invalid Authorization header" - raise CreatePolicyException(message, 400, message) + raise CreatePolicyException(message, None, 400) # Token token = split_header[1] if not token or len(token) < 1: message = "Invalid Authorization header" - raise CreatePolicyException(message, 400, message) + raise CreatePolicyException(message, None, 400) return token @@ -141,13 +141,13 @@ def get_ar_token(conf): internal_message = "Error retrieving token from AR: {}".format(e) if response.json(): internal_message += ", received JSON response: {}".format(response.json()) - raise CreatePolicyException(message, 400, internal_message) + raise CreatePolicyException(message, internal_message, 400) auth_data = response.json() if (not 'access_token' in auth_data) or (not 'expires_in' in auth_data): message = "AS received invalid response from AR when obtaining token" internal_message = "AS received invalid response from AR when obtaining token: {}".format(auth_data) - raise CreatePolicyException(message, 400, internal_message) + raise CreatePolicyException(message, internal_message, 400) return auth_data['access_token'] @@ -171,7 +171,7 @@ def check_create_delegation_evidence(conf, eori, access_token): internal_message = "Error when querying delegationEvidence at AR: {}".format(e) if response.json(): internal_message += ", received JSON response: {}".format(response.json()) - raise CreatePolicyException(message, 400, internal_message) + raise CreatePolicyException(message, internal_message, 400) # Check response query_data = response.json() @@ -179,49 +179,49 @@ def check_create_delegation_evidence(conf, eori, access_token): if not query_data['delegation_token']: message = "Ar was not providing valid response" internal_message = message + ": {}".format(query_data) - raise CreatePolicyException(message, 400, internal_message) + raise CreatePolicyException(message, internal_message, 400) delegation_token = query_data['delegation_token'] decoded_token = jwt.decode(delegation_token, options={"verify_signature": False}) del_ev = decoded_token['delegationEvidence'] message = "AR did not provide valid delegationEvidence to create policies." if not del_ev: - raise CreatePolicyException(message, 400, "Missing 'delegationEvidence' object") + raise CreatePolicyException(message, "Missing 'delegationEvidence' object", 400) psets = del_ev['policySets'] if not psets or len(psets) < 1: - raise CreatePolicyException(message, 400, "Missing 'policySets'") + raise CreatePolicyException(message, "Missing 'policySets'", 400) pset = psets[0] policies = pset['policies'] if not policies or len(policies) < 1: - raise CreatePolicyException(message, 400, "Missing 'policies'") + raise CreatePolicyException(message, "Missing 'policies'", 400) p = policies[0] target = p['target'] if not target: - raise CreatePolicyException(message, 400, "Missing 'target'") + raise CreatePolicyException(message, "Missing 'target'", 400) resource = target['resource'] if not resource: - raise CreatePolicyException(message, 400, "Missing 'resource'") + raise CreatePolicyException(message, "Missing 'resource'", 400) ptype = resource['type'] if not ptype: - raise CreatePolicyException(message, 400, "Missing 'type'") + raise CreatePolicyException(message, "Missing 'type'", 400) if ptype != "delegationEvidence": - raise CreatePolicyException(message, 400, "Wrong type: {} != delegationEvidence".format(ptype)) + raise CreatePolicyException(message, "Wrong type: {} != delegationEvidence".format(ptype), 400) rules = p['rules'] if not rules or len(rules) < 1: - raise CreatePolicyException(message, 400, "Missing 'rules'") + raise CreatePolicyException(message, "Missing 'rules'", 400) r = rules[0] effect = r['effect'] if not effect: - raise CreatePolicyException(message, 400, "Missing 'effect'") + raise CreatePolicyException(message, "Missing 'effect'", 400) if effect != "Permit": - raise CreatePolicyException(message, 400, "Wrong effect: {} != Permit".format(effect)) + raise CreatePolicyException(message, "Wrong effect: {} != Permit".format(effect), 400) return None @@ -232,7 +232,7 @@ def create_delegation_evidence(conf, access_token, request): payload = request.json if not payload: message = "Missing payload in /createpolicy request" - raise CreatePolicyException(message, 400, message) + raise CreatePolicyException(message, None, 400) # Create policy at AR url = conf['ar']['policy'] @@ -247,7 +247,7 @@ def create_delegation_evidence(conf, access_token, request): message = "Policy could not be created at AR: {}".format(e) if response.json(): message += ", received JSON response: {}".format(response.json()) - raise CreatePolicyException(message, 400, message) + raise CreatePolicyException(message, None, 400) # Check response query_data = response.json() diff --git a/api/util/db_handler.py b/api/util/db_handler.py index a5ecdca..1dd1a5b 100644 --- a/api/util/db_handler.py +++ b/api/util/db_handler.py @@ -1,67 +1,75 @@ from api.models.token import Token import time +from api.exceptions.database_exception import DatabaseException + # Insert token -def insert_token(token, app): - app.logger.debug("Inserting token: {}".format(token)) - db = app.config['db'] +def insert_token(token, db): + try: db.session.add(token) #TODO: check for existing token and delete it beforehand? db.session.commit() return None except Exception as error: db.session.rollback() - app.logger.error("Error inserting token: {}".format(error)) - return "Error inserting token" + message = "Internal server error when accessing DB" + internal_message = "Error inserting token: {}".format(error) + raise DatabaseException(message, internal_message, 500) # Get token by EORI -def get_token_by_eori(eori, app): - app.logger.debug("Get token by EORI: {}".format(eori)) - db = app.config['db'] - - # First clean entries - clean_err = clean_token(app) - if clean_err: - return None - - # Perform query +def get_token_by_eori(eori, db): + try: + # First clean entries + deleted = clean_token(db) + + # Perform query token = Token.query.filter_by(eori=eori).first() - return token + return { + 'token': token, + 'deleted': deleted + } + + except DatabaseException as dex: + raise dex except Exception as error: db.session.rollback() - app.logger.error("Error retrieving token: {}".format(error)) - return None + message = "Internal server error when accessing DB" + internal_message = "Error retrieving token: {}".format(error) + raise DatabaseException(message, internal_message, 500) # Get token by access_token -def get_token_by_token(token, app): - app.logger.debug("Get token by access_token: {}...".format(token[:50])) - db = app.config['db'] - - # First clean entries - clean_err = clean_token(app) - if clean_err: - return None - - # Perform query +def get_token_by_token(token, db): + try: + # First clean entries + deleted = clean_token(db) + + # Perform query r_token = Token.query.filter_by(access_token=token).first() - return r_token + return { + 'token': r_token, + 'deleted': deleted + } + + except DatabaseException as dex: + raise dex except Exception as error: db.session.rollback() - app.logger.error("Error retrieving token: {}".format(error)) - return None + message = "Internal server error when accessing DB" + internal_message = "Error retrieving token: {}".format(error) + raise DatabaseException(message, internal_message, 500) # Clean expired tokens -def clean_token(app): - app.logger.debug("Removing expired tokens...") - db = app.config['db'] +def clean_token(db): + try: deleted = Token.query.filter(Token.expires Date: Fri, 26 May 2023 12:18:51 +0200 Subject: [PATCH 07/16] Adding unit tests for createpolicy_handler --- api/util/createpolicy_handler.py | 61 ++-- tests/pytest/test_createpolicy_handler.py | 379 ++++++++++++++++++++++ tests/pytest/test_token_handler.py | 9 - 3 files changed, 416 insertions(+), 33 deletions(-) create mode 100644 tests/pytest/test_createpolicy_handler.py diff --git a/api/util/createpolicy_handler.py b/api/util/createpolicy_handler.py index 17ec8a1..f2cb573 100644 --- a/api/util/createpolicy_handler.py +++ b/api/util/createpolicy_handler.py @@ -104,7 +104,7 @@ def extract_access_token(request): # Token token = split_header[1] if not token or len(token) < 1: - message = "Invalid Authorization header" + message = "Invalid Authorization header, empty token" raise CreatePolicyException(message, None, 400) return token @@ -175,51 +175,64 @@ def check_create_delegation_evidence(conf, eori, access_token): # Check response query_data = response.json() - print(query_data) - if not query_data['delegation_token']: - message = "Ar was not providing valid response" + if not 'delegation_token' in query_data: + message = "AR was not providing valid response" internal_message = message + ": {}".format(query_data) raise CreatePolicyException(message, internal_message, 400) - + + message = "AR did not provide valid delegationEvidence to create policies." delegation_token = query_data['delegation_token'] decoded_token = jwt.decode(delegation_token, options={"verify_signature": False}) - del_ev = decoded_token['delegationEvidence'] - message = "AR did not provide valid delegationEvidence to create policies." - if not del_ev: + if not 'delegationEvidence' in decoded_token: raise CreatePolicyException(message, "Missing 'delegationEvidence' object", 400) + del_ev = decoded_token['delegationEvidence'] - psets = del_ev['policySets'] - if not psets or len(psets) < 1: + if not 'policySets' in del_ev: raise CreatePolicyException(message, "Missing 'policySets'", 400) + psets = del_ev['policySets'] + if len(psets) < 1: + raise CreatePolicyException(message, "Empty 'policySets'", 400) pset = psets[0] - policies = pset['policies'] - if not policies or len(policies) < 1: + if not 'policies' in pset: raise CreatePolicyException(message, "Missing 'policies'", 400) + policies = pset['policies'] + if len(policies) < 1: + raise CreatePolicyException(message, "Empty 'policies'", 400) p = policies[0] - target = p['target'] - if not target: + if not 'target' in p: raise CreatePolicyException(message, "Missing 'target'", 400) + target = p['target'] - resource = target['resource'] - if not resource: + if not 'actions' in target: + raise CreatePolicyException(message, "Missing 'actions'", 400) + actions = target['actions'] + if len(actions) < 1: + raise CreatePolicyException(message, "Empty 'actions'", 400) + if not 'POST' in actions: + raise CreatePolicyException(message, "'actions' is missing 'POST': {}".format(actions), 400) + + if not 'resource' in target: raise CreatePolicyException(message, "Missing 'resource'", 400) - - ptype = resource['type'] - if not ptype: + resource = target['resource'] + + if not 'type' in resource: raise CreatePolicyException(message, "Missing 'type'", 400) + ptype = resource['type'] if ptype != "delegationEvidence": raise CreatePolicyException(message, "Wrong type: {} != delegationEvidence".format(ptype), 400) - rules = p['rules'] - if not rules or len(rules) < 1: + if not 'rules' in p: raise CreatePolicyException(message, "Missing 'rules'", 400) + rules = p['rules'] + if len(rules) < 1: + raise CreatePolicyException(message, "Empty 'rules'", 400) r = rules[0] - effect = r['effect'] - if not effect: + if not 'effect' in r: raise CreatePolicyException(message, "Missing 'effect'", 400) + effect = r['effect'] if effect != "Permit": raise CreatePolicyException(message, "Wrong effect: {} != Permit".format(effect), 400) @@ -252,7 +265,7 @@ def create_delegation_evidence(conf, access_token, request): # Check response query_data = response.json() if query_data: - if query_data['policy_token']: + if 'policy_token' in query_data: return { 'policy_token': query_data['policy_token'] } diff --git a/tests/pytest/test_createpolicy_handler.py b/tests/pytest/test_createpolicy_handler.py new file mode 100644 index 0000000..8cc6ae0 --- /dev/null +++ b/tests/pytest/test_createpolicy_handler.py @@ -0,0 +1,379 @@ +import pytest +import time, copy +from api import app + +from tests.pytest.util.config_handler import load_config +from tests.pytest.util.token_handler import build_signed_jwt + +from api.util.createpolicy_handler import extract_access_token +from api.util.createpolicy_handler import get_ar_token +from api.util.createpolicy_handler import check_create_delegation_evidence +from api.util.createpolicy_handler import create_delegation_evidence + +from api.exceptions.create_policy_exception import CreatePolicyException + +# Get AS config +as_config = load_config("tests/config/as.yml", app) +app.config['as'] = as_config + +# Dummy access token +ACCESS_TOKEN = 'gfgarhgrfha' + +# Token endpoint +TOKEN_ENDPOINT = as_config['ar']['token'] + +# Policy endpoint +POLICY_ENDPOINT = as_config['ar']['policy'] + +# Delegation endpoint +DELEGATION_ENDPOINT = as_config['ar']['delegation'] + +# Client EORI +CLIENT_EORI = 'EU.EORI.DEMARKETPLACE' + +# delegationEvidence template for client AR access +TEMPLATE_DELEGATION_EVIDENCE = { + 'delegationEvidence': { + 'notBefore': int(str(time.time()).split('.')[0]), + 'notOnOrAfter': int(str(time.time()).split('.')[0]) + 60000, + 'policyIssuer': as_config['client']['id'], + 'target': { + 'accessSubject': CLIENT_EORI + }, + 'policySets': [ + { + 'policies': [ + { + 'target': { + 'resource': { + 'type': "delegationEvidence", + 'identifiers': ["*"], + 'attributes': ["*"] + }, + 'actions': ["POST"] + }, + 'rules': [ + { + 'effect': "Permit" + } + ] + } + ] + } + ] + } +} + +# Tests for function extract_access_token(request) +class TestExtractAccessToken: + + @pytest.fixture + def mock_request_ok(self, mocker): + def headers_get(attr): + if attr == "Authorization": return "Bearer " + ACCESS_TOKEN + else: return None + request = mocker.Mock() + request.headers.get.side_effect = headers_get + return request + + @pytest.fixture + def mock_request_empty_token(self, mocker): + def headers_get(attr): + if attr == "Authorization": return "Bearer " + else: return None + request = mocker.Mock() + request.headers.get.side_effect = headers_get + return request + + @pytest.fixture + def mock_request_invalid_header(self, mocker): + def headers_get(attr): + if attr == "Authorization": return "Bearer " + ACCESS_TOKEN + " invalid" + else: return None + request = mocker.Mock() + request.headers.get.side_effect = headers_get + return request + + @pytest.fixture + def mock_request_no_bearer(self, mocker): + def headers_get(attr): + if attr == "Authorization": return ACCESS_TOKEN + else: return None + request = mocker.Mock() + request.headers.get.side_effect = headers_get + return request + + @pytest.fixture + def mock_request_no_headers(self, mocker): + def headers_get(attr): + return None + request = mocker.Mock() + request.headers.get.side_effect = headers_get + return request + + @pytest.mark.ok + @pytest.mark.it('should successfully extract access token') + def test_extract_ok(self, mock_request_ok): + + # Call function with request mock + token = extract_access_token(mock_request_ok) + assert token == ACCESS_TOKEN, "should return correct access token" + + @pytest.mark.failure + @pytest.mark.it('should fail due to missing Authorization header') + def test_extract_missing_header(self, mock_request_no_headers): + + # Call function with request mock + with pytest.raises(CreatePolicyException, match=r'Missing Authorization header'): + token = extract_access_token(mock_request_no_headers) + + @pytest.mark.failure + @pytest.mark.it('should fail due to missing Bearer in Authorization header') + def test_extract_missing_bearer(self, mock_request_no_bearer): + + # Call function with request mock + with pytest.raises(CreatePolicyException, match=r'Invalid Authorization header'): + token = extract_access_token(mock_request_no_bearer) + + @pytest.mark.failure + @pytest.mark.it('should fail due to invalid Authorization header') + def test_extract_invalid_header(self, mock_request_invalid_header): + + # Call function with request mock + with pytest.raises(CreatePolicyException, match=r'Invalid Authorization header'): + token = extract_access_token(mock_request_invalid_header) + + @pytest.mark.failure + @pytest.mark.it('should fail due to empty_token') + def test_extract_empty_token(self, mock_request_empty_token): + + # Call function with request mock + with pytest.raises(CreatePolicyException, match=r'Invalid Authorization header, empty token'): + token = extract_access_token(mock_request_empty_token) + + +# Tests for get_ar_token(conf) +class TestGetARToken: + + @pytest.fixture + def mock_post_token_ok(self, requests_mock): + return requests_mock.post(TOKEN_ENDPOINT, + json={ + 'access_token': ACCESS_TOKEN, + 'expires_in': 3600, + 'token_type': "Bearer" + }) + + @pytest.mark.ok + @pytest.mark.it('should successfully obtain access token') + def test_token_ok(self, mock_post_token_ok): + + # Call function + access_token = get_ar_token(as_config) + + # Asserts + assert access_token == ACCESS_TOKEN, "should return correct access_token" + +# Tests for check_create_delegation_evidence(conf, eori, access_token) +class TestCheckCreateDelegationEvidence: + + @pytest.fixture + def mock_post_delegation_ok(self, requests_mock): + payload = TEMPLATE_DELEGATION_EVIDENCE + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + + @pytest.fixture + def mock_post_no_delegation_evidence(self, requests_mock): + payload = TEMPLATE_DELEGATION_EVIDENCE + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationRequest", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + + @pytest.fixture + def mock_post_no_policy_sets(self, requests_mock): + payload = copy.deepcopy(TEMPLATE_DELEGATION_EVIDENCE) + payload['delegationEvidence'].pop('policySets', None) + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + + @pytest.fixture + def mock_post_empty_policies(self, requests_mock): + payload = copy.deepcopy(TEMPLATE_DELEGATION_EVIDENCE) + payload['delegationEvidence']['policySets'][0]['policies'] = [] + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + + @pytest.fixture + def mock_post_no_post_action(self, requests_mock): + payload = copy.deepcopy(TEMPLATE_DELEGATION_EVIDENCE) + payload['delegationEvidence']['policySets'][0]['policies'][0]['target']['actions'] = ["GET","PATCH"] + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + + @pytest.fixture + def mock_post_no_permit_rule(self, requests_mock): + payload = copy.deepcopy(TEMPLATE_DELEGATION_EVIDENCE) + payload['delegationEvidence']['policySets'][0]['policies'][0]['rules'][0] = { + 'effect': "Deny" + } + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + + @pytest.fixture + def mock_post_no_delegation_token(self, requests_mock): + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'empty_token': "EMPTY" + }) + + @pytest.mark.ok + @pytest.mark.it('should accept and throw no exception') + def test_check_ok(self, mock_post_delegation_ok): + + # Call function + try: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + except Exception as ex: + pytest.fail("should throw no exception: {}".format(ex)) + + @pytest.mark.failure + @pytest.mark.it('should throw exception about missing delegation_token') + def test_check_missing_token(self, mock_post_no_delegation_token): + + # Call function + with pytest.raises(CreatePolicyException, match=r'AR was not providing valid response') as ex: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + + @pytest.mark.failure + @pytest.mark.it('should throw exception about missing delegationEvidence') + def test_check_missing_delegation_evidence(self, mock_post_no_delegation_evidence): + + # Call function + with pytest.raises(CreatePolicyException, match=r'AR did not provide valid delegationEvidence to create policies.') as ex: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + + assert "Missing 'delegationEvidence' object" in ex.value.internal_msg, "should report correct error message" + + @pytest.mark.failure + @pytest.mark.it('should throw exception about missing policySets') + def test_check_no_policy_sets(self, mock_post_no_policy_sets): + + # Call function + with pytest.raises(CreatePolicyException, match=r'AR did not provide valid delegationEvidence to create policies.') as ex: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + + assert "Missing 'policySets'" in ex.value.internal_msg, "should report correct error message" + + @pytest.mark.failure + @pytest.mark.it('should throw exception about empty policies') + def test_check_empty_policies(self, mock_post_empty_policies): + + # Call function + with pytest.raises(CreatePolicyException, match=r'AR did not provide valid delegationEvidence to create policies.') as ex: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + + assert "Empty 'policies'" in ex.value.internal_msg, "should report correct error message" + + @pytest.mark.failure + @pytest.mark.it('should throw exception about missing POST action') + def test_check_missing_post_action(self, mock_post_no_post_action): + + # Call function + with pytest.raises(CreatePolicyException, match=r'AR did not provide valid delegationEvidence to create policies.') as ex: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + + assert "'actions' is missing 'POST'" in ex.value.internal_msg, "should report correct error message" + + @pytest.mark.failure + @pytest.mark.it('should throw exception about missing Permit rule') + def test_check_missing_permit_rule(self, mock_post_no_permit_rule): + + # Call function + with pytest.raises(CreatePolicyException, match=r'AR did not provide valid delegationEvidence to create policies.') as ex: + check_create_delegation_evidence(as_config, CLIENT_EORI, ACCESS_TOKEN) + + assert "Wrong effect: Deny != Permit" in ex.value.internal_msg, "should report correct error message" + +# Test create_delegation_evidence(conf, access_token, request) +class TestCreateDelegationEvidence: + + @pytest.fixture + def mock_post_policy_ok(self, requests_mock): + # Re-use template also for simulation of policy vreation + payload = TEMPLATE_DELEGATION_EVIDENCE + policy_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(POLICY_ENDPOINT, + json={ + 'policy_token': policy_token + }) + + @pytest.fixture + def mock_post_policy_empty_response(self, requests_mock): + return requests_mock.post(POLICY_ENDPOINT, + json={ }) + + @pytest.fixture + def mock_request_ok(self, mocker): + request = mocker.Mock() + request.json = TEMPLATE_DELEGATION_EVIDENCE + return request + + @pytest.fixture + def mock_request_no_payload(self, mocker): + request = mocker.Mock() + request.json = None + return request + + @pytest.mark.ok + @pytest.mark.it('should successfully create policy and return policy_token') + def test_policy_ok(self, mock_post_policy_ok, mock_request_ok): + + # Call function + try: + response = create_delegation_evidence(as_config, ACCESS_TOKEN, mock_request_ok) + except Exception as ex: + pytest.fail("should throw no exception: {}".format(ex)) + + # Asserts + assert 'policy_token' in response, "response should contain policy_token" + + @pytest.mark.ok + @pytest.mark.it('should successfully create policy and return empty response') + def test_policy_ok_empty_response(self, mock_post_policy_empty_response, mock_request_ok): + + # Call function + try: + response = create_delegation_evidence(as_config, ACCESS_TOKEN, mock_request_ok) + except Exception as ex: + pytest.fail("should throw no exception: {}".format(ex)) + + # Asserts + assert not response, "response should be empty" + + @pytest.mark.failure + @pytest.mark.it('should fail due to missing payload') + def test_policy_missing_payload(self, mock_request_no_payload): + + # Call function + with pytest.raises(CreatePolicyException, match=r'Missing payload in /createpolicy request') as ex: + response = create_delegation_evidence(as_config, ACCESS_TOKEN, mock_request_no_payload) + + diff --git a/tests/pytest/test_token_handler.py b/tests/pytest/test_token_handler.py index f31efde..0f1ceca 100644 --- a/tests/pytest/test_token_handler.py +++ b/tests/pytest/test_token_handler.py @@ -68,9 +68,6 @@ def mock_proxy_request_ok(mocker): @pytest.mark.it('should successfully forward token request') def test_forward_token_ok(mocker, mock_request_ok, mock_proxy_request_ok): - # Mock abort function - abort = mocker.Mock() - # Call function response = forward_token(mock_request_ok, app) @@ -85,9 +82,6 @@ def test_forward_token_ok(mocker, mock_request_ok, mock_proxy_request_ok): @pytest.mark.it('should fail due to missing attr') def test_forward_token_missing_attr(mocker, mock_request_missing_attr, mock_proxy_request_ok): - # Mock abort function - abort = mocker.Mock() - # Call function with pytest.raises(TokenException, match=r'Missing'): response = forward_token(mock_request_missing_attr, app) @@ -97,9 +91,6 @@ def test_forward_token_missing_attr(mocker, mock_request_missing_attr, mock_prox @pytest.mark.it('should fail due to missing client_id') def test_forward_token_missing_client_id(mocker, mock_request_missing_client_id, mock_proxy_request_ok): - # Mock abort function - abort = mocker.Mock() - # Call function with pytest.raises(TokenException, match=r'Missing client_id'): response = forward_token(mock_request_missing_client_id, app) From 3543cdc6d44cad45dd801171c5d47cef3d51c1a3 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Fri, 26 May 2023 12:22:38 +0200 Subject: [PATCH 08/16] Add missing module --- tests/pytest/util/token_handler.py | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/pytest/util/token_handler.py diff --git a/tests/pytest/util/token_handler.py b/tests/pytest/util/token_handler.py new file mode 100644 index 0000000..a1f65e2 --- /dev/null +++ b/tests/pytest/util/token_handler.py @@ -0,0 +1,42 @@ +import jwt +import time +import uuid + +# Retrieves x5c cert chain array from config string +def get_x5c_chain(cert): + sp = cert.split('-----BEGIN CERTIFICATE-----\n') + sp = sp[1:] + + ca_chain = [] + for ca in sp: + ca_sp = ca.split('\n-----END CERTIFICATE-----') + ca_chain.append(ca_sp[0].replace('\n','')) + + return ca_chain + +# Build JWT from response, encode and sign +def build_signed_jwt(conf, payload, payload_name, aud): + + # Build return object + result = {} + + result['iss'] = conf['client']['id'] + result['sub'] = conf['client']['id'] + result['aud'] = aud + + iat = int(str(time.time()).split('.')[0]) + exp = iat + 60000 + result['iat'] = iat + result['exp'] = exp + + result['jti'] = str(uuid.uuid4()) + + header = { + 'x5c': get_x5c_chain(conf['client']['crt']) + } + + # Append payload + result[payload_name] = payload + + token = jwt.encode(result, conf['client']['key'], algorithm="RS256", headers=header) + return token From caa47cffb92759589831cb9bcfd9e4e2a569a199 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Fri, 26 May 2023 13:05:22 +0200 Subject: [PATCH 09/16] Add tests for: * createpolicy endpoint * full iSHARE flow: get token and create policy --- tests/pytest/test_createpolicy.py | 197 ++++++++++++++++++++++++++++++ tests/pytest/test_policy_flow.py | 195 +++++++++++++++++++++++++++++ tests/pytest/test_token.py | 20 +-- 3 files changed, 404 insertions(+), 8 deletions(-) create mode 100644 tests/pytest/test_createpolicy.py create mode 100644 tests/pytest/test_policy_flow.py diff --git a/tests/pytest/test_createpolicy.py b/tests/pytest/test_createpolicy.py new file mode 100644 index 0000000..8246c94 --- /dev/null +++ b/tests/pytest/test_createpolicy.py @@ -0,0 +1,197 @@ +import pytest +import os, time +from api import app +from flask_sqlalchemy import SQLAlchemy + +from tests.pytest.util.config_handler import load_config +from tests.pytest.util.token_handler import build_signed_jwt + +# Get AS config +as_config = load_config("tests/config/as.yml", app) +app.config['as'] = as_config + +# Database +db = None +if not 'db' in app.config: + db_conf = as_config['db'] + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath + db = SQLAlchemy(app) + app.config['db'] = db +else: + db = app.config['db'] + +# Token endpoint +TOKEN_ENDPOINT = as_config['ar']['token'] + +# Policy endpoint +POLICY_ENDPOINT = as_config['ar']['policy'] + +# Delegation endpoint +DELEGATION_ENDPOINT = as_config['ar']['delegation'] + +# Dummy access token +ACCESS_TOKEN = 'gfgarhgrfha' + +# Client EORI +CLIENT_EORI = 'EU.EORI.DEMARKETPLACE' + +# Policy access subject +ACCESS_SUBJECT_EORI = "EU.EORI.DECONSUMERONE" + +# delegationEvidence template for policy to create +TEMPLATE_POLICY_CREATE = { + 'delegationEvidence': { + 'notBefore': int(str(time.time()).split('.')[0]), + 'notOnOrAfter': int(str(time.time()).split('.')[0]) + 60000, + 'policyIssuer': as_config['client']['id'], + 'target': { + 'accessSubject': ACCESS_SUBJECT_EORI + }, + 'policySets': [ + { + 'policies': [ + { + 'target': { + 'resource': { + 'type': "EntityType", + 'identifiers': ["id1"], + 'attributes': ["attr1", "attr2"] + }, + 'actions': ["POST","GET"] + }, + 'rules': [ + { + 'effect': "Permit" + } + ] + } + ] + } + ] + } +} + +# delegationEvidence template for client AR access +TEMPLATE_DELEGATION_EVIDENCE = { + 'delegationEvidence': { + 'notBefore': int(str(time.time()).split('.')[0]), + 'notOnOrAfter': int(str(time.time()).split('.')[0]) + 60000, + 'policyIssuer': as_config['client']['id'], + 'target': { + 'accessSubject': CLIENT_EORI + }, + 'policySets': [ + { + 'policies': [ + { + 'target': { + 'resource': { + 'type': "delegationEvidence", + 'identifiers': ["*"], + 'attributes': ["*"] + }, + 'actions': ["POST"] + }, + 'rules': [ + { + 'effect': "Permit" + } + ] + } + ] + } + ] + } +} + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + +@pytest.fixture +def clean_db(): + with app.app_context(): + from api.models import token + db.drop_all() + db.create_all() + +@pytest.fixture +def insert_token(): + with app.app_context(): + from api.models import token + db.drop_all() + db.create_all() + ar_token = token.Token( + eori=CLIENT_EORI, + access_token=ACCESS_TOKEN, + expires=int(time.time() * 1000) + 3600) + from api.util.db_handler import insert_token + insert_token(ar_token, db) + +@pytest.fixture +def mock_post_token_ok(requests_mock): + return requests_mock.post(TOKEN_ENDPOINT, + json={ + 'access_token': ACCESS_TOKEN+"xyz", + 'expires_in': 3600, + 'token_type': "Bearer" + }) + +@pytest.fixture +def mock_post_delegation_ok(requests_mock): + payload = TEMPLATE_DELEGATION_EVIDENCE + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + +@pytest.fixture +def mock_post_policy_ok(requests_mock): + payload = TEMPLATE_POLICY_CREATE + policy_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(POLICY_ENDPOINT, + json={ + 'policy_token': policy_token + }) + +@pytest.mark.ok +@pytest.mark.it('should successfully obtain access token') +def test_policy_ok(client, mock_post_token_ok, mock_post_delegation_ok, mock_post_policy_ok, insert_token): + + # Invoke request + headers = { + 'Authorization': "Bearer " + ACCESS_TOKEN + } + response = client.post("/createpolicy", json=TEMPLATE_POLICY_CREATE, headers=headers) + + # Asserts on response + assert mock_post_token_ok.called + assert mock_post_token_ok.call_count == 1 + assert mock_post_delegation_ok.called + assert mock_post_delegation_ok.call_count == 1 + assert mock_post_policy_ok.called + assert mock_post_policy_ok.call_count == 1 + assert 'policy_token' in response.json, 'Response should contain policy_token' + +@pytest.mark.failure +@pytest.mark.it('should fail due to invalid access_token') +def test_policy_invalid_access_token(client, mock_post_token_ok, mock_post_delegation_ok, mock_post_policy_ok, insert_token): + + # Invoke request + headers = { + 'Authorization': "Bearer " + ACCESS_TOKEN+"gsrfghhsh" + } + response = client.post("/createpolicy", json=TEMPLATE_POLICY_CREATE, headers=headers) + + # Asserts on response + assert not mock_post_token_ok.called + assert not mock_post_delegation_ok.called + assert not mock_post_policy_ok.called + assert response.status_code == 400, "should return code 400" + assert "No valid token has been provided" in response.json['description'], "should return correct error message" + + diff --git a/tests/pytest/test_policy_flow.py b/tests/pytest/test_policy_flow.py new file mode 100644 index 0000000..7cf4c58 --- /dev/null +++ b/tests/pytest/test_policy_flow.py @@ -0,0 +1,195 @@ +import pytest +import os, time +from api import app +from flask_sqlalchemy import SQLAlchemy + +from tests.pytest.util.config_handler import load_config +from tests.pytest.util.token_handler import build_signed_jwt + +# Get AS config +as_config = load_config("tests/config/as.yml", app) +app.config['as'] = as_config + +# Database +db = None +if not 'db' in app.config: + db_conf = as_config['db'] + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath + db = SQLAlchemy(app) + app.config['db'] = db +else: + db = app.config['db'] + +# Token endpoint +TOKEN_ENDPOINT = as_config['ar']['token'] + +# Policy endpoint +POLICY_ENDPOINT = as_config['ar']['policy'] + +# Delegation endpoint +DELEGATION_ENDPOINT = as_config['ar']['delegation'] + +# Dummy access token +ACCESS_TOKEN = 'gfgarhgrfha' + +# Client EORI +CLIENT_EORI = 'EU.EORI.DEMARKETPLACE' + +# Form parameters +REQ_FORM = { + 'client_id': CLIENT_EORI, + 'grant_type': 'client_credentials', + 'scope': 'iSHARE', + 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion': 'dfggrghaerhahahhahp' +} + +# Policy access subject +ACCESS_SUBJECT_EORI = "EU.EORI.DECONSUMERONE" + +# delegationEvidence template for policy to create +TEMPLATE_POLICY_CREATE = { + 'delegationEvidence': { + 'notBefore': int(str(time.time()).split('.')[0]), + 'notOnOrAfter': int(str(time.time()).split('.')[0]) + 60000, + 'policyIssuer': as_config['client']['id'], + 'target': { + 'accessSubject': ACCESS_SUBJECT_EORI + }, + 'policySets': [ + { + 'policies': [ + { + 'target': { + 'resource': { + 'type': "EntityType", + 'identifiers': ["id1"], + 'attributes': ["attr1", "attr2"] + }, + 'actions': ["POST","GET"] + }, + 'rules': [ + { + 'effect': "Permit" + } + ] + } + ] + } + ] + } +} + +# delegationEvidence template for client AR access +TEMPLATE_DELEGATION_EVIDENCE = { + 'delegationEvidence': { + 'notBefore': int(str(time.time()).split('.')[0]), + 'notOnOrAfter': int(str(time.time()).split('.')[0]) + 60000, + 'policyIssuer': as_config['client']['id'], + 'target': { + 'accessSubject': CLIENT_EORI + }, + 'policySets': [ + { + 'policies': [ + { + 'target': { + 'resource': { + 'type': "delegationEvidence", + 'identifiers': ["*"], + 'attributes': ["*"] + }, + 'actions': ["POST"] + }, + 'rules': [ + { + 'effect': "Permit" + } + ] + } + ] + } + ] + } +} + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + +@pytest.fixture +def clean_db(): + with app.app_context(): + from api.models import token + db.drop_all() + db.create_all() + +@pytest.fixture +def mock_post_token_ok(requests_mock): + return requests_mock.post(TOKEN_ENDPOINT, + json={ + 'access_token': ACCESS_TOKEN, + 'expires_in': 3600, + 'token_type': "Bearer" + }) + +@pytest.fixture +def mock_post_delegation_ok(requests_mock): + payload = TEMPLATE_DELEGATION_EVIDENCE + delegation_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(DELEGATION_ENDPOINT, + json={ + 'delegation_token': delegation_token + }) + +@pytest.fixture +def mock_post_policy_ok(requests_mock): + payload = TEMPLATE_POLICY_CREATE + policy_token = build_signed_jwt(as_config, payload['delegationEvidence'], "delegationEvidence", CLIENT_EORI) + return requests_mock.post(POLICY_ENDPOINT, + json={ + 'policy_token': policy_token + }) + +@pytest.mark.ok +@pytest.mark.it('should successfully obtain a token and create the policy') +def test_policy_flow_ok(client, mock_post_token_ok, mock_post_delegation_ok, mock_post_policy_ok, clean_db): + + # Invoke request for /token + response = client.post(TOKEN_ENDPOINT, data=REQ_FORM) + + # Asserts on response + assert mock_post_token_ok.called + assert mock_post_token_ok.call_count == 1 + assert 'access_token' in response.json, 'Response should contain access_token' + assert response.json['access_token'] == ACCESS_TOKEN, "should have correct access token" + assert response.json['expires_in'] == 3600, "should have correct expiration period" + + # Get and assert DB entry + with app.app_context(): + from api.models.token import Token + db_token = Token.query.filter_by(eori=CLIENT_EORI).first() + assert db_token.eori == CLIENT_EORI, "DB entry should have correct EORI" + assert db_token.access_token == ACCESS_TOKEN, "DB entry should have correct access token" + + # Get access token + access_token = response.json['access_token'] + + # Invoke request for /createpolicy + headers = { + 'Authorization': "Bearer " + ACCESS_TOKEN + } + response = client.post("/createpolicy", json=TEMPLATE_POLICY_CREATE, headers=headers) + + # Asserts on response + assert mock_post_token_ok.called + assert mock_post_token_ok.call_count == 2 + assert mock_post_delegation_ok.called + assert mock_post_delegation_ok.call_count == 1 + assert mock_post_policy_ok.called + assert mock_post_policy_ok.call_count == 1 + assert 'policy_token' in response.json, 'Response should contain policy_token' + diff --git a/tests/pytest/test_token.py b/tests/pytest/test_token.py index c831e82..1526751 100644 --- a/tests/pytest/test_token.py +++ b/tests/pytest/test_token.py @@ -10,12 +10,16 @@ app.config['as'] = as_config # Database -db_conf = as_config['db'] -basedir = os.path.abspath(os.path.dirname(__file__)) -dbpath = os.path.join(basedir, db_conf['useFile']) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath -db = SQLAlchemy(app) -app.config['db'] = db +db = None +if not 'db' in app.config: + db_conf = as_config['db'] + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath + db = SQLAlchemy(app) + app.config['db'] = db +else: + db = app.config['db'] # Token endpoint TOKEN_ENDPOINT = as_config['ar']['token'] @@ -62,7 +66,7 @@ def mock_post_token_ok(requests_mock): def test_token_ok(client, mock_post_token_ok, clean_db): # Invoke request - response = client.post(TOKEN_ENDPOINT, data=REQ_FORM) + response = client.post("/token", data=REQ_FORM) # Asserts on response assert mock_post_token_ok.called @@ -88,7 +92,7 @@ def test_token_missing_id(client, mock_post_token_ok, clean_db): form.pop('client_id', None) # Invoke request - response = client.post(TOKEN_ENDPOINT, data=form) + response = client.post("/token", data=form) # Asserts on response assert response.status_code == 400, "should return code 400" From 2d2c508d592d64d9d95ea8dbf2712da09c736bd9 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Fri, 26 May 2023 13:13:31 +0200 Subject: [PATCH 10/16] Add push to quay.io --- .github/workflows/prerelease.yml | 7 +++++++ .github/workflows/release.yml | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 803582d..b6c5511 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -11,6 +11,7 @@ on: env: IMAGE_NAME: i4trust/activation-service + IMAGE_NAME_QUAY: quay.io/fiware/ishare-satellite jobs: @@ -79,6 +80,12 @@ jobs: docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} . docker push ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + - name: Push to quay.io + run: | + echo "${{ secrets.QUAY_TOKEN }}" | docker login quay.io -u "${{ secrets.QUAY_USERNAME }}" --password-stdin + docker tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} ${{ env.IMAGE_NAME_QUAY }}:${{ env.IMAGE_TAG }} + docker push ${{ env.IMAGE_NAME_QUAY }}:${{ env.IMAGE_TAG }} + - uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fda34e3..9b4a1c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,8 +47,12 @@ jobs: run: | docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} . docker push ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - docker tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} ${{ env.IMAGE_NAME }}:latest - docker push ${{ env.IMAGE_NAME }}:latest + + - name: Push to quay.io + run: | + echo "${{ secrets.QUAY_TOKEN }}" | docker login quay.io -u "${{ secrets.QUAY_USERNAME }}" --password-stdin + docker tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} ${{ env.IMAGE_NAME_QUAY }}:${{ env.IMAGE_TAG }} + docker push ${{ env.IMAGE_NAME_QUAY }}:${{ env.IMAGE_TAG }} - uses: "marvinpinto/action-automatic-releases@latest" with: From 654675e070026e2e60ca39bcce8b1b32daa81673 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Fri, 26 May 2023 13:17:13 +0200 Subject: [PATCH 11/16] Fix quay image name --- .github/workflows/prerelease.yml | 2 +- .github/workflows/release.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b6c5511..7cddf9a 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -11,7 +11,7 @@ on: env: IMAGE_NAME: i4trust/activation-service - IMAGE_NAME_QUAY: quay.io/fiware/ishare-satellite + IMAGE_NAME_QUAY: quay.io/i4trust/activation-service jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b4a1c8..cd39608 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: env: IMAGE_NAME: i4trust/activation-service + IMAGE_NAME_QUAY: quay.io/i4trust/activation-service jobs: From 513f8f9466337d3051e6565d4e18d7be5c6800af Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Fri, 26 May 2023 14:23:36 +0200 Subject: [PATCH 12/16] Add in-memory option for DB. Allow to configure filepath for file-based sqlite. --- Dockerfile | 6 ++++++ bin/run.sh | 2 -- config/as.yml | 10 ++++++++-- tests/config/as.yml | 8 ++++++-- tests/pytest/test_createpolicy.py | 2 +- tests/pytest/test_policy_flow.py | 2 +- tests/pytest/test_token.py | 2 +- wsgi.py | 9 +++++++-- 8 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index bd2a67c..50c2a08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,16 @@ ENV AS_PORT=8080 RUN apk update && \ apk add gcc build-base libc-dev libffi-dev openssl-dev bash curl +RUN addgroup --gid 5000 aservice \ + && adduser --uid 5000 -G aservice -D -s /bin/sh -k /dev/null aservice + WORKDIR /var/aservice COPY ./ ./ RUN pip install --no-cache-dir -r requirements.txt +USER aservice +WORKDIR /var/aservice + HEALTHCHECK CMD curl --fail http://localhost:${AS_PORT}/health || exit 1 EXPOSE $AS_PORT CMD [ "./bin/run.sh" ] diff --git a/bin/run.sh b/bin/run.sh index 359e309..a338bd9 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -id - # Port AS_PORT="${AS_PORT:-8080}" diff --git a/config/as.yml b/config/as.yml index 65f295e..3efe24b 100644 --- a/config/as.yml +++ b/config/as.yml @@ -11,8 +11,14 @@ client: # Configuration of database db: - # Use sqlite file database - useFile: "as.db" + # Use sqlite file database (make sure that the volume is writeable) + #useFile: + # # Filename for DB SQLite file + # filename: "as.db" + # # If empty, will use app base path + # filepath: "" + # Use sqlite in-memory database + useMemory: true # Use URI to external DB (e.g., MySQL, PostgreSQL) #useURI: "" # Enable tracking of modifications diff --git a/tests/config/as.yml b/tests/config/as.yml index 2fe9349..6a1d69f 100644 --- a/tests/config/as.yml +++ b/tests/config/as.yml @@ -168,8 +168,12 @@ client: # Configuration of SQLite database db: - # Use sqlite file database - useFile: "as.db" + # Use sqlite file database (make sure that there is write access) + useFile: + filename: "as.db" + filepath: "" + # Use sqlite in-memory database + #useMemory: true # Use URI to external DB (e.g., MySQL, PostgreSQL) #useURI: "" # Enable tracking of modifications diff --git a/tests/pytest/test_createpolicy.py b/tests/pytest/test_createpolicy.py index 8246c94..8f04f40 100644 --- a/tests/pytest/test_createpolicy.py +++ b/tests/pytest/test_createpolicy.py @@ -15,7 +15,7 @@ if not 'db' in app.config: db_conf = as_config['db'] basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']) + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath db = SQLAlchemy(app) app.config['db'] = db diff --git a/tests/pytest/test_policy_flow.py b/tests/pytest/test_policy_flow.py index 7cf4c58..4672a70 100644 --- a/tests/pytest/test_policy_flow.py +++ b/tests/pytest/test_policy_flow.py @@ -15,7 +15,7 @@ if not 'db' in app.config: db_conf = as_config['db'] basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']) + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath db = SQLAlchemy(app) app.config['db'] = db diff --git a/tests/pytest/test_token.py b/tests/pytest/test_token.py index 1526751..f0ae97a 100644 --- a/tests/pytest/test_token.py +++ b/tests/pytest/test_token.py @@ -14,7 +14,7 @@ if not 'db' in app.config: db_conf = as_config['db'] basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']) + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath db = SQLAlchemy(app) app.config['db'] = db diff --git a/wsgi.py b/wsgi.py index 458b58a..90680cb 100644 --- a/wsgi.py +++ b/wsgi.py @@ -40,9 +40,14 @@ if os.environ.get('AS_DATABASE_URI'): app.logger.info("... taking URI from ENV...") app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('AS_DATABASE_URI') -elif 'useFile' in db_conf and db_conf['useFile'] and len(db_conf['useFile']) > 0: +if 'useMemory' in db_conf and db_conf['useMemory']: + app.logger.info("... using in-memory SQLite ...") + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' +elif 'useFile' in db_conf and db_conf['useFile'] and 'filename' in db_conf['useFile']: basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']) + if 'filepath' in db_conf['useFile'] and len(db_conf['useFile']['filepath']) > 0: + basedir = db_conf['useFile']['filepath'] + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) app.logger.info("... using file-based SQLite (" + dbpath + ") ...") app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath elif 'useURI' in db_conf and db_conf['useURI'] and len(db_conf['useURI']) > 0: From 593ff111859b1d2e4e935e9632baa8d77c6b3304 Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Tue, 30 May 2023 10:04:14 +0200 Subject: [PATCH 13/16] Adapt Readme --- README.md | 90 ++++++++++++++++++++++--------- tests/pytest/test_createpolicy.py | 11 ++-- tests/pytest/test_policy_flow.py | 11 ++-- tests/pytest/test_token.py | 11 ++-- 4 files changed, 87 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 8af28fc..23af6d0 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,80 @@ # activation-service -Service allowing to activate services and create policies in an iSHARE authorisation registry during the acquisition step. +Service allowing to activate services and create access rights during the acquisition step via: +* creating policies in an iSHARE authorisation registry +* creating entries at a trusted issuer list (TBD) -## Configuration +It is based on Python Flask using gunicorn. The service requires to store data in an SQL database. +It ca be configured to use external databases (e.g., MySQL, PostgreSQL) or SQLite. + + +## Preparation + +Requirements: +* python >= 3.7 +* [./requirements.txt](./requirements.txt) + +Required python modules can be installed with +```shell +pip install -r requirements.txt +``` + + + +### Configuration Configuration is done in the file `config/as.yml`. You need to modify the values according to your -environment and add your private key and certificate chain. +environment and add your private key and certificate chain for the iSHARE flow. Private key and certificate chain can be also provided as ENVs as given below. In this case, the values from `config/as.yml` would be overwritten. * Private key: `AS_CLIENT_KEY` * Certificate chain: `AS_CLIENT_CRT` -In case of very large JWTs in the `Authorization` header, one needs to increase the max. HTTP header size -of the node server application. This can be done by setting the following ENV (here: max. `32kb`): -* `AS_MAX_HEADER_SIZE=32768` (Default: 8192) +In case of very large JWTs in the Authorization header, one needs to increase the max. HTTP header size of +gunicorn. This can be done by setting the following ENV (here: max. 32kb): + +* `AS_MAX_HEADER_SIZE=32768` (Default: 32768) + +WHen using a flie-based SQLite, make sure that the volume is writeable. + +Further ENVs control the execution of the activation service. Below is a list of the supported ENVs: + +| ENV | Default | Description | +|:---------------------------------------|:------------:|:------------| +| AS_PORT | 8080 | Listen port | +| AS_GUNICORN_WORKERS | 1 | Number of workers that should be created (note that multiple workers can result in conflicts when using in-memory or file-based databases) | +| AS_MAX_HEADER_SIZE | 32768 | Maximum header size in bytes | +| AS_LOG_LEVEL | 'info' | Log level | +| AS_DATABASE_URI | | Database URI to use instead of config from configuration file | +| AS_CLIENT_KEY | | iSHARE private key provided as ENV (compare to [config/as.yml](./config/as.yml#L8)) | +| AS_CLIENT_CERTS | | iSHARE certificate chain provided as ENV (compare to [config/as.yml](./config/as.yml#L10)) | ## Usage ### Local -Run locally using `node.js`: +After placing a configuration file at `config/as.yml`, the activation service can be started with ```shell -npm install -npm start +bin/run.sh ``` ### Docker -A Dockerfile is provided for building the image: -```shell -docker build -t activation-service:my-tag . -``` +A Dockerfile is provided to build a docker image. Releases automatically create Docker images +at [DockerHub](https://hub.docker.com/r/i4trust/activation-service) and +[quay.io](https://quay.io/repository/i4trust/activation-service). -Make a copy of the configuration file `config/as.yml` and modify according to your environment. -Then run the image: +Using Docker, the activation service can be run with: ```shell -docker run --rm -it -p 7000:7000 -v /as.yml:/home/aservice/config/as.yml activation-service:my-tag +docker run --rm -p 8080:8080 -v $PWD/config/as.yml:/var/aservice/config/as.yml quay.io/i4trust/activation-service:{RELEASE} ``` +To enable DEBUG output, add the ENV: +* `-e "AS_LOG_LEVEL=DEBUG"` + + ### Kubernetes A Helm chart is provided on [GitHub](https://github.com/i4Trust/helm-charts/tree/main/charts/activation-service) @@ -50,24 +85,31 @@ and [Artifacthub](https://artifacthub.io/packages/helm/i4trust/activation-servic ## Endpoints * `/health`: Get health output of web server -* `/token`: Forwards a token request to the `/token` endpoint at the locally configured authorisation registry -* `/createpolicy`: Activates the service by creating a policy at the locally configured authorisation registry +* `/token`: Forwards a token request to the `/token` endpoint at the locally configured authorisation registry (iSHARE flow) +* `/createpolicy`: Activates the service by creating a policy at the locally configured authorisation registry (iSHARE flow) ## Extend -This version just allows to create policies at the local authorisation registry when the `/createpolicy` endpoint -is called. +This version just allows to create policies at the local authorisation registry or entries at a trusted issuer list +during acquisition/activation. However, depending on the service provided, it might be needed that further steps are required when activating -a service, e.g. booting worker nodes or adding other resources. Such steps could be added as additional -modules in the `./activation/` folder and be integrated in the `/createpolicy` endpoint implementation -in `server.js`. +a service, e.g. booting worker nodes or adding other resources. Such steps require to extend this activation service +adding the necessary steps into the execution chain of the corresponding route. ## Debug Enable debugging by setting the environment variable: ```shell -DEBUG="as:*" +AS_LOG_LEVEL=DEBUG" +``` + + +## Tests + +Tests can be run with `pytest` via +```shell +pytest ``` diff --git a/tests/pytest/test_createpolicy.py b/tests/pytest/test_createpolicy.py index 8f04f40..3ef11b7 100644 --- a/tests/pytest/test_createpolicy.py +++ b/tests/pytest/test_createpolicy.py @@ -13,10 +13,13 @@ # Database db = None if not 'db' in app.config: - db_conf = as_config['db'] - basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']['filename']) - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath + if os.environ.get('AS_DATABASE_URI'): + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('AS_DATABASE_URI') + else: + db_conf = as_config['db'] + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath db = SQLAlchemy(app) app.config['db'] = db else: diff --git a/tests/pytest/test_policy_flow.py b/tests/pytest/test_policy_flow.py index 4672a70..e224591 100644 --- a/tests/pytest/test_policy_flow.py +++ b/tests/pytest/test_policy_flow.py @@ -13,10 +13,13 @@ # Database db = None if not 'db' in app.config: - db_conf = as_config['db'] - basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']['filename']) - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath + if os.environ.get('AS_DATABASE_URI'): + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('AS_DATABASE_URI') + else: + db_conf = as_config['db'] + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath db = SQLAlchemy(app) app.config['db'] = db else: diff --git a/tests/pytest/test_token.py b/tests/pytest/test_token.py index f0ae97a..bec27ab 100644 --- a/tests/pytest/test_token.py +++ b/tests/pytest/test_token.py @@ -12,10 +12,13 @@ # Database db = None if not 'db' in app.config: - db_conf = as_config['db'] - basedir = os.path.abspath(os.path.dirname(__file__)) - dbpath = os.path.join(basedir, db_conf['useFile']['filename']) - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath + if os.environ.get('AS_DATABASE_URI'): + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('AS_DATABASE_URI') + else: + db_conf = as_config['db'] + basedir = os.path.abspath(os.path.dirname(__file__)) + dbpath = os.path.join(basedir, db_conf['useFile']['filename']) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + dbpath db = SQLAlchemy(app) app.config['db'] = db else: From 0d1e4927242f0477f0410ea6e494373331cedb4c Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Tue, 30 May 2023 10:10:53 +0200 Subject: [PATCH 14/16] Fix ENV name, fix typos --- README.md | 2 +- api/util/createpolicy_handler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 23af6d0..bbed433 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ gunicorn. This can be done by setting the following ENV (here: max. 32kb): * `AS_MAX_HEADER_SIZE=32768` (Default: 32768) -WHen using a flie-based SQLite, make sure that the volume is writeable. +When using a file-based SQLite, make sure that the volume is writeable. Further ENVs control the execution of the activation service. Below is a list of the supported ENVs: diff --git a/api/util/createpolicy_handler.py b/api/util/createpolicy_handler.py index f2cb573..dc93a2c 100644 --- a/api/util/createpolicy_handler.py +++ b/api/util/createpolicy_handler.py @@ -7,10 +7,10 @@ from api.exceptions.create_policy_exception import CreatePolicyException # ENV for PRIVATE_KEY -ENV_PRIVATE_KEY = "AS_KEY" +ENV_PRIVATE_KEY = "AS_CLIENT_KEY" # ENV for certificate chain -ENV_CERTIFICATES = "AS_CERTS" +ENV_CERTIFICATES = "AS_CLIENT_CERTS" # Obtain private key from yaml or ENV def _get_private_key(config): From 0ac7c4ce0bff86a9518cd6255e1bcc35c999978b Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Tue, 30 May 2023 11:11:07 +0200 Subject: [PATCH 15/16] DIsable dropping of tables on startup --- wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsgi.py b/wsgi.py index 90680cb..2a8dded 100644 --- a/wsgi.py +++ b/wsgi.py @@ -69,7 +69,7 @@ with app.app_context(): app.logger.info("Creating database tables...") from api.models import token - db.drop_all() # TODO: Make configurable + #db.drop_all() # TODO: Make configurable db.create_all() app.logger.info("... database tables created") From 8240bed523c23bc7768a855b4a9feb6252bd162c Mon Sep 17 00:00:00 2001 From: Dennis Wendland Date: Tue, 30 May 2023 12:57:58 +0200 Subject: [PATCH 16/16] Remove debug print --- api/token.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/token.py b/api/token.py index ee8c910..3239041 100644 --- a/api/token.py +++ b/api/token.py @@ -38,7 +38,6 @@ def index(): access_token=auth_data['access_token'], expires=int(time.time() * 1000) + (1000*auth_data['expires_in'])) except Exception as bex: - print(bex) current_app.logger.error("Internal error when building Token object for DB insert: {}".format(ex)) abort(500, "Internal error") current_app.logger.debug('Received access token with data: {}'.format(ar_token))