Skip to content

Commit 9e66913

Browse files
committed
NC | add bypass governance flag
Signed-off-by: nadav mizrahi <nadav.mizrahi16@gmail.com>
1 parent 5d4129c commit 9e66913

File tree

11 files changed

+124
-21
lines changed

11 files changed

+124
-21
lines changed

docs/NooBaaNonContainerized/AccountsAndBuckets.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ See all available account properties - [NC Account Schema](../../src/server/syst
3838

3939
- `new_buckets_path` - When an account creates a bucket using the S3 protocol, NooBaa will create the underlying file system directory. This directory will be created under new_buckets_path. Note that the account must have read and write access to its `new_buckets_path`. Must be an absolute path.
4040

41-
- `custom_bucket_path_allowed_list` - When an account creates a bucket using the S3 protocol, He can override the default bucket path location (under new_buckets_path) using `x-noobaa-custom-bucket-path` HTTP header. This directory will be created only if this path will be under one of the provided allowed list paths in custom_bucket_path_allowed_list. Must be a list of absolute paths (divided by colons).
41+
- `custom_bucket_path_allowed_list` - When an account creates a bucket using the S3 protocol, He can override the default bucket path location (under new_buckets_path) using `x-noobaa-custom-bucket-path` HTTP header. This directory will be created only if this path will be under one of the provided allowed list paths in custom_bucket_path_allowed_list. Must be a list of absolute paths (divided by colons).
42+
43+
- `allow_bypass_governance` - give permission to the user to bypass governance mode retention object lock. allowing him to set `x-amz-bypass-governance-retention` header for deleting the object and modifying retention lock. see https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html#object-lock-retention-modes
4244

4345
### Account configuration
4446
Currently, an account can be configured via NooBaa CLI, see - [NooBaa CLI](./NooBaaCLI.md).

docs/NooBaaNonContainerized/NooBaaCLI.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ noobaa-cli account add --name <account_name> --uid <uid> --gid <gid> [--user]
145145
- Description: Specifies an allowed list where this account can create buckets in using
146146
x-noobaa-custom-bucket-path header in create_bucket
147147

148+
- `allow_bypass_governance`
149+
- Type: Boolean
150+
- Description: allow user to bypass governance-mode retention object lock when deleting or modifying locked object
151+
148152
### Update Account
149153

150154
The `account update` command is used to update an existing account with customizable options.
@@ -154,7 +158,7 @@ The `account update` command is used to update an existing account with customiz
154158
noobaa-cli account update --name <account_name> [--new_name][--uid][--gid][--user]
155159
[--new_buckets_path][--access_key][--secret_key][--regenerate][--fs_backend]
156160
[--allow_bucket_creation][--force_md5_etag][--anonymous][--iam_operate_on_root_account][--default_connection]
157-
[--custom_bucket_path_allowed_list]
161+
[--custom_bucket_path_allowed_list][--allow_bypass_governance]
158162
```
159163
#### Flags -
160164
- `name` (Required)
@@ -227,6 +231,10 @@ noobaa-cli account update --name <account_name> [--new_name][--uid][--gid][--use
227231
- Description: Specifies an allowed list where this account can create buckets in using
228232
x-noobaa-custom-bucket-path header in create_bucket
229233

234+
- `allow_bypass_governance`
235+
- Type: Boolean
236+
- Description: allow user to bypass governance-mode retention object lock when deleting or modifying locked object
237+
230238
### Account Status
231239

232240
The `account status` command is used to print the status of the account.

src/cmd/manage_nsfs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ async function fetch_account_data(action, user_input) {
505505
new_buckets_path: user_input.new_buckets_path,
506506
fs_backend: user_input.fs_backend ? String(user_input.fs_backend) : config.NSFS_NC_STORAGE_BACKEND,
507507
custom_bucket_path_allowed_list: user_input.custom_bucket_path_allowed_list,
508+
allow_bypass_governance: user_input.allow_bypass_governance === undefined || user_input.allow_bypass_governance === '' ? user_input.allow_bypass_governance : get_boolean_or_string_value(user_input.allow_bypass_governance),
508509
},
509510
default_connection: user_input.default_connection === undefined ? undefined : String(user_input.default_connection)
510511
};
@@ -545,6 +546,7 @@ async function fetch_account_data(action, user_input) {
545546
}
546547
// custom_bucket_path_allowed_list deletion specified with empty string ''
547548
data.nsfs_account_config.custom_bucket_path_allowed_list = data.nsfs_account_config.custom_bucket_path_allowed_list || undefined;
549+
data.nsfs_account_config.allow_bypass_governance = data.nsfs_account_config.allow_bypass_governance === '' ? undefined : data.nsfs_account_config.allow_bypass_governance;
548550

549551
return data;
550552
}

src/manage_nsfs/manage_nsfs_constants.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ const FROM_FILE = 'from_file';
4646
const ANONYMOUS = 'anonymous';
4747

4848
const VALID_OPTIONS_ACCOUNT = {
49-
'add': new Set(['name', 'uid', 'gid', 'supplemental_groups', 'new_buckets_path', 'custom_bucket_path_allowed_list', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'iam_operate_on_root_account', 'default_connection', FROM_FILE, ...CLI_MUTUAL_OPTIONS]),
50-
'update': new Set(['name', 'uid', 'gid', 'supplemental_groups', 'new_buckets_path', 'custom_bucket_path_allowed_list', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'iam_operate_on_root_account', 'new_name', 'regenerate', 'default_connection', ...CLI_MUTUAL_OPTIONS]),
49+
'add': new Set(['name', 'uid', 'gid', 'supplemental_groups', 'new_buckets_path', 'custom_bucket_path_allowed_list', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'iam_operate_on_root_account', 'default_connection', 'allow_bypass_governance', FROM_FILE, ...CLI_MUTUAL_OPTIONS]),
50+
'update': new Set(['name', 'uid', 'gid', 'supplemental_groups', 'new_buckets_path', 'custom_bucket_path_allowed_list', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'iam_operate_on_root_account', 'new_name', 'regenerate', 'default_connection', 'allow_bypass_governance', ...CLI_MUTUAL_OPTIONS]),
5151
'delete': new Set(['name', ...CLI_MUTUAL_OPTIONS]),
5252
'list': new Set(['wide', 'show_secrets', 'gid', 'uid', 'user', 'name', 'access_key', ...CLI_MUTUAL_OPTIONS]),
5353
'status': new Set(['name', 'access_key', 'show_secrets', ...CLI_MUTUAL_OPTIONS]),
@@ -177,12 +177,13 @@ const OPTION_TYPE = {
177177
// bucket tagging
178178
tag: 'string',
179179
merge_tag: 'string',
180+
allow_bypass_governance: 'boolean',
180181
};
181182

182183
const BOOLEAN_STRING_VALUES = ['true', 'false'];
183184
const BOOLEAN_STRING_OPTIONS = new Set(['allow_bucket_creation', 'regenerate', 'wide', 'show_secrets', 'force',
184185
'force_md5_etag', 'iam_operate_on_root_account', 'all_account_details', 'all_bucket_details', 'anonymous',
185-
'disable_service_validation', 'disable_runtime_validation', 'short_status', 'skip_verification', 'continue']);
186+
'disable_service_validation', 'disable_runtime_validation', 'short_status', 'skip_verification', 'continue', 'allow_bypass_governance']);
186187

187188
// CLI UNSET VALUES
188189
const CLI_EMPTY_STRING = '';
@@ -199,6 +200,7 @@ const UNSETTABLE_OPTIONS_OBJ = Object.freeze({
199200
'new_buckets_path': CLI_EMPTY_STRING,
200201
'custom_bucket_path_allowed_list': CLI_EMPTY_STRING,
201202
'ips': CLI_EMPTY_STRING_ARRAY,
203+
'allow_bypass_governance': CLI_EMPTY_STRING,
202204
});
203205

204206
const LIST_ACCOUNT_FILTERS = ['uid', 'gid', 'user', 'name', 'access_key'];

src/manage_nsfs/manage_nsfs_help_utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Flags:
144144
--iam_operate_on_root_account <true | false> (optional) Set the account to create root accounts instead of IAM users in IAM API requests.
145145
--from_file <string> (optional) Use details from the JSON file, there is no need to mention all the properties individually in the CLI
146146
--custom_bucket_path_allowed_list <string> (optional) Set the list of allowed custom bucket paths, separated by colons (:) example: '/gpfs/data/custom1/:/gpfs/data/custom2/'
147+
--allow_bypass_governance <true | false> (optional) Set the account to allow bypassing governance mode object lock for object deletion and retention configuration (unset with '')
147148
`;
148149

149150
const ACCOUNT_FLAGS_UPDATE = `
@@ -172,6 +173,7 @@ Flags:
172173
--force_md5_etag <true | false> (optional) Update the account to force md5 etag calculation (unset with '') (will override default config.NSFS_NC_STORAGE_BACKEND)
173174
--iam_operate_on_root_account <true | false> (optional) Update the account to create root accounts instead of IAM users in IAM API requests.
174175
--custom_bucket_path_allowed_list <string> (optional) Update the list of allowed custom bucket paths, separated by colons (:) example: '/gpfs/data/custom1/:/gpfs/data/custom2/' (override;unset with '')
176+
--allow_bypass_governance <true | false> (optional) Update the account to allow bypassing governance mode object lock for object deletion and retention configuration (unset with '')
175177
`;
176178

177179
const ACCOUNT_FLAGS_DELETE = `

src/sdk/accountspace_fs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ class AccountSpaceFS {
609609
new_buckets_path: requesting_account.nsfs_account_config.new_buckets_path,
610610
fs_backend: requesting_account.nsfs_account_config.fs_backend,
611611
custom_bucket_path_allowed_list: requesting_account.nsfs_account_config.custom_bucket_path_allowed_list,
612+
allow_bypass_governance: requesting_account.nsfs_account_config.allow_bypass_governance,
612613
}
613614
};
614615
if (requesting_account.iam_operate_on_root_account) {

src/sdk/namespace_fs.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,18 +2317,22 @@ class NamespaceFS {
23172317
return { retention: { mode: default_retention.mode, retain_until_date } };
23182318
}
23192319

2320+
_check_bypass_governance_permission(fs_context) {
2321+
return fs_context?.allow_bypass_governance;
2322+
}
2323+
23202324
/**
23212325
* check if the object deletion should be blocked by retention lock. if the object is blocked will throw AccessDenied error
23222326
* @param {Object} retention - object retention lock settings
23232327
* @param {boolean} bypass_governance - if true, and user has permission to use this flag, will allow to bypass governance mode retention lock. compliance mode retention lock cannot be bypassed.
23242328
* @throws {S3Error.AccessDenied} if the object is protected by object lock and the user does not have permission to bypass the lock
23252329
*/
2326-
_check_object_retention(retention, bypass_governance) {
2330+
_check_object_retention(fs_context, retention, bypass_governance) {
23272331
if (retention) {
23282332
const retain_until_date = new Date(retention.retain_until_date);
23292333
const now = new Date();
23302334
if (now < retain_until_date) {
2331-
//TODO should check for bypass_governance permission
2335+
bypass_governance = bypass_governance && this._check_bypass_governance_permission(fs_context);
23322336
if (retention.mode === 'COMPLIANCE' ||
23332337
(retention.mode === 'GOVERNANCE' && !bypass_governance)) {
23342338
const err = new S3Error(S3Error.AccessDenied);
@@ -2349,13 +2353,13 @@ class NamespaceFS {
23492353
* @param {boolean} bypass_governance - if true, and user has permission to use this flag, will allow to bypass governance mode retention lock. compliance mode retention lock cannot be bypassed.
23502354
* @throws {S3Error.AccessDenied} if the object is protected by object lock and the user does not have permission to bypass the lock
23512355
*/
2352-
_compare_object_retention(current_retention, new_retention, bypass_governance) {
2356+
_compare_object_retention(fs_context, current_retention, new_retention, bypass_governance) {
23532357
if (current_retention) {
23542358
const retain_until_date = new Date(current_retention.retain_until_date);
23552359
const new_date = new Date(new_retention.retain_until_date);
2356-
//can always increase retention time
2360+
//can always increase retention time when mode is unchanged
23572361
if (new_date >= retain_until_date && new_retention.mode === current_retention.mode) return;
2358-
//TODO should check for bypass_governance permission
2362+
bypass_governance = bypass_governance && this._check_bypass_governance_permission(fs_context);
23592363
if (current_retention.mode === 'COMPLIANCE' ||
23602364
(current_retention.mode === 'GOVERNANCE' && !bypass_governance)) {
23612365
const err = new S3Error(S3Error.AccessDenied);
@@ -2371,13 +2375,13 @@ class NamespaceFS {
23712375
* @param {Object} params - request params, contains bypass_governance flag
23722376
* @throws {S3Error.AccessDenied} if the object is protected by object lock and the user does not have permission to bypass the lock
23732377
*/
2374-
_check_object_lock(version_id, params) {
2378+
_check_object_lock(fs_context, version_id, params) {
23752379
if (version_id.legal_hold === 'ON') {
23762380
const err = new S3Error(S3Error.AccessDenied);
23772381
err.message = 'Access Denied because object protected by object lock.';
23782382
throw err;
23792383
}
2380-
this._check_object_retention(version_id.retention, params.bypass_governance);
2384+
this._check_object_retention(fs_context, version_id.retention, params.bypass_governance);
23812385
}
23822386

23832387

@@ -2468,7 +2472,7 @@ class NamespaceFS {
24682472
try {
24692473
const stat = await nb_native().fs.stat(fs_context, file_path);
24702474
const current_retention = this._get_retention_mode_from_xattr(stat.xattr);
2471-
this._compare_object_retention(current_retention, params.retention, params.bypass_governance);
2475+
this._compare_object_retention(fs_context, current_retention, params.retention, params.bypass_governance);
24722476
const fs_xattr = {};
24732477
fs_xattr[XATTR_RETENTION_MODE] = params.retention.mode;
24742478
fs_xattr[XATTR_RETENTION_DATE] = params.retention.retain_until_date.toISOString();
@@ -3420,7 +3424,7 @@ class NamespaceFS {
34203424
await this._check_path_in_bucket_boundaries(fs_context, file_path);
34213425
const version_info = await this._get_version_info(fs_context, file_path);
34223426
if (!version_info) return;
3423-
this._check_object_lock(version_info, params);
3427+
this._check_object_lock(fs_context, version_info, params);
34243428

34253429
const deleted_latest = file_path === latest_version_path;
34263430
if (deleted_latest) {

src/server/system_services/schemas/nsfs_account_schema.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ module.exports = {
8989
$ref: 'common_api#/definitions/fs_backend'
9090
},
9191
custom_bucket_path_allowed_list: { type: 'string' },
92+
allow_bypass_governance: { type: 'boolean' },
9293
}
9394
}, {
9495
type: 'object',
@@ -100,6 +101,7 @@ module.exports = {
100101
$ref: 'common_api#/definitions/fs_backend'
101102
},
102103
custom_bucket_path_allowed_list: { type: 'string' },
104+
allow_bypass_governance: { type: 'boolean' },
103105
}
104106
}]
105107
},

src/test/integration_tests/api/s3/test_s3_worm.js

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ mocha.describe('s3 worm', function() {
4444
let version_id3;
4545
let version_id4;
4646
const user_a = 'alicia';
47+
const user_a_mail = 'alicia@test.com';
4748
let s3_owner;
4849
//let s3_a;
4950
mocha.before(async function() {
@@ -70,19 +71,23 @@ mocha.describe('s3 worm', function() {
7071
account.nsfs_account_config = {
7172
uid: process.getuid(),
7273
gid: process.getgid(),
73-
new_buckets_path: tmp_fs_root
74+
new_buckets_path: tmp_fs_root,
75+
allow_bypass_governance: 'true',
7476
};
7577
}
7678
const admin_keys = (await rpc_client.account.read_account({ email: EMAIL, })).access_keys;
7779
account.name = user_a;
7880
account.email = user_a;
7981
const user_a_keys = (await rpc_client.account.create_account(account)).access_keys;
80-
s3_creds.credentials.accessKeyId = user_a_keys[0].access_key.unwrap();
81-
s3_creds.credentials.secretAccessKey = user_a_keys[0].secret_key.unwrap();
82-
//s3_a = new AWS.S3(s3_creds);
83-
s3_creds.credentials.accessKeyId = admin_keys[0].access_key.unwrap();
84-
s3_creds.credentials.secretAccessKey = admin_keys[0].secret_key.unwrap();
85-
s3_owner = new S3(s3_creds);
82+
if (is_nc_coretest) {
83+
s3_creds.credentials.accessKeyId = user_a_keys[0].access_key.unwrap();
84+
s3_creds.credentials.secretAccessKey = user_a_keys[0].secret_key.unwrap();
85+
s3_owner = new S3(s3_creds);
86+
} else {
87+
s3_creds.credentials.accessKeyId = admin_keys[0].access_key.unwrap();
88+
s3_creds.credentials.secretAccessKey = admin_keys[0].secret_key.unwrap();
89+
s3_owner = new S3(s3_creds);
90+
}
8691
});
8792
mocha.describe('buckets creation', function() {
8893
mocha.it('create bucket BKT & enable lock', async function() {
@@ -977,4 +982,43 @@ mocha.describe('s3 worm', function() {
977982
}), 'NoSuchObjectLockConfiguration', 'The specified object does not have a ObjectLock configuration');
978983
});
979984
});
985+
986+
mocha.describe('no bypass permissions for user', async function() {
987+
let version_id;
988+
mocha.before(async function() {
989+
const update_conf = {
990+
email: user_a_mail,
991+
nsfs_account_config: {allow_bypass_governance: 'false'}};
992+
await rpc_client.account.update_account_s3_access(update_conf);
993+
994+
const conf = await s3_owner.putObject({
995+
Bucket: BKT,
996+
Key: OBJ1,
997+
Body: file_body,
998+
ContentType: 'text/plain',
999+
ObjectLockMode: 'GOVERNANCE',
1000+
ObjectLockRetainUntilDate: tomorrow
1001+
});
1002+
version_id = conf.VersionId;
1003+
});
1004+
1005+
mocha.it('should fail to put retention without bypass flag', async function() {
1006+
await assert_throws_async(s3_owner.putObjectRetention({
1007+
Bucket: BKT,
1008+
Key: OBJ1,
1009+
Retention: { Mode: 'COMPLIANCE', RetainUntilDate: tomorrow },
1010+
VersionId: version_id,
1011+
BypassGovernanceRetention: true
1012+
}), 'AccessDenied', is_nc_coretest ? 'Access Denied because object protected by object lock.' : 'Access Denied');
1013+
});
1014+
1015+
mocha.it('should fail to delete object with retention without bypass flag', async function() {
1016+
await assert_throws_async(s3_owner.deleteObject({
1017+
Bucket: BKT,
1018+
Key: OBJ1,
1019+
VersionId: version_id,
1020+
BypassGovernanceRetention: true,
1021+
}), 'AccessDenied', is_nc_coretest ? 'Access Denied because object protected by object lock.' : 'Access Denied');
1022+
});
1023+
});
9801024
});

0 commit comments

Comments
 (0)