Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,10 @@ jobs:
run: |-
set -o pipefail;
yarn run ft_healthchecks | tee /tmp/artifacts/${{ matrix.job-name }}/ft_healthchecks.log
- name: Run scripts tests
run: |-
set -o pipefail;
yarn run ft_scripts | tee /tmp/artifacts/${{ matrix.job-name }}/ft_scripts.log
- name: Teardown CI services
run: docker compose down redis sproxyd metadata-standalone vault cloudserver-sse-before-migration
working-directory: .github/docker
Expand Down
3 changes: 2 additions & 1 deletion bin/ensureServiceUser
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
PutUserPolicyCommand,
ListAccessKeysCommand,
CreateAccessKeyCommand,
NoSuchEntityException,
} = require('@aws-sdk/client-iam');
const { version } = require('../package.json');

Expand Down Expand Up @@ -181,7 +182,7 @@ function collectResource(v, done) {
v.collect()
.then(res => done(null, res))
.catch((err) => {
if (err.name === 'NoSuchEntity') {
Comment thread
tcarmet marked this conversation as resolved.
if (err instanceof NoSuchEntityException) {
return done();
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenko/cloudserver",
"version": "9.3.8",
"version": "9.3.9",
"description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol",
"main": "index.js",
"engines": {
Expand Down Expand Up @@ -126,6 +126,7 @@
"ft_healthchecks": "cd tests/functional/healthchecks && yarn test",
"ft_s3cmd": "cd tests/functional/s3cmd && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",
"ft_s3curl": "cd tests/functional/s3curl && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",
"ft_scripts": "cd tests/functional/scripts && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",
"ft_util": "cd tests/functional/utilities && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",
"ft_test": "npm-run-all -s ft_awssdk ft_s3cmd ft_s3curl ft_node ft_healthchecks ft_management ft_util ft_backbeat",
Comment thread
tcarmet marked this conversation as resolved.
"ft_search": "cd tests/functional/aws-node-sdk && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 90000 test/mdSearch --exit",
Expand Down
176 changes: 176 additions & 0 deletions tests/functional/scripts/ensureServiceUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const assert = require('assert');
const crypto = require('crypto');
const path = require('path');
const { execFile } = require('child_process');
const { promisify } = require('util');

const {
IAMClient,
GetUserCommand,
GetUserPolicyCommand,
ListAccessKeysCommand,
DeleteUserCommand,
DeleteUserPolicyCommand,
DeleteAccessKeyCommand,
CreateUserCommand,
NoSuchEntityException,
} = require('@aws-sdk/client-iam');

const { getCredentials } = require('../aws-node-sdk/test/support/credentials');

const execFileAsync = promisify(execFile);

const script = path.join(__dirname, '../../../bin/ensureServiceUser');
const iamEndpoint = process.env.IAM_ENDPOINT || 'http://localhost:8600';
const { accessKeyId, secretAccessKey } = getCredentials();

const systemPrefix = '/scality-internal/';

const iamClient = new IAMClient({
endpoint: iamEndpoint,
region: 'us-east-1',
credentials: {
accessKeyId,
secretAccessKey,
},
});

function randomUserName() {
return `ensure-service-user-test-${crypto.randomBytes(4).toString('hex')}`;
}

function runScript(userName) {
return execFileAsync('node', [script, 'apply', userName, '--iam-endpoint', iamEndpoint], {
env: {
...process.env,
AWS_ACCESS_KEY_ID: accessKeyId,
AWS_SECRET_ACCESS_KEY: secretAccessKey,
},
});
}

async function ignoreNoSuchEntity(promise) {
try {
return await promise;
} catch (err) {
if (err instanceof NoSuchEntityException) {
return null;
}
throw err;
}
}

// the cleanup runs whatever state the test left behind, so every
// deletion has to tolerate resources that were never created
async function deleteServiceUser(userName) {
const keys = await ignoreNoSuchEntity(
iamClient.send(
new ListAccessKeysCommand({
UserName: userName,
MaxItems: 100,
}),
),
);
for (const key of keys ? keys.AccessKeyMetadata : []) {
await ignoreNoSuchEntity(
iamClient.send(
new DeleteAccessKeyCommand({
UserName: userName,
AccessKeyId: key.AccessKeyId,
}),
),
);
}
await ignoreNoSuchEntity(
iamClient.send(
new DeleteUserPolicyCommand({
UserName: userName,
PolicyName: userName,
}),
),
);
await ignoreNoSuchEntity(
iamClient.send(
new DeleteUserCommand({
UserName: userName,
}),
),
);
}

describe('ensureServiceUser script', () => {
let userName;

beforeEach(() => {
userName = randomUserName();
});

afterEach(async () => {
await deleteServiceUser(userName);
});

it('should create the service user, policy and access key when none exist', async () => {
const { stdout } = await runScript(userName);

assert.notStrictEqual(stdout, '');
assert.ok(!stdout.includes('"level":"error"'), `unexpected error in output: ${stdout}`);

const result = JSON.parse(stdout);
assert.strictEqual(result.message, 'success');
assert.ok(result.data.AccessKeyId);
assert.ok(result.data.SecretAccessKey);
assert.strictEqual(result.data.UserName, userName);

const user = await iamClient.send(new GetUserCommand({ UserName: userName }));
assert.strictEqual(user.User.Path, systemPrefix);

const policy = await iamClient.send(
new GetUserPolicyCommand({
UserName: userName,
PolicyName: userName,
}),
);
const policyDocument = JSON.parse(decodeURIComponent(policy.PolicyDocument));
assert.strictEqual(policyDocument.Statement[0].Sid, 'RateLimitAdminAPIs');

const keys = await iamClient.send(
new ListAccessKeysCommand({
UserName: userName,
MaxItems: 100,
}),
);
assert.strictEqual(keys.AccessKeyMetadata.length, 1);
assert.strictEqual(keys.AccessKeyMetadata[0].AccessKeyId, result.data.AccessKeyId);
});

it('should succeed without creating a new access key when the user already exists', async () => {
const first = JSON.parse((await runScript(userName)).stdout);
const { stdout } = await runScript(userName);

assert.ok(!stdout.includes('"level":"error"'), `unexpected error in output: ${stdout}`);

const result = JSON.parse(stdout);
assert.strictEqual(result.message, 'success');
// on re-run the script reports the existing key metadata instead of creating one
assert.strictEqual(result.data.length, 1);
assert.strictEqual(result.data[0].AccessKeyId, first.data.AccessKeyId);

const keys = await iamClient.send(
new ListAccessKeysCommand({
UserName: userName,
MaxItems: 100,
}),
);
assert.strictEqual(keys.AccessKeyMetadata.length, 1);
});

it('should fail when the user exists outside the scality-internal path', async () => {
await iamClient.send(new CreateUserCommand({ UserName: userName }));

await assert.rejects(runScript(userName), err => {
assert.strictEqual(err.code, 1);
assert.match(err.stdout, /EntityAlreadyExists/);
return true;
});
});
});
Loading