forked from Georg/pyacl
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
ad30b3529b | |||
112650190a | |||
59b2f06566 | |||
fd1cf9b337 | |||
0f1a6431e5 | |||
5221ec8f51 | |||
f343587a62 | |||
efc419757e | |||
092156f3d5 | |||
8bf35baf4f | |||
81d37afea3 | |||
b55392b4c6 | |||
d1f6319b8d | |||
0828faa6bc | |||
e343ae2eb9 | |||
67d9f915bc | |||
37a7ea6341 | |||
1336676b33 | |||
d22b176521 | |||
c6496c68f9 | |||
328652d59e |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
dist
|
dist
|
||||||
venv
|
venv
|
||||||
__pycache__
|
__pycache__
|
||||||
|
.venv
|
||||||
|
env
|
||||||
|
*.egg-info
|
||||||
|
@ -75,3 +75,8 @@ build_acl(target_name, target_type, read=False, write=False, execute=False)
|
|||||||
## Hacking/Tests
|
## Hacking/Tests
|
||||||
|
|
||||||
Functionality is tested through `pytest`. As it requires a certain test user to be present, easiest is to use the purpose-built container image. A wrapper is provided at `scripts/test.sh`.
|
Functionality is tested through `pytest`. As it requires a certain test user to be present, easiest is to use the purpose-built container image. A wrapper is provided at `scripts/test.sh`.
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
The authoritative source of this project is at https://git.com.de/Georg/pyacl.
|
||||||
|
A mirror is provided at https://github.com/tacerus/pyacl.
|
||||||
|
@ -7,3 +7,5 @@ You may not use this work except in compliance with the Licence.
|
|||||||
An English copy of the Licence is shipped in a file called LICENSE along with this applications source code.
|
An English copy of the Licence is shipped in a file called LICENSE along with this applications source code.
|
||||||
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.
|
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .__version__ import __version__ as __version__
|
||||||
|
10
pyacl/__version__.py
Normal file
10
pyacl/__version__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024, pyacl contributors
|
||||||
|
|
||||||
|
Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence").
|
||||||
|
You may not use this work except in compliance with the Licence.
|
||||||
|
An English copy of the Licence is shipped in a file called LICENSE along with this applications source code.
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = '1.0.5'
|
60
pyacl/acl.py
60
pyacl/acl.py
@ -58,8 +58,7 @@ def reduce_entries(acl):
|
|||||||
Return: List of entries converted to strings
|
Return: List of entries converted to strings
|
||||||
"""
|
"""
|
||||||
entries = acl.to_any_text().decode().split()
|
entries = acl.to_any_text().decode().split()
|
||||||
entries = [entry for entry in entries if entry not in DEFAULT_ENTRIES]
|
return [entry for entry in entries if entry not in DEFAULT_ENTRIES]
|
||||||
return entries
|
|
||||||
|
|
||||||
|
|
||||||
def parse_permission_string(strpermission):
|
def parse_permission_string(strpermission):
|
||||||
@ -134,7 +133,7 @@ def parse_acl(acl): # noqa PLR0912, FIXME: uncomplexify this
|
|||||||
Return: Complete ACL map
|
Return: Complete ACL map
|
||||||
"""
|
"""
|
||||||
permap = {
|
permap = {
|
||||||
permission: False for permission in DEFAULT_PERMISSIONS.keys()
|
permission: False for permission in DEFAULT_PERMISSIONS
|
||||||
}
|
}
|
||||||
outmap = {
|
outmap = {
|
||||||
group: {
|
group: {
|
||||||
@ -166,7 +165,7 @@ def parse_acl(acl): # noqa PLR0912, FIXME: uncomplexify this
|
|||||||
for tag_high, tag_low in LIBACL_TAGS.items():
|
for tag_high, tag_low in LIBACL_TAGS.items():
|
||||||
if tag_low == tag_type:
|
if tag_low == tag_type:
|
||||||
lowmap = permap.copy()
|
lowmap = permap.copy()
|
||||||
for permission in lowmap.keys():
|
for permission in lowmap:
|
||||||
lowmap[permission] = getattr(permset, permission)
|
lowmap[permission] = getattr(permset, permission)
|
||||||
outtag = tag_high
|
outtag = tag_high
|
||||||
if tag_type == ACL_USER_OBJ:
|
if tag_type == ACL_USER_OBJ:
|
||||||
@ -179,7 +178,7 @@ def parse_acl(acl): # noqa PLR0912, FIXME: uncomplexify this
|
|||||||
outname = name
|
outname = name
|
||||||
if outtag not in outmap:
|
if outtag not in outmap:
|
||||||
outmap[outtag] = {}
|
outmap[outtag] = {}
|
||||||
if len(outmap[outtag]) == 1 and list(outmap[outtag].keys())[0] is None:
|
if len(outmap[outtag]) == 1 and next(iter(outmap[outtag].keys())) is None:
|
||||||
del outmap[outtag][None]
|
del outmap[outtag][None]
|
||||||
outmap[outtag][outname] = lowmap
|
outmap[outtag][outname] = lowmap
|
||||||
break
|
break
|
||||||
@ -240,7 +239,45 @@ def apply_acl_to_path(acl, path):
|
|||||||
"""
|
"""
|
||||||
if acl.valid() is not True:
|
if acl.valid() is not True:
|
||||||
return ValueError('ACL is not ready to be applied.')
|
return ValueError('ACL is not ready to be applied.')
|
||||||
acl.applyto(path)
|
return 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] and tag_type == existing_tag_type and ( 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):
|
def read_acl_from_path(path):
|
||||||
@ -266,3 +303,14 @@ def parse_acl_from_path(path):
|
|||||||
Return: Complete ACL map
|
Return: Complete ACL map
|
||||||
"""
|
"""
|
||||||
return parse_acl(read_acl_from_path(path))
|
return parse_acl(read_acl_from_path(path))
|
||||||
|
|
||||||
|
|
||||||
|
# ignoring T201, debug function is expected to print
|
||||||
|
def debug_dump_acl_entries(acl):
|
||||||
|
for entry in acl:
|
||||||
|
print(f'tag: {entry.tag_type}', end='') # noqa T201
|
||||||
|
try:
|
||||||
|
print(f' qual: {entry.qualifier}') # noqa T201
|
||||||
|
except TypeError:
|
||||||
|
print() # noqa T201
|
||||||
|
print(f'read: {entry.permset.read}') # noqa T201
|
||||||
|
@ -1,20 +1,54 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = ["setuptools", "wheel"]
|
||||||
'hatchling',
|
build-backend = "setuptools.build_meta"
|
||||||
]
|
|
||||||
build-backend = 'hatchling.build'
|
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = 'pyacl'
|
name = 'pyacl'
|
||||||
description = 'High level abstractions over pylibacl'
|
description = 'High level abstractions over pylibacl'
|
||||||
version = '0.0.1'
|
dynamic = ['license', 'readme', 'version']
|
||||||
authors = [
|
authors = [
|
||||||
{ name='Georg Pfuetzenreuter', email='georg+python@lysergic.dev' },
|
{ name='Georg Pfuetzenreuter', email='georg+python@lysergic.dev' },
|
||||||
]
|
]
|
||||||
readme = 'README.md'
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: Education",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Software Development",
|
||||||
|
"Typing :: Typed",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
]
|
||||||
requires-python = '>=3.6'
|
requires-python = '>=3.6'
|
||||||
|
|
||||||
[tool.hatch.build.targets.pyacl]
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
'pylibacl',
|
'pylibacl==0.5.4',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest",
|
||||||
|
"ruff",
|
||||||
|
"PyYAML"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = true # include non-python files in the package (default)
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "pyacl.__version__"}
|
||||||
|
readme = {file = ["README.md"]}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
exclude = ["scripts", "scripts.*", "docs.*", "tests", "tests.*", "ruff.toml"]
|
||||||
|
|
||||||
|
16
ruff.toml
16
ruff.toml
@ -2,18 +2,27 @@
|
|||||||
# https://docs.astral.sh/ruff/rules/
|
# https://docs.astral.sh/ruff/rules/
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"A", # flake8-builtins
|
"A", # flake8-builtins
|
||||||
|
"ARG", # flake8-unused-arguments
|
||||||
"BLE", # flake8-blind-except
|
"BLE", # flake8-blind-except
|
||||||
"C4", # flake8-comprehensions
|
"C4", # flake8-comprehensions
|
||||||
"COM", # flake8-commas
|
"COM", # flake8-commas
|
||||||
|
"CPY001",# flake8-copyright
|
||||||
"E", # pycodestyle
|
"E", # pycodestyle
|
||||||
"E261", # spaces before inline comments
|
"E261", # spaces before inline comments
|
||||||
"ERA", # eradicate
|
"ERA", # eradicate
|
||||||
"EXE", # flake8-executable
|
"EXE", # flake8-executable
|
||||||
"FBT", # flake8-boolean-trap
|
"FBT", # flake8-boolean-trap
|
||||||
"I", # isort
|
"I", # isort
|
||||||
|
"INP", # flake8-no-pep420
|
||||||
"ISC", # flake8-implicit-str-concat
|
"ISC", # flake8-implicit-str-concat
|
||||||
|
"N", # pep8-naming
|
||||||
"PL", # Pylint
|
"PL", # Pylint
|
||||||
|
"RET", # flake8-return
|
||||||
|
"RSE", # flake8-raise
|
||||||
|
"RUF", # Ruff-specific rules
|
||||||
"S", # flake8-bandit
|
"S", # flake8-bandit
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
"T20", # flake8-print
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"W", # pycodestyle
|
"W", # pycodestyle
|
||||||
"YTT", # flake8-2020
|
"YTT", # flake8-2020
|
||||||
@ -28,7 +37,12 @@ preview = true
|
|||||||
explicit-preview-rules = true
|
explicit-preview-rules = true
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
[lint.per-file-ignores]
|
||||||
"tests/*.py" = ["S101"] # allow "assert" in test suites
|
"pyacl/__init__.py" = ["PLC0414"] # allow explicit re-exports / avoid conflict with F401
|
||||||
|
"tests/*.py" = [
|
||||||
|
"INP001", # tests do not need to be part of a package
|
||||||
|
"S101", # allow "assert" in test suites
|
||||||
|
"T201", # lazy printing is ok in tests
|
||||||
|
]
|
||||||
|
|
||||||
[lint.pydocstyle]
|
[lint.pydocstyle]
|
||||||
convention = "pep257"
|
convention = "pep257"
|
||||||
|
21
setup.cfg
Normal file
21
setup.cfg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[metadata]
|
||||||
|
name = pyacl
|
||||||
|
version = attr: pyacl.__version__
|
||||||
|
author = Georg Pfuetzenreuter
|
||||||
|
author_email = georg+python@lysergic.dev
|
||||||
|
description = High level abstractions over pylibacl
|
||||||
|
license = EUPL-1.2
|
||||||
|
long_description = file: README.md, LICENSE
|
||||||
|
|
||||||
|
[options]
|
||||||
|
package_dir=
|
||||||
|
=.
|
||||||
|
packages=find:
|
||||||
|
python_requires = >=3.6
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
exclude=
|
||||||
|
scripts*
|
||||||
|
tests*
|
||||||
|
*.toml
|
||||||
|
where=.
|
24
tests/matrix-apply-update.yaml
Normal file
24
tests/matrix-apply-update.yaml
Normal file
@ -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
|
@ -23,22 +23,61 @@ def load_yaml(file):
|
|||||||
return list(data.items())
|
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'))
|
@mark.parametrize('aclin, aclout', load_yaml('matrix.yaml'))
|
||||||
def test_parse_acl_through_string(sample_file_with_acl, aclin, aclout):
|
def test_parse_acl_through_string(sample_file_with_acl, aclout):
|
||||||
have = acl.parse_acl_from_path_via_string(sample_file_with_acl)
|
have = acl.parse_acl_from_path_via_string(sample_file_with_acl)
|
||||||
assert aclout == have
|
assert aclout == have
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize('aclin, aclout', load_yaml('matrix.yaml'))
|
@mark.parametrize('aclin, aclout', load_yaml('matrix.yaml'))
|
||||||
def test_parse_acl_native(sample_file_with_acl, aclin, aclout):
|
def test_parse_acl_native(sample_file_with_acl, aclout):
|
||||||
have = acl.parse_acl_from_path(sample_file_with_acl)
|
have = acl.parse_acl_from_path(sample_file_with_acl)
|
||||||
assert aclout == have
|
assert aclout == have
|
||||||
|
|
||||||
|
|
||||||
|
@mark.parametrize('mode', ['fresh', 'update'])
|
||||||
@mark.parametrize('scenario, data', load_yaml('matrix-apply.yaml'))
|
@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): # noqa ARG001, scenario is only used for easier inspection of the dataset
|
||||||
built_acl = acl.build_acl(**data['args'])
|
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 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
|
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)
|
read_acl = acl.parse_acl_from_path(sample_file)
|
||||||
assert read_acl == data['expect']
|
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']
|
||||||
|
Loading…
x
Reference in New Issue
Block a user