Skip to content

Commit

Permalink
Merge pull request #8119 from shirady/nsfs-iam-account-access-keys
Browse files Browse the repository at this point in the history
NSFS | NC | IAM Service - Access Keys CRUD API Implementation
  • Loading branch information
shirady authored Jun 23, 2024
2 parents 25aab25 + d416089 commit d5921f4
Show file tree
Hide file tree
Showing 20 changed files with 1,449 additions and 296 deletions.
2 changes: 1 addition & 1 deletion docs/design/iam.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ At this point we will not support additional IAM resources (group, policy, role,
- `owner` vs `creator` - owner is permission wise, creator is for internal information.


### The user creation flow:
### The user and access keys creation flow:
One root and one user (just to understand the basic API relations and hierarchy)
![One root and one user diagram](https://github.com/noobaa/noobaa-core/assets/57721533/b77ade91-11dd-415c-b3f0-5f3f1747a694)

Expand Down
44 changes: 36 additions & 8 deletions docs/dev_guide/nc_nsfs_iam_developer_doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,41 @@ This will be the argument for:
- `path` in the buckets commands `/tmp/nsfs_root1/my-bucket` (that we will use in bucket commands).
2. Create the root user account with the CLI:
`sudo node src/cmd/manage_nsfs account add --name <name>> --new_buckets_path /tmp/nsfs_root1 --access_key <access-key> --secret_key <secret-key> --uid <uid> --gid <gid>`.
2. Start the NSFS server (using debug mode and the port for IAM): `sudo node src/cmd/nsfs --debug 5 --https_port_iam 7005`
3. Start the NSFS server (using debug mode and the port for IAM): `sudo node src/cmd/nsfs --debug 5 --https_port_iam 7005`
Note: before starting the server please add this line: `process.env.NOOBAA_LOG_LEVEL = 'nsfs';` in the endpoint.js (before the condition `if (process.env.NOOBAA_LOG_LEVEL) {`)
4. Create the alias for IAM service:
`alias s3-nc-user-1-iam='AWS_ACCESS_KEY_ID=<acess-key> AWS_SECRET_ACCESS_KEY=<secret-key> aws --no-verify-ssl --endpoint-url https://localhost:7005'`.
11. Use AWS CLI to send requests to the IAM service, for example:
`s3-nc-user-1-iam iam create-user --user-name Bob --path /division_abc/subdivision_xyz/`
`s3-nc-user-1-iam iam get-user --user-name Bob`
`s3-nc-user-1-iam iam update-user --user-name Bob --new-path /division_abc/subdivision_abc/`
`s3-nc-user-1-iam iam delete-user --user-name Bob`
`s3-nc-user-1-iam iam list-users`
`alias nc-user-1-iam='AWS_ACCESS_KEY_ID=<access-key> AWS_SECRET_ACCESS_KEY=<secret-key> aws --no-verify-ssl --endpoint-url https://localhost:7005'`.
5. Use AWS CLI to send requests to the IAM service, for example:
`nc-user-1-iam iam create-user --user-name Bob --path /division_abc/subdivision_xyz/`
`nc-user-1-iam iam get-user --user-name Bob`
`nc-user-1-iam iam update-user --user-name Bob --new-path /division_abc/subdivision_abc/`
`nc-user-1-iam iam delete-user --user-name Bob`
`nc-user-1-iam iam list-users`

`nc-user-1-iam iam create-access-key --user-name Bob`
`nc-user-1-iam iam update-access-key --access-key-id <access-key> --user-name Bob --status Inactive`
`nc-user-1-iam iam delete-access-key --access-key-id <access-key> --user-name Bob`
`nc-user-1-iam iam list-access-keys --user-name Bob`

Create the alias for IAM service for the user that was created (with its access keys):
`alias nc-user-1-iam-regular='AWS_ACCESS_KEY_ID=<access-key> AWS_SECRET_ACCESS_KEY=<secret-key> aws --no-verify-ssl --endpoint-url https://localhost:7005'`.
`nc-user-1-iam-regular iam get-access-key-last-used --access-key-id <access-key>`

### Demo Examples:
#### Deactivate Access Key:
`alias nc-user-1-iam-regular='AWS_ACCESS_KEY_ID=<access-key> AWS_SECRET_ACCESS_KEY=<secret-key> aws --no-verify-ssl --endpoint-url https://localhost:6443'` (port for s3)
1. Use the root account credentials to create a user: `nc-user-1-iam iam create-user --user-name <username>`
2. Use the root account credentials to create access keys for the user: `nc-user-1-iam iam create-access-key --user-name <username>`
3. The alias for s3 service: `alias nc-user-1-s3-regular='AWS_ACCESS_KEY_ID=<access-key> AWS_SECRET_ACCESS_KEY=<secret-key> aws --no-verify-ssl --endpoint-url https://localhost:6443'`
2. Create a bucket (so we can list it) `nc-user-1-s3-regular s3 mb s3://<bucket-name>>`
3. List bucket (use s3 service)`nc-user-1-s3-regular s3 ls`
4. List access keys (use IAM service) `nc-user-1-iam-regular iam list-access-keys`
5. Deactivate access keys: `nc-user-1-iam iam update-access-key --access-key-id <access-key> --user-name <username> --status Inactive`
6. It should throw an error for both s3 service (`nc-user-1-s3-regular s3 ls`) and iam service (`nc-user-1-iam-regular iam list-access-keys`) that uses the deactivated access key.
Note: Currently we clean the cache after update, but it happens for the specific endpoint, if there are more endpoints (using forks) developers can change the expiry cache in the line `expiry_ms: 1` inside `account_cache` (currently inside object_sdk).

#### Rename Username:
1. Use the root account credentials to create a user: `nc-user-1-iam iam create-user --user-name <username>` (You should see the config file in under the accounts directory).
2. Use the root account credentials to create access keys for the user:(first time): `nc-user-1-iam iam create-access-key --user-name <username>` (You should see the first symbolic link in under the access_keys directory).
3. Use the root account credentials to create access keys for the user (second time): `nc-user-1-iam iam create-access-key --user-name <username>` (You should see the second symbolic link in under the access_keys directory).
4. Update the username: `nc-user-1-iam iam update-user --user-name <username> --new-user-name <new-username>` (You should see the following changes: config file name updated, symlinks updated according to the current config).
66 changes: 17 additions & 49 deletions src/endpoint/iam/iam_errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ IamError.InvalidClientTokenId = Object.freeze({
http_code: 403,
type: error_type_enum.SENDER,
});
IamError.InvalidClientTokenIdInactiveAccessKey = Object.freeze({
code: 'InvalidClientTokenId',
message: 'The security token included in the request is invalid.',
http_code: 403,
type: error_type_enum.SENDER,
});
IamError.NotAuthorized = Object.freeze({
code: 'NotAuthorized',
message: 'You do not have permission to perform this action.',
Expand Down Expand Up @@ -122,13 +128,20 @@ IamError.NotImplemented = Object.freeze({
});

// These errors were copied from IAM APIs errors
// Users
// CreateUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html#API_CreateUser_Errors
// DeleteUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html#API_DeleteUser_Errors
// GetUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetUser.html#API_GetUser_Errors
// UpdateUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateUser.html#API_UpdateUser_Errors
// DeleteUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html#API_DeleteUser_Errors
// ListUsers errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUsers.html
// Access keys
// CreateAccessKey errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateAccessKey.html
// GetAccessKeyLastUsed errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetAccessKeyLastUsed.html (nothing appears)
// UpdateAccessKey errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html
// DeleteAccessKey errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteAccessKey.html
// ListAccessKeys errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListAccessKeys.html
IamError.ConcurrentModification = Object.freeze({
code: 'EntityAlreadyExists',
code: 'ConcurrentModification',
message: 'The request was rejected because multiple requests to change this object were submitted simultaneously. Wait a few minutes and submit your request again.',
http_code: 409,
type: error_type_enum.SENDER,
Expand All @@ -140,13 +153,13 @@ IamError.EntityAlreadyExists = Object.freeze({
type: error_type_enum.SENDER,
});
IamError.InvalidInput = Object.freeze({
code: 'EntityAlreadyExists',
code: 'InvalidInput',
message: 'The request was rejected because an invalid or out-of-range value was supplied for an input parameter.',
http_code: 400,
type: error_type_enum.SENDER,
});
IamError.LimitExceeded = Object.freeze({
code: 'EntityAlreadyExists',
code: 'LimitExceeded',
message: 'The request was rejected because it attempted to create resources beyond the current AWS account limits. The error message describes the limit exceeded.',
http_code: 409,
type: error_type_enum.SENDER,
Expand Down Expand Up @@ -175,59 +188,14 @@ IamError.EntityTemporarilyUnmodifiable = Object.freeze({
http_code: 409,
type: error_type_enum.SENDER,
});
// These errors were actually send after performing IAM actions
IamError.AccessDenied = Object.freeze({
code: 'AccessDenied',
message: 'user is not authorized to perform action on resource',
http_code: 400,
type: error_type_enum.SENDER,
});


// These errors were copied from STS errors
// TODO - can be deleted after verifying we will not use them
IamError.InvalidParameterCombination = Object.freeze({
code: 'InvalidParameterCombination',
message: 'Parameters that must not be used together were used together.',
http_code: 400,
type: error_type_enum.SENDER,
});
IamError.InvalidParameterValue = Object.freeze({
code: 'InvalidParameterValue',
message: 'An invalid or out-of-range value was supplied for the input parameter.',
http_code: 400,
type: error_type_enum.SENDER,
});
IamError.InvalidQueryParameter = Object.freeze({
code: 'InvalidQueryParameter',
message: 'The AWS query string is malformed or does not adhere to AWS standards.',
http_code: 400,
type: error_type_enum.SENDER,
});
IamError.MalformedQueryString = Object.freeze({
code: 'MalformedQueryString',
message: 'The query string contains a syntax error.',
http_code: 404,
type: error_type_enum.SENDER,
});
IamError.MissingAction = Object.freeze({
code: 'MissingAction',
message: 'The request is missing an action or a required parameter.',
http_code: 400,
type: error_type_enum.SENDER,
});
IamError.MissingAuthenticationToken = Object.freeze({
code: 'MissingAuthenticationToken',
message: 'The request must contain either a valid (registered) AWS access key ID or X.509 certificate.',
http_code: 403,
type: error_type_enum.SENDER,
});
IamError.MissingParameter = Object.freeze({
code: 'MissingParameter',
message: 'A required parameter for the specified action is not supplied.',
http_code: 400,
type: error_type_enum.SENDER,
});
IamError.ExpiredToken = Object.freeze({
code: 'ExpiredToken',
message: 'The security token included in the request is expired',
Expand Down
5 changes: 3 additions & 2 deletions src/endpoint/iam/iam_rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const IAM_XML_ROOT_ATTRS = Object.freeze({
const RPC_ERRORS_TO_IAM = Object.freeze({
SIGNATURE_DOES_NOT_MATCH: IamError.AccessDeniedException,
UNAUTHORIZED: IamError.AccessDeniedException,
INVALID_ACCESS_KEY_ID: IamError.AccessDeniedException,
INVALID_ACCESS_KEY_ID: IamError.InvalidClientTokenId,
DEACTIVATED_ACCESS_KEY_ID: IamError.InvalidClientTokenIdInactiveAccessKey,
NO_SUCH_ACCOUNT: IamError.AccessDeniedException,
NO_SUCH_ROLE: IamError.AccessDeniedException
});
Expand Down Expand Up @@ -44,7 +45,7 @@ const IAM_OPS = js_utils.deep_freeze({
post_list_users: require('./ops/iam_list_users'),
// access key CRUD
post_create_access_key: require('./ops/iam_create_access_key'),
post_get_access_key_last_used: require('./ops/iam_get_access_key'),
post_get_access_key_last_used: require('./ops/get_access_key_last_used'),
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'),
Expand Down
15 changes: 15 additions & 0 deletions src/endpoint/iam/iam_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ const ACTION_MESSAGE_TITLE_MAP = {
'list_access_keys': 'ListAccessKeys',
};

const MAX_NUMBER_OF_ACCESS_KEYS = 2;

const access_key_status_enum = Object.freeze({
ACTIVE: 'Active',
INACTIVE: 'Inactive',
});

const identity_enum = Object.freeze({
ROOT_ACCOUNT: 'ROOT_ACCOUNT',
USER: 'USER',
});

/**
* format_iam_xml_date return the date without milliseconds
* @param {any} input
Expand Down Expand Up @@ -73,3 +85,6 @@ exports.IAM_DEFAULT_PATH = IAM_DEFAULT_PATH;
exports.AWS_NOT_USED = AWS_NOT_USED;
exports.get_action_message_title = get_action_message_title;
exports.check_iam_path_was_set = check_iam_path_was_set;
exports.MAX_NUMBER_OF_ACCESS_KEYS = MAX_NUMBER_OF_ACCESS_KEYS;
exports.access_key_status_enum = access_key_status_enum;
exports.identity_enum = identity_enum;
File renamed without changes.
2 changes: 1 addition & 1 deletion src/endpoint/iam/ops/iam_delete_access_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { CONTENT_TYPE_APP_FORM_URLENCODED } = require('../../../util/http_utils')
async function delete_access_key(req, res) {

const params = {
user_name: req.body.user_name,
username: req.body.user_name,
access_key: req.body.access_key_id
};
dbg.log1('IAM DELETE ACCESS KEY', params);
Expand Down
2 changes: 1 addition & 1 deletion src/endpoint/iam/ops/iam_update_access_key.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function update_access_key(req, res) {
const params = {
access_key: req.body.access_key_id,
status: req.body.status,
user_name: req.body.user_name,
username: req.body.user_name,
};
dbg.log1('IAM UPDATE ACCESS KEY', params);
await req.account_sdk.update_access_key(params);
Expand Down
1 change: 1 addition & 0 deletions src/endpoint/s3/s3_errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ S3Error.RPC_ERRORS_TO_S3 = Object.freeze({
INVALID_REQUEST: S3Error.InvalidRequest,
NOT_IMPLEMENTED: S3Error.NotImplemented,
INVALID_ACCESS_KEY_ID: S3Error.InvalidAccessKeyId,
DEACTIVATED_ACCESS_KEY_ID: S3Error.InvalidAccessKeyId,
SIGNATURE_DOES_NOT_MATCH: S3Error.SignatureDoesNotMatch,
SERVICE_UNAVAILABLE: S3Error.ServiceUnavailable,
INVALID_RANGE: S3Error.InvalidRange,
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 @@ -19,6 +19,7 @@ const RPC_ERRORS_TO_STS = Object.freeze({
SIGNATURE_DOES_NOT_MATCH: StsError.AccessDeniedException,
UNAUTHORIZED: StsError.AccessDeniedException,
INVALID_ACCESS_KEY_ID: StsError.AccessDeniedException,
DEACTIVATED_ACCESS_KEY_ID: StsError.AccessDeniedException,
NO_SUCH_ACCOUNT: StsError.AccessDeniedException,
NO_SUCH_ROLE: StsError.AccessDeniedException
});
Expand Down
29 changes: 21 additions & 8 deletions src/manage_nsfs/nc_master_key_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,14 @@ class NCMasterKeysManager {
async encrypt_access_keys(account) {
await this.init();
const master_key_id = this.active_master_key.id;
const encrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => ({
access_key: access_keys.access_key,
encrypted_secret_key: await this.encrypt(access_keys.secret_key, master_key_id)
})));
const encrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => {
const encrypted_access_keys_object = { ...access_keys };
if (encrypted_access_keys_object.secret_key) {
encrypted_access_keys_object.encrypted_secret_key = await this.encrypt(access_keys.secret_key, master_key_id);
delete encrypted_access_keys_object.secret_key;
}
return encrypted_access_keys_object;
}));
return { ...account, access_keys: encrypted_access_keys, master_key_id };
}

Expand All @@ -340,13 +344,22 @@ class NCMasterKeysManager {
* @returns {Promise<Object>}
*/
async decrypt_access_keys(account) {
const decrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => ({
access_key: access_keys.access_key,
secret_key: await this.decrypt(access_keys.encrypted_secret_key, account.master_key_id)
})));
const decrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => {
const decrypted_access_keys_object = { ...access_keys };
if (decrypted_access_keys_object.encrypted_secret_key) {
decrypted_access_keys_object.secret_key = await this.decrypt(access_keys.encrypted_secret_key, account.master_key_id);
delete decrypted_access_keys_object.encrypted_secret_key;
}
return decrypted_access_keys_object;
}));
return decrypted_access_keys;
}

async get_active_master_key_id() {
await this.init();
return this.active_master_key.id;
}

/**
* _validate_master_key_manager validates the master keys before decrypt/encrypt
* @param {nb.ID} master_key_id
Expand Down
2 changes: 1 addition & 1 deletion src/sdk/account_sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class AccountSDK {
const token = this.get_auth_token();
// If the request is signed (authenticated)
if (token) {
signature_utils.authorize_request_account_by_token(token, this.requesting_account, false);
signature_utils.authorize_request_account_by_token(token, this.requesting_account);
return;
}
throw new RpcError('UNAUTHORIZED', `No permission to access`);
Expand Down
Loading

0 comments on commit d5921f4

Please sign in to comment.