diff --git a/README.md b/README.md index b4db049..6bf36d2 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ By instantiating new apps from template apps (that follow the coding standards), * When new apps are created, their reload scripts are either left intact, or replaced with a new script (there are two different REST endpoints for this). If the script is replaced, the new script is retrieved from a URL - typically from a revision control system system such as Github. * Create a custom property (by using the Sense QMC) called "AppIsTemplate". It should only exist for Apps. Possible values should be "Yes" and "No". +* As of version 1.1.0, the app duplicator only works using https. It's a bit more work setting up, but security is important. No shortcuts. # Usage @@ -57,7 +58,7 @@ You can use any tool capable of creating REST calls to test the service, includi Standard curl is used below, the assumption is that the command is executed on the same server where the app duplicator service is running. ```sh -curl -X GET "http://localhost:8000/getTemplateList" +curl -X GET "https://localhost:8000/getTemplateList" ``` Returns a HTTP 200 response, with a JSON structure such as @@ -83,7 +84,7 @@ When creating a new app that should get its load script from the file specified The new app will be reloaded (using the new load script) before saved to disk. ```sh -curl -X GET "http://localhost:8000/duplicateNewScript?appName=My%20new%20app&templateAppId=11111111-2222-3333-4444-555555555555&ownerUserId=joe" +curl -X GET "https://localhost:8000/duplicateNewScript?appName=My%20new%20app&templateAppId=11111111-2222-3333-4444-555555555555&ownerUserId=joe" ``` Returns @@ -124,7 +125,7 @@ The new app will be reloaded (using the load script from the template app) befor ```sh -curl -X GET "http://localhost:8000/duplicateKeepScript?appName=My%20new%20app&templateAppId=11111111-2222-3333-4444-555555555555&ownerUserId=joe" +curl -X GET "https://localhost:8000/duplicateKeepScript?appName=My%20new%20app&templateAppId=11111111-2222-3333-4444-555555555555&ownerUserId=joe" ``` The response from the service is exactly the same as for the /duplicateNewScript endpoint (see above). diff --git a/config/default_config.json b/config/default_config.json index cb56c27..5119fc0 100644 --- a/config/default_config.json +++ b/config/default_config.json @@ -5,10 +5,14 @@ // Possible log levels are silly, debug, verbose, info, warn, error "defaultLogLevel": "info", - // Paths to client certificates to use when connecting to Sense server + // Paths to client certificates to use when connecting to Sense server. Can be pem or pvk/cer "clientCertPath": "/path/to/client/cert/client.pem", "clientCertKeyPath": "/path/to/client/cert/client_key.pem", + // Paths to ssl certificate and key used to secure the communication to the duplicate server. Can be pem or pvk/cer + "sslCertPath": "/path/to/ssl/cert/client.cer", + "sslCertKeyPath": "/path/to/ssl/cert/client_key.pvk", + // URL where newly created apps' load scripts are retrieved from. Change to something more relevant than the sample code.. "loadScriptURL": "https://raw.githubusercontent.com/mountaindude/testdata/master/test-load-script-1.qvs", @@ -20,6 +24,9 @@ // Newly created apps will be assigned to specific users. Next config option specifies what user directory this user belongs to. // For example, if your Sense users full id looks like MYCOMPANY/username, MYCOMPANY would be listed below. - "senseUserDirectory": "MYCOMPANY" + "senseUserDirectory": "MYCOMPANY", + // Directory where log files are stored + "logDirectory": "/path/to/log/directory" + } \ No newline at end of file diff --git a/index.js b/index.js index 80654e8..a3547f5 100644 --- a/index.js +++ b/index.js @@ -4,24 +4,39 @@ const util = require('util') var qrsInteract = require('qrs-interact'); var request = require('request'); var restify = require('restify'); -var sleep = require('sleep'); var winston = require('winston'); var config = require('config'); -// Set up default log format for Winston logger -var logger = new(winston.Logger)({ +// Set up Winston logger, logging both to console and different disk files +var logger = new (winston.Logger)({ transports: [ - new(winston.transports.Console)({ + new (winston.transports.Console)({ + name: 'console_log', 'timestamp': true, 'colorize': true + }), + new (winston.transports.File)({ + name: 'file_info', + filename: config.get('logDirectory') + '/info.log', + level: 'info' + }), + new (winston.transports.File)({ + name: 'file_verbose', + filename: config.get('logDirectory') + '/verbose.log', + level: 'verbose' + }), + new (winston.transports.File)({ + name: 'file_error', + filename: config.get('logDirectory') + '/error.log', + level: 'error' }) ] }); // Set default log level -logger.transports.console.level = config.get('defaultLogLevel'); +logger.transports.console_log.level = config.get('defaultLogLevel'); logger.log('info', 'Starting Qlik Sense template app duplicator.'); @@ -56,10 +71,11 @@ var configQRS = { } - var restServer = restify.createServer({ name: 'Qlik Sense app duplicator', - version: '1.0.0' + version: '1.1.0', + certificate: fs.readFileSync(config.get('sslCertPath')), + key: fs.readFileSync(config.get('sslCertKeyPath')) }); @@ -67,7 +83,7 @@ var restServer = restify.createServer({ restServer.use(restify.queryParser()); // Set up CORS handling -restServer.use( restify.CORS( {origins: ['*']}) ); +restServer.use(restify.CORS({ origins: ['*'] })); // Set up endpoints for REST server restServer.get('/duplicateNewScript', respondDuplicateNewScript); @@ -127,6 +143,11 @@ function respondGetTemplateList(req, res, next) { // appName: Name of the new app that is created // ownerUserId: User ID that should be set as owner of the created app function respondDuplicateNewScript(req, res, next) { + + // Add owner of new app as header in call to QRS. That way this user will automatically be owner of the newly created app. + configQRS.headers = { 'X-Qlik-User': 'UserDirectory=' + config.get('senseUserDirectory') + '; UserId=' + req.params.ownerUserId }; + logger.log('verbose', configQRS); + var qrsInteractInstance = new qrsInteract(configQRS); // Load script from git @@ -173,38 +194,11 @@ function respondDuplicateNewScript(req, res, next) { qrsInteractInstance.Post('app/' + req.params.templateAppId + '/copy?name=' + req.params.appName, {}, 'json') .then(result => { - sleep.sleep(5); logger.log('info', 'App created with ID %s, using %s as a template ', result.body.id, req.params.templateAppId); newAppId = result.body.id; - // Get user details - return qrsInteractInstance.Get("user?filter=userId eq '" + newOwnerUserId + "' and UserDirectory eq '" + config.get('senseUserDirectory') + "'"); - }) - .then(result => { - // We have details of the user who should be made owner of the new app - logger.log('debug', 'User details: ' + JSON.stringify(result.body)); - - newOwnerId = result.body[0].id; - newOwnerUserDirectory = result.body[0].userDirectory; - newOwnerName = result.body[0].name; - - return qrsInteractInstance.Get('app/' + newAppId); - }) - .then(result => { - // We have a reference to the new app. Update its owner. - // http://help.qlik.com/en-US/sense-developer/3.1/Subsystems/RepositoryServiceAPI/Content/RepositoryServiceAPI/RepositoryServiceAPI-Connect-API-Conflict-Handling.htm - // http://help.qlik.com/en-US/sense-developer/3.1/Subsystems/RepositoryServiceAPI/Content/RepositoryServiceAPI/RepositoryServiceAPI-Update.htm - logger.log('debug', 'Metadata of new app: ', JSON.stringify(result.body)); - - var newBody = result.body; - newBody.owner.id = newOwnerId; - newBody.owner.userDirectory = newOwnerUserDirectory; - newBody.owner.userId = newOwnerUserId; - newBody.owner.name = newOwnerName; - logger.log('debug', 'Modified metadata of new app: ', JSON.stringify(result.body)); - - return qrsInteractInstance.Put("app/" + newAppId, newBody); + return; }) .then(() => { // Connect to engine @@ -242,7 +236,7 @@ function respondDuplicateNewScript(req, res, next) { }) .then(() => { logger.log('info', 'Done duplicating, new app id=' + newAppId); - var jsonResult = {result:"Done duplicating app", newAppId:newAppId } + var jsonResult = { result: "Done duplicating app", newAppId: newAppId } res.send(jsonResult); next(); }) @@ -260,7 +254,7 @@ function respondDuplicateNewScript(req, res, next) { .catch(err => { // Return error msg logger.log('error', 'Duplication error: ' + err); - res.send(err); + // res.send(err); next(new restify.BadRequestError("Error occurred when test app template status."));; return; }) @@ -277,139 +271,106 @@ function respondDuplicateNewScript(req, res, next) { // appName: Name of the new app that is created // ownerUserId: User ID that should be set as owner of the created app function respondDuplicateKeepScript(req, res, next) { - var qrsInteractInstance = new qrsInteract(configQRS); - // Load script from git - request.get(loadScriptURL, function (error, response, body) { - if (!error && response.statusCode == 200) { - logger.log('verbose', 'Retrieved load script'); - - var loadScript = body; - logger.log('debug', 'Load script: ' + loadScript); - - var newAppId = ''; - var globalEngine = ''; - - var newOwnerId, newOwnerUserDirectory, newOwnerName; - var newOwnerUserId = req.params.ownerUserId; - - - // Make sure the app to be duplicated really is a template - qrsInteractInstance.Get('app/' + req.params.templateAppId) - .then(result => { - logger.log('verbose', 'Testing if specifiec template app really is a template'); + // Add owner of new app as header in call to QRS. That way this user will automatically be owner of the newly created app. + configQRS.headers = { 'X-Qlik-User': 'UserDirectory=' + config.get('senseUserDirectory') + '; UserId=' + req.params.ownerUserId }; + logger.log('verbose', configQRS); - var appIsTemplate = false; - result.body.customProperties.forEach(function (item) { - logger.log('debug', 'Item: ' + item); + var qrsInteractInstance = new qrsInteract(configQRS); - if (item.definition.name == 'AppIsTemplate' && item.value == 'Yes') { - appIsTemplate = true; - } - }) + var newAppId = ''; + var globalEngine = ''; - logger.log('verbose', 'App is template: ' + appIsTemplate); + var newOwnerId, newOwnerUserDirectory, newOwnerName; + var newOwnerUserId = req.params.ownerUserId; - if (!appIsTemplate) { - logger.log('warn', 'The provided app ID does not belong to a template app'); - next(new restify.InvalidArgumentError("The provided app ID does not belong to a template app.")); - } + // Make sure the app to be duplicated really is a template + qrsInteractInstance.Get('app/' + req.params.templateAppId) + .then(result => { + logger.log('verbose', 'Testing if specifiec template app really is a template'); - return appIsTemplate; - }) - .then(result => { - // result == true if the provided app ID belongs to a template app - if (result) { + var appIsTemplate = false; + result.body.customProperties.forEach(function (item) { + logger.log('debug', 'Item: ' + item); - qrsInteractInstance.Post('app/' + req.params.templateAppId + '/copy?name=' + req.params.appName, {}, 'json') - .then(result => { - sleep.sleep(5); - logger.log('info', 'App created with ID %s, using %s as a template ', result.body.id, req.params.templateAppId); + if (item.definition.name == 'AppIsTemplate' && item.value == 'Yes') { + appIsTemplate = true; + } + }) - newAppId = result.body.id; - // Get user details - return qrsInteractInstance.Get("user?filter=userId eq '" + newOwnerUserId + "' and UserDirectory eq '" + config.get('senseUserDirectory') + "'"); - }) - .then(result => { - // We have details of the user who should be made owner of the new app - logger.log('debug', 'User details: ' + JSON.stringify(result.body)); + logger.log('verbose', 'App is template: ' + appIsTemplate); - newOwnerId = result.body[0].id; - newOwnerUserDirectory = result.body[0].userDirectory; - newOwnerName = result.body[0].name; + if (!appIsTemplate) { + logger.log('warn', 'The provided app ID does not belong to a template app'); + next(new restify.InvalidArgumentError("The provided app ID does not belong to a template app.")); + } - return qrsInteractInstance.Get('app/' + newAppId); - }) - .then(result => { - // We have a reference to the new app. Update its owner. - // http://help.qlik.com/en-US/sense-developer/3.1/Subsystems/RepositoryServiceAPI/Content/RepositoryServiceAPI/RepositoryServiceAPI-Connect-API-Conflict-Handling.htm - // http://help.qlik.com/en-US/sense-developer/3.1/Subsystems/RepositoryServiceAPI/Content/RepositoryServiceAPI/RepositoryServiceAPI-Update.htm - logger.log('debug', 'Metadata of new app: ', JSON.stringify(result.body)); + return appIsTemplate; + }) + .then(result => { + // result == true if the provided app ID belongs to a template app + if (result) { - var newBody = result.body; - newBody.owner.id = newOwnerId; - newBody.owner.userDirectory = newOwnerUserDirectory; - newBody.owner.userId = newOwnerUserId; - newBody.owner.name = newOwnerName; + qrsInteractInstance.Post('app/' + req.params.templateAppId + '/copy?name=' + req.params.appName, {}, 'json') + .then(result => { + logger.log('info', 'App created with ID %s, using %s as a template ', result.body.id, req.params.templateAppId); - logger.log('debug', 'Modified metadata of new app: ', JSON.stringify(result.body)); + newAppId = result.body.id; - return qrsInteractInstance.Put("app/" + newAppId, newBody); - }) - .then(() => { - // Connect to engine - logger.log('verbose', 'Connecting to engine...'); - return qsocks.Connect(configEngine); - }) - .then(global => { - // Connected. Open the newly created app - logger.log('verbose', 'Opening app...'); - globalEngine = global; - return global.openDoc(newAppId) - }) - .then(app => { - // Load the data - logger.log('verbose', 'Reload app...'); - app.doReload(); - return app - }) - .then(app => { - // Save our data. Will persist to disk. - logger.log('verbose', 'Save app to disk...'); - app.doSave(); - return; - }) - .then(() => { - // Close our connection. - logger.log('verbose', 'Close connection to engine...'); - return globalEngine.connection.close(); - }) - .then(() => { - logger.log('info', 'Done duplicating, new app id=' + newAppId); - var jsonResult = {result:"Done duplicating app", newAppId:newAppId } - res.send(jsonResult); - next(); - }) - .catch(err => { - // Failed to create app. In Desktop application names are unique. - logger.log('error', 'Duplication error: ' + err); - res.send(err); - next(new restify.BadRequestError("Error occurred when test app template status 2."));; + return; + }) + .then(() => { + // Connect to engine + logger.log('verbose', 'Connecting to engine...'); + return qsocks.Connect(configEngine); + }) + .then(global => { + // Connected. Open the newly created app + logger.log('verbose', 'Opening app...'); + globalEngine = global; + return global.openDoc(newAppId) + }) + .then(app => { + // Load the data + logger.log('verbose', 'Reload app...'); + app.doReload(); + return app + }) + .then(app => { + // Save our data. Will persist to disk. + logger.log('verbose', 'Save app to disk...'); + app.doSave(); + return; + }) + .then(() => { + // Close our connection. + logger.log('verbose', 'Close connection to engine...'); + return globalEngine.connection.close(); + }) + .then(() => { + logger.log('info', 'Done duplicating, new app id=' + newAppId); + var jsonResult = { result: "Done duplicating app", newAppId: newAppId } + res.send(jsonResult); + next(); + }) + .catch(err => { + // Failed to create app. In Desktop application names are unique. + logger.log('error', 'Duplication error: ' + err); + res.send(err); + next(new restify.BadRequestError("Error occurred when test app template status 2."));; - }) - } + }) + } - }) - .catch(err => { - // Return error msg - logger.log('error', 'Duplication error: ' + err); - res.send(err); - next(new restify.BadRequestError("Error occurred when test app template status."));; - return; - }) + }) + .catch(err => { + // Return error msg + logger.log('error', 'Duplication error: ' + err); + // res.send(err); + next(new restify.BadRequestError("Error occurred when test app template status."));; + return; + }) - } - }); -} \ No newline at end of file +}