From 915a3b8299da05db936f536893215e5d4ef7ed7b Mon Sep 17 00:00:00 2001 From: Xin LI Date: Mon, 14 Feb 2022 18:47:40 +0100 Subject: [PATCH] improvement: ZENKO-3925-add ARWWI e2e tests --- eve/workers/end2end/scripts/run-e2e-test.sh | 14 ++ .../iam_policies/cloudserver/AssumeRole.js | 4 +- .../cloudserver/AssumeRoleWithWebIdentity.js | 137 ++++++++++++++++++ .../node_tests/utils/getWebIdentityToken.js | 85 +++++++++++ 4 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 tests/zenko_tests/node_tests/utils/getWebIdentityToken.js diff --git a/eve/workers/end2end/scripts/run-e2e-test.sh b/eve/workers/end2end/scripts/run-e2e-test.sh index b6548f434d..7e07bc06bb 100755 --- a/eve/workers/end2end/scripts/run-e2e-test.sh +++ b/eve/workers/end2end/scripts/run-e2e-test.sh @@ -26,6 +26,13 @@ ZENKO_ACCESS_KEY=$(kubectl get secret end2end-account-zenko -o jsonpath='{.data. ZENKO_SECRET_KEY=$(kubectl get secret end2end-account-zenko -o jsonpath='{.data.SecretAccessKey}' | base64 -d) ZENKO_SESSION_TOKEN=$(kubectl get secret end2end-account-zenko -o jsonpath='{.data.SessionToken}' | base64 -d) OIDC_FULLNAME="${OIDC_FIRST_NAME} ${OIDC_LAST_NAME}" +KEYCLOAK_TEST_USER="${OIDC_USERNAME}-norights" +KEYCLOAK_TEST_PASSWORD=${OIDC_PASSWORD} +KEYCLOAK_TEST_HOST=${OIDC_ENDPOINT} +KEYCLOAK_TEST_PORT="80" +KEYCLOAK_TEST_REALM_NAME=${OIDC_REALM} +KEYCLOAK_TEST_CLIENT_ID=${OIDC_CLIENT_ID} +KEYCLOAK_TEST_GRANT_TYPE="password" run_e2e_test() { kubectl run ${1} ${POD_NAME} \ @@ -75,6 +82,13 @@ run_e2e_test() { --env=RING_S3C_ENDPOINT=${RING_S3C_ENDPOINT} \ --env=RING_S3C_BACKEND_SOURCE_LOCATION=${RING_S3C_BACKEND_SOURCE_LOCATION} \ --env=RING_S3C_INGESTION_SRC_BUCKET_NAME=${RING_S3C_INGESTION_SRC_BUCKET_NAME} \ + --env=KEYCLOAK_TEST_USER=${KEYCLOAK_TEST_USER} \ + --env=KEYCLOAK_TEST_PASSWORD=${KEYCLOAK_TEST_PASSWORD} \ + --env=KEYCLOAK_TEST_HOST=${KEYCLOAK_TEST_HOST} \ + --env=KEYCLOAK_TEST_PORT=${KEYCLOAK_TEST_PORT} \ + --env=KEYCLOAK_TEST_REALM_NAME=${KEYCLOAK_TEST_REALM_NAME} \ + --env=KEYCLOAK_TEST_CLIENT_ID=${KEYCLOAK_TEST_CLIENT_ID} \ + --env=KEYCLOAK_TEST_GRANT_TYPE=${KEYCLOAK_TEST_GRANT_TYPE} \ --command -- sh -c "${2}" } diff --git a/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRole.js b/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRole.js index 41b8ecd3c5..0a13d6ead0 100644 --- a/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRole.js +++ b/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRole.js @@ -305,10 +305,10 @@ describe('iam policies - cloudserver - AssumeRole - Metadata', () => { secretAccessKey: res.Credentials.SecretAccessKey, sessionToken: res.Credentials.SessionToken, }; - return async.eachOf(test.buckets, (path, idx, eachCb) => { + return async.eachOf(test.buckets, (bucket, idx, eachCb) => { // make metadataSearch request on specific buckets using session user's credentials // and see if can get the correct response - metadataSearchResponseCode(sessionUserCredentials, test.buckets[idx], (err, res) => { + metadataSearchResponseCode(sessionUserCredentials, bucket, (err, res) => { if (err) { assert.ifError(err); return done(err); diff --git a/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRoleWithWebIdentity.js b/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRoleWithWebIdentity.js index e69de29bb2..7b85501d28 100644 --- a/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRoleWithWebIdentity.js +++ b/tests/zenko_tests/node_tests/iam_policies/cloudserver/AssumeRoleWithWebIdentity.js @@ -0,0 +1,137 @@ +const assert = require('assert'); +const async = require('async'); +const VaultClient = require('../../VaultClient'); +const { getS3Client } = require('../../s3SDK'); +const { getSTSClient } = require('../../stsSDK'); +const { getTokenForIdentity } = require('../../utils/getWebIdentityToken'); +const { metadataSearchResponseCode } = require('./utils'); + +let iamClient = null; +let stsClient = null; +let s3Client = null; + +const clientAdmin = VaultClient.getAdminClient(); +const accountName = 'AccountTest'; +const accountInfo = { + email: `${accountName}@test.com`, + password: 'test', +}; +const externalAccessKey = 'DZMMJUPWIUK8IWXRP0HQ'; +const externalSecretKey = 'iTuJdlidzrLipymvAGrLP66Yxghl4NQxLZR3cLlu'; + +const duration = '1000'; + +const bucket1 = 'bucket1'; + +const storageManagerName = 'storage_manager'; +const storageAccountOwnerName = 'storage_account_owner'; +const dataConsumerName = 'data_consumer'; +const storageManagerRole = 'storage-manager-role'; +const storageAccountOwnerRole = 'storage-account-owner-role'; +const dataConsumerRole = 'data-consumer-role'; + +describe('iam policies - cloudserver - AssumeRoleWithWebIdentity - Metadata', () => { + + before(done => { + async.series([ + // create an account, generateAccountAccessKey for it + // get iam client, sts client and s3 client of this account + next => clientAdmin.createAccount(accountName, accountInfo, next), + next => clientAdmin.generateAccountAccessKey(accountName, next, { externalAccessKey, externalSecretKey }), + next => { + iamClient = VaultClient.getIamClient(externalAccessKey, externalSecretKey); + stsClient = getSTSClient(externalAccessKey, externalSecretKey); + s3Client = getS3Client(externalAccessKey, externalSecretKey); + next(); + }, + // use s3 client to create a bucket and put 2 objects + next => { + async.series([ + next => s3Client.createBucket({ Bucket: bucket1 }, next), + next => s3Client.putObject({ Bucket: bucket1, Key: 'file1' }, next), + next => s3Client.putObject({ Bucket: bucket1, Key: 'file2' }, next), + ], next); + }, + ], done); + }); + + after(done => { + async.series([ + next => s3Client.deleteObjects({ + Bucket: bucket1, + Delete: { + Objects: [{ Key: 'file1' }, { Key: 'file2' }], + Quiet: false, + }, + }, next), + next => s3Client.deleteBucket({ Bucket: bucket1 }, next), + next => clientAdmin.deleteAccount(accountName, next), + ], done); + }); + + + const tests = [ + { + name: 'should be able to perform metadata search on all buckets for storage manager role', + oidcIdentity: storageManagerName, + roleName: storageManagerRole, + assertion: result => assert.strictEqual(result.statusCode, 200), + }, + { + name: 'should be able to perform metadata search on all buckets for storage account owner role', + oidcIdentity: storageAccountOwnerName, + roleName: storageAccountOwnerRole, + assertion: result => assert.strictEqual(result.statusCode, 200), + }, + { + name: 'should be able to perform metadata search on all buckets for data consumer role', + oidcIdentity: dataConsumerName, + roleName: dataConsumerRole, + assertion: result => assert.strictEqual(result.statusCode, 200), + }, + ]; + + tests.forEach((test, i) => { + it(test.name, done => { + let jwtToken = null; + let roleArn = null; + async.waterfall([ + next => getTokenForIdentity(test.oidcIdentity, (err, token) => { + assert.ifError(err); + jwtToken = token; + next(); + }), + next => iamClient.getRole({ RoleName: test.roleName }, (err, res) => { + assert.ifError(err); + roleArn = res.Role.Arn; + next(); + }), + next => stsClient.assumeRoleWithWebIdentity({ + RoleArn: roleArn, + DurationSeconds: duration, + WebIdentityToken: jwtToken, + RoleSessionName: `session-name-test-${i}`, + }, (err, res) => { + assert.ifError(err); + return next(null, res); + }), + (res, next) => { + const sessionUserCredentials = { + accessKeyId: res.Credentials.AccessKeyId, + secretAccessKey: res.Credentials.SecretAccessKey, + sessionToken: res.Credentials.SessionToken, + }; + // make metadataSearch request using session user's credentials + // and see if can get the correct response + metadataSearchResponseCode(sessionUserCredentials, bucket1, (err, res) => { + if (err) { + assert.ifError(err); + return done(err); + } + test.assertion(res); + return next(); + }); + }], done); + }); + }); +}); diff --git a/tests/zenko_tests/node_tests/utils/getWebIdentityToken.js b/tests/zenko_tests/node_tests/utils/getWebIdentityToken.js new file mode 100644 index 0000000000..ac97b29dd1 --- /dev/null +++ b/tests/zenko_tests/node_tests/utils/getWebIdentityToken.js @@ -0,0 +1,85 @@ +const querystring = require("querystring"); +const http = require("http"); +const assert = require('assert'); + +const USER_1_PASSWORD = process.env.KEYCLOAK_TEST_PASSWORD || '123'; +const HOST_1_URL = process.env.KEYCLOAK_TEST_HOST || 'http://keycloak.zenko.local'; +const HOST_1_PORT = parseInt(process.env.KEYCLOAK_TEST_PORT, 10) || 80; +const REALM_NAME = process.env.KEYCLOAK_TEST_REALM_NAME || 'zenko'; +const KEYCLOAK_PATH = `/auth/realms/${REALM_NAME}/protocol/openid-connect/token`; +const CLIENT_ID = process.env.KEYCLOAK_TEST_CLIENT_ID || 'zenko-ui'; +const GRANT_TYPE = process.env.KEYCLOAK_TEST_GRANT_TYPE || 'password'; + + +function getTokenForIdentity(identity, callback) { + getWebIdentityToken(identity, USER_1_PASSWORD, HOST_1_URL, + HOST_1_PORT, KEYCLOAK_PATH, CLIENT_ID, GRANT_TYPE, (err, token) => { + assert(err === null); + callback(err, token); + }); +} + + +/** + * HTTP client to request JWT token given the username and password. + * + * @param {string} username - username of user requesting token + * @param {string} password - password of user requesting token + * @param {string} host - host URL of keycloak service + * @param {number} port - port of keycloak service + * @param {string} path - path of keycloak service authentication API + * @param {string} client_id - id of the client of the user + * @param {string} grant_type - grant of the user + * @param {function} callback - callback function called with error or result + * @returns {undefined} undefined + */ +function getWebIdentityToken(username, password, host, port, path, + client_id, grant_type, callback) { + // In Zenko, we are using an endpoint as the `KEYCLOAK_TEST_HOST` env variable + // So we should remove any existing http of https prefix in HOST_1_URL. + host = host.replace('https://', '').replace('http://', ''); + const userData = querystring.stringify({ + username, + password, + client_id, + grant_type, + }); + const options = { + host, + port, + method: 'POST', + path, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': userData.length, + }, + rejectUnauthorized: false, + }; + const req = http.request(options, response => { + if (response.statusCode < 200 || response.statusCode >= 300) { + response.resume(); + return callback(new Error(`Status Code: ${response.statusCode}`)); + } + const data = []; + return response + .on('data', chunk => data.push(chunk)) + .on('end', () => { + let accessToken = null; + let error = null; + try { + accessToken = (JSON.parse(Buffer.concat(data))).access_token; + } catch (err) { + error = err; + } + return callback(error, accessToken); + }) + .on('error', err => callback(err)); + }); + req.on('error', err => callback(err)); + req.write(userData); + req.end(); +} + +module.exports = { + getTokenForIdentity, +}