From 008ec0c8896948615306dd50bd1f8346d05f342d Mon Sep 17 00:00:00 2001 From: Thomas Diepenbrock Date: Wed, 20 May 2020 17:20:20 -0400 Subject: [PATCH 1/9] WIP changes to test/deploy --- package.json | 6 +- server.js | 22 +++- src/koop/authMarkLogic.js | 74 +++++++++++++ src/koop/dbClientManager.js | 131 +++++++++++++++++++++++ src/koop/marklogic.js | 8 +- src/koop/query.js | 6 +- src/koop/tokenMarkLogicAuthentication.js | 106 ++++++++++++++++++ 7 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 src/koop/authMarkLogic.js create mode 100644 src/koop/dbClientManager.js create mode 100644 src/koop/tokenMarkLogicAuthentication.js diff --git a/package.json b/package.json index 98d950c..249322b 100755 --- a/package.json +++ b/package.json @@ -14,7 +14,11 @@ "koop": "3.11.0", "featureserver": "2.17.0", "@koopjs/auth-direct-file": "^2.0.0", - "marklogic": "^2.2.0" + "marklogic": "^2.2.0", + "jsonwebtoken":"^8.5.1", + "node-cache":"^5.1.0", + "yakaa":"^1.0.1", + "uuid":"^8.0.0" }, "author": "MarkLogic", "license": "Apache-2.0", diff --git a/server.js b/server.js index ab4e7da..cc24cb3 100755 --- a/server.js +++ b/server.js @@ -15,6 +15,9 @@ const express = require('express'); const http = require('http'); const proxy = require('./src/koop/proxy'); const Koop = require('koop'); +//Important to require dbClientManager case sensitively as "dbClientManager" +//to get the node module cache to effectively make this be a singleton. +const dbClientManager = require('./src/koop/dbClientManager'); const koop = new Koop(); // Configure the auth plugin by executing its exported function with required args @@ -22,14 +25,27 @@ if (config.auth && config.auth.enabled) { let auth = null; if (config.auth.plugin === 'auth-direct-file') { auth = require('@koopjs/auth-direct-file')(config.auth.options.secret, config.auth.options.identityStore, config.auth.options); - } else { - throw new Error(`auth plugin ${config.auth.plugin} not recognized`); + dbClientManager.useStaticClient(true); + } else if (config.auth.plugin === 'auth-marklogic-digest-basic') { + auth = require("./authMarkLogic")(config.auth.options); + } else if (config.auth.plugin) { + //if it's something we don't recognize, try to require it by plugin name and pass in the options object + //if this provider wants to use the static client, it will have to call dbClientManager.useStaticClient() + //itself in the exported function. + try { + auth = require(config.auth.plugin)(config.auth.options); + } + catch(err) { + throw new Error(`auth plugin ${config.auth.plugin} not recognized`); + } } if (auth) { koop.register(auth); } -} +} else { + //if there's no auth provider configured, we have to use a direct pre-authenticated db client + dbClientManager.useStaticClient(true); // install the Marklogic Provider const provider = require('./src/koop'); diff --git a/src/koop/authMarkLogic.js b/src/koop/authMarkLogic.js new file mode 100644 index 0000000..f675b02 --- /dev/null +++ b/src/koop/authMarkLogic.js @@ -0,0 +1,74 @@ +const log = require('./logger'); + +let _tokenMarklogicAuthentication; + +function auth(options) { + _tokenMarklogicAuthentication = require("./tokenMarkLogicAuthentication")(options); + return { + type:'auth', + authenticationSpecification, + authenticate, + authorize + }; +} + +function authenticationSpecification() { + return { + useHttp: true + } +}; + +function authenticate(req) { + log.debug("called authenticate"); + + return new Promise((resolve, reject) => { + let inputUsername; + let inputPassword; + + if (req && req.query) { + inputUsername = req.query.username; + inputPassword = req.query.password; + } + + log.debug("username:"); + log.debug(inputUsername); + log.debug("password:"); + log.debug(inputPassword); + + // Validate user's credentials + return _tokenMarklogicAuthentication.validateCredentials(req, inputUsername, inputPassword, resolve, reject); + }) +}; + +function authorize(req) { + return _tokenMarklogicAuthentication.authorize(req); +} +/* +function authorize(req) { + log.debug("called authorize"); + return new Promise((resolve, reject) => { + let token; + //if (req && req.query && req.query.token) token = req.query.token; + if (req && req.query && req.query.token) token = req.query.token; + if ((req && req.headers && req.headers.authorization)) token = req.headers.authorization; + if (!token) { + let err = new Error('No authorization token.') + err.code = 401 + reject(err) + } + // Verify token with async decoded function + jwt.verify(token, _secret, function (err, decoded) { + // If token invalid, reject + if (err) { + err.code = 401; + reject(err); + } + // Resolve the decoded token (an object) + resolve(decoded); + }); + }); +} +*/ + + +module.exports=auth; \ No newline at end of file diff --git a/src/koop/dbClientManager.js b/src/koop/dbClientManager.js new file mode 100644 index 0000000..9344803 --- /dev/null +++ b/src/koop/dbClientManager.js @@ -0,0 +1,131 @@ +/** +* This module is intended to be a singleton manager of marklogic databaseClient +* instances. It's important to always require it the same way (case sensitive) in order +* for node's module caching to make it effectively a singleton, and to also track any +* changes to node's implementation and module caching behavior. +*/ +const config = require('config'); +const marklogic = require('marklogic'); +const http = require('http'); +const Agent = require('yakaa'); +const log = require('./logger'); +const NodeCache = require( "node-cache" ); + +let _dbClientCache = new NodeCache({useClones:false}); +let _staticClient = null; +let _keepAliveAgent = new Agent({ keepAlive: true }); +let _useStaticClient = false; + +const dbc = { + getDBClient, + useStaticClient, + connectClient, + ensureClientConnected + }; + +function getDBClient(username) { + + if (config.auth == null || _useStaticClient) { + log.debug("getting static db client"); + if (_staticClient == null) { + log.debug("creating static dbClient"); + let connectionParams = JSON.parse(JSON.stringify(config.marklogic.connection)); + connectionParams.agent = _keepAliveAgent; + _staticClient = marklogic.createDatabaseClient(connectionParams); + } + return _staticClient; + } + log.debug("getting dbClient for username " + username); + return _dbClientCache.get(username); +} + +function ensureClientConnected(username, password) { + if (config.auth == null || _useStaticClient) { + return true; + } + if (_dbClientCache.has(username)) { + return true; + } + else { + return connectClient(username, password).then(response => { + log.debug("ensureClientConnected connectClient() result: "); + log.debug(response); + return response.authenticated; + }).catch(err => { + log.debug("ensureClientConnected connectClient() error: "); + log.debug(err); + return false; + }); + } +} + +function useStaticClient(staticClientEnabled) { + if (staticClientEnabled == true) { + _useStaticClient = true; + } else { + _useStaticClient = false; + } +} + +function checkDbClientConnection(db, username) { + let connectionInfo = {}; + return new Promise((resolve, reject) => { + db.checkConnection().result(function(response) { + log.debug("db.checkConnection result:"); + log.debug(response); + if (response.connected) { + log.debug("response is connected"); + connectionInfo.authenticated = true; + log.debug("setting db client in cache..."); + _dbClientCache.set(username, db); + resolve(connectionInfo); + } + else { + log.debug("response is not connected"); + connectionInfo.authenticated = false; + connectionInfo.httpStatusMessage = response.httpStatusMessage; + connectionInfo.httpStatusCode = response.httpStatusCode; + log.debug("clearing db client from cache..."); + _dbClientCache.del(username); + log.debug("resolving connectionInfo:"); + log.debug(connectionInfo); + resolve(connectionInfo); + } + }, + function(error) { + log.debug("error occurred:"); + log.error(error); + connectionInfo.error = true; + connectionInfo.errorMsg = error.message; + _dbClientCache.del(username); + log.debug("resolving connectionInfo:"); + log.debug(connectionInfo); + resolve(connectionInfo); + }).catch(error => { + log.error(error); + //throw new Error("Error connecting client"); + log.debug("rejecting connectionInfo:"); + log.debug(connectionInfo); + reject(connectionInfo); + }) + }); +} + +function connectClient(username, password) { + //we make a deep copy of the connection params so we don't leak + //usernames/passwords across requests and so we can specify the + //agent. Setting the agent to the global agent makes the + //marklogic.createDatabaseClient() call as well as the database client + //itself both lightweight + let connectionParams = JSON.parse(JSON.stringify(config.marklogic.connection)); + connectionParams.user = username; + connectionParams.password = password; + connectionParams.agent = _keepAliveAgent; + + log.debug("creating dbClient..."); + let db = marklogic.createDatabaseClient(connectionParams); + log.debug (db) + return checkDbClientConnection(db,username); +} + +module.exports=dbc; diff --git a/src/koop/marklogic.js b/src/koop/marklogic.js index a422ab3..632e1a2 100755 --- a/src/koop/marklogic.js +++ b/src/koop/marklogic.js @@ -7,6 +7,10 @@ const config = require('config'); const options = require('winnow/dist/options'); const log = require('./logger'); + + //Important to require dbClientManager as exactly "./dbClientManager" +//to get the node module cache to make this be a singleton. +const dbClientManager = require("./dbClientManager"); const MarkLogicQuery = require('./query'); function MarkLogic () {} @@ -43,8 +47,10 @@ MarkLogic.prototype.getData = function getData (req, callback) { log.debug("provider request: ", providerRequest); + let dbClient = dbClientManager.getDBClient(req.marklogicUsername); + var mq = new MarkLogicQuery(); - mq.providerGetData(providerRequest) + mq.providerGetData(providerRequest, dbClient) .then(data => { logResult(data); callback(null, data); diff --git a/src/koop/query.js b/src/koop/query.js index 374bb4d..0ca1ebc 100644 --- a/src/koop/query.js +++ b/src/koop/query.js @@ -7,13 +7,11 @@ const config = require('config'); const log = require('./logger'); function MarkLogicQuery() { - this.conn = config.marklogic.connection; - this.db = marklogic.createDatabaseClient(this.conn); } -MarkLogicQuery.prototype.providerGetData = function providerGetData(request) { +MarkLogicQuery.prototype.providerGetData = function providerGetData(request, dbClient) { return new Promise((resolve, reject) => { - this.db.resources.post({ + dbClient.resources.post({ name: 'geoQueryService', params: { }, documents : request diff --git a/src/koop/tokenMarkLogicAuthentication.js b/src/koop/tokenMarkLogicAuthentication.js new file mode 100644 index 0000000..150ac5a --- /dev/null +++ b/src/koop/tokenMarkLogicAuthentication.js @@ -0,0 +1,106 @@ +const jwt = require('jsonwebtoken'); +const log = require('./logger'); +const { v4: uuidv4 } = require('uuid'); + +//Important to require dbClientManager as exactly "./dbClientManager" +//to get the node module cache to make this be a singleton. +const dbClientManager = require("./dbClientManager"); + +function tokenMarkLogicAuthentication(options) { + options = options || {}; + this._secret = options.secret || uuidv4(); + this._tokenExpirationMinutes = options.tokenExpirationMintes || 60; + return { + authorize, + validateCredentials + } +} + +function authorize(req) { + log.debug("called authorize"); + return new Promise((resolve, reject) => { + let token; + //if (req && req.query && req.query.token) token = req.query.token; + if (req && req.query && req.query.token) token = req.query.token; + if ((req && req.headers && req.headers.authorization)) token = req.headers.authorization; + if (!token) { + let tokenErr = new Error('No authorization token.') + tokenErr.code = 401 + reject(tokenErr) + } + // Verify token with async decoded function + jwt.verify(token, _secret, function (err, decoded) { + // If token invalid, reject + if (err) { + log.debug(err); + err.code = 403; + reject(err); + } + log.debug("setting req.marklogicUsername to " + decoded.sub); + let username = decoded.sub; + req.marklogicUsername = username; + + //now we need to make sure the client didn't get booted out of cache + if (dbClientManager.getDBClient(username)) { + log.debug("authorize successful: dbClient previusly connected"); + // Resolve the decoded token (an object) + resolve(decoded); + } + else { + //if it did get booted out, we can't talk to ML until the + //client re-authenticates; reject with a 401 + reject({message:"Please re-authenticate", code:401}); + } + }); + }); +} + +function validateCredentials(req, username, password, resolve, reject) { + log.debug("calling dbClientManager.connectClient()..."); + dbClientManager.connectClient(username, password).then(response => { + log.debug("response from connectClient: "); + log.debug(response); + if (response.authenticated) { + log.debug("successfully authenticated " + username); + let expiration = _tokenExpirationMinutes; + // Create access token and wrap in response object + if (req && req.query) { + expiration = req.query.expiration || _tokenExpirationMinutes; + if (expiration > _tokenExpirationMinutes) + expiration = _tokenExpirationMinutes; + } + log.debug("about to make jwt"); + let expires = Date.now() + (expiration * 60 * 1000) + let json = { + token: jwt.sign({exp: Math.floor(expires / 1000), sub: username}, _secret), + expires + } + req.marklogicUsername = username; + + log.debug("json token:"); + log.debug(json); + resolve(json); + } else if (response.authenticated == false) { + let err = new Error('Invalid credentials.') + err.code = 401; + err.httpStatusMessage = response.httpStatusMessage; + err.httpStatusCode = response.httpStatusCode; + reject(err); + } + else { + let err = new Error("MarkLogic error"); + err.code(500); + log.debug("MarkLogic error"); + reject(err); + } +}).catch(caughtErr => { + let err = new Error("MarkLogic error:"); + err.code=500; + err.message = caughtErr.message; + log.debug("MarkLogic error:"); + log.debug(caughtErr); + throw err; +}); +} + +module.exports = tokenMarkLogicAuthentication; From 37f0cb11f0670890a2fe6022f90ba0a1652aa991 Mon Sep 17 00:00:00 2001 From: Thomas Diepenbrock Date: Thu, 28 May 2020 12:27:51 -0400 Subject: [PATCH 2/9] WIP --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index cc24cb3..a3b8cbe 100755 --- a/server.js +++ b/server.js @@ -46,7 +46,7 @@ if (config.auth && config.auth.enabled) { } else { //if there's no auth provider configured, we have to use a direct pre-authenticated db client dbClientManager.useStaticClient(true); - +} // install the Marklogic Provider const provider = require('./src/koop'); koop.register(provider); From cf677b33d6a53b1d1db6e532c87d34c02a3590fd Mon Sep 17 00:00:00 2001 From: Thomas Diepenbrock Date: Fri, 29 May 2020 14:30:57 -0400 Subject: [PATCH 3/9] fixing uncaught reject and making error logging more differentiated --- src/koop/dbClientManager.js | 2 +- src/koop/tokenMarkLogicAuthentication.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/koop/dbClientManager.js b/src/koop/dbClientManager.js index 9344803..53225fb 100644 --- a/src/koop/dbClientManager.js +++ b/src/koop/dbClientManager.js @@ -98,7 +98,7 @@ function checkDbClientConnection(db, username) { connectionInfo.error = true; connectionInfo.errorMsg = error.message; _dbClientCache.del(username); - log.debug("resolving connectionInfo:"); + log.debug("resolving connection error connectionInfo:"); log.debug(connectionInfo); resolve(connectionInfo); }).catch(error => { diff --git a/src/koop/tokenMarkLogicAuthentication.js b/src/koop/tokenMarkLogicAuthentication.js index 150ac5a..d162e25 100644 --- a/src/koop/tokenMarkLogicAuthentication.js +++ b/src/koop/tokenMarkLogicAuthentication.js @@ -89,7 +89,7 @@ function validateCredentials(req, username, password, resolve, reject) { } else { let err = new Error("MarkLogic error"); - err.code(500); + err.code = 500; log.debug("MarkLogic error"); reject(err); } @@ -99,7 +99,7 @@ function validateCredentials(req, username, password, resolve, reject) { err.message = caughtErr.message; log.debug("MarkLogic error:"); log.debug(caughtErr); - throw err; + reject(err); }); } From e2f8db8809c633237322a88b47e1d14122707a85 Mon Sep 17 00:00:00 2001 From: Mark Gumban Date: Wed, 15 Jul 2020 15:11:13 +0800 Subject: [PATCH 4/9] test cases --- config/test-auth-direct-file.json | 31 +++ config/test-auth-ml.json | 26 +++ config/test-default.json | 21 ++ config/test.json | 21 -- package.json | 29 ++- run-tests.sh | 5 + server.js | 11 +- src/koop/tokenMarkLogicAuthentication.js | 12 +- tests/.jshintrc | 3 + tests/auth-direct-file.test.js | 143 ++++++++++++ tests/auth-ml.test.js | 219 ++++++++++++++++++ tests/default.test.js | 50 ++++ .../org.eclipse.buildship.core.prefs | 13 ++ tests/ml/build.gradle | 19 ++ tests/ml/gradle.properties | 7 + .../ml-config/databases/content-database.json | 13 ++ .../ml-config/databases/schemas-database.json | 3 + .../ml-config/security/roles/dept-a-role.json | 7 + .../ml-config/security/roles/dept-b-role.json | 7 + .../ml-config/security/users/dept-a-user.json | 5 + .../ml-config/security/users/dept-admin.json | 5 + .../ml-config/security/users/dept-b-user.json | 5 + .../data/dept-a/collections.properties | 1 + .../ml-data/data/dept-a/gkg_geojson_6000.json | 1 + .../data/dept-a/permissions.properties | 1 + .../data/dept-b/collections.properties | 1 + .../ml-data/data/dept-b/gkg_geojson_6001.json | 1 + .../data/dept-b/permissions.properties | 1 + .../main/ml-data/services/GDeltExample.json | 35 +++ .../ml-data/services/collections.properties | 1 + .../ml-data/services/permissions.properties | 1 + tests/ml/src/main/ml-schemas/example-gkg.tdex | 168 ++++++++++++++ tests/user-store.json | 6 + 33 files changed, 837 insertions(+), 35 deletions(-) create mode 100644 config/test-auth-direct-file.json create mode 100644 config/test-auth-ml.json create mode 100644 config/test-default.json delete mode 100644 config/test.json create mode 100755 run-tests.sh create mode 100644 tests/.jshintrc create mode 100644 tests/auth-direct-file.test.js create mode 100644 tests/auth-ml.test.js create mode 100644 tests/default.test.js create mode 100644 tests/ml/.settings/org.eclipse.buildship.core.prefs create mode 100644 tests/ml/build.gradle create mode 100644 tests/ml/gradle.properties create mode 100644 tests/ml/src/main/ml-config/databases/content-database.json create mode 100644 tests/ml/src/main/ml-config/databases/schemas-database.json create mode 100644 tests/ml/src/main/ml-config/security/roles/dept-a-role.json create mode 100644 tests/ml/src/main/ml-config/security/roles/dept-b-role.json create mode 100644 tests/ml/src/main/ml-config/security/users/dept-a-user.json create mode 100644 tests/ml/src/main/ml-config/security/users/dept-admin.json create mode 100644 tests/ml/src/main/ml-config/security/users/dept-b-user.json create mode 100644 tests/ml/src/main/ml-data/data/dept-a/collections.properties create mode 100755 tests/ml/src/main/ml-data/data/dept-a/gkg_geojson_6000.json create mode 100644 tests/ml/src/main/ml-data/data/dept-a/permissions.properties create mode 100644 tests/ml/src/main/ml-data/data/dept-b/collections.properties create mode 100755 tests/ml/src/main/ml-data/data/dept-b/gkg_geojson_6001.json create mode 100644 tests/ml/src/main/ml-data/data/dept-b/permissions.properties create mode 100644 tests/ml/src/main/ml-data/services/GDeltExample.json create mode 100644 tests/ml/src/main/ml-data/services/collections.properties create mode 100644 tests/ml/src/main/ml-data/services/permissions.properties create mode 100644 tests/ml/src/main/ml-schemas/example-gkg.tdex create mode 100644 tests/user-store.json diff --git a/config/test-auth-direct-file.json b/config/test-auth-direct-file.json new file mode 100644 index 0000000..8f2d6e7 --- /dev/null +++ b/config/test-auth-direct-file.json @@ -0,0 +1,31 @@ +{ + "logger": { + "level": "debug" + }, + "port": 9000, + "ssl": { + "enabled": false, + "port": 443, + "cert": "/ssl/cert.pem", + "key": "/ssl/key.pem" + }, + "marklogic": { + "connection": { + "host": "localhost", + "port": 8097, + "user": "koop-marklogic-provider-test-dept-admin", + "password": "test", + "authType": "DIGEST" + } + }, + "auth": { + "plugin": "auth-direct-file", + "enabled": true, + "options": { + "secret": "7072c433-a4e7-4749-86f3-849a3ed0ee95", + "identityStore": "tests/user-store.json", + "tokenExpirationMinutes": 60, + "useHttp": true + } + } +} \ No newline at end of file diff --git a/config/test-auth-ml.json b/config/test-auth-ml.json new file mode 100644 index 0000000..8dd2a43 --- /dev/null +++ b/config/test-auth-ml.json @@ -0,0 +1,26 @@ +{ + "logger": { + "level": "debug" + }, + "port": 9000, + "ssl": { + "enabled": false, + "port": 443, + "cert": "/ssl/cert.pem", + "key": "/ssl/key.pem" + }, + "marklogic": { + "connection": { + "host": "localhost", + "port": 8097 + } + }, + "auth": { + "plugin": "auth-marklogic-digest-basic", + "enabled": true, + "options": { + "secret": "7072c433-a4e7-4749-86f3-849a3ed0ee95", + "tokenExpirationMinutes": 60 + } + } +} \ No newline at end of file diff --git a/config/test-default.json b/config/test-default.json new file mode 100644 index 0000000..0d8a913 --- /dev/null +++ b/config/test-default.json @@ -0,0 +1,21 @@ +{ + "logger": { + "level": "debug" + }, + "port": 9000, + "ssl": { + "enabled": false, + "port": 443, + "cert": "/ssl/cert.pem", + "key": "/ssl/key.pem" + }, + "marklogic": { + "connection": { + "host": "localhost", + "port": 8097, + "user": "koop-marklogic-provider-test-dept-admin", + "password": "test", + "authType": "DIGEST" + } + } +} \ No newline at end of file diff --git a/config/test.json b/config/test.json deleted file mode 100644 index f5cbb5d..0000000 --- a/config/test.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "logger" : { - "level" : "debug" - }, - "port" : 80, - "ssl": { - "enabled" : false, - "port" : 443, - "cert" : "/ssl/cert.pem", - "key" : "/ssl/key.pem" - }, - "marklogic": { - "connection": { - "host": "localhost", - "port": 8096, - "user": "test-geo-data-services-reader", - "password": "test-geo-data-services-reader", - "authType": "DIGEST" - } - } -} diff --git a/package.json b/package.json index 249322b..7a5b4aa 100755 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "A Marklogic provider for Koop", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test:default": "cross-env NODE_ENV=test-default jest --silent --testTimeout=10000 --testNamePattern=test-default", + "test:auth-ml": "cross-env NODE_ENV=test-auth-ml jest --silent --testTimeout=10000 --testNamePattern=test-auth-ml", + "test:auth-direct-file": "cross-env NODE_ENV=test-auth-direct-file jest --silent --testTimeout=10000 --testNamePattern=test-auth-direct-file", "start": "node server.js" }, "dependencies": { @@ -15,10 +17,10 @@ "featureserver": "2.17.0", "@koopjs/auth-direct-file": "^2.0.0", "marklogic": "^2.2.0", - "jsonwebtoken":"^8.5.1", - "node-cache":"^5.1.0", - "yakaa":"^1.0.1", - "uuid":"^8.0.0" + "jsonwebtoken": "^8.5.1", + "node-cache": "^5.1.0", + "yakaa": "^1.0.1", + "uuid": "^8.0.0" }, "author": "MarkLogic", "license": "Apache-2.0", @@ -38,5 +40,20 @@ "data", "big", "data" - ] + ], + "devDependencies": { + "cross-env": "^7.0.2", + "jest": "^26.1.0", + "supertest": "^4.0.2" + }, + "jest": { + "testEnvironment": "node", + "coveragePathIgnorePatterns": [ + "/node_modules/" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "build" + ] + } } diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..3d35ea1 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +npm run test:default +npm run test:auth-ml +npm run test:auth-direct-file \ No newline at end of file diff --git a/server.js b/server.js index a3b8cbe..41c7478 100755 --- a/server.js +++ b/server.js @@ -27,7 +27,7 @@ if (config.auth && config.auth.enabled) { auth = require('@koopjs/auth-direct-file')(config.auth.options.secret, config.auth.options.identityStore, config.auth.options); dbClientManager.useStaticClient(true); } else if (config.auth.plugin === 'auth-marklogic-digest-basic') { - auth = require("./authMarkLogic")(config.auth.options); + auth = require("./src/koop/authMarkLogic")(config.auth.options); } else if (config.auth.plugin) { //if it's something we don't recognize, try to require it by plugin name and pass in the options object //if this provider wants to use the static client, it will have to call dbClientManager.useStaticClient() @@ -43,9 +43,14 @@ if (config.auth && config.auth.enabled) { if (auth) { koop.register(auth); } + + log.info(`Using auth plugin ${config.auth.plugin}`); + } else { //if there's no auth provider configured, we have to use a direct pre-authenticated db client dbClientManager.useStaticClient(true); + + log.info(`No auth plugin specified, relying on configured MarkLogic credentials`); } // install the Marklogic Provider const provider = require('./src/koop'); @@ -65,7 +70,7 @@ log.info(`Service proxy for geo data services is ${(config.enableServiceProxy ? app.use('/', koop.server); // create HTTP server -http.createServer(app) +const server = http.createServer(app) .listen(config.port || 80); log.info(`Koop MarkLogic Provider listening for HTTP on ${config.port}`); @@ -82,3 +87,5 @@ if (config.ssl.enabled) { } console.log('Press control + c to exit'); + +module.exports = server; // make it testable \ No newline at end of file diff --git a/src/koop/tokenMarkLogicAuthentication.js b/src/koop/tokenMarkLogicAuthentication.js index d162e25..dc39c27 100644 --- a/src/koop/tokenMarkLogicAuthentication.js +++ b/src/koop/tokenMarkLogicAuthentication.js @@ -9,11 +9,11 @@ const dbClientManager = require("./dbClientManager"); function tokenMarkLogicAuthentication(options) { options = options || {}; this._secret = options.secret || uuidv4(); - this._tokenExpirationMinutes = options.tokenExpirationMintes || 60; + this._tokenExpirationMinutes = options.tokenExpirationMinutes || 60; return { authorize, validateCredentials - } + }; } function authorize(req) { @@ -24,10 +24,10 @@ function authorize(req) { if (req && req.query && req.query.token) token = req.query.token; if ((req && req.headers && req.headers.authorization)) token = req.headers.authorization; if (!token) { - let tokenErr = new Error('No authorization token.') - tokenErr.code = 401 - reject(tokenErr) - } + let tokenErr = new Error('No authorization token.'); + tokenErr.code = 401; + reject(tokenErr); + } // Verify token with async decoded function jwt.verify(token, _secret, function (err, decoded) { // If token invalid, reject diff --git a/tests/.jshintrc b/tests/.jshintrc new file mode 100644 index 0000000..80fc4c0 --- /dev/null +++ b/tests/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 8 +} \ No newline at end of file diff --git a/tests/auth-direct-file.test.js b/tests/auth-direct-file.test.js new file mode 100644 index 0000000..bbbe31b --- /dev/null +++ b/tests/auth-direct-file.test.js @@ -0,0 +1,143 @@ +const users = require('./user-store.json'); +const user = users[0]; + +describe('test-auth-direct-file tests', () => { + let request = null; + let app = null; + + beforeAll(done => { + app = require('../server'); + request = require('supertest')(app); + done(); + }); + + afterAll(done => { + if (app) + app.close(); + done(); + }); + + it('should reject the request', done => { + request + .get('/marklogic/GDeltExample/FeatureServer') + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.error && body.error.code && body.error.code === 499)) + throw new Error("Expected error code of 499"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + + it('should reject the request', done => { + request + .get('/marklogic/GDeltExample/FeatureServer/0/query') + .query({ "where": "1=1" }) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.error && body.error.code && body.error.code === 499)) + throw new Error("Expected error code of 499"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should provide a token', done => { + request + .get('/marklogic/tokens') + .query({ "username": user.username, "password": user.password }) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!body.token) + throw new Error("Expected token property"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should not provide a token', done => { + request + .get('/marklogic/tokens') + .query({ "username": "__an_invalid_user__", "password": "an_invalid_password" }) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.error && body.error.code && body.error.code === 400)) + throw new Error("Expected error code of 400"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should return the feature server information', done => { + request + .get('/marklogic/tokens') + .query({ "username": user.username, "password": user.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) return done(err); + + const token = res.body.token; + request + .get('/marklogic/GDeltExample/FeatureServer') + .set('Authorization', token) + .expect('Content-Type', /json/) + .expect(res => { + const body = res.body; + if (!(body.serviceDescription && body.serviceDescription === 'GDeltExample')) + throw new Error("Expected serviceDescription property with value of GDeltExample"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + }); + + it('should return all feature objects', done => { + request + .get('/marklogic/tokens') + .query({ "username": user.username, "password": user.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) return done(err); + + const token = res.body.token; + request + .get('/marklogic/GDeltExample/FeatureServer/0/query') + .query({ "where": "1=1" }) + .set('Authorization', token) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.features && Array.isArray(body.features))) + throw new Error("Response expected to have a features array"); + if (body.features.length !== 2) + throw new Error("Response expected to have 2 features"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + }); +}); diff --git a/tests/auth-ml.test.js b/tests/auth-ml.test.js new file mode 100644 index 0000000..ded8af4 --- /dev/null +++ b/tests/auth-ml.test.js @@ -0,0 +1,219 @@ +const users = { + deptA: { + username: "koop-marklogic-provider-test-dept-a-user", + password: "test" + }, + deptB: { + username: "koop-marklogic-provider-test-dept-b-user", + password: "test" + }, + deptAdmin: { + username: "koop-marklogic-provider-test-dept-admin", + password: "test" + } +}; + +describe('test-auth-ml tests', () => { + let request = null; + let app = null; + + beforeAll(done => { + app = require('../server'); + request = require('supertest')(app); + done(); + }); + + afterAll(done => { + if (app) + app.close(); + done(); + }); + + it('should reject the request', done => { + request + .get('/marklogic/GDeltExample/FeatureServer') + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.error && body.error.code && body.error.code === 499)) + throw new Error("Expected error code of 499"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + + it('should reject the request', done => { + request + .get('/marklogic/GDeltExample/FeatureServer/0/query') + .query({ "where": "1=1" }) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.error && body.error.code && body.error.code === 499)) + throw new Error("Expected error code of 499"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should provide a token', done => { + request + .get('/marklogic/tokens') + .query({ "username": users.deptA.username, "password": users.deptA.password }) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!body.token) + throw new Error("Expected token property"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should not provide a token', done => { + request + .get('/marklogic/tokens') + .query({ "username": "__an_invalid_user__", "password": "an_invalid_password" }) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.error && body.error.code && body.error.code === 400)) + throw new Error("Expected error code of 400"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should return the feature server information', done => { + request + .get('/marklogic/tokens') + .query({ "username": users.deptA.username, "password": users.deptA.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) return done(err); + + const token = res.body.token; + request + .get('/marklogic/GDeltExample/FeatureServer') + .set('Authorization', token) + .expect('Content-Type', /json/) + .expect(res => { + const body = res.body; + if (!(body.serviceDescription && body.serviceDescription === 'GDeltExample')) + throw new Error("Expected serviceDescription property with value of GDeltExample"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + }); + + it('should return one feature object visible by department A', done => { + request + .get('/marklogic/tokens') + .query({ "username": users.deptA.username, "password": users.deptA.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) return done(err); + + const token = res.body.token; + request + .get('/marklogic/GDeltExample/FeatureServer/0/query') + .query({ "where": "1=1" }) + .set('Authorization', token) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.features && Array.isArray(body.features))) + throw new Error("Response expected to have a features array"); + if (body.features.length !== 1) + throw new Error("Response expected to have 1 feature"); + if (body.features[0].attributes.OBJECTID !== 6000) + throw new Error("Expected feature to have an OBJECTID of 6000"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + }); + + it('should return one feature object visible by department B', done => { + request + .get('/marklogic/tokens') + .query({ "username": users.deptB.username, "password": users.deptB.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) return done(err); + + const token = res.body.token; + request + .get('/marklogic/GDeltExample/FeatureServer/0/query') + .query({ "where": "1=1" }) + .set('Authorization', token) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.features && Array.isArray(body.features))) + throw new Error("Response expected to have a features array"); + if (body.features.length !== 1) + throw new Error("Response expected to have 1 feature"); + if (body.features[0].attributes.OBJECTID !== 6001) + throw new Error("Expected feature to have an OBJECTID of 6001"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + }); + + it('should return all feature objects', done => { + request + .get('/marklogic/tokens') + .query({ "username": users.deptAdmin.username, "password": users.deptAdmin.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) return done(err); + + const token = res.body.token; + request + .get('/marklogic/GDeltExample/FeatureServer/0/query') + .query({ "where": "1=1" }) + .set('Authorization', token) + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.features && Array.isArray(body.features))) + throw new Error("Response expected to have a features array"); + if (body.features.length !== 2) + throw new Error("Response expected to have 2 features"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + }); +}); diff --git a/tests/default.test.js b/tests/default.test.js new file mode 100644 index 0000000..a9ede6b --- /dev/null +++ b/tests/default.test.js @@ -0,0 +1,50 @@ +describe('test-default tests', () => { + let request = null; + let app = null; + + beforeAll(done => { + app = require('../server'); + request = require('supertest')(app); + done(); + }); + + afterAll(done => { + if (app) + app.close(); + done(); + }); + + it('should return the feature server information', done => { + request + .get('/marklogic/GDeltExample/FeatureServer') + .expect('Content-Type', /json/) + .expect(res => { + const body = res.body; + if (!(body.serviceDescription && body.serviceDescription === 'GDeltExample')) + throw new Error("Expected serviceDescription property with value of GDeltExample"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + + it('should return all features', done => { + request + .get('/marklogic/GDeltExample/FeatureServer/0/query?where=1=1') + .expect('Content-Type', /json/) + .expect(200) + .expect(res => { + const body = res.body; + if (!(body.features && Array.isArray(body.features))) + throw new Error("Response expected to have a features array"); + if (body.features.length !== 2) + throw new Error("Response expected to have 2 features"); + }) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); +}); \ No newline at end of file diff --git a/tests/ml/.settings/org.eclipse.buildship.core.prefs b/tests/ml/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..67a1397 --- /dev/null +++ b/tests/ml/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.3)) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/tests/ml/build.gradle b/tests/ml/build.gradle new file mode 100644 index 0000000..1536d8c --- /dev/null +++ b/tests/ml/build.gradle @@ -0,0 +1,19 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "com.marklogic:marklogic-geo-data-services-modules:1.1.2" + } +} +plugins { + id "net.saliman.properties" version "1.5.1" + id "com.marklogic.ml-gradle" version "4.0.3" +} +repositories { + jcenter() +} + +dependencies { + mlBundle "com.marklogic:marklogic-geo-data-services-modules:1.1.2" +} \ No newline at end of file diff --git a/tests/ml/gradle.properties b/tests/ml/gradle.properties new file mode 100644 index 0000000..cd4ceb6 --- /dev/null +++ b/tests/ml/gradle.properties @@ -0,0 +1,7 @@ +# Properties generated by mlNewProject at Tue Jul 14 17:25:16 PST 2020 +# See the Property Reference page in the ml-gradle Wiki for a list of all supported properties +mlAppName=koop-provider-marklogic-test +mlHost=localhost +mlUsername=admin +mlPassword=admin +mlRestPort=8097 \ No newline at end of file diff --git a/tests/ml/src/main/ml-config/databases/content-database.json b/tests/ml/src/main/ml-config/databases/content-database.json new file mode 100644 index 0000000..e99b5ee --- /dev/null +++ b/tests/ml/src/main/ml-config/databases/content-database.json @@ -0,0 +1,13 @@ +{ + "database-name" : "%%DATABASE%%", + "schema-database": "%%SCHEMAS_DATABASE%%", + "geospatial-path-index": [ + { + "path-expression": "geometry[type='Point']/array-node('coordinates')", + "coordinate-system": "wgs84", + "point-format": "long-lat-point", + "range-value-positions": false, + "invalid-values": "reject" + } + ] +} diff --git a/tests/ml/src/main/ml-config/databases/schemas-database.json b/tests/ml/src/main/ml-config/databases/schemas-database.json new file mode 100644 index 0000000..b76421f --- /dev/null +++ b/tests/ml/src/main/ml-config/databases/schemas-database.json @@ -0,0 +1,3 @@ +{ + "database-name": "%%SCHEMAS_DATABASE%%" +} \ No newline at end of file diff --git a/tests/ml/src/main/ml-config/security/roles/dept-a-role.json b/tests/ml/src/main/ml-config/security/roles/dept-a-role.json new file mode 100644 index 0000000..d723bac --- /dev/null +++ b/tests/ml/src/main/ml-config/security/roles/dept-a-role.json @@ -0,0 +1,7 @@ +{ + "role-name": "koop-marklogic-provider-test-dept-a-role", + "role": [ + "geo-data-services-reader", + "rest-extension-user" + ] +} \ No newline at end of file diff --git a/tests/ml/src/main/ml-config/security/roles/dept-b-role.json b/tests/ml/src/main/ml-config/security/roles/dept-b-role.json new file mode 100644 index 0000000..274e3b4 --- /dev/null +++ b/tests/ml/src/main/ml-config/security/roles/dept-b-role.json @@ -0,0 +1,7 @@ +{ + "role-name": "koop-marklogic-provider-test-dept-b-role", + "role": [ + "geo-data-services-reader", + "rest-extension-user" + ] +} \ No newline at end of file diff --git a/tests/ml/src/main/ml-config/security/users/dept-a-user.json b/tests/ml/src/main/ml-config/security/users/dept-a-user.json new file mode 100644 index 0000000..0c8c93d --- /dev/null +++ b/tests/ml/src/main/ml-config/security/users/dept-a-user.json @@ -0,0 +1,5 @@ +{ + "user-name" : "koop-marklogic-provider-test-dept-a-user", + "password" : "test", + "role" : [ "koop-marklogic-provider-test-dept-a-role" ] +} \ No newline at end of file diff --git a/tests/ml/src/main/ml-config/security/users/dept-admin.json b/tests/ml/src/main/ml-config/security/users/dept-admin.json new file mode 100644 index 0000000..68dad0b --- /dev/null +++ b/tests/ml/src/main/ml-config/security/users/dept-admin.json @@ -0,0 +1,5 @@ +{ + "user-name" : "koop-marklogic-provider-test-dept-admin", + "password" : "test", + "role" : [ "koop-marklogic-provider-test-dept-a-role", "koop-marklogic-provider-test-dept-b-role" ] +} \ No newline at end of file diff --git a/tests/ml/src/main/ml-config/security/users/dept-b-user.json b/tests/ml/src/main/ml-config/security/users/dept-b-user.json new file mode 100644 index 0000000..8061642 --- /dev/null +++ b/tests/ml/src/main/ml-config/security/users/dept-b-user.json @@ -0,0 +1,5 @@ +{ + "user-name" : "koop-marklogic-provider-test-dept-b-user", + "password" : "test", + "role" : [ "koop-marklogic-provider-test-dept-b-role" ] +} \ No newline at end of file diff --git a/tests/ml/src/main/ml-data/data/dept-a/collections.properties b/tests/ml/src/main/ml-data/data/dept-a/collections.properties new file mode 100644 index 0000000..ee67d7d --- /dev/null +++ b/tests/ml/src/main/ml-data/data/dept-a/collections.properties @@ -0,0 +1 @@ +*=example-gkg diff --git a/tests/ml/src/main/ml-data/data/dept-a/gkg_geojson_6000.json b/tests/ml/src/main/ml-data/data/dept-a/gkg_geojson_6000.json new file mode 100755 index 0000000..3acc243 --- /dev/null +++ b/tests/ml/src/main/ml-data/data/dept-a/gkg_geojson_6000.json @@ -0,0 +1 @@ +{"type":"Feature","geometry":{"type":"Point","coordinates":[103.8000,1.3667]},"properties":{"urlpubtimedate":"2020-03-08T17:45:00Z","name":"Singapore","urltone":-10.64,"domain":"larepublica.pe","urllangcode":"spa","urlsocialimage":"https://larepublica.pe/resizer/TdHq-xzSAwAYF9cDYLmjCN_rj1A=/1200x660/top/arc-anglerfish-arc2-prod-gruporepublica.s3.amazonaws.com/public/JPRUVILVUVFVDLFOPQJ6PN33HQ.png","url":"https://larepublica.pe/mundo/2020/03/08/inglaterra-coronavirus-joven-asiatico-fue-salvajemente-atacado-fotos-discriminacion-rddr/","geores":1,"mentionedthemes":";EPU_CATS_MIGRATION_FEAR_FEAR;CRISISLEX_T02_INJURED;TAX_FNCACT_VICTIMS;CRISISLEX_CRISISLEXREC;CRISISLEX_T08_MISSINGFOUNDTRAPPEDPEOPLE;TAX_FNCACT_VICTIM;TAX_DISEASE_CORONAVIRUS;TAX_DISEASE_CORONAVIRUS;TAX_ETHNICITY_ASIANS;WOUND;CRISISLEX_C03_WELLBEING_HEALTH;DISCRIMINATION;UNGP_FREEDOM_FROM_DISCRIMINATION;DISCRIMINATION;UNGP_FREEDOM_FROM_DISCRIMINATION;TAX_FNCACT_MAN;WB_2433_CONFLICT_AND_VIOLENCE;WB_2432_FRAGILITY_CONFLICT_AND_VIOLENCE;UNGP_CRIME_VIOLENCE;","urlwordcnt":0,"urlnumamounts":0,"OBJECTID":6000},"coordinates":{"lat":1.3667,"lon":103.8000}} \ No newline at end of file diff --git a/tests/ml/src/main/ml-data/data/dept-a/permissions.properties b/tests/ml/src/main/ml-data/data/dept-a/permissions.properties new file mode 100644 index 0000000..c3aa42d --- /dev/null +++ b/tests/ml/src/main/ml-data/data/dept-a/permissions.properties @@ -0,0 +1 @@ +*=koop-marklogic-provider-test-dept-a-role,read,geo-data-services-writer,update \ No newline at end of file diff --git a/tests/ml/src/main/ml-data/data/dept-b/collections.properties b/tests/ml/src/main/ml-data/data/dept-b/collections.properties new file mode 100644 index 0000000..ee67d7d --- /dev/null +++ b/tests/ml/src/main/ml-data/data/dept-b/collections.properties @@ -0,0 +1 @@ +*=example-gkg diff --git a/tests/ml/src/main/ml-data/data/dept-b/gkg_geojson_6001.json b/tests/ml/src/main/ml-data/data/dept-b/gkg_geojson_6001.json new file mode 100755 index 0000000..5a69edd --- /dev/null +++ b/tests/ml/src/main/ml-data/data/dept-b/gkg_geojson_6001.json @@ -0,0 +1 @@ +{"type":"Feature","geometry":{"type":"Point","coordinates":[-0.1167,51.5000]},"properties":{"urlpubtimedate":"2020-03-08T17:45:00Z","name":"London, London, City Of, United Kingdom","urltone":-10.64,"domain":"larepublica.pe","urllangcode":"spa","urlsocialimage":"https://larepublica.pe/resizer/TdHq-xzSAwAYF9cDYLmjCN_rj1A=/1200x660/top/arc-anglerfish-arc2-prod-gruporepublica.s3.amazonaws.com/public/JPRUVILVUVFVDLFOPQJ6PN33HQ.png","url":"https://larepublica.pe/mundo/2020/03/08/inglaterra-coronavirus-joven-asiatico-fue-salvajemente-atacado-fotos-discriminacion-rddr/","geores":3,"mentionedthemes":";EPU_CATS_MIGRATION_FEAR_FEAR;CRISISLEX_T02_INJURED;TAX_FNCACT_VICTIMS;CRISISLEX_CRISISLEXREC;CRISISLEX_T08_MISSINGFOUNDTRAPPEDPEOPLE;TAX_DISEASE_AUTISM;TAX_FNCACT_VICTIM;TAX_DISEASE_CORONAVIRUS;TAX_ETHNICITY_ASIANS;WOUND;CRISISLEX_C03_WELLBEING_HEALTH;DISCRIMINATION;UNGP_FREEDOM_FROM_DISCRIMINATION;DISCRIMINATION;UNGP_FREEDOM_FROM_DISCRIMINATION;LGBT;TAX_FNCACT_MAN;WB_2433_CONFLICT_AND_VIOLENCE;WB_2432_FRAGILITY_CONFLICT_AND_VIOLENCE;UNGP_CRIME_VIOLENCE;TAX_FNCACT_CHILDREN;","urlwordcnt":0,"urlnumamounts":0,"OBJECTID":6001},"coordinates":{"lat":51.5000,"lon":-0.1167}} \ No newline at end of file diff --git a/tests/ml/src/main/ml-data/data/dept-b/permissions.properties b/tests/ml/src/main/ml-data/data/dept-b/permissions.properties new file mode 100644 index 0000000..8066c9d --- /dev/null +++ b/tests/ml/src/main/ml-data/data/dept-b/permissions.properties @@ -0,0 +1 @@ +*=koop-marklogic-provider-test-dept-b-role,read,geo-data-services-writer,update \ No newline at end of file diff --git a/tests/ml/src/main/ml-data/services/GDeltExample.json b/tests/ml/src/main/ml-data/services/GDeltExample.json new file mode 100644 index 0000000..c919dfb --- /dev/null +++ b/tests/ml/src/main/ml-data/services/GDeltExample.json @@ -0,0 +1,35 @@ +{ + "info" : { + "name" : "GDeltExample", + "description" : "GDeltExample" + }, + + "layers" : [ + { + "id": 0, + "name" : "GKG", + "description": "All GDelt articles", + "geometryType": "Point", + "idField": "OBJECTID", + "displayField": "name", + "extent" : { + "xmin" : -180, + "ymin" : -90, + "xmax" : 180, + "ymax" : 90, + "spatialReference" : { + "wkid" : 4326, + "latestWkid" : 4326 + } + }, + "schema": "GDeltGKG", + "view" : "Article", + "geometry" : { + "type" : "Point", + "format" : "geojson", + "coordinateSystem" : "wgs84", + "xpath" : "//geometry" + } + } + ] +} diff --git a/tests/ml/src/main/ml-data/services/collections.properties b/tests/ml/src/main/ml-data/services/collections.properties new file mode 100644 index 0000000..b38626e --- /dev/null +++ b/tests/ml/src/main/ml-data/services/collections.properties @@ -0,0 +1 @@ +*=http://marklogic.com/feature-services diff --git a/tests/ml/src/main/ml-data/services/permissions.properties b/tests/ml/src/main/ml-data/services/permissions.properties new file mode 100644 index 0000000..1f64d19 --- /dev/null +++ b/tests/ml/src/main/ml-data/services/permissions.properties @@ -0,0 +1 @@ +*=geo-data-services-reader,read,geo-data-services-writer,update \ No newline at end of file diff --git a/tests/ml/src/main/ml-schemas/example-gkg.tdex b/tests/ml/src/main/ml-schemas/example-gkg.tdex new file mode 100644 index 0000000..b065191 --- /dev/null +++ b/tests/ml/src/main/ml-schemas/example-gkg.tdex @@ -0,0 +1,168 @@ + diff --git a/tests/user-store.json b/tests/user-store.json new file mode 100644 index 0000000..21a1242 --- /dev/null +++ b/tests/user-store.json @@ -0,0 +1,6 @@ +[ + { + "username": "koop-auth-direct-file-user", + "password": "test" + } +] \ No newline at end of file From 60797cc9ea250622c131c6f90041801505cb050c Mon Sep 17 00:00:00 2001 From: Mark Gumban Date: Wed, 15 Jul 2020 16:14:16 +0800 Subject: [PATCH 5/9] lint fixes --- src/koop/authMarkLogic.js | 8 ++++---- src/koop/tokenMarkLogicAuthentication.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/koop/authMarkLogic.js b/src/koop/authMarkLogic.js index f675b02..07c5b92 100644 --- a/src/koop/authMarkLogic.js +++ b/src/koop/authMarkLogic.js @@ -15,8 +15,8 @@ function auth(options) { function authenticationSpecification() { return { useHttp: true - } -}; + }; +} function authenticate(req) { log.debug("called authenticate"); @@ -37,8 +37,8 @@ function authenticate(req) { // Validate user's credentials return _tokenMarklogicAuthentication.validateCredentials(req, inputUsername, inputPassword, resolve, reject); - }) -}; + }); +} function authorize(req) { return _tokenMarklogicAuthentication.authorize(req); diff --git a/src/koop/tokenMarkLogicAuthentication.js b/src/koop/tokenMarkLogicAuthentication.js index dc39c27..63b5793 100644 --- a/src/koop/tokenMarkLogicAuthentication.js +++ b/src/koop/tokenMarkLogicAuthentication.js @@ -70,18 +70,18 @@ function validateCredentials(req, username, password, resolve, reject) { expiration = _tokenExpirationMinutes; } log.debug("about to make jwt"); - let expires = Date.now() + (expiration * 60 * 1000) + let expires = Date.now() + (expiration * 60 * 1000); let json = { token: jwt.sign({exp: Math.floor(expires / 1000), sub: username}, _secret), expires - } + }; req.marklogicUsername = username; log.debug("json token:"); log.debug(json); resolve(json); } else if (response.authenticated == false) { - let err = new Error('Invalid credentials.') + let err = new Error('Invalid credentials.'); err.code = 401; err.httpStatusMessage = response.httpStatusMessage; err.httpStatusCode = response.httpStatusCode; From efeb48870698b830f84a6eee011e0a2c9002dc59 Mon Sep 17 00:00:00 2001 From: Mark Gumban Date: Wed, 15 Jul 2020 17:15:20 +0800 Subject: [PATCH 6/9] updated readme --- README.md | 108 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 8459596..79b6822 100644 --- a/README.md +++ b/README.md @@ -61,47 +61,66 @@ We could only get Esri Insights to work with HTTP requests on port 80 and HTTPS The project uses the node [config](https://www.npmjs.com/package/config) package to manage configurations. Update the necessary `config/FILENAME.json` file. You can use the `config/default.json` as a starting point. To make use of your configuration execute `export NODE_ENV=` before you run `node server.js`. -### Test Settings +### Authentication -The test project uses the above node settings for the node server and [gradle properties plugin](https://github.com/stevesaliman/gradle-properties-plugin) to manage environment properties for gradle. Create a `/test/gradle-.properties` file to specify your environment settings that match your `/config/.json`. +The project supports the following authentication strategies: -The following properties can be overriden: +- None (Default) +- MarkLogic: Koop relies on MarkLogic for authenticating user credentials. Users must have a valid MarkLogic server user account. +- Direct File: -``` -mlAppName= - -mlHost= -mlRestPort= -koopMlUsername= -koopMlPassword= - - -# Koop server setttings -# Port the feature service will service HTTP requests on -koopPort= -# Whether or not to enable an HTTPS server as well -koopSSLEnabled= -# Port the feature service will service HTTPS requests on -koopSSLPort= -# The SSL certificate to user for the HTTPS server -koopSSLCert= -# The SSL certificate key to user for the HTTPS server -koopSSLKey= -``` +#### No Authentication -Add the `-PenvironmentName=` argument when running gradle to utilize your configuration, you can copy the `/test/gradle.properties` default file as a starting point. +All Koop services will be publicly accessible. Koop will still need valid MarkLogic credentials to communicate with MarkLogic. See `config/default.json` for an example. ---- +#### MarkLogic Authentication -### Tested Configuration +This uses a _direct authentication_ pattern similar to [Koop-Auth-Direct-File](https://github.com/koopjs/koop-auth-direct-file), with MarkLogic being responsible for authenticating user credentials. The user credentials supplied must match a valid MarkLogic server user account. +To setup, add an `auth` section to your `config/FILENAME.json`, for example: + +```json +"auth": { + "plugin": "auth-marklogic-digest-basic", + "enabled": true, + "options": { + "secret": "7072c433-a4e7-4749-86f3-849a3ed0ee95", + "tokenExpirationMinutes": 60 + } +} ``` - NPM 6.4.1 - Node 10.2.1 - MarkLogic 9.0-10 -MarkLogic Geo Data Services 1.1.0 + +| Option | Description | Value | Default value | +|------------------------|-----------------------------------------------|-----------------------------|---------------------| +| secret | Used to verify JSON web tokens | string | auto-generated UUID | +| tokenExpirationMinutes | The validity of tokens in minutes | Number | 60 | + +#### Direct File Authentication + +This uses the [Koop-Auth-Direct-File](https://github.com/koopjs/koop-auth-direct-file) module for authentication. Check out its [official project page](https://github.com/koopjs/koop-auth-direct-file) for more information. + +To setup, add an `auth` section to your `config/FILENAME.json`, for example: + +```json +"auth": { + "plugin": "auth-direct-file", + "enabled": true, + "options": { + "secret": "7072c433-a4e7-4749-86f3-849a3ed0ee95", + "tokenExpirationMinutes": 60, + "identityStore": "tests/user-store.json", + "useHttp": true + } +} ``` +| Option | Description | Value | Default value | +|------------------------|-----------------------------------------------|-----------------------------|---------------------| +| secret | Used to verify JSON web tokens | string | auto-generated UUID | +| tokenExpirationMinutes | The validity of tokens in minutes | Number | 60 | +| identityStore | Path to JSON file containing list of users | string | none | +| useHttp | Allow HTTP for token services | boolean | false | + ## Running the Connector The connector uses Koop as the client-facing HTTP/S service. Koop utilizes a Node.js Express server to handle feature service requests. @@ -148,3 +167,30 @@ The [MarkLogic ArcGIS Pro add-in](https://github.com/marklogic-community/marklog ``` For security purposes, the *service proxy* is **disabled** by default. + +## Tests + +### Setup Test Environment + +1. Ensure you have a local MarkLogic server running. +2. Go to `tests/ml` directory. +3. Run `./gradlew mlDeploy` to deploy the test database. + +### Run Tests + +1. Go to the project root directory. +2. Run `./run-tests.sh` to run all tests. + +### Run Individual Tests + +The tests are organized in several test suites: + +1. test:default - no authentication tests +2. test:auth-ml - MarkLogic authentication tests +3. test:auth-direct-file - Direct File authentication tests + +Use `npm run` to run them individually, for example: + +```bash +npm run test:auth-ml +``` \ No newline at end of file From 4bf0fae20fcf36f0bfcac577a22040f8972510ba Mon Sep 17 00:00:00 2001 From: Mark Gumban Date: Wed, 15 Jul 2020 17:19:20 +0800 Subject: [PATCH 7/9] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a5b4aa..901a188 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@koopjs/provider-marklogic", - "version": "1.1.1", + "version": "1.2", "description": "A Marklogic provider for Koop", "main": "index.js", "scripts": { From 0ea3d92b1f185118f2110ba303442cf9fd885c62 Mon Sep 17 00:00:00 2001 From: Mark Gumban Date: Wed, 15 Jul 2020 17:22:33 +0800 Subject: [PATCH 8/9] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 901a188..ab6285e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@koopjs/provider-marklogic", - "version": "1.2", + "version": "1.2.0", "description": "A Marklogic provider for Koop", "main": "index.js", "scripts": { From d64a87006342d43899af2dfa59c060938bb9ce6c Mon Sep 17 00:00:00 2001 From: Mark Gumban Date: Wed, 15 Jul 2020 17:26:15 +0800 Subject: [PATCH 9/9] update readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 79b6822..ff8d41b 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ The project uses the node [config](https://www.npmjs.com/package/config) package The project supports the following authentication strategies: - None (Default) -- MarkLogic: Koop relies on MarkLogic for authenticating user credentials. Users must have a valid MarkLogic server user account. -- Direct File: +- MarkLogic +- File-based #### No Authentication @@ -75,7 +75,7 @@ All Koop services will be publicly accessible. Koop will still need valid MarkL #### MarkLogic Authentication -This uses a _direct authentication_ pattern similar to [Koop-Auth-Direct-File](https://github.com/koopjs/koop-auth-direct-file), with MarkLogic being responsible for authenticating user credentials. The user credentials supplied must match a valid MarkLogic server user account. +This uses a _direct authentication_ pattern similar to [Koop-Auth-Direct-File](https://github.com/koopjs/koop-auth-direct-file), with MarkLogic being responsible for authenticating user credentials. The credentials supplied must match a valid MarkLogic server user account. To setup, add an `auth` section to your `config/FILENAME.json`, for example: @@ -95,7 +95,7 @@ To setup, add an `auth` section to your `config/FILENAME.json`, for example: | secret | Used to verify JSON web tokens | string | auto-generated UUID | | tokenExpirationMinutes | The validity of tokens in minutes | Number | 60 | -#### Direct File Authentication +#### File-based Authentication This uses the [Koop-Auth-Direct-File](https://github.com/koopjs/koop-auth-direct-file) module for authentication. Check out its [official project page](https://github.com/koopjs/koop-auth-direct-file) for more information.