Skip to content

Commit 3ec1994

Browse files
Added support for tags for IAM users
Signed-off-by: Aayush Chouhan <achouhan@redhat.com>
1 parent a566cda commit 3ec1994

File tree

11 files changed

+442
-14
lines changed

11 files changed

+442
-14
lines changed

src/endpoint/iam/iam_constants.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const IAM_ACTIONS = Object.freeze({
1313
UPDATE_ACCESS_KEY: 'update_access_key',
1414
DELETE_ACCESS_KEY: 'delete_access_key',
1515
LIST_ACCESS_KEYS: 'list_access_keys',
16+
TAG_USER: 'tag_user',
17+
UNTAG_USER: 'untag_user',
18+
LIST_USER_TAGS: 'list_user_tags',
1619
PUT_USER_POLICY: 'put_user_policy',
1720
GET_USER_POLICY: 'get_user_policy',
1821
DELETE_USER_POLICY: 'delete_user_policy',
@@ -33,6 +36,9 @@ const ACTION_MESSAGE_TITLE_MAP = Object.freeze({
3336
'update_access_key': 'UpdateAccessKey',
3437
'delete_access_key': 'DeleteAccessKey',
3538
'list_access_keys': 'ListAccessKeys',
39+
'tag_user': 'TagUser',
40+
'untag_user': 'UntagUser',
41+
'list_user_tags': 'ListUserTags',
3642
'put_user_policy': 'PutUserPolicy',
3743
'get_user_policy': 'GetUserPolicy',
3844
'delete_user_policy': 'DeleteUserPolicy',
@@ -51,6 +57,7 @@ const IDENTITY_ENUM = Object.freeze({
5157

5258
// miscellaneous
5359
const DEFAULT_MAX_ITEMS = 100;
60+
const DEFAULT_MAX_TAGS = 50;
5461
const MAX_NUMBER_OF_ACCESS_KEYS = 2;
5562
const IAM_DEFAULT_PATH = '/';
5663
const AWS_NOT_USED = 'N/A'; // can be used in case the region or the service name were not used
@@ -76,6 +83,7 @@ exports.ACTION_MESSAGE_TITLE_MAP = ACTION_MESSAGE_TITLE_MAP;
7683
exports.ACCESS_KEY_STATUS_ENUM = ACCESS_KEY_STATUS_ENUM;
7784
exports.IDENTITY_ENUM = IDENTITY_ENUM;
7885
exports.DEFAULT_MAX_ITEMS = DEFAULT_MAX_ITEMS;
86+
exports.DEFAULT_MAX_TAGS = DEFAULT_MAX_TAGS;
7987
exports.MAX_NUMBER_OF_ACCESS_KEYS = MAX_NUMBER_OF_ACCESS_KEYS;
8088
exports.IAM_DEFAULT_PATH = IAM_DEFAULT_PATH;
8189
exports.AWS_NOT_USED = AWS_NOT_USED;

src/endpoint/iam/iam_rest.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const ACTIONS = Object.freeze({
3535
'UpdateAccessKey': 'update_access_key',
3636
'DeleteAccessKey': 'delete_access_key',
3737
'ListAccessKeys': 'list_access_keys',
38+
'TagUser': 'tag_user',
39+
'UntagUser': 'untag_user',
40+
'ListUserTags': 'list_user_tags',
3841
'PutUserPolicy': 'put_user_policy',
3942
'GetUserPolicy': 'get_user_policy',
4043
'DeleteUserPolicy': 'delete_user_policy',
@@ -65,7 +68,6 @@ const ACTIONS = Object.freeze({
6568
'ListServiceSpecificCredentials': 'list_service_specific_credentials',
6669
'ListSigningCertificates': 'list_signing_certificates',
6770
'ListSSHPublicKeys': 'list_ssh_public_keys',
68-
'ListUserTags': 'list_user_tags',
6971
'ListVirtualMFADevices': 'list_virtual_mfa_devices',
7072
});
7173

@@ -83,6 +85,10 @@ const IAM_OPS = js_utils.deep_freeze({
8385
post_update_access_key: require('./ops/iam_update_access_key'),
8486
post_delete_access_key: require('./ops/iam_delete_access_key'),
8587
post_list_access_keys: require('./ops/iam_list_access_keys'),
88+
// user tagging
89+
post_tag_user: require('./ops/iam_tag_user'),
90+
post_untag_user: require('./ops/iam_untag_user'),
91+
post_list_user_tags: require('./ops/iam_list_user_tags'),
8692
// user policy
8793
post_put_user_policy: require('./ops/iam_put_user_policy'),
8894
post_get_user_policy: require('./ops/iam_get_user_policy'),
@@ -115,7 +121,6 @@ const IAM_OPS = js_utils.deep_freeze({
115121
post_list_service_specific_credentials: require('./ops/iam_list_service_specific_credentials'),
116122
post_list_signing_certificates: require('./ops/iam_list_signing_certificates'),
117123
post_list_ssh_public_keys: require('./ops/iam_list_ssh_public_keys'),
118-
post_list_user_tags: require('./ops/iam_list_user_tags'),
119124
post_list_virtual_mfa_devices: require('./ops/iam_list_virtual_mfa_devices'),
120125
});
121126

src/endpoint/iam/iam_utils.js

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,15 @@ function parse_max_items(input_max_items) {
8181
}
8282

8383
/**
84-
* validate_params will call the aquivalent function in user or access key
84+
* validate_params will call the aquivalent function in user, access key, or tagging
85+
* Note: The order of conditions matters - check 'tag' before 'user' to avoid misrouting
8586
* @param {string} action
8687
* @param {object} params
8788
*/
8889
function validate_params(action, params) {
89-
if (action.includes('policy') || action.includes('policies')) {
90+
if (action.includes('tag')) {
91+
validate_tagging_params(action, params);
92+
} else if (action.includes('policy') || action.includes('policies')) {
9093
validate_policy_params(action, params);
9194
} else if (action.includes('user')) {
9295
validate_user_params(action, params);
@@ -151,6 +154,27 @@ function validate_access_keys_params(action, params) {
151154
}
152155
}
153156

157+
/**
158+
* validate_tagging_params will call the aquivalent function for each action in tagging API
159+
* @param {string} action
160+
* @param {object} params
161+
*/
162+
function validate_tagging_params(action, params) {
163+
switch (action) {
164+
case iam_constants.IAM_ACTIONS.TAG_USER:
165+
validate_tag_user_params(params);
166+
break;
167+
case iam_constants.IAM_ACTIONS.UNTAG_USER:
168+
validate_untag_user_params(params);
169+
break;
170+
case iam_constants.IAM_ACTIONS.LIST_USER_TAGS:
171+
validate_list_user_tags_params(params);
172+
break;
173+
default:
174+
throw new RpcError('INTERNAL_ERROR', `${action} is not supported`);
175+
}
176+
}
177+
154178
/**
155179
* validate_policy_params will call the aquivalent function for each action in user policy API
156180
* @param {string} action
@@ -581,6 +605,92 @@ function translate_rpc_error(err) {
581605
throw err;
582606
}
583607

608+
/**
609+
* parse_indexed_members parses indexed array members
610+
* generic parser for AWS-style indexed request parameters
611+
* @param {object} body - request body
612+
* @param {string} base_key - the base key pattern (e.g., 'tags_member_{index}_key)
613+
* @param {Function} [mapper] - optional function to convert each item
614+
*/
615+
function parse_indexed_members(body, base_key, mapper) {
616+
const result = [];
617+
let index = 1;
618+
let check_key = base_key.replace('{index}', String(index));
619+
620+
while (body[check_key] !== undefined) {
621+
result.push(mapper ? mapper(body, index) : body[check_key]);
622+
index += 1;
623+
check_key = base_key.replace('{index}', String(index));
624+
}
625+
return result;
626+
}
627+
628+
/**
629+
* parse_tags_from_request_body parses tags from request body
630+
* @param {object} body - request body
631+
*/
632+
function parse_tags_from_request_body(body) {
633+
return parse_indexed_members(
634+
body,
635+
'tags_member_{index}_key',
636+
(req_body, index) => ({
637+
key: req_body[`tags_member_${index}_key`],
638+
value: req_body[`tags_member_${index}_value`] || ''
639+
})
640+
);
641+
}
642+
643+
/**
644+
* parse_tag_keys_from_request_body parses tag keys from request body
645+
* @param {object} body - request body
646+
*/
647+
function parse_tag_keys_from_request_body(body) {
648+
return parse_indexed_members(body, 'tag_keys_member_{index}');
649+
}
650+
651+
/**
652+
* validate_tag_user_params checks the params for tag_user action
653+
* @param {object} params
654+
*/
655+
function validate_tag_user_params(params) {
656+
try {
657+
check_required_username(params);
658+
validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME);
659+
validation_utils.validate_iam_tags(params.tags);
660+
} catch (err) {
661+
translate_rpc_error(err);
662+
}
663+
}
664+
665+
/**
666+
* validate_untag_user_params checks the params for untag_user action
667+
* @param {object} params
668+
*/
669+
function validate_untag_user_params(params) {
670+
try {
671+
check_required_username(params);
672+
validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME);
673+
validation_utils.validate_iam_tag_keys(params.tag_keys);
674+
} catch (err) {
675+
translate_rpc_error(err);
676+
}
677+
}
678+
679+
/**
680+
* validate_list_user_tags_params checks the params for list_user_tags action
681+
* @param {object} params
682+
*/
683+
function validate_list_user_tags_params(params) {
684+
try {
685+
check_required_username(params);
686+
validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME);
687+
validate_marker(params.marker);
688+
validate_max_items(params.max_items);
689+
} catch (err) {
690+
translate_rpc_error(err);
691+
}
692+
}
693+
584694
// EXPORTS
585695
exports.format_iam_xml_date = format_iam_xml_date;
586696
exports.create_arn_for_user = create_arn_for_user;
@@ -593,4 +703,10 @@ exports.validate_iam_path = validate_iam_path;
593703
exports.validate_marker = validate_marker;
594704
exports.validate_access_key_id = validate_access_key_id;
595705
exports.validate_status = validate_status;
706+
exports.parse_indexed_members = parse_indexed_members;
707+
exports.parse_tags_from_request_body = parse_tags_from_request_body;
708+
exports.parse_tag_keys_from_request_body = parse_tag_keys_from_request_body;
709+
exports.validate_tag_user_params = validate_tag_user_params;
710+
exports.validate_untag_user_params = validate_untag_user_params;
711+
exports.validate_list_user_tags_params = validate_list_user_tags_params;
596712

src/endpoint/iam/ops/iam_list_user_tags.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,19 @@ async function list_user_tags(req, res) {
1414
const params = {
1515
username: req.body.user_name,
1616
marker: req.body.marker,
17-
max_items: iam_utils.parse_max_items(req.body.max_items) ?? iam_constants.DEFAULT_MAX_ITEMS,
17+
max_items: iam_utils.parse_max_items(req.body.max_items) ?? iam_constants.DEFAULT_MAX_TAGS,
1818
};
1919

20-
dbg.log1('To check that we have the user we will run the IAM GET USER', params);
21-
iam_utils.validate_params(iam_constants.IAM_ACTIONS.GET_USER, params);
22-
await req.account_sdk.get_user(params);
23-
24-
dbg.log1('IAM LIST USER TAGS (returns empty list on every request)', params);
20+
dbg.log1('IAM LIST USER TAGS', params);
21+
iam_utils.validate_params(iam_constants.IAM_ACTIONS.LIST_USER_TAGS, params);
22+
const reply = await req.account_sdk.list_user_tags(params);
23+
dbg.log2('list_user_tags reply', reply);
2524

2625
return {
2726
ListUserTagsResponse: {
2827
ListUserTagsResult: {
29-
Tags: [],
30-
IsTruncated: false,
28+
Tags: reply.tags || [],
29+
IsTruncated: reply.is_truncated || false
3130
},
3231
ResponseMetadata: {
3332
RequestId: req.request_id,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* Copyright (C) 2024 NooBaa */
2+
'use strict';
3+
4+
const dbg = require('../../../util/debug_module')(__filename);
5+
const iam_utils = require('../iam_utils');
6+
const iam_constants = require('../iam_constants');
7+
const { CONTENT_TYPE_APP_FORM_URLENCODED } = require('../../../util/http_utils');
8+
9+
/**
10+
* https://docs.aws.amazon.com/IAM/latest/APIReference/API_TagUser.html
11+
*/
12+
async function tag_user(req, res) {
13+
14+
const params = {
15+
username: req.body.user_name,
16+
tags: iam_utils.parse_tags_from_request_body(req.body),
17+
};
18+
19+
dbg.log1('IAM TAG USER', params);
20+
iam_utils.validate_params(iam_constants.IAM_ACTIONS.TAG_USER, params);
21+
await req.account_sdk.tag_user(params);
22+
23+
return {
24+
TagUserResponse: {
25+
ResponseMetadata: {
26+
RequestId: req.request_id,
27+
}
28+
},
29+
};
30+
}
31+
32+
module.exports = {
33+
handler: tag_user,
34+
body: {
35+
type: CONTENT_TYPE_APP_FORM_URLENCODED,
36+
},
37+
reply: {
38+
type: 'xml',
39+
},
40+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* Copyright (C) 2024 NooBaa */
2+
'use strict';
3+
4+
const dbg = require('../../../util/debug_module')(__filename);
5+
const iam_utils = require('../iam_utils');
6+
const iam_constants = require('../iam_constants');
7+
const { CONTENT_TYPE_APP_FORM_URLENCODED } = require('../../../util/http_utils');
8+
9+
/**
10+
* https://docs.aws.amazon.com/IAM/latest/APIReference/API_UntagUser.html
11+
*/
12+
async function untag_user(req, res) {
13+
14+
const params = {
15+
username: req.body.user_name,
16+
tag_keys: iam_utils.parse_tag_keys_from_request_body(req.body),
17+
};
18+
19+
dbg.log1('IAM UNTAG USER', params);
20+
iam_utils.validate_params(iam_constants.IAM_ACTIONS.UNTAG_USER, params);
21+
await req.account_sdk.untag_user(params);
22+
23+
return {
24+
UntagUserResponse: {
25+
ResponseMetadata: {
26+
RequestId: req.request_id,
27+
}
28+
},
29+
};
30+
}
31+
32+
module.exports = {
33+
handler: untag_user,
34+
body: {
35+
type: CONTENT_TYPE_APP_FORM_URLENCODED,
36+
},
37+
reply: {
38+
type: 'xml',
39+
},
40+
};

src/sdk/account_sdk.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ class AccountSDK {
117117
return accountspace.list_users(params, this);
118118
}
119119

120+
////////////
121+
// TAGS //
122+
////////////
123+
124+
async tag_user(params) {
125+
const accountspace = this._get_accountspace();
126+
return accountspace.tag_user(params, this);
127+
}
128+
129+
async untag_user(params) {
130+
const accountspace = this._get_accountspace();
131+
return accountspace.untag_user(params, this);
132+
}
133+
134+
async list_user_tags(params) {
135+
const accountspace = this._get_accountspace();
136+
return accountspace.list_user_tags(params, this);
137+
}
138+
120139
////////////////
121140
// ACCESS KEY //
122141
////////////////

0 commit comments

Comments
 (0)