Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vaultclient update to allow usage of superadmin APIs without superadmin credentials #411

Draft
wants to merge 20 commits into
base: development/8.3
Choose a base branch
from
Draft
3 changes: 1 addition & 2 deletions bin/vaultclient
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ program
email: args.email,
quota: args.quota || null,
externalAccountId: args.accountid || null
},
handleVaultResponse);
}, handleVaultResponse);
}));

program
Expand Down
203 changes: 148 additions & 55 deletions lib/IAMClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
/* eslint-disable no-underscore-dangle */
'use strict'; // eslint-disable-line

const { auth, errors, constants } = require('arsenal');
const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { Sha256 } = require('@aws-crypto/sha256-universal');
// const { auth, errors, constants } = require('arsenal');
const { httpClientFreeSocketTimeout, convertUTCtoISO8601 } = require('./constants');
const assert = require('assert');
const werelogs = require('werelogs');
const http = require('http');
Expand Down Expand Up @@ -32,9 +35,11 @@ class VaultClient {
* for the Logger object
* @param {string} [path] - prefix requests with this path
* @param {string} [sessionToken] - session token for v4 signature
* @param {string} [webIdentityToken] - OIDC token for Federated access
*/
constructor(host, port, useHttps, key, cert, ca, ignoreCa,
accessKey, secretKeyValue, logApi, path, sessionToken) {
accessKey, secretKeyValue, logApi, path, sessionToken,
webIdentityToken) {
assert(typeof host === 'string' && host !== '', 'host is required');
assert(port === undefined || typeof port === 'number',
'port must be a number');
Expand All @@ -46,7 +51,7 @@ class VaultClient {
'ca must be a string');
assert(path === undefined || path === null
|| (typeof path === 'string' && path.startsWith('/')),
'path must be a string and start with a "/"');
'path must be a string and start with a "/"');
this.serverHost = host;
this.serverPort = port || 8600;
this._key = key;
Expand All @@ -59,23 +64,39 @@ class VaultClient {
keepAlive: true,
requestCert: true,
rejectUnauthorized: !(ignoreCa === true),
freeSocketTimeout: constants.httpClientFreeSocketTimeout,
freeSocketTimeout: httpClientFreeSocketTimeout,
});
} else {
this._agent = new HttpAgent({
keepAlive: true,
freeSocketTimeout: constants.httpClientFreeSocketTimeout,
freeSocketTimeout: httpClientFreeSocketTimeout,
});
}
this.accessKey = accessKey;
this.secretKeyValue = secretKeyValue;
this.sessionToken = sessionToken;
this.webIdentityToken = webIdentityToken;
this.logApi = logApi || werelogs;
this.log = new this.logApi.Logger('VaultClient');
this._path = path;
this.useAuthenticatedAdminRoutes = false;
}

setCustomEndpointForSignature(host, path) {
this._host = host;
this.__path = path;
}

disableLogging() {
this.log = null;
}

setWebIdentityToken(webIdentityToken) {
assert(webIdentityToken === undefined || typeof webIdentityToken === 'string', 'WebIdentity token must be a string');
this.webIdentityToken = webIdentityToken;
return this;
}

enableIAMOnAdminRoutes() {
this.useAuthenticatedAdminRoutes = true;
return this;
Expand Down Expand Up @@ -147,7 +168,7 @@ class VaultClient {
assert(typeof externalAccountId === 'string'
&& !conv
&& regexAccountId.test(externalAccountId),
'invalid account id supplied');
'invalid account id supplied');
data.externalAccountId = externalAccountId;
}
if (customAttributes) {
Expand All @@ -167,7 +188,7 @@ class VaultClient {
return callback(null, {
account: result.account.data,
});
}, data);
}, data, undefined, undefined);
}

