diff --git a/src/middlewared/middlewared/plugins/pool_/dataset_quota_and_perms.py b/src/middlewared/middlewared/plugins/pool_/dataset_quota_and_perms.py index 486b5359208be..e62dd34d35831 100644 --- a/src/middlewared/middlewared/plugins/pool_/dataset_quota_and_perms.py +++ b/src/middlewared/middlewared/plugins/pool_/dataset_quota_and_perms.py @@ -10,181 +10,6 @@ class PoolDatasetService(Service): class Config: namespace = 'pool.dataset' - @accepts( - Str('id', required=True), - Dict( - 'pool_dataset_permission', - Str('user'), - Str('group'), - UnixPerm('mode', null=True), - OROperator( - Ref('nfs4_acl'), - Ref('posix1e_acl'), - name='acl' - ), - Dict( - 'options', - Bool('set_default_acl', default=False), - Bool('stripacl', default=False), - Bool('recursive', default=False), - Bool('traverse', default=False), - ), - register=True, - ), - roles=['DATASET_WRITE'] - ) - @returns(Ref('pool_dataset_permission')) - @item_method - @job(lock="dataset_permission_change") - async def permission(self, job, id_, data): - """ - Set permissions for a dataset `id`. Permissions may be specified as - either a posix `mode` or an `acl`. This method is a wrapper around - `filesystem.setperm`, `filesystem.setacl`, and `filesystem.chown` - - `filesystem.setperm` is called if `mode` is specified. - `filesystem.setacl` is called if `acl` is specified or if the - option `set_default_acl` is selected. - `filesystem.chown` is called if neither `mode` nor `acl` is - specified. - - The following `options` are supported: - - `set_default_acl` - apply a default ACL appropriate for specified - dataset. Default ACL is `NFS4_RESTRICTED` or `POSIX_RESTRICTED` - ACL template builtin with additional entries builtin_users group - and builtin_administrators group. See documentation for - `filesystem.acltemplate` for more details. - - `stripacl` - this option must be set in order to apply a POSIX - mode to a dataset that has a non-trivial ACL. The effect will - be to remove existing ACL and replace with specified mode. - - `recursive` - apply permissions recursively to dataset (all files - and directories will be impacted. - - `traverse` - permit recursive job to traverse filesystem boundaries - (child datasets). - - .. examples(websocket):: - - Change permissions of dataset "tank/myuser" to myuser:wheel and 755. - - :::javascript - { - "id": "6841f242-840a-11e6-a437-00e04d680384", - "msg": "method", - "method": "pool.dataset.permission", - "params": ["tank/myuser", { - "user": "myuser", - "acl": [], - "group": "builtin_users", - "mode": "755", - "options": {"recursive": true, "stripacl": true}, - }] - } - - """ - dataset_info = await self.middleware.call('pool.dataset.get_instance', id_) - path = dataset_info['mountpoint'] - acltype = dataset_info['acltype']['value'] - user = data.get('user', None) - group = data.get('group', None) - uid = gid = -1 - mode = data.get('mode', None) - options = data.get('options', {}) - set_default_acl = options.pop('set_default_acl') - acl = data.get('acl', []) - - if mode is None and set_default_acl: - acl_template = 'POSIX_RESTRICTED' if acltype == 'POSIX' else 'NFS4_RESTRICTED' - acl = (await self.middleware.call('filesystem.acltemplate.by_path', { - 'query-filters': [('name', '=', acl_template)], - 'format-options': {'canonicalize': True, 'ensure_builtins': True}, - }))[0]['acl'] - - pjob = None - - verrors = ValidationErrors() - if user is not None: - try: - uid = (await self.middleware.call('user.get_user_obj', {'username': user}))['pw_uid'] - except Exception as e: - verrors.add('pool_dataset_permission.user', str(e)) - - if group is not None: - try: - gid = (await self.middleware.call('group.get_group_obj', {'groupname': group}))['gr_gid'] - except Exception as e: - verrors.add('pool_dataset_permission.group', str(e)) - - if acl and mode: - verrors.add('pool_dataset_permission.mode', - 'setting mode and ACL simultaneously is not permitted.') - - if acl and options['stripacl']: - verrors.add('pool_dataset_permissions.acl', - 'Simultaneously setting and removing ACL is not permitted.') - - if mode and not options['stripacl']: - if not await self.middleware.call('filesystem.acl_is_trivial', path): - verrors.add('pool_dataset_permissions.options', - f'{path} has an extended ACL. The option "stripacl" must be selected.') - verrors.check() - - if not acl and mode is None and not options['stripacl']: - """ - Neither an ACL, mode, or removing the existing ACL are - specified in `data`. Perform a simple chown. - """ - options.pop('stripacl', None) - pjob = await self.middleware.call('filesystem.chown', { - 'path': path, - 'uid': uid, - 'gid': gid, - 'options': options - }) - - elif acl: - pjob = await self.middleware.call('filesystem.setacl', { - 'path': path, - 'dacl': acl, - 'uid': uid, - 'gid': gid, - 'options': options - }) - - elif mode or options['stripacl']: - """ - `setperm` performs one of two possible actions. If - `mode` is not set, but `stripacl` is specified, then - the existing ACL on the file is converted in place via - `acl_strip_np()`. This preserves the existing posix mode - while removing any extended ACL entries. - - If `mode` is set, then the ACL is removed from the file - and the new `mode` is applied. - """ - pjob = await self.middleware.call('filesystem.setperm', { - 'path': path, - 'mode': mode, - 'uid': uid, - 'gid': gid, - 'options': options - }) - else: - """ - This should never occur, but fail safely to avoid undefined - or unintended behavior. - """ - raise CallError(f"Unexpected parameter combination: {data}", - errno.EINVAL) - - await pjob.wait() - if pjob.error: - raise CallError(pjob.error) - return data - # TODO: Document this please @accepts( Str('ds', required=True), diff --git a/tests/api2/test_347_posix_mode.py b/tests/api2/test_347_posix_mode.py index c7fb7cce2b72e..0ddf109d7c7de 100644 --- a/tests/api2/test_347_posix_mode.py +++ b/tests/api2/test_347_posix_mode.py @@ -2,20 +2,18 @@ # License: BSD -import sys -import os import pytest import stat -apifolder = os.getcwd() -sys.path.append(apifolder) -from functions import DELETE, GET, POST, SSH_TEST, wait_on_job + from auto_config import pool_name, user, password -from pytest_dependency import depends +from middlewared.test.integration.assets.pool import dataset +from middlewared.test.integration.utils import call, ssh + -MODE_DATASET = f'{pool_name}/modetest' +MODE_DATASET = 'modetest' dataset_url = MODE_DATASET.replace('/', '%2F') -MODE_SUBDATASET = f'{pool_name}/modetest/sub1' +MODE_SUBDATASET = 'modetest/sub1' subdataset_url = MODE_SUBDATASET.replace('/', '%2F') OWNER_BITS = { @@ -43,148 +41,99 @@ MODE_PWD = "modetesting" -def test_01_check_dataset_endpoint(): - assert isinstance(GET('/pool/dataset/').json(), list) +@pytest.fixture(scope='module'): +def get_dataset + with dataset(MODE_DATASET) as ds: + path = os.path.join('/mnt', ds) + ssh(f'mkdir -p {path}/dir1/dir2') + ssh(f'touch {path}/dir1/dir2/testfile') + with dataset(MODE_SUBDATASET): + yield ds -@pytest.mark.dependency(name="DATASET_CREATED") -def test_02_create_dataset(request): - result = POST( - '/pool/dataset/', { - 'name': MODE_DATASET - } - ) - assert result.status_code == 200, result.text +def get_mode_octal(path): + mode = call('filesystem.stat', path)['mode'] + server_mode = f"{stat.S_IMODE(mode):03o}" @pytest.mark.dependency(name="IS_TRIVIAL") -def test_03_verify_acl_is_trivial(request): +def test_verify_acl_is_trivial(get_dataset): depends(request, ["DATASET_CREATED"]) - results = POST('/filesystem/stat/', f'/mnt/{MODE_DATASET}') - assert results.status_code == 200, results.text - assert results.json()['acl'] is False, results.text + st = call('filesystem.stat', os.path.join('/mnt', get_dataset)) + assert st['acl'] is False + + +def get_mode_octal(path): + mode = call('filesystem.stat', path)['mode'] + return f"{stat.S_IMODE(mode):03o}" @pytest.mark.parametrize('mode_bit', MODE.keys()) -def test_04_verify_setting_mode_bits_nonrecursive(request, mode_bit): +def test_verify_setting_mode_bits_nonrecursive(get_dataset, mode_bit): """ This test iterates through possible POSIX permissions bits and verifies that they are properly set on the remote server. """ - depends(request, ["IS_TRIVIAL"]) new_mode = f"{MODE[mode_bit]:03o}" - result = POST( - f'/pool/dataset/id/{dataset_url}/permission/', { - 'acl': [], - 'mode': new_mode, - 'group': 'nogroup', - 'user': 'nobody' - } - ) - assert result.status_code == 200, result.text - JOB_ID = result.json() - job_status = wait_on_job(JOB_ID, 180) - assert job_status['state'] == 'SUCCESS', str(job_status['results']) - - results = POST('/filesystem/stat/', f'/mnt/{MODE_DATASET}') - assert results.status_code == 200, results.text - server_mode = f"{stat.S_IMODE(results.json()['mode']):03o}" - assert new_mode == server_mode, results.text - - -@pytest.mark.dependency(name="RECURSIVE_PREPARED") -def test_05_prepare_recursive_tests(request): - depends(request, ["IS_TRIVIAL"], scope="session") - result = POST( - '/pool/dataset/', { - 'name': MODE_SUBDATASET - } - ) - assert result.status_code == 200, result.text + path = os.path.join('/mnt', get_dataset) - cmd = f'mkdir -p /mnt/{MODE_DATASET}/dir1/dir2' - results = SSH_TEST(cmd, user, password) - assert results['result'] is True, results['output'] + call('filesystem.setperm', { + 'path': path, + 'mode': new_mode, + 'uid': 65534, + 'gid': 65534 + }, job=True) - cmd = f'touch /mnt/{MODE_DATASET}/dir1/dir2/testfile' - results = SSH_TEST(cmd, user, password) - assert results['result'] is True, results['output'] - - results = POST('/filesystem/stat/', f'/mnt/{MODE_SUBDATASET}') - assert results.status_code == 200, results.text - current_mode = results.json()['mode'] - # new datasets should be created with 755 permissions" - assert f"{stat.S_IMODE(current_mode):03o}" == "755", results.text + server_mode = get_mode_octal(path) + assert new_mode == server_mode @pytest.mark.parametrize('mode_bit', MODE.keys()) -def test_06_verify_setting_mode_bits_recursive_no_traverse(request, mode_bit): +def test_verify_setting_mode_bits_recursive_no_traverse(get_dataset, mode_bit): """ Perform recursive permissions change and verify new mode written to files and subdirectories. """ - depends(request, ["RECURSIVE_PREPARED"]) - new_mode = f"{MODE[mode_bit]:03o}" - result = POST( - f'/pool/dataset/id/{dataset_url}/permission/', { - 'acl': [], - 'mode': new_mode, - 'group': 'nogroup', - 'user': 'nobody', - 'options': {'recursive': True} - } - ) - assert result.status_code == 200, result.text - JOB_ID = result.json() - job_status = wait_on_job(JOB_ID, 180) - assert job_status['state'] == 'SUCCESS', str(job_status['results']) - - results = POST('/filesystem/stat/', f'/mnt/{MODE_DATASET}') - assert results.status_code == 200, results.text - server_mode = f"{stat.S_IMODE(results.json()['mode']):03o}" - assert new_mode == server_mode, results.text - - results = POST('/filesystem/stat/', f'/mnt/{MODE_DATASET}/dir1/dir2') - assert results.status_code == 200, results.text - server_mode = f"{stat.S_IMODE(results.json()['mode']):03o}" - assert new_mode == server_mode, results.text + ds_path = os.path.join('/mnt', get_dataset) + sub_ds_path = os.path.join('/mnt', MODE_SUBDATASET) - results = POST('/filesystem/stat/', - f'/mnt/{MODE_DATASET}/dir1/dir2/testfile') - assert results.status_code == 200, results.text - server_mode = f"{stat.S_IMODE(results.json()['mode']):03o}" - assert new_mode == server_mode, results.text - - -def test_07_verify_mode_not_set_on_child_dataset(request): - depends(request, ["RECURSIVE_PREPARED"]) - results = POST('/filesystem/stat/', f'/mnt/{MODE_SUBDATASET}') - assert results.status_code == 200, results.text - current_mode = results.json()['mode'] - # new datasets should be created with 755 permissions" - assert f"{stat.S_IMODE(current_mode):03o}" == "755", results.text - - -def test_08_verify_traverse_to_child_dataset(request): - depends(request, ["RECURSIVE_PREPARED"]) - result = POST( - f'/pool/dataset/id/{dataset_url}/permission/', { - 'acl': [], - 'mode': 777, - 'group': 'nogroup', - 'user': 'nobody', - 'options': {'recursive': True, 'traverse': True} - } - ) - assert result.status_code == 200, result.text - JOB_ID = result.json() - job_status = wait_on_job(JOB_ID, 180) - assert job_status['state'] == 'SUCCESS', str(job_status['results']) - - results = POST('/filesystem/stat/', f'/mnt/{MODE_SUBDATASET}') - assert results.status_code == 200, results.text - current_mode = results.json()['mode'] - assert f"{stat.S_IMODE(current_mode):03o}" == "777", results.text + new_mode = f"{MODE[mode_bit]:03o}" + call('filesystem.setperm', { + 'path': ds_path, + 'mode': new_mode, + 'uid': 65534, + 'gid': 65534 + 'options': {'recursive': True} + }, job=True) + + server_mode = get_mode_octal(ds_path) + assert new_mode == server_mode + + server_mode = get_mode_octal(os.path.join(ds_path, 'dir1', 'dir2')) + assert new_mode == server_mode + + server_mode = get_mode_octal(os.path.join(ds_path, 'dir1', 'dir2', 'testfile')) + assert new_mode == server_mode + + # child dataset shouldn't be touched + server_mode = get_mode_octal(sub_ds_path) + assert server_mode == "755" + + +def test_verify_traverse_to_child_dataset(get_dataset): + ds_path = os.path.join('/mnt', get_dataset) + sub_ds_path = os.path.join('/mnt', MODE_SUBDATASET) + + call('filesystem.setperm', { + 'path': ds_path, + 'mode': '777', + 'uid': 65534, + 'gid': 65534 + 'options': {'recursive': True, 'traverse': True} + }, job=True) + + server_mode = get_mode_octal(sub_ds_path) + assert server_mode == "777" """