Skip to content

Commit

Permalink
Zowe Suite 1.8.0
Browse files Browse the repository at this point in the history
  • Loading branch information
zowe-robot authored Feb 5, 2020
2 parents f78cbea + d31a131 commit c14a37b
Show file tree
Hide file tree
Showing 7 changed files with 885 additions and 1 deletion.
2 changes: 1 addition & 1 deletion lib/webauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function setAuthPluginSession(req, pluginID, authPluginSession) {

function getRelevantHandlers(authManager, body) {
let handlers = authManager.getAllHandlers();
if (body && body.categories) {
if (body && Array.isArray(body.categories)) {
const authCategories = {};
body.categories.map(t => authCategories[t] = true);
handlers = handlers.filter(h =>
Expand Down
200 changes: 200 additions & 0 deletions plugins/sso-auth/lib/apimlHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
This program and the accompanying materials are
made available under the terms of the Eclipse Public License v2.0 which accompanies
this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html
SPDX-License-Identifier: EPL-2.0
Copyright Contributors to the Zowe Project.
*/

const Promise = require('bluebird');
const https = require('https');
const fs = require('fs');

/*495 minutes default session length for zosmf
* TODO: This is the session length of a zosmf session according to their documentation.
* However, it is not clear if that is configurable or if APIML may use a different value under other circumstances
*/
const DEFAULT_EXPIRATION_MS = 29700000;

function readUtf8FilesToArray(fileArray) {
var contentArray = [];
for (var i = 0; i < fileArray.length; i++) {
const filePath = fileArray[i];
try {
var content = fs.readFileSync(filePath);
if (content.indexOf('-BEGIN CERTIFICATE-') > -1) {
contentArray.push(content);
}
else {
content = fs.readFileSync(filePath, 'utf8');
if (content.indexOf('-BEGIN CERTIFICATE-') > -1) {
contentArray.push(content);
}
else {
this.logger.warn('Error: file ' + filePath + ' is not a certificate')
}
}
} catch (e) {
this.logger.warn('Error when reading file=' + filePath + '. Error=' + e.message);
}
}

if (contentArray.length > 0) {
return contentArray;
} else {
return null;
}
}


class ApimlHandler {
constructor(pluginDef, pluginConf, serverConf, context) {
this.logger = context.logger;
this.apimlConf = serverConf.node.mediationLayer.server;
this.gatewayUrl = `https://${this.apimlConf.hostname}:${this.apimlConf.gatewayPort}`;

if (serverConf.node.https.certificateAuthorities === undefined) {
this.logger.warn("This server is not configured with certificate authorities, so it will not validate certificates with APIML");
this.httpsAgent = new https.Agent({
rejectUnauthorized: false
});
} else {
this.httpsAgent = new https.Agent({
rejectUnauthorized: true,
ca: readUtf8FilesToArray(serverConf.node.https.certificateAuthorities)
});
}
}

/**
* Should be called e.g. when the users enters credentials
*
* Supposed to change the state of the client-server session. NOP for
* stateless authentication (e.g. HTTP basic).
*
* `request` must be treated as read-only by the code. `sessionState` is this
* plugin's private storage within the session (if stateful)
*
* If auth doesn't fail, should return an object containing at least
* { success: true }. Should not reject the promise.
*/
authenticate(request, sessionState) {
return new Promise((resolve, reject) => {
const gatewayUrl = this.gatewayUrl;
const data = JSON.stringify({
username: request.body.username,
password: request.body.password
});
const options = {
hostname: this.apimlConf.hostname,
port: this.apimlConf.gatewayPort,
path: '/api/v1/apicatalog/auth/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
},
agent: this.httpsAgent
}

const req = https.request(options, (res) => {
res.on('data', (d) => {});
res.on('end', () => {
let apimlCookie;
if (res.statusCode == 204) {
if (typeof res.headers['set-cookie'] === 'object') {
for (const cookie of res.headers['set-cookie']) {
const content = cookie.split(';')[0];
if (content.indexOf('apimlAuthenticationToken') >= 0) {
apimlCookie = content;
}
}
}
}

if (apimlCookie) {
sessionState.username = request.body.username;
sessionState.apimlCookie = apimlCookie;
sessionState.apimlToken = apimlCookie.split("=")[1];
resolve({ success: true, username: sessionState.username, expms: DEFAULT_EXPIRATION_MS });
} else {
let response = {
success: false,
reason: 'Unknown',
error: {
message: `APIML ${res.statusCode} ${res.statusMessage}`
}
};
//Seems that when auth is first called, it may not be loaded yet, so you get a 405.
if (res.statusCode == 405) {
response.reason = 'TryAgain';
}
resolve(response);
}
});
});

req.on('error', (error) => {
this.logger.warn("APIML login has failed:");
this.logger.warn(error);
var details = error.message;
if ((error.response !== undefined) && (error.response.data !== undefined)) {
details = error.response.data;
}
resolve({
success: false,
reason: 'Unknown',
error: { message: `APIML ${details}`}
});
});

req.write(data);
req.end();
});
}

cleanupSession(sessionState) {
delete sessionState.apimlToken;
delete sessionState.apimlCookie;
}

/**
* Invoked for every service call by the middleware.
*
* Checks if the session is valid in a stateful scheme, or authenticates the
* request in a stateless scheme. Then checks if the user can access the
* resource. Modifies the request if necessary.
*
* `sessionState` is this plugin's private storage within the session (if
* stateful)
*
* The promise should resolve to an object containing, at least,
* { authorized: true } if everything is fine. Should not reject the promise.
*/
authorized(request, sessionState) {
if (sessionState.authenticated) {
request.username = sessionState.username;
request.ssoToken = sessionState.apimlToken;
return Promise.resolve({ authenticated: true, authorized: true });
} else {
return Promise.resolve({ authenticated: false, authorized: false });
}
}

addProxyAuthorizations(req1, req2Options, sessionState, usingSso) {
if (!sessionState.apimlCookie) {
return;
}
//apimlToken vs apimlAuthenticationToken ???
req2Options.headers['apimlToken'] = sessionState.apimlToken;
if (this.usingSso) {
req2Options.headers['Authorization'] = 'Bearer '+sessionState.apimlToken;
}
}
}

module.exports = function(pluginDef, pluginConf, serverConf, context) {
return new ApimlHandler(pluginDef, pluginConf, serverConf, context);
}
151 changes: 151 additions & 0 deletions plugins/sso-auth/lib/safprofile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
This program and the accompanying materials are
made available under the terms of the Eclipse Public License v2.0 which accompanies
this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html
SPDX-License-Identifier: EPL-2.0
Copyright Contributors to the Zowe Project.
*/

const ZOWE_PROFILE_NAME_LEN = 246;
const DEFAULT_INSTANCE_ID = "0";

function partsUpToTotalLength(parts, maxLen) {
let curLen = 0;
const outParts = [];

for (let p of parts) {
curLen += p.length;
if (curLen > maxLen) {
break;
}
curLen++; //account for the separator
outParts.push(p);
}
return outParts;
}

function rootServiceProfileName(parms){
if (parms.productCode == null) {
throw new Error("productCode missing");
}
if (parms.instanceID == null) {
throw new Error("instanceID missing");
}
if (parms.rootServiceName == null) {
throw new Error("rootServiceName missing");
}
if (parms.method == null) {
throw new Error("method missing");
}
return `${parms.productCode}.${parms.instanceID}.COR`
+ `.${parms.method}.${parms.rootServiceName}`;
}

function serviceProfileName(parms) {
if (parms.productCode == null) {
throw new Error("productCode missing");
}
if (parms.instanceID == null) {
throw new Error("instanceID missing");
}
if (parms.pluginID == null) {
throw new Error("pluginID missing");
}
if (parms.serviceName == null) {
throw new Error("serviceName missing");
}
if (parms.method == null) {
throw new Error("method missing");
}
return `${parms.productCode}.${parms.instanceID}.SVC.${parms.pluginID}`
+ `.${parms.serviceName}.${parms.method}`;
}

function configProfileName(parms) {
if (parms.productCode == null) {
throw new Error("productCode missing");
}
if (parms.instanceID == null) {
throw new Error("instanceID missing");
}
if (parms.pluginID == null) {
throw new Error("pluginID missing");
}
if (parms.method == null) {
throw new Error("method missing");
}
if (parms.scope == null) {
throw new Error("scope missing");
}
return `${parms.productCode}.${parms.instanceID}.CFG.${parms.pluginID}.`
+ `${parms.method}.${parms.scope}`;
}

function makeProfileName(type, parms) {
let makeProfileName;
switch(type){
case "service":
makeProfileName = serviceProfileName;
break;
case "config":
makeProfileName = configProfileName;
break;
case "core":
makeProfileName = rootServiceProfileName;
break;
}
let profileName = makeProfileName(parms);
if (profileName.length > ZOWE_PROFILE_NAME_LEN) {
throw new Error("SAF resource name too long");
}
if (parms.subUrl.length > 0) {
const usableParts = partsUpToTotalLength(parms.subUrl,
ZOWE_PROFILE_NAME_LEN - profileName.length - 1);
if (usableParts.length > 0) {
profileName += '.' + usableParts.join('.');
}
}
return profileName;
}

function makeProfileNameForRequest(url, method, instanceID) {
let urlData;
let type;
if (!url.match(/^\/[A-Za-z0-9]+\/plugins\//)) {
url = url.toUpperCase();
type = "core";
let splitUrl = url.split('/');
splitUrl = splitUrl.filter(x => x);
let productCode = "ZLUX";
let rootServiceName = splitUrl[0];
let subUrl = splitUrl.slice(1);
if (!instanceID) {
instanceID = DEFAULT_INSTANCE_ID;
}
urlData = { productCode, instanceID, rootServiceName, method, subUrl };
} else {
url = url.toUpperCase();
let [_l, productCode, _p, pluginID, _s, serviceName, _v, ...subUrl] = url.split('/');
if (!instanceID) {
instanceID = DEFAULT_INSTANCE_ID;
}
subUrl = subUrl.filter(x => x);
if ((pluginID === "ORG.ZOWE.CONFIGJS") && (serviceName === "DATA")) {
type = "config";
pluginID = subUrl[0];
let scope = subUrl[1];
subUrl = subUrl.slice(2);
urlData = { productCode, instanceID, pluginID, method, scope, subUrl };
} else {
type = "service";
urlData = { productCode, instanceID, pluginID, serviceName, method, subUrl };
}
urlData.pluginID = urlData.pluginID? urlData.pluginID.replace(/\./g, "_") : null;
}
return makeProfileName(type, urlData);
};

exports.makeProfileNameForRequest = makeProfileNameForRequest;
exports.ZOWE_PROFILE_NAME_LEN = ZOWE_PROFILE_NAME_LEN;
Loading

0 comments on commit c14a37b

Please sign in to comment.