Skip to content

IAM backports to 5.18.4 #9038

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

Merged
merged 4 commits into from
May 22, 2025
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
3 changes: 3 additions & 0 deletions docs/design/iam.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ Here attached a diagram with all the accounts that we have in our system:
- IAM DeleteAccessKey: AccessKeyId, UserName
- IAM ListAccessKeys: UserName (not supported: Marker, MaxItems)

### Other
- IAM ListGroupsForUser - would always return empty list (to check that the user exists it runs GetUser).

### Configuration Directory Components With users
If account creates a user its config file will be created under identities/<user-id>.identity.json and under the account will be created `users/` directory and inside it it will link to the config.
Example:
Expand Down
4 changes: 4 additions & 0 deletions src/endpoint/iam/iam_rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const ACTIONS = Object.freeze({
'UpdateAccessKey': 'update_access_key',
'DeleteAccessKey': 'delete_access_key',
'ListAccessKeys': 'list_access_keys',
'ListGroupsForUser': 'list_groups_for_user',
});

// notice: shows all methods as method post
Expand All @@ -50,6 +51,8 @@ const IAM_OPS = js_utils.deep_freeze({
post_update_access_key: require('./ops/iam_update_access_key'),
post_delete_access_key: require('./ops/iam_delete_access_key'),
post_list_access_keys: require('./ops/iam_list_access_keys'),
// other (currently ops that return empty just not to fail them)
post_list_groups_for_user: require('./ops/iam_list_groups_for_user.js'),
});

