diff --git a/pyacl/acl.py b/pyacl/acl.py index 1402ca1..3a12baa 100644 --- a/pyacl/acl.py +++ b/pyacl/acl.py @@ -8,7 +8,17 @@ An English copy of the Licence is shipped in a file called LICENSE along with th You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. """ -import posix1e +from pwd import getpwnam + +from posix1e import ( + ACL, + ACL_GROUP, + ACL_GROUP_OBJ, + ACL_MASK, + ACL_OTHER, + ACL_USER, + ACL_USER_OBJ, +) DEFAULT_ENTRIES = [ 'u::rw-', @@ -32,6 +42,15 @@ DEFAULT_ENTRYTYPES = [ MAX_PERMBITS = 3 +LIBACL_TAGS = { + 'user': ACL_USER, + 'group': ACL_GROUP, + 'user_obj': ACL_USER_OBJ, + 'group_obj': ACL_GROUP_OBJ, + 'other': ACL_OTHER, + 'mask': ACL_MASK, +} + def reduce_entries(acl): entries = acl.to_any_text().decode().split() @@ -108,8 +127,41 @@ def parse_entries(acl): return outmap +def buildacl(target_name, target_type, read=False, write=False, execute=False): + target_types = ['user', 'group'] + if target_type not in target_types or not isinstance(target_name, str): + return ValueError('Invalid use of buildacl()') + + myacl = ACL() + mytags = [tag for tag in LIBACL_TAGS if tag == target_type or tag in [ltag for ltag in LIBACL_TAGS if ltag not in target_types]] + + aclmap = { + entry: myacl.append() + for entry in mytags + } + + for entry, reference in aclmap.items(): + reference.tag_type = LIBACL_TAGS[entry] + + aclmap[target_type].qualifier = getpwnam(target_name).pw_uid + + for pentry in ['mask', target_type]: + perms = aclmap[pentry].permset + perms.read = read + perms.write = write + perms.execute = execute + + return myacl + + +def acltofile(acl, path): + if acl.valid() is not True: + return ValueError('ACL is not ready to be applied.') + acl.applyto(path) + + def aclfromfile(path): - return posix1e.ACL(file=path) + return ACL(file=path) def entriesfromfile(path): diff --git a/ruff.toml b/ruff.toml index 8d3d2fd..bddbc59 100644 --- a/ruff.toml +++ b/ruff.toml @@ -20,6 +20,7 @@ extend-select = [ ] ignore = [ "E501", # line lengths + "FBT002", # booleans as function arguments "S603", # https://github.com/astral-sh/ruff/issues/4045 "S607", # makes subprocess calls in test suite more portable ] @@ -31,3 +32,6 @@ explicit-preview-rules = true [lint.pydocstyle] convention = "pep257" + +[lint.isort] +force-wrap-aliases = true diff --git a/tests/conftest.py b/tests/conftest.py index 576d959..99daa59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,9 +14,18 @@ from subprocess import run import pytest -#@pytest.fixture(scope='session') @pytest.fixture -def sample_file(tmp_path_factory, aclin): +def sample_file(tmp_path_factory): + directory = tmp_path_factory.mktemp('sample_files') + file = directory / 'file_to_be_acled' + file.touch() + assert not file.read_text() # file should exist + yield file + rmtree(directory) + + +@pytest.fixture +def sample_file_with_acl(tmp_path_factory, aclin): directory = tmp_path_factory.mktemp('sample_files') file = directory / 'file_with_user_read_acl' file.touch() diff --git a/tests/matrix-apply.yaml b/tests/matrix-apply.yaml new file mode 100644 index 0000000..154c271 --- /dev/null +++ b/tests/matrix-apply.yaml @@ -0,0 +1,23 @@ +--- +user:user:r: + args: + target_name: user + target_type: user + read: true + expect: + user: + user: + read: true + write: false + execute: false + group: &null_allfalse + null: + read: false + write: false + execute: false + mask: &null_ro + null: + read: true + write: false + execute: false + other: *null_allfalse diff --git a/tests/test_pyacl.py b/tests/test_pyacl.py index 97eec9a..d96e95f 100644 --- a/tests/test_pyacl.py +++ b/tests/test_pyacl.py @@ -24,6 +24,15 @@ def load_yaml(file): @mark.parametrize('aclin, aclout', load_yaml('matrix.yaml')) -def test_parse_acl(sample_file, aclin, aclout): - have = acl.parsefromfile(sample_file) +def test_parse_acl(sample_file_with_acl, aclin, aclout): + have = acl.parsefromfile(sample_file_with_acl) assert aclout == have + + +@mark.parametrize('scenario, data', load_yaml('matrix-apply.yaml')) +def test_build_and_apply_acl(sample_file, scenario, data): + built_acl = acl.buildacl(**data['args']) + assert len(list(built_acl)) == 5 # noqa PLR2004, this is the expected size of the built ACL + assert acl.acltofile(built_acl, sample_file) is None + read_acl = acl.parsefromfile(sample_file) + assert read_acl == data['expect']