Skip to content

Commit 6437ba1

Browse files
authored
Merge pull request #8488 from naveenpaul1/bucket_owner_check
NSFS | Healthcheck is not reporting error for buckets with out access
2 parents a94be3b + 4211c25 commit 6437ba1

File tree

7 files changed

+166
-16
lines changed

7 files changed

+166
-16
lines changed

docs/NooBaaNonContainerized/Health.md

+15
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,18 @@ The following error codes will be associated with a specific Bucket or Account s
351351
- Resolutions:
352352
- Check for FS user on the host running the Health CLI.
353353
354+
#### 6. Bucket with invalid account owner
355+
- Error code: `INVALID_ACCOUNT_OWNER`
356+
- Error message: Bucket account owner is invalid
357+
- Reasons:
358+
- The bucket owner account is invalid.
359+
- Resolutions:
360+
- Compare bucket account owner and account ids in account dir.
361+
362+
#### 7. Bucket missing account owner
363+
- Error code: `MISSING_ACCOUNT_OWNER`
364+
- Error message: Bucket account owner not found
365+
- Reasons:
366+
- Bucket missing owner account.
367+
- Resolutions:
368+
- Check for owner_account property in bucket config file.

src/manage_nsfs/health.js

+43-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const native_fs_utils = require('../util/native_fs_utils');
1111
const { read_stream_join } = require('../util/buffer_utils');
1212
const { make_https_request } = require('../util/http_utils');
1313
const { TYPES } = require('./manage_nsfs_constants');
14-
const { get_boolean_or_string_value, throw_cli_error, write_stdout_response } = require('./manage_nsfs_cli_utils');
14+
const { get_boolean_or_string_value, throw_cli_error, write_stdout_response, get_bucket_owner_account_by_id } = require('./manage_nsfs_cli_utils');
1515
const { ManageCLIResponse } = require('./manage_nsfs_cli_responses');
1616
const ManageCLIError = require('./manage_nsfs_cli_errors').ManageCLIError;
1717