/**
Expand Down Expand Up @@ -223,21 +244,21 @@ class VaultClient {
assert(typeof externalAccessKey === 'string'
&& regexpAccessKey.test(externalAccessKey)
&& externalAccessKey !== '',
'invalid access key supplied');
'invalid access key supplied');
data.externalAccessKey = externalAccessKey;
}
if (externalSecretKey) {
assert(typeof externalSecretKey === 'string'
&& regexpSecretKey.test(externalSecretKey)
&& externalSecretKey !== '',
'invalid secret key supplied');
'invalid secret key supplied');
data.externalSecretKey = externalSecretKey;
}
if (durationSeconds) {
assert(typeof durationSeconds === 'string'
&& Number.parseInt(durationSeconds, 10)
&& Number.parseInt(durationSeconds, 10) > 0,
'invalid expiration time supplied');
'invalid expiration time supplied');
data.DurationSeconds = durationSeconds;
}
}
Expand Down Expand Up @@ -314,7 +335,7 @@ class VaultClient {
* @param {VaultClient~requestCallback} callback - callback
* @returns {undefined}
*/
updateAccountAttributes(accountName, customAttributes, callback) {
updateAccountAttributes(accountName, customAttributes, callback, webIdentityToken) {
assert(typeof accountName === 'string' && accountName !== '',
'accountName is required');
assert(typeof customAttributes === 'object');
Expand Down Expand Up @@ -422,7 +443,7 @@ class VaultClient {
'filterKeyStartsWith should be a string');
data.filterKeyStartsWith = filterKeyStartsWith;
}
this.request('POST', '/', true, callback, data);
this.request('POST', '/', true, callback, data, undefined, undefined);
}

/**
Expand Down Expand Up @@ -799,10 +820,19 @@ class VaultClient {
* @returns{undefined}
*/
checkPolicies(requestContextParams, userArn, options, callback) {
assert(typeof requestContextParams === 'object',
'need requestContextParams');
assert(typeof requestContextParams.constantParams === 'object',
'need constantParams');
if (Array.isArray(requestContextParams)) {
requestContextParams.forEach(rc => {
assert(typeof rc === 'object',
'need requestContextParams');
assert(typeof rc.constantParams === 'object',
'need constantParams');
});
} else {
assert(typeof requestContextParams === 'object',
'need requestContextParams');
assert(typeof requestContextParams.constantParams === 'object',
'need constantParams');
}
assert(typeof userArn === 'string', 'need user arn');
const data = {
Action: 'CheckPolicies',
Expand All @@ -823,6 +853,27 @@ class VaultClient {
}, data, options.reqUid, 'application/json');
}

checkPermissions(actionParams, options, callback) {
assert(typeof actionParams === 'string',
'need actionParams');
const data = {
Action: 'CheckPermissions',
actionParams,
};
this.request('POST', '/', true, (err, data, code) => {
if (err) {
return callback(err);
}
return callback(null, {
message: {
body: data,
code,
message: 'Policies checked',
},
});
}, data, options.reqUid, 'application/json');
}

