Skip to content
This repository has been archived by the owner on Dec 1, 2021. It is now read-only.

Added custom metrics binding credential generation and management in APIServer #376

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
module.exports = function(settings, callback) {
module.exports = function(settings, credentialCache, callback) {
var https = require('https');
var http = require('http');
var fs = require('fs');
Expand Down Expand Up @@ -104,8 +104,9 @@ module.exports = function(settings, callback) {
var scalingHistories = require('./lib/routes/scalingHistories')(settings);
var metrics = require('./lib/routes/metrics')(settings);
var aggregatedMetrics = require('./lib/routes/aggregated_metrics')(settings);

var creds = require('./lib/routes/credentials')(models,credentialCache,settings.cacheTTL);
app.use('/v1/apps',policies);
app.use('/v1/apps',creds);
app.use(function(err, req, res, next) {
var errorResponse = {};
if (err) {
Expand Down
1 change: 1 addition & 0 deletions api/config/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"infoFilePath": "../api/config/info.json",
"cfApi": "https://api.bosh-lite.com",
"skipSSLValidation": false,
"cacheTTL": 10,
"db": {
"maxConnections": 10,
"minConnections": 0,
Expand Down
4 changes: 3 additions & 1 deletion api/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'
var path = require('path');
var fs = require('fs');
var NodeCache = require('node-cache');
var args = process.argv;
if (!(args.length == 4 && args[2] == "-c" && args[3] != "")) {
throw new Error("missing config file\nUsage:use '-c' option to specify the config file path");
Expand All @@ -19,4 +20,5 @@ var errorCallback = function(err) {
throw err;
}
}
var apiServer = require(path.join(__dirname, 'app.js'))(settings, errorCallback);
var credentialCache = new NodeCache();
var apiServer = require(path.join(__dirname, 'app.js'))(settings, credentialCache, errorCallback);
7 changes: 7 additions & 0 deletions api/lib/config/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module.exports = function (settingsObj) {
port: settingsObj.port,
cfApi: addProtocol(cleanUpUri(settingsObj.cfApi)),
skipSSLValidation: settingsObj.skipSSLValidation,
cacheTTL: settingsObj.cacheTTL,
publicPort: settingsObj.publicPort,
scheduler: settingsObj.scheduler,
scalingEngine: settingsObj.scalingEngine,
Expand Down Expand Up @@ -125,6 +126,12 @@ module.exports = function (settingsObj) {
if (!isBoolean(settings.skipSSLValidation)) {
return { valid: false, message: 'skipSSLValidation must be a boolean' };
}
if (isMissing(settings.cacheTTL)) {
return { valid: false, message: "cacheTTL is required" }
}
if (!isNumber(settings.cacheTTL)) {
return { valid: false, message: "cacheTTL must be a number" };
}
if (isMissing(settings.db.maxConnections)) {
return { valid: false, message: "db.maxConnections is required" };
}
Expand Down
27 changes: 27 additions & 0 deletions api/lib/models/creds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

module.exports = function(sequelize, DataTypes) {
var CustomMetricsCredentials = sequelize.define('credentials', {
id: {
type: DataTypes.STRING,
field: 'id',
primaryKey: true
},
username: {
type: DataTypes.STRING,
field: 'username',
allowNull: false
},
password: {
type: DataTypes.STRING,
field: 'password',
allowNull: false
}
},{
freezeTableName: true,
timestamps: true,
createdAt: false,
updatedAt: 'updated_at'
});
return CustomMetricsCredentials;
};
196 changes: 196 additions & 0 deletions api/lib/routes/credentialHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
'use strict';
module.exports = function(models, credentialCache, cacheTTL) {
var logger = require('../log/logger');
var HttpStatus = require('http-status-codes');
var uuidv4 = require('uuid/v4');
var bcrypt = require('bcrypt-nodejs');
var credhelper = {};

function generateHash(input) {
return bcrypt.hashSync(input, bcrypt.genSaltSync(8));
}

function validateHash(input, hash) {
return bcrypt.compareSync(input, hash);
}

function validateCredentialDetails(username,usernamehash,password, passwordhash){
var isUsernameValid = validateHash(username, usernamehash);
var isPasswordValid = validateHash(password, passwordhash);
var isValidCred = isUsernameValid && isPasswordValid;
return isValidCred;
}

credhelper.createOrUpdateCredentials = function(req, callback) {
var username = uuidv4();
var password = uuidv4();
var appId = req.params.app_id;
models.credentials.upsert({
id: req.params.app_id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use appId.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which one? id => appId or app_id => appId ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var appId = req.params.app_id;
appId has been defined, so better to " id: appId" ?

username: generateHash(username),
password: generateHash(password)
}).then(function(createdData) {
if (createdData) {
logger.info('New credentials hasbeen generated successfully', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if updatedData, cache entry needs to be invalidated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

@boyang9527 boyang9527 Aug 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I forgot there will be multiple instances of api server, so invalidate local cache will not invalidate cache of other instances. So if we don't have distributed remote cache, we will need to allow using old credentials until it expires in cache. However if the check with cache fails, we need to recheck with database to make sure we don't reject request which uses new credentials while the old credential is not expired in cache yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasbeen -> has been?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

'app_id': appId
});
callback(null, {
'statusCode': HttpStatus.CREATED,
'username': username,
'password': password
});
}
else {
logger.info('Existing credentials hasbeen updated successfully', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasbeen -> has been?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

'app_id': appId
});
var deleted = credentialCache.del(appId);
if (deleted != 1) {
logger.info('Cache invalidation failed', {
'app_id': appId
});
}
callback(null, {
'statusCode': HttpStatus.OK,
'username': username,
'password': password
});
}
}).catch(function(error) {
logger.error('Failed to create custom metrics credentials', {
'app_id': appId,
'error': error
});
error.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
callback(error);
});
}

credhelper.deleteCredentials = function(req, callback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete credential will need to invalidate the entry in cache

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache invalidation added.

var appId = req.params.app_id;
models.credentials.destroy({
where: {
id: appId
}
}).then(function(result) {
if (result > 0) {
logger.info('Successfully deleted the custom metrics credentials for application', {
'app id': appId
});
var deleted = credentialCache.del(appId);
if (deleted != 1) {
logger.info('Cache invalidation failed', {
'app_id': appId
});
}
callback(null, {
'statusCode': HttpStatus.OK
});
}
else {
var error = {
message: 'No custom metrics credentials exists with application',
statusCode: HttpStatus.NOT_FOUND
}
logger.error('No custom metrics credentials exists with application', {
'app id': appId,
error: error
});
callback(error);
}
}).catch(function(error) {
logger.error('Internal Error while deleting custom metrics credentials', {
'app id': appId,
'error': error
});
error.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
callback(error);
});
};

credhelper.validateCredentials = function(req, callback) {
var appId = req.params.app_id;
var username = req.query["username"];
var password = req.query["password"];
var creds,isValidCred,cachedCred;
if (!username || !password) {
var insufficientQueryparamError = new Error();
insufficientQueryparamError.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
insufficientQueryparamError.message = 'insufficient query parameters';
logger.error('Failed to validate custom metrics credentials due to insufficient query parameters', {
'app_id': appId,
'error': insufficientQueryparamError
});
callback(insufficientQueryparamError);
return;
}
// Try to find credentials in cache
try{
creds = credentialCache.get(appId, true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to get data from database directly when there is not data in cache rather than throwing an error and catching it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As getting creds directly from database is being already handled in catch block ..tried handling in the same way.

isValidCred = validateCredentialDetails(username, creds.username, password, creds.password);
// If cache contains old or invalid credentials
if (!isValidCred){
logger.info('Credentials not valid', {
'app_id': appId,
'isValid': isValidCred
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it is not valid, we may need to check with database... and refresh cache.

});
throw new Error('Invalid or old credentials found in cache');
}
else {
logger.info('valid credentials hasbeen found successfully in cache', {
'app_id': appId,
'isValid': isValidCred
});
callback(null, {
'statusCode': HttpStatus.OK,
'isValid': isValidCred,
});
}
}
catch(err){
// Did not find credentials in cache, lets find in database.
models.credentials.find({
where: {
id: appId
}
}).then(function(creds) {
if (!creds) {
var error = {
message: 'No credentials found',
statusCode: HttpStatus.NOT_FOUND
}
logger.info('No credentials found', {
'app_id': appId,
'error': error
});
callback(error);
}
else {
isValidCred = validateCredentialDetails(username, creds.username, password, creds.password);
logger.info('Credentials hasbeen found successfully in database', {
'app_id': appId,
'isValid': isValidCred
});
cachedCred = {
'username': creds.username,
'password': creds.password
};
var isCached = credentialCache.set(appId, cachedCred, cacheTTL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason the cred is not cached? if it is not cached, it is not good to have log message saying "credential cached"

logger.info('Credential cached',{ 'app_id':appId, 'isCached':isCached });
callback(null, {
'statusCode': HttpStatus.OK,
'isValid': isValidCred
});
}
}).catch(function(err) {
logger.error('Failed to validate custom metrics credentials', {
'app_id': appId,
'error': err
});
err.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
callback(err);
});
}
}
return credhelper;
}
78 changes: 78 additions & 0 deletions api/lib/routes/credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict';
module.exports = function(models, credentialCache, cacheTTL) {

var express = require('express');
var router = express.Router();
var logger = require('../log/logger');
var credHelper = require('./credentialHelper')(models, credentialCache, cacheTTL);

router.post('/:app_id/credentials', function(req, resp) {
var appId = req.params.app_id;
logger.info('Request for credentials creation received', {
'app_id': appId
});
credHelper.createOrUpdateCredentials(req, function(err, result) {
var responseBody = {};
var statusCode;
if (err) {
statusCode = err.statusCode;
responseBody = {
'error': err.message
};
}
else {
statusCode = result.statusCode;
responseBody = {
'username': result.username,
'password': result.password
}
}
resp.status(statusCode).json(responseBody)
});
});

router.delete('/:app_id/credentials', function(req, res) {
logger.info('Request for credentials deletion received', {
'app_id': req.params.app_id
});
credHelper.deleteCredentials(req, function(err, result) {
var responseBody = {};
var statusCode;
if (err) {
statusCode = err.statusCode;
responseBody = {
'error': err.message
};
}
else {
statusCode = result.statusCode;
}
res.status(statusCode).json(responseBody);
});
});

router.post('/:app_id/credentials/validate', function(req, resp) {
var appId = req.params.app_id;
logger.info('Request for credential validation received', {
'app_id': appId
});
credHelper.validateCredentials(req, function(err, result) {
var responseBody = {};
var statusCode;
if (err) {
statusCode = err.statusCode;
responseBody = {
'error': err.message
};
}
else {
statusCode = result.statusCode;
responseBody = {
'isValid': result.isValid
}
}
resp.status(statusCode).json(responseBody)
});
});
return router;
}
3 changes: 1 addition & 2 deletions api/lib/validation/schemaValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,11 @@ var getPolicySchema = function() {
var getScalingRuleSchema = function() {
var validOperators = getValidOperators();
var adjustmentPattern = getAdjustmentPattern();
var metricTypeEnum = getMetricTypes();
var schema = {
'type': 'object',
'id':'/scaling_rules',
'properties' : {
'metric_type':{ 'type':'string' ,'enum':metricTypeEnum },
'metric_type':{ 'type':'string' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably will need to add a prefix for custom metrics, to distinguish whether it is custom metrics or not. This will also avoid conflict with built-in metrics.

We need to call this out in doc as well.

'breach_duration_secs':{ 'type':'number','minimum': 60,'maximum': 3600 },
'threshold':{ 'type':'number'},
'operator':{ 'type':'string','enum': validOperators },
Expand Down
Loading