async function iam_rest(req, res) {
Expand Down Expand Up @@ -145,6 +148,7 @@ function parse_op_name(req, action) {
if (ACTIONS[action]) {
return `${method}_${ACTIONS[action]}`;
}
dbg.error('IAM parse_op_name - NotImplemented', action, method, req.originalUrl);
throw new IamError(IamError.NotImplemented);
}

Expand Down
45 changes: 45 additions & 0 deletions src/endpoint/iam/ops/iam_list_groups_for_user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* Copyright (C) 2024 NooBaa */
'use strict';

const dbg = require('../../../util/debug_module')(__filename);
const { CONTENT_TYPE_APP_FORM_URLENCODED } = require('../../../util/http_utils');
const iam_utils = require('../iam_utils');
const iam_constants = require('../iam_constants');

/**
* https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListGroupsForUser.html
*/
async function list_groups_for_user(req, res) {

const params = {
username: req.body.user_name,
};

dbg.log1('To check that we have the user we will run the IAM GET USER', params);
iam_utils.validate_params(iam_constants.IAM_ACTIONS.GET_USER, params);
await req.account_sdk.get_user(params);

dbg.log1('IAM LIST GROUPS FOR USER (returns empty list on every request)', params);

return {
ListGroupsForUserResponse: {
ListGroupsForUserResult: {
Groups: [],
IsTruncated: false,
},
ResponseMetadata: {
RequestId: req.request_id,
}
},
};
}

module.exports = {
handler: list_groups_for_user,
body: {
type: CONTENT_TYPE_APP_FORM_URLENCODED,
},
reply: {
type: 'xml',
},
};
4 changes: 2 additions & 2 deletions src/endpoint/s3/ops/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ exports.get_bucket_object_lock = require('./s3_get_bucket_object_lock');
exports.get_bucket_policy = require('./s3_get_bucket_policy');
exports.get_bucket_policy_status = require('./s3_get_bucket_policy_status');
exports.get_bucket_replication = require('./s3_get_bucket_replication');
exports.get_bucket_requestPayment = require('./s3_get_bucket_requestPayment');
exports.get_bucket_request_payment = require('./s3_get_bucket_request_payment');
exports.get_bucket_tagging = require('./s3_get_bucket_tagging');
exports.get_bucket_uploads = require('./s3_get_bucket_uploads');
exports.get_bucket_versioning = require('./s3_get_bucket_versioning');
Expand Down Expand Up @@ -67,7 +67,7 @@ exports.put_bucket_notification = require('./s3_put_bucket_notification');
exports.put_bucket_object_lock = require('./s3_put_bucket_object_lock');
exports.put_bucket_policy = require('./s3_put_bucket_policy');
exports.put_bucket_replication = require('./s3_put_bucket_replication');
exports.put_bucket_requestPayment = require('./s3_put_bucket_requestPayment');
exports.put_bucket_request_payment = require('./s3_put_bucket_request_payment');
exports.put_bucket_tagging = require('./s3_put_bucket_tagging');
exports.put_bucket_versioning = require('./s3_put_bucket_versioning');
exports.put_bucket_website = require('./s3_put_bucket_website');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
/* Copyright (C) 2016 NooBaa */
'use strict';

const dbg = require('../../../util/debug_module')(__filename);

/**
* http://docs.aws.amazon.com/AmazonS3/latest/API/RESTrequestPaymentGET.html
*/
async function get_bucket_requestPayment(req) {
async function get_bucket_request_payment(req) {
await req.object_sdk.read_bucket({ name: req.params.bucket });
const payer = 'BucketOwner';
dbg.log1(`s3_get_bucket_request_payment (returns ${payer} on every request)`);
return {
RequestPaymentConfiguration: {
Payer: 'BucketOwner'
Payer: payer
}
};
}

module.exports = {
handler: get_bucket_requestPayment,
handler: get_bucket_request_payment,
body: {
type: 'empty',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const S3Error = require('../s3_errors').S3Error;
/**
* http://docs.aws.amazon.com/AmazonS3/latest/API/RESTrequestPaymentPUT.html
*/
async function put_bucket_requestPayment(req) {
async function put_bucket_request_payment(req) {
await req.object_sdk.read_bucket({ name: req.params.bucket });
// TODO S3 put_bucket_requestPayment not implemented
throw new S3Error(S3Error.NotImplemented);
}

module.exports = {
handler: put_bucket_requestPayment,
handler: put_bucket_request_payment,
body: {
type: 'xml',
},
Expand Down
4 changes: 2 additions & 2 deletions src/endpoint/s3/s3_bucket_policy_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const OP_NAME_TO_ACTION = Object.freeze({
get_bucket_policy: { regular: "s3:GetBucketPolicy" },
get_bucket_policy_status: { regular: "s3:GetBucketPolicyStatus" },
get_bucket_replication: { regular: "s3:GetReplicationConfiguration" },
get_bucket_requestpayment: { regular: "s3:GetBucketRequestPayment" },
get_bucket_request_payment: { regular: "s3:GetBucketRequestPayment" },
get_bucket_tagging: { regular: "s3:GetBucketTagging" },
get_bucket_uploads: { regular: "s3:ListBucketMultipartUploads" },
get_bucket_versioning: { regular: "s3:GetBucketVersioning" },
Expand Down Expand Up @@ -75,7 +75,7 @@ const OP_NAME_TO_ACTION = Object.freeze({
put_bucket_notification: { regular: "s3:PutBucketNotification" },
put_bucket_policy: { regular: "s3:PutBucketPolicy" },
put_bucket_replication: { regular: "s3:PutReplicationConfiguration" },
put_bucket_requestpayment: { regular: "s3:PutBucketRequestPayment" },
put_bucket_request_payment: { regular: "s3:PutBucketRequestPayment" },
put_bucket_tagging: { regular: "s3:PutBucketTagging" },
put_bucket_versioning: { regular: "s3:PutBucketVersioning" },
put_bucket_website: { regular: "s3:PutBucketWebsite" },
Expand Down
2 changes: 1 addition & 1 deletion src/endpoint/s3/s3_rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const BUCKET_SUB_RESOURCES = Object.freeze({
'policy': 'policy',
'policyStatus': 'policy_status',
'replication': 'replication',
'requestPayment': 'requestPayment',
'requestPayment': 'request_payment',
'tagging': 'tagging',
'uploads': 'uploads',
'versioning': 'versioning',
Expand Down
1 change: 1 addition & 0 deletions src/endpoint/sts/sts_rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ function parse_op_name(req, action) {
if (ACTIONS[action]) {
return `${method}_${ACTIONS[action]}`;
}
dbg.error('STS parse_op_name - NotImplemented', action, method, req.originalUrl);
throw new StsError(StsError.NotImplemented);
}

Expand Down
2 changes: 1 addition & 1 deletion src/sdk/bucketspace_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
const { name } = params;
dbg.log0('BucketSpaceFS.get_bucket_encryption: Bucket name', name);
const bucket = await this.config_fs.get_bucket_by_name(name);
return bucket.encryption;
return { encryption: bucket.encryption };
} catch (err) {
throw translate_error_codes(err, entity_enum.BUCKET);
}
Expand Down
13 changes: 9 additions & 4 deletions src/test/unit_tests/test_bucketspace_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,11 @@ mocha.describe('bucketspace_fs', function() {
});

mocha.describe('bucket encryption operations', function() {
mocha.it('get_bucket_encryption (return empty encryption)', async function() {
const param = { name: test_bucket };
const empty_encryption = await bucketspace_fs.get_bucket_encryption(param);
assert.ok(empty_encryption.encryption === undefined);
});
mocha.it('put_bucket_encryption ', async function() {
const encryption = {
algorithm: 'AES256',
Expand All @@ -726,19 +731,19 @@ mocha.describe('bucketspace_fs', function() {
await bucketspace_fs.put_bucket_encryption(param);

const output_encryption = await bucketspace_fs.get_bucket_encryption(param);
assert.deepEqual(output_encryption, encryption);
assert.deepEqual(output_encryption.encryption, encryption);
});
mocha.it('delete_bucket_encryption ', async function() {
mocha.it('delete_bucket_encryption', async function() {
const encryption = {
algorithm: 'AES256',
kms_key_id: 'kms-123'
};
const param = { name: test_bucket };
const output_encryption = await bucketspace_fs.get_bucket_encryption(param);
assert.deepEqual(output_encryption, encryption);
assert.deepEqual(output_encryption.encryption, encryption);
await bucketspace_fs.delete_bucket_encryption(param);
const empty_encryption = await bucketspace_fs.get_bucket_encryption(param);
assert.ok(empty_encryption === undefined);
assert.ok(empty_encryption.encryption === undefined);
});
});

Expand Down
53 changes: 52 additions & 1 deletion src/test/unit_tests/test_nc_iam_basic_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const { TMP_PATH, generate_nsfs_account, get_new_buckets_path_by_test_env, gener
get_coretest_path } = require('../system_tests/test_utils');
const { ListUsersCommand, CreateUserCommand, GetUserCommand, UpdateUserCommand, DeleteUserCommand,
ListAccessKeysCommand, CreateAccessKeyCommand, GetAccessKeyLastUsedCommand,
UpdateAccessKeyCommand, DeleteAccessKeyCommand } = require('@aws-sdk/client-iam');
UpdateAccessKeyCommand, DeleteAccessKeyCommand,
ListGroupsForUserCommand } = require('@aws-sdk/client-iam');
const { ACCESS_KEY_STATUS_ENUM } = require('../../endpoint/iam/iam_constants');
const IamError = require('../../endpoint/iam/iam_errors').IamError;


const coretest_path = get_coretest_path();
const coretest = require(coretest_path);
Expand Down Expand Up @@ -229,6 +232,54 @@ mocha.describe('IAM basic integration tests - happy path', async function() {
_check_status_code_ok(response);
});
});

mocha.describe('IAM other APIs (currently returns empty value)', async function() {
const username3 = 'Emi';

mocha.before(async () => {
// create a user
const input = {
UserName: username3
};
const command = new CreateUserCommand(input);
const response = await iam_account.send(command);
_check_status_code_ok(response);
});

mocha.after(async () => {
// delete a user
const input = {
UserName: username3
};
const command = new DeleteUserCommand(input);
const response = await iam_account.send(command);
_check_status_code_ok(response);
});

mocha.it('list groups for non existing user - should throw an error', async function() {
try {
const input = {
UserName: 'non-existing-user'
};
const command = new ListGroupsForUserCommand(input);
await iam_account.send(command);
assert.fail('list groups for non existing user - should throw an error');
} catch (err) {
const err_code = err.Error.Code;
assert.equal(err_code, IamError.NoSuchEntity.code);
}
});

mocha.it('list groups for user - should be empty', async function() {
const input = {
UserName: username3
};
const command = new ListGroupsForUserCommand(input);
const response = await iam_account.send(command);
_check_status_code_ok(response);
assert.equal(response.Groups.length, 0);
});
});
});

/**
Expand Down
14 changes: 14 additions & 0 deletions src/test/unit_tests/test_nsfs_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,20 @@ mocha.describe('bucket operations - namespace_fs', function() {
const res_without_metadata = _.omit(res, '$metadata');
assert.deepEqual(res_without_metadata, {});
});
mocha.it('get bucket request payment (currently returns a mock) without failing', async function() {
const res = await s3_correct_uid_default_nsr.getBucketRequestPayment({ Bucket: bucket_name});
assert.equal(res.$metadata.httpStatusCode, 200);
const expected_payer = 'BucketOwner'; // this is the mock that we use
assert.equal(res.Payer, expected_payer);
});
mocha.it('get bucket encryption (before put bucket encryption) - throws an error', async function() {
try {
await s3_correct_uid_default_nsr.getBucketEncryption({ Bucket: bucket_name});
assert.fail('get bucket encryption when encryption not set should fail');
} catch (err) {
assert.strictEqual(err.Code, 'ServerSideEncryptionConfigurationNotFoundError');
}
});

mocha.it('delete multiple non existing objects without failing', async function() {
const keys_to_delete = [
Expand Down
22 changes: 22 additions & 0 deletions src/test/unit_tests/test_s3_ops.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ mocha.describe('s3_ops', function() {
const res = await s3.listBuckets({});
assert(res.Buckets.find(bucket => bucket.Name === BKT1));
});
mocha.it('should not fail request of get bucket request payment (currently returns a mock)', async function() {
const res = await s3.getBucketRequestPayment({ Bucket: BKT1 });
assert.equal(res.$metadata.httpStatusCode, 200);
const expected_payer = 'BucketOwner'; // this is the mock that we use
assert.equal(res.Payer, expected_payer);
});
mocha.it('should allow get_bucket_encryption (no put before the get)', async function() {
const res = await s3.getBucketEncryption({ Bucket: BKT1 });
const expected_response = {
ServerSideEncryptionConfiguration: {
Rules: [{
ApplyServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256'
},
BucketKeyEnabled: false
}]
}
};
assert.equal(res.$metadata.httpStatusCode, 200);
const res_without_metadata = _.omit(res, '$metadata');
assert.deepEqual(res_without_metadata, expected_response);
});
mocha.it('should enable bucket logging', async function() {
await s3.createBucket({ Bucket: BKT2 });
await s3.putBucketLogging({
Expand Down