UNPKG

atlassian-connect-express

Version:

Library for building Atlassian Add-ons on top of Express

295 lines (253 loc) 8.36 kB
const request = require("request"); const _ = require("lodash"); const moment = require("moment"); const jwt = require("atlassian-jwt"); const URI = require("urijs"); const querystring = require("querystring"); const OAuth2 = require("./oauth2"); const utils = require("./utils"); class HostClient { constructor(addon, context, clientKey) { utils.checkNotNull(addon, "addon"); utils.checkNotNull(addon.settings, "addon.settings"); this.addon = addon; this.context = context || {}; this.clientKey = clientKey; this.oauth2 = new OAuth2(addon); } defaults(options) { return request.defaults.apply(null, this.modifyArgs(options)); } cookie() { return request.cookie.apply(null, arguments); } jar() { return request.jar(); } /** * Make a request to the host product as the specific user. Will request and retrieve an access token if necessary * * @param userKey - the key referencing the remote user to impersonate when making the request * @returns HostClient - `hostClient` object suitable for chaining */ asUser(userKey) { if (!userKey) { throw new Error("A userKey must be provided to make a request as a user"); } const product = this.addon.config.product(); if (!product.isJIRA && !product.isConfluence) { throw new Error( `the asUser method is not available for ${product.id} add-ons` ); } // Warn that this is deprecated console.warn( "This has been deprecated, as per https://ecosystem.atlassian.net/browse/ACEJS-115" ); const impersonatingClient = new HostClient( this.addon, this.context, this.clientKey ); impersonatingClient.userKey = userKey; return impersonatingClient; } /** * Make a request to the host product as the specific user. Will request and retrieve and access token if necessary * * @param userAccountId - the Atlassian Account Id of the remote user to impersonate when making the request * @returns HostClient - `hostClient` object suitable for chaining. */ asUserByAccountId(userAccountId) { if (!userAccountId) { throw new Error( "A userAccountId must be provided to make a request as a user" ); } const product = this.addon.config.product(); if (!product.isJIRA && !product.isConfluence) { throw new Error( `the asUserByAccountId method is not available for ${product.id} add-ons` ); } const impersonatingClient = new HostClient( this.addon, this.context, this.clientKey ); impersonatingClient.userAccountId = userAccountId; return impersonatingClient; } modifyArgs(options, augmentHeaders, callback, hostBaseUrl) { const args = []; if (_.isString(options)) { options = { uri: options }; } if (options.url) { options.uri = options.url; delete options.url; } if (options.form) { options.multipartFormData = options.form; delete options.form; this.addon.logger.warn( "options.form is deprecated: please use options.multipartFormData" ); } if (options.urlEncodedFormData) { options.form = options.urlEncodedFormData; delete options.urlEncodedFormData; } let originalUri = options.uri; const targetUri = new URI(originalUri); const hostBaseUri = new URI(hostBaseUrl); if (options.qs) { targetUri.query(querystring.encode(options.qs)); originalUri = targetUri.toString(); delete options.qs; } if (!targetUri.origin()) { targetUri.origin(hostBaseUri.origin()); const newPath = URI.joinPaths(hostBaseUri.path(), targetUri.path()); targetUri.path(newPath.path()); } options.uri = targetUri.toString(); args.push(options); if (targetUri.origin() === hostBaseUri.origin()) { if (!options.headers) { options.headers = {}; } if (augmentHeaders) { augmentHeaders(options.headers, originalUri); } options.jar = false; } if (callback) { args.push(callback); } return args; } createJwtPayload(req) { const now = moment().utc(), jwtTokenValidityInMinutes = this.addon.config.jwt().validityInMinutes; const token = { iss: this.addon.key, iat: now.unix(), exp: now.add(jwtTokenValidityInMinutes, "minutes").unix(), qsh: jwt.createQueryStringHash(jwt.fromExpressRequest(req)) }; if (this.addon.config.product().isBitbucket) { token.sub = this.clientKey; } else if ( this.addon.config.product().isJIRA || this.addon.config.product().isConfluence ) { token.aud = [this.clientKey]; } return token; } getUserBearerToken(scopes, clientSettings) { utils.checkNotNull(clientSettings.baseUrl, "clientSettings.baseUrl"); utils.checkNotNull( clientSettings.oauthClientId, "clientSettings.oauthClientId" ); utils.checkNotNull( clientSettings.sharedSecret, "clientSettings.sharedSecret" ); if (this.userAccountId) { return this.oauth2.getUserBearerTokenByUserAccountId( this.userAccountId, scopes, clientSettings ); } else if (this.userKey) { return this.oauth2.getUserBearerToken( this.userKey, scopes, clientSettings ); } else { throw new Error( "One of userAccountId or userKey must be provided. Did you call asUserByAccountId(userAccountId)?" ); } } } ["get", "post", "put", "del", "head", "patch"].forEach(method => { // hostClient.get -> return function // hostClient.get(options, callback) -> get client settings -> augment options -> callback HostClient.prototype[method] = function (options, callback) { const self = this; return this.addon.settings .get("clientInfo", this.clientKey) .then(clientSettings => { if (!clientSettings) { const message = `There are no "clientInfo" settings in the store for tenant "${self.clientKey}"`; self.addon.logger.warn(message); return Promise.reject(message); } const clientContext = { clientSettings }; if (self.userKey || self.userAccountId) { return self.getUserBearerToken([], clientSettings).then(token => { clientContext.bearerToken = token.access_token; return Promise.resolve(clientContext); }); } else { return Promise.resolve(clientContext); } }) .then( clientContext => { const augmentHeaders = function (headers, relativeUri) { const uri = new URI(relativeUri); const query = uri.search(true); const httpMethod = method === "del" ? "delete" : method; if (!(self.userKey || self.userAccountId)) { const jwtPayload = self.createJwtPayload({ method: httpMethod, path: uri.path(), query }), jwtToken = jwt.encode( jwtPayload, clientContext.clientSettings.sharedSecret, "HS256" ); headers.authorization = `JWT ${jwtToken}`; } else { headers.authorization = `Bearer ${clientContext.bearerToken}`; } headers["User-Agent"] = self.addon.config.userAgent(); }; const args = self.modifyArgs( options, augmentHeaders, callback, clientContext.clientSettings.baseUrl ); const multipartFormData = options.multipartFormData; delete options.multipartFormData; const _request = request[method].apply(null, args); if (multipartFormData) { const form = _request.form(); _.forOwn(multipartFormData, (value, key) => { if (Array.isArray(value)) { form.append.apply(form, [key].concat(value)); } else { form.append.apply(form, [key, value]); } }); } return _request; }, err => { callback(err); } ); }; }); module.exports = HostClient;