@@ -52,6 +52,14 @@ const health_errors = {
5252
error_code: 'INVALID_DISTINGUISHED_NAME',
5353
error_message: 'Account distinguished name was not found',
5454
},
55+
INVALID_ACCOUNT_OWNER: {
56+
error_code: 'INVALID_ACCOUNT_OWNER',
57+
error_message: 'Bucket account owner is invalid',
58+
},
59+
MISSING_ACCOUNT_OWNER: {
60+
error_code: 'MISSING_ACCOUNT_OWNER',
61+
error_message: 'Bucket account owner not found',
62+
},
5563
UNKNOWN_ERROR: {
5664
error_code: 'UNKNOWN_ERROR',
5765
error_message: 'An unknown error occurred',
@@ -364,7 +372,7 @@ class NSFSHealth {
364372
const config_file_path = this.config_fs.get_account_path_by_name(config_file_name);
365373
res = await is_new_buckets_path_valid(config_file_path, config_data, storage_path);
366374
} else if (type === TYPES.BUCKET) {
367-
res = await is_bucket_storage_path_exists(this.config_fs.fs_context, config_data, storage_path);
375+
res = await is_bucket_storage_and_owner_exists(this.config_fs, config_data, storage_path);
368376
}
369377
if (all_details && res.valid_storage) {
370378
valid_storages.push(res.valid_storage);
@@ -498,17 +506,18 @@ async function is_new_buckets_path_valid(config_file_path, config_data, new_buck
498506
}
499507

500508
/**
501-
* is_bucket_storage_path_exists checks if the underlying storage path of a bucket exists
502-
* @param {nb.NativeFSContext} fs_context
509+
* is_bucket_storage_and_owner_exists checks if the underlying storage path of a bucket exists
510+
* @param {import('../sdk/config_fs').ConfigFS} config_fs
503511
* @param {object} config_data
504512
* @param {string} storage_path
505513
* @returns {Promise<object>}
506514
*/
507-
async function is_bucket_storage_path_exists(fs_context, config_data, storage_path) {
515+
async function is_bucket_storage_and_owner_exists(config_fs, config_data, storage_path) {
508516
let res_obj;
509517
try {
510518
if (!_should_skip_health_access_check()) {
511-
await nb_native().fs.stat(fs_context, storage_path);
519+
const account_fs_context = await get_account_owner_context(config_fs, config_data.owner_account);
520+
await nb_native().fs.stat(account_fs_context, storage_path);
512521
}
513522
res_obj = get_valid_object(config_data.name, undefined, storage_path);
514523
} catch (err) {
@@ -519,12 +528,40 @@ async function is_bucket_storage_path_exists(fs_context, config_data, storage_pa
519528
} else if (err.code === 'EACCES' || (err.code === 'EPERM' && err.message === 'Operation not permitted')) {
520529
dbg.log1('Error: Storage path should be accessible to account: ', storage_path);
521530
err_code = health_errors.ACCESS_DENIED.error_code;
531+
} else if (err.code === health_errors.INVALID_ACCOUNT_OWNER.error_code ||
532+
err.code === health_errors.MISSING_ACCOUNT_OWNER.error_code) {
533+
dbg.log1('Error: Bucket account owner should be existing and valid account', config_data.owner_account);
534+
err_code = err.code;
522535
}
523536
res_obj = get_invalid_object(config_data.name, undefined, storage_path, err_code);
524537
}
525538
return res_obj;
526539
}
527540

541+
/**
542+
* get_account_owner_context returns bucket account owner specific FS context
543+
* @param {import('../sdk/config_fs').ConfigFS} config_fs
544+
* @param {string} owner_account
545+
* @returns {Promise<object>}
546+
*/
547+
async function get_account_owner_context(config_fs, owner_account) {
548+
if (!owner_account) {
549+
const new_err = new Error(health_errors.MISSING_ACCOUNT_OWNER.error_message);
550+
new_err.code = health_errors.MISSING_ACCOUNT_OWNER.error_code;
551+
throw new_err;
552+
}
553+
try {
554+
// when account owner is invalid method will throw error
555+
const owner_account_data = await get_bucket_owner_account_by_id(config_fs, owner_account, false, false);
556+
const account_fs_context = await native_fs_utils.get_fs_context(owner_account_data.nsfs_account_config,
557+
owner_account_data.nsfs_account_config.fs_backend);
558+
return account_fs_context;
559+
} catch (err) {
560+
const new_err = new Error(`Bucket account owner ${owner_account} is invalid`);
561+
new_err.code = health_errors.INVALID_ACCOUNT_OWNER.error_code;
562+
throw new_err;
563+
}
564+
}
528565

529566
/**
530567
* get_valid_object returns an object which repersents a valid account/bucket and contains defined parameters

src/manage_nsfs/manage_nsfs_cli_utils.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ async function get_bucket_owner_account_by_name(config_fs, bucket_owner) {
5757
* otherwise it would throw an error
5858
* @param {import('../sdk/config_fs').ConfigFS} config_fs
5959
* @param {string} owner_account
60+
* @param {boolean} show_secrets
61+
* @param {boolean} decrypt_secret_key
6062
*/
61-
async function get_bucket_owner_account_by_id(config_fs, owner_account) {
63+
async function get_bucket_owner_account_by_id(config_fs, owner_account, show_secrets = true, decrypt_secret_key = true) {
6264
try {
63-
const account = await account_id_cache.get_with_cache({ _id: owner_account, config_fs });
65+
const account = await account_id_cache.get_with_cache({ _id: owner_account, show_secrets, decrypt_secret_key, config_fs });
6466
return account;
6567
} catch (err) {
6668
if (err.code === 'ENOENT') {

src/sdk/accountspace_fs.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ const account_id_cache = new LRUCache({
3030
name: 'AccountIDCache',
3131
// TODO: Decide on a time that we want to invalidate
3232
expiry_ms: Number(config.ACCOUNTS_ID_CACHE_EXPIRY),
33-
/**
34-
* @param {{ _id: string, config_fs: import('./config_fs').ConfigFS }} params
35-
*/
3633
make_key: ({ _id }) => _id,
37-
load: async ({ _id, config_fs }) => config_fs.get_identity_by_id(_id, CONFIG_TYPES.ACCOUNT,
38-
{ show_secrets: true, decrypt_secret_key: true }),
34+
/**
35+
* Accounts are added to the cache based on id, Default value for show_secrets and decrypt_secret_key will be true,
36+
* and show_secrets and decrypt_secret_key `false` only when we load cache from the health script,
37+
* health script doesn't need to fetch or decrypt the secret.
38+
* @param {{ _id: string,
39+
* show_secrets?: boolean,
40+
* decrypt_secret_key?: boolean,
41+
* config_fs: import('./config_fs').ConfigFS }} params
42+
*/
43+
load: async ({ _id, show_secrets = true, decrypt_secret_key = true, config_fs}) =>
44+
config_fs.get_identity_by_id(_id, CONFIG_TYPES.ACCOUNT, { show_secrets: show_secrets, decrypt_secret_key: decrypt_secret_key }),
3945
});
4046

4147

src/test/unit_tests/nc_coretest.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const http_address = `http://localhost:${http_port}`;
5151
const https_address = `https://localhost:${https_port}`;
5252

5353
const FIRST_BUCKET = 'first.bucket';
54-
const NC_CORETEST_STORAGE_PATH = p.join(TMP_PATH, '/nc_coretest_storage_root_path/');
54+
const NC_CORETEST_STORAGE_PATH = p.join(TMP_PATH, 'nc_coretest_storage_root_path/');
5555
const FIRST_BUCKET_PATH = p.join(NC_CORETEST_STORAGE_PATH, FIRST_BUCKET, '/');
5656
const CONFIG_FILE_PATH = p.join(NC_CORETEST_CONFIG_DIR_PATH, 'config.json');
5757
const NC_CORETEST_CONFIG_FS = new ConfigFS(NC_CORETEST_CONFIG_DIR_PATH);

src/test/unit_tests/test_nc_nsfs_cli.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ mocha.describe('manage_nsfs cli', function() {
8080
};
8181

8282
mocha.before(async () => {
83+
config.NSFS_NC_CONF_DIR = config_root;
8384
await fs_utils.create_fresh_path(root_path);
8485
});
8586
mocha.after(async () => {

src/test/unit_tests/test_nc_nsfs_health.js

+91-2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ mocha.describe('nsfs nc health', function() {
9595
});
9696

9797
mocha.describe('health check', function() {
98+
this.timeout(10000);// eslint-disable-line no-invalid-this
9899
const new_buckets_path = `${root_path}new_buckets_path_user1/`;
99100
const account1_options = {
100101
name: 'account1',
@@ -107,7 +108,14 @@ mocha.describe('nsfs nc health', function() {
107108
path: new_buckets_path + '/bucket1',
108109
owner: account1_options.name
109110
};
111+
const account2_options = {
112+
name: 'account2',
113+
uid: process.getuid(),
114+
gid: process.getgid(),
115+
new_buckets_path: new_buckets_path
116+
};
110117
const account_inaccessible_options = { name: 'account_inaccessible', uid: 999, gid: 999, new_buckets_path: bucket_storage_path };
118+
const bucket_inaccessible_options = { name: 'bucket2', path: bucket_storage_path + '/bucket2', owner: account_inaccessible_options.name};
111119
const account_inaccessible_dn_options = { name: 'account_inaccessible_dn', user: 'inaccessible_dn', new_buckets_path: bucket_storage_path };
112120
const invalid_account_dn_options = { name: 'invalid_account_dn', user: 'invalid_account_dn', new_buckets_path: bucket_storage_path };
113121
const fs_users = {
@@ -118,6 +126,8 @@ mocha.describe('nsfs nc health', function() {
118126
}
119127
};
120128
mocha.before(async () => {
129+
this.timeout(5000);// eslint-disable-line no-invalid-this
130+
config.NSFS_NC_CONF_DIR = config_root;
121131
const https_port = 6443;
122132
Health = new NSFSHealth({ config_root, https_port, config_fs });
123133
await fs_utils.create_fresh_path(new_buckets_path);
@@ -126,6 +136,7 @@ mocha.describe('nsfs nc health', function() {
126136
await fs_utils.file_must_exist(new_buckets_path + '/bucket1');
127137
await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.ADD, {config_root, ...account1_options});
128138
await exec_manage_cli(TYPES.BUCKET, ACTIONS.ADD, {config_root, ...bucket1_options});
139+
await fs_utils.file_must_exist(path.join(config_root, 'master_keys.json'));
129140
const get_service_memory_usage = sinon.stub(Health, "get_service_memory_usage");
130141
get_service_memory_usage.onFirstCall().returns(Promise.resolve(100));
131142
for (const user of Object.values(fs_users)) {
@@ -134,13 +145,15 @@ mocha.describe('nsfs nc health', function() {
134145
});
135146

136147
mocha.after(async () => {
148+
this.timeout(5000);// eslint-disable-line no-invalid-this
137149
fs_utils.folder_delete(new_buckets_path);
138150
fs_utils.folder_delete(path.join(new_buckets_path, 'bucket1'));
139151
await exec_manage_cli(TYPES.BUCKET, ACTIONS.DELETE, {config_root, name: bucket1_options.name});
140152
await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.DELETE, {config_root, name: account1_options.name});
141153
for (const user of Object.values(fs_users)) {
142154
await delete_fs_user_by_platform(user.distinguished_name);
143155
}
156+
await fs_utils.folder_delete(config_root);
144157
});
145158

146159
mocha.afterEach(async () => {
@@ -209,10 +222,13 @@ mocha.describe('nsfs nc health', function() {
209222
});
210223

211224
mocha.it('NSFS bucket with invalid storage path', async function() {
225+
this.timeout(5000);// eslint-disable-line no-invalid-this
212226
Health.get_service_state.restore();
213227
Health.get_endpoint_response.restore();
228+
const resp = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.ADD, { config_root, ...account2_options });
229+
const parsed_res = JSON.parse(resp).response.reply;
214230
// create it manually because we can not skip invalid storage path check on the CLI
215-
const bucket_invalid = { _id: mongo_utils.mongoObjectId(), name: 'bucket_invalid', path: new_buckets_path + '/bucket1/invalid', owner: account1_options.name };
231+
const bucket_invalid = { _id: mongo_utils.mongoObjectId(), name: 'bucket_invalid', path: new_buckets_path + '/bucket1/invalid', owner_account: parsed_res._id };
216232
await test_utils.write_manual_config_file(TYPES.BUCKET, config_fs, bucket_invalid);
217233
const get_service_state = sinon.stub(Health, "get_service_state");
218234
get_service_state.onFirstCall().returns(Promise.resolve({ service_status: 'active', pid: 1000 }))
@@ -223,7 +239,81 @@ mocha.describe('nsfs nc health', function() {
223239
assert.strictEqual(health_status.status, 'OK');
224240
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets.length, 1);
225241
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].name, bucket_invalid.name);
242+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].code, "STORAGE_NOT_EXIST");
226243
await exec_manage_cli(TYPES.BUCKET, ACTIONS.DELETE, { config_root, name: bucket_invalid.name});
244+
await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.DELETE, { config_root, name: account2_options.name});
245+
});
246+
247+
mocha.it('Bucket with inaccessible path - uid gid', async function() {
248+
this.timeout(5000);// eslint-disable-line no-invalid-this
249+
await config_fs.create_config_json_file(JSON.stringify({ NC_DISABLE_ACCESS_CHECK: true }));
250+
await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.ADD, { config_root, ...account_inaccessible_options });
251+
await exec_manage_cli(TYPES.BUCKET, ACTIONS.ADD, {config_root, ...bucket_inaccessible_options});
252+
await config_fs.delete_config_json_file();
253+
Health.get_service_state.restore();
254+
Health.get_endpoint_response.restore();
255+
Health.all_account_details = true;
256+
Health.all_bucket_details = true;
257+
const get_service_state = sinon.stub(Health, "get_service_state");
258+
get_service_state.onFirstCall().returns(Promise.resolve({ service_status: 'active', pid: 1000 }))
259+
.onSecondCall().returns(Promise.resolve({ service_status: 'active', pid: 2000 }));
260+
const get_endpoint_response = sinon.stub(Health, "get_endpoint_response");
261+
get_endpoint_response.onFirstCall().returns(Promise.resolve({response: {response_code: 'RUNNING', total_fork_count: 0}}));
262+
const health_status = await Health.nc_nsfs_health();
263+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets.length, 1);
264+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].code, "ACCESS_DENIED");
265+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].name, bucket_inaccessible_options.name);
266+
assert.strictEqual(health_status.checks.accounts_status.valid_accounts.length, 1);
267+
assert.strictEqual(health_status.checks.accounts_status.invalid_accounts.length, 1);
268+
assert.strictEqual(health_status.checks.accounts_status.invalid_accounts[0].code, "ACCESS_DENIED");
269+
assert.strictEqual(health_status.checks.accounts_status.invalid_accounts[0].name, account_inaccessible_options.name);
270+
271+
await exec_manage_cli(TYPES.BUCKET, ACTIONS.DELETE, { config_root, name: bucket_inaccessible_options.name});
272+
await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.DELETE, { config_root, name: account_inaccessible_options.name});
273+
});
274+
275+
mocha.it('Bucket with inaccessible owner', async function() {
276+
this.timeout(5000);// eslint-disable-line no-invalid-this
277+
//create bucket manually, cli wont allow bucket with invalid owner
278+
const bucket_invalid_owner = { _id: mongo_utils.mongoObjectId(), name: 'bucket_invalid_account', path: new_buckets_path + '/bucket_account', owner_account: 'invalid_account' };
279+
await test_utils.write_manual_config_file(TYPES.BUCKET, config_fs, bucket_invalid_owner);
280+
Health.get_service_state.restore();
281+
Health.get_endpoint_response.restore();
282+
Health.all_account_details = true;
283+
Health.all_bucket_details = true;
284+
const get_service_state = sinon.stub(Health, "get_service_state");
285+
get_service_state.onFirstCall().returns(Promise.resolve({ service_status: 'active', pid: 1000 }))
286+
.onSecondCall().returns(Promise.resolve({ service_status: 'active', pid: 2000 }));
287+
const get_endpoint_response = sinon.stub(Health, "get_endpoint_response");
288+
get_endpoint_response.onFirstCall().returns(Promise.resolve({response: {response_code: 'RUNNING', total_fork_count: 0}}));
289+
const health_status = await Health.nc_nsfs_health();
290+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets.length, 1);
291+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].code, "INVALID_ACCOUNT_OWNER");
292+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].name, 'bucket_invalid_account');
293+
294+
await exec_manage_cli(TYPES.BUCKET, ACTIONS.DELETE, { config_root, name: 'bucket_invalid_account'});
295+
});
296+
297+
mocha.it('Bucket with empty owner', async function() {
298+
this.timeout(5000);// eslint-disable-line no-invalid-this
299+
//create bucket manually, cli wont allow bucket with empty owner
300+
const bucket_invalid_owner = { _id: mongo_utils.mongoObjectId(), name: 'bucket_invalid_account', path: new_buckets_path + '/bucket_account' };
301+
await test_utils.write_manual_config_file(TYPES.BUCKET, config_fs, bucket_invalid_owner);
302+
Health.get_service_state.restore();
303+
Health.get_endpoint_response.restore();
304+
Health.all_account_details = true;
305+
Health.all_bucket_details = true;
306+
const get_service_state = sinon.stub(Health, "get_service_state");
307+
get_service_state.onFirstCall().returns(Promise.resolve({ service_status: 'active', pid: 1000 }))
308+
.onSecondCall().returns(Promise.resolve({ service_status: 'active', pid: 2000 }));
309+
const get_endpoint_response = sinon.stub(Health, "get_endpoint_response");
310+
get_endpoint_response.onFirstCall().returns(Promise.resolve({response: {response_code: 'RUNNING', total_fork_count: 0}}));
311+
const health_status = await Health.nc_nsfs_health();
312+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets.length, 1);
313+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].code, "MISSING_ACCOUNT_OWNER");
314+
assert.strictEqual(health_status.checks.buckets_status.invalid_buckets[0].name, 'bucket_invalid_account');
315+
316+
await exec_manage_cli(TYPES.BUCKET, ACTIONS.DELETE, { config_root, name: 'bucket_invalid_account'});
227317
});
228318

229319
mocha.it('NSFS invalid bucket schema json', async function() {
@@ -527,4 +617,3 @@ mocha.describe('nsfs nc health', function() {
527617
});
528618
});
529619
});
530-

0 commit comments

Comments
 (0)