From f343587a62826fa03709950aa1cf85d2670f5453 Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Mon, 23 Sep 2024 21:21:40 +0200 Subject: [PATCH 1/2] Implement ACL merging/updating This allows to update or extend the existing ACL on path. Signed-off-by: Georg Pfuetzenreuter --- pyacl/acl.py | 40 +++++++++++++++++++++++++++++++++ tests/matrix-apply-update.yaml | 24 ++++++++++++++++++++ tests/test_pyacl.py | 41 +++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/matrix-apply-update.yaml diff --git a/pyacl/acl.py b/pyacl/acl.py index ca32bb6..86af0d7 100644 --- a/pyacl/acl.py +++ b/pyacl/acl.py @@ -243,6 +243,46 @@ def apply_acl_to_path(acl, path): acl.applyto(path) +def merge_acls(acl1, acl2): + """ + Example usage: merge_acls(posix1e.ACL, posix1e.ACL) + Return: posix1e.ACL + """ + acl3 = ACL(acl=acl1) + for entry in acl2: + tag_type = entry.tag_type + + # keep existing entries which may only exist once + if tag_type not in [ACL_USER_OBJ, ACL_GROUP_OBJ, ACL_OTHER]: + + # replace existing user/group entries with new ones if the uid/gid matches + if tag_type in [ACL_USER, ACL_GROUP, ACL_MASK]: + + for existing_entry in acl3: + existing_tag_type = existing_entry.tag_type + + if tag_type in [ACL_USER, ACL_GROUP, ACL_MASK]: + if tag_type == existing_tag_type: + if tag_type == ACL_MASK or entry.qualifier == existing_entry.qualifier: + acl3.delete_entry(existing_entry) + + acl3.append(entry) + + acl3.calc_mask() + + return acl3 + + +def update_acl_on_path(new_acl, path): + """ + Example usage: update_acl_on_path(posix1e.ACL, '/etc/foo.txt') + Return: None + """ + acl = merge_acls(read_acl_from_path(path), new_acl) + + return apply_acl_to_path(acl, path) + + def read_acl_from_path(path): """ Example usage: read_acl_from_path('/etc/foo.txt') diff --git a/tests/matrix-apply-update.yaml b/tests/matrix-apply-update.yaml new file mode 100644 index 0000000..2f6e2a5 --- /dev/null +++ b/tests/matrix-apply-update.yaml @@ -0,0 +1,24 @@ +--- +user:user:rw: + args: + target_name: user + target_type: user + read: false + write: true + expect: + user: + user: + read: false + write: true + execute: false + group: &null_allfalse + null: + read: true + write: false + execute: false + mask: &null_ro + null: + read: true + write: true + execute: false + other: *null_allfalse diff --git a/tests/test_pyacl.py b/tests/test_pyacl.py index 73134ce..fd28b2d 100644 --- a/tests/test_pyacl.py +++ b/tests/test_pyacl.py @@ -23,6 +23,19 @@ def load_yaml(file): return list(data.items()) +def load_yamls(file1, file2): + data1 = load_yaml(file1) + data2 = load_yaml(file2) + + out = [] + + for lentry in data1: + for i in range(len(lentry)): + ix = i - 1 + out.append( ({'first': data1[ix], 'second': data2[ix]}) ) # noqa UP034, pytest mangles this in the parameters + + return out + @mark.parametrize('aclin, aclout', load_yaml('matrix.yaml')) def test_parse_acl_through_string(sample_file_with_acl, aclin, aclout): have = acl.parse_acl_from_path_via_string(sample_file_with_acl) @@ -35,10 +48,36 @@ def test_parse_acl_native(sample_file_with_acl, aclin, aclout): assert aclout == have +@mark.parametrize('mode', ['fresh', 'update']) @mark.parametrize('scenario, data', load_yaml('matrix-apply.yaml')) -def test_build_and_apply_acl(sample_file, scenario, data): +def test_build_and_apply_acl(sample_file, mode, scenario, data): built_acl = acl.build_acl(**data['args']) assert len(list(built_acl)) == 5 # noqa PLR2004, this is the expected size of the built ACL assert acl.apply_acl_to_path(built_acl, sample_file) is None + if mode == 'update': + assert acl.update_acl_on_path(built_acl, sample_file) is None read_acl = acl.parse_acl_from_path(sample_file) assert read_acl == data['expect'] + + +@mark.parametrize('data', load_yamls('matrix-apply.yaml', 'matrix-apply-update.yaml') ) +def test_build_and_update_acl(sample_file, data): + # we're updating the default instead of overwriting with apply + # one better way would be to have both "before" and "after" in matrix-apply-update.yaml instead of re-using the one for apply + # another would be to somehow adjust the apply map + data['first'][1]['expect']['group'][None]['read'] = True + data['first'][1]['expect']['other'][None]['read'] = True + + built_acl1 = acl.build_acl(**data['first'][1]['args']) + assert len(list(built_acl1)) == 5 # noqa PLR2004, this is the expected size of the built ACL + + built_acl2 = acl.build_acl(**data['second'][1]['args']) + assert len(list(built_acl2)) == 5 # noqa PLR2004 + + assert acl.update_acl_on_path(built_acl1, sample_file) is None + read_acl1 = acl.parse_acl_from_path(sample_file) + assert read_acl1 == data['first'][1]['expect'] + + assert acl.update_acl_on_path(built_acl2, sample_file) is None + read_acl2 = acl.parse_acl_from_path(sample_file) + assert read_acl2 == data['second'][1]['expect'] From 5221ec8f512e8264d0d1c4cfc9977207ce35e073 Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Mon, 23 Sep 2024 23:43:31 +0200 Subject: [PATCH 2/2] Add debug function This allows for easier troubleshooting of invalid ACLs. Signed-off-by: Georg Pfuetzenreuter --- pyacl/acl.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyacl/acl.py b/pyacl/acl.py index 86af0d7..c877097 100644 --- a/pyacl/acl.py +++ b/pyacl/acl.py @@ -306,3 +306,13 @@ def parse_acl_from_path(path): Return: Complete ACL map """ return parse_acl(read_acl_from_path(path)) + + +def debug_dump_acl_entries(acl): + for entry in acl: + print(f'tag: {entry.tag_type}', end='') + try: + print(f' qual: {entry.qualifier}') + except TypeError: + print() + print(f'read: {entry.permset.read}')