/**
* A getter for account canonical IDs given a list of account IDs
* @param{String[]} userIds - list of account IDs
Expand Down Expand Up @@ -874,11 +925,14 @@ class VaultClient {
* @param {string} [contentType] - content type of body
* @returns {undefined}
*/
request(method, path, iamAuthenticate, callback, data, reqUid,
async request(method, path, iamAuthenticate, callback, data, reqUid,
contentType) {
const log = reqUid
? this.log.newRequestLoggerFromSerializedUids(reqUid)
: this.log.newRequestLogger();
let log = null;
if (this.log) {
log = reqUid
? this.log.newRequestLoggerFromSerializedUids(reqUid)
: this.log.newRequestLogger();
}
const options = {
method,
path: this._path || path,
Expand All @@ -903,10 +957,38 @@ class VaultClient {
}
const req = this.useHttps
? https.request(options) : http.request(options);
if (iamAuthenticate) {
auth.client.generateV4Headers(req, data,
this.accessKey, this.secretKeyValue, 'iam', path,
this.sessionToken);
if (this.webIdentityToken) {
req.setHeader('authorization', `Bearer ${this.webIdentityToken}`);
} else if (iamAuthenticate) {
options.headers = {
Host: this._host || options.host,
};
if (this._host && this.__path) {
options.path = this.__path;
options.host = this._host;
}
const signer = new SignatureV4({
credentials: {
accessKeyId: this.accessKey,
secretAccessKey: this.secretKeyValue,
sessionToken: this.sessionToken,
},
region: 'us-east-1',
service: 'iam',
sha256: Sha256,
host: this._host ? this._host : options.host,
});
const signedReq = await signer.sign(options);
Object.keys(signedReq.headers).forEach(key => {
req.setHeader(key, signedReq.headers[key]);
});
if (this._host && this.__path) {
req._headers.host = {
name: 'host',
value: options.host,
};
options.path = this._path || path;
}
}
if (method === 'POST') {
if (contentType === 'application/json') {
Expand All @@ -925,27 +1007,31 @@ class VaultClient {
})

.on('error', err => {
log.debug('error receiving data', {
component: 'vaultclient',
method: 'VaultClient:request()',
error: err.message,
errorStack: err.stack,
});
return callback(errors.InternalError);
if (this.log) {
log.debug('error receiving data', {
component: 'vaultclient',
method: 'VaultClient:request()',
error: err.message,
errorStack: err.stack,
});
}
return callback(InternalError);
})

.on('end', () => {
this.handleResponse(res, ret, log, callback);
});
});
req.on('error', err => {
log.debug('error during request', {
component: 'vaultclient',
method: 'VaultClient:request()',
error: err.message,
errorStack: err.stack,
});
return callback(errors.InternalError);
if (this.log) {
log.debug('error during request', {
component: 'vaultclient',
method: 'VaultClient:request()',
error: err.message,
errorStack: err.stack,
});
}
return callback(InternalError);
});
req.end();
}
Expand Down Expand Up @@ -1004,10 +1090,12 @@ class VaultClient {
handleResponse(res, ret, log, cb) {
this.getObj(ret, (err, obj) => {
if (err) {
log.error('error from vault', {
error: err,
method: 'VaultClient.handleResponse',
});
if (this.log) {
log.error('error from vault', {
error: err,
method: 'VaultClient.handleResponse',
});
}
return cb(err, null, res.statusCode);
}
/* This is not an error */
Expand All @@ -1017,25 +1105,30 @@ class VaultClient {

// Load the error from errors(arsenal)
if (obj && obj.ErrorResponse && obj.ErrorResponse.Error) {
log.debug('error from vault', {
error: obj,
method: 'VaultClient.handleResponse',
});
return cb(
errors[obj.ErrorResponse.Error.Code],
if (this.log) {
log.debug('error from vault', {
error: obj,
method: 'VaultClient.handleResponse',
});
}
return cb({
code: obj.ErrorResponse.Error.Code,
description: obj.ErrorResponse.Error.Message,
},
null,
res.statusCode
);
}
if (obj && obj.InternalError) {
return cb(errors.InternalError, null, res.statusCode);
return cb(InternalError, null, res.statusCode);
}
log.error('unable to translate error from vault', {
error: obj,
method: 'VaultClient.handleResponse',
});
return cb(errors.InternalError
.customizeDescription('unable to translate error from vault'));
if (this.log) {
log.error('unable to translate error from vault', {
error: obj,
method: 'VaultClient.handleResponse',
});
}
return cb(InternalError);
});
}
}
Expand Down
11 changes: 11 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const httpClientFreeSocketTimeout = 55000;

const InternalError = {
code: 500,
description: 'We encountered an internal error. Please try again.',
};

module.exports = {
httpClientFreeSocketTimeout,
InternalError,
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
},
"homepage": "https://github.com/scality/vaultclient#readme",
"dependencies": {
"@aws-crypto/sha256-universal": "^2.0.1",
"@aws-sdk/signature-v4": "^3.130.0",
"agentkeepalive": "^4.1.3",
"arsenal": "git+https://github.com/scality/Arsenal#8.1.62",
"commander": "2.20.0",
"werelogs": "git+https://github.com/scality/werelogs#8.1.0",
"xml2js": "0.4.19"
Expand Down
Loading