Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions posix1e_acl-formula/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Formula to assist with POSIX.1e [0] ACLs, providing extension and state module alternatives to the `linux_acl` equivalents shipped with Salt.
The upstream Salt modules were deemed to miss features which were not easy to implement given the use of octal mathematics. The modules shipped with this formula utilize the [pyacl](https://github.com/tacerus/pyacl) library which aims to provide a simple, high level, abstraction over ACLs, utilizing the [pylibacl](https://github.com/iustin/pylibacl) low level library underneath.

[0] These are theoretically not a thing, as the associated standard was never approved, but they are widely implemented, for example in Linux.
26 changes: 26 additions & 0 deletions posix1e_acl-formula/_modules/posix1e_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Extension module helping with managing POSIX.1e style ACLs

Copyright (C) 2024 Georg Pfuetzenreuter <mail+opensuse@georg-pfuetzenreuter.net>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from pyacl import acl

def getfacl(path):
return acl.parse_acl_from_path(path)

def setfacl(path, aclargs):
return acl.update_acl_on_path(acl.build_acl(**aclargs), path) is None
93 changes: 93 additions & 0 deletions posix1e_acl-formula/_states/posix1e_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
State module for managing POSIX.1e style ACLs

Copyright (C) 2024 Georg Pfuetzenreuter <mail+opensuse@georg-pfuetzenreuter.net>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from salt import exceptions

def present(name, acl_type, acl_name, permissions={}, path=None):
ret = {'name': name, 'result': False, 'changes': {'old': {}, 'new': {}}, 'comment': ''}
is_test = __opts__['test']

if path is not None:
name = path

valid_types = ['user', 'group']
if acl_type not in valid_types:
raise exceptions.SaltInvocationError(f'Argument "acl_type" must be one of {valid_types}.')

if not isinstance(permissions, dict):
raise exceptions.SaltInvocationError('Argument "perms" must be a dictionary.')

for p in ['read', 'write', 'execute']:
if p not in permissions:
permissions.update({p: False})

have_aclmap = __salt__['posix1e_acl.getfacl'](name).get(acl_type, {})
changes = ret['changes']

found_changes = False

if acl_name in have_aclmap:
have_aclmap = have_aclmap.get(acl_name, {})

for permission, value_have in have_aclmap.items():
value_want = permissions[permission]
if value_have != value_want:
found_changes = True
changes['new'].update({permission: value_want})
changes['old'].update({permission: value_have})

else:
found_changes = True

msg = f'for {acl_type} "{acl_name}" on {name}'

if found_changes and not ( changes['new'] and changes['old'] ):
changes['new'] = permissions
del changes['old']

#if is_test:
# ret['comment'] = f'Would have created ACL {msg}.'
# ret['result'] = None

# return ret

if not found_changes:
del changes['new']
del changes['old']

if not changes:
ret['comment'] = f'ACL {msg} is already in the right state.'
ret['result'] = True

return ret

if changes and is_test:
ret['comment'] = f'Would have changed ACL {msg}.'
ret['result'] = None

return ret

ret['result'] = __salt__['posix1e_acl.setfacl'](name, {'target_name': acl_name, 'target_type': acl_type, **permissions})

if ret['result']:
ret['comment'] = f'Changed ACL {msg}.'
else:
ret['comment'] = f'Failed to change ACL {msg}.'

return ret
5 changes: 5 additions & 0 deletions posix1e_acl-formula/metadata/metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
summary:
Salt helpers for POSIX.1e ACLs
description:
Formula containing modules to help with managing POSIX.1e style ACLs.
13 changes: 13 additions & 0 deletions posix1e_acl-formula/pillar.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
acl:
# file or directory to set ACLs on
/tmp/test:
group:
myusers:
read: True
# possible permissions are "read", "write", and "execute"
# if one is not specified, it defaults to False
user:
myadmin:
read: True
write: True
execute: True
34 changes: 34 additions & 0 deletions posix1e_acl-formula/posix1e_acl/init.sls
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
posix1e_packages:
pkg.installed:
- pkgs:
{#- the zypper pkg modules fail to resolve capabilities, in our Salt we have a custom grain to work around this limitation #}
{%- if grains['osfullname'] != 'Leap' and 'system_python' in grains %}
- {{ grains['system_python'] }}-pyacl
{#- needs to be kept up to date if the system interpreter changes on Tumbleweed .. #}
{%- elif grains['osfullname'] == 'openSUSE Tumbleweed' %}
- python311-pyacl
{#- Leap #}
{%- else %}
- python3-pyacl
{%- endif %}
- reload_modules: True

{%- set mypillar = pillar.get('acl', {}) %}
{%- if mypillar %}
posix1e_acls:
posix1e_acl.present:
- names:
{%- for path, aclmap in mypillar.items() %}
{%- for acl_type in ['user', 'group'] %}
{%- if acl_type in aclmap %}
{%- for acl_name, permissions in aclmap[acl_type].items() %}
- {{ path }}_{{ acl_type }}_{{ acl_name }}:
- acl_type: {{ acl_type }}
- acl_name: {{ acl_name }}
- permissions: {{ permissions }}
- path: {{ path }}
{%- endfor %} {#- close aclmap loop #}
{%- endif %} {#- close acl_type check #}
{%- endfor %} {#- close acl_type loop #}
{%- endfor %} {#- close pillar loop #}
{%- endif %} {#- close pillar check #}
43 changes: 43 additions & 0 deletions posix1e_acl-formula/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Helpers for testing the posix1e_acl formula
Copyright (C) 2024 Georg Pfuetzenreuter <mail+opensuse@georg-pfuetzenreuter.net>

This program is free software: you can redminetribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from json import loads
from yaml import safe_load
import pytest

def salt(host, command):
result = host.run(f'salt-call --local --out json {command}')
output = loads(result.stdout)['local']
return output, result.stderr, result.rc

@pytest.fixture
def pillar(request):
pillar = {'acl': {}}
if hasattr(request, 'param'):
print(request.param)
pillar['acl'].update(request.param)
return pillar

@pytest.fixture
def salt_apply(host, pillar, test):
print(f'sa pillar: {pillar}')
print(f'sa test: {test}')
host.run('touch /tmp/posix1e_formula_acl_test{1,2,3}') # temporary file through pytest on remote host possible? needs to align with pillar.
yield salt(host, f'state.apply posix1e_acl pillar="{pillar}" test={test}')
#host.run('zypper -n rm -u python*-pyacl') # needs to be installed to evaluate test mode ...
host.run('rm -f /tmp/posix1e_formula_acl_test*')
71 changes: 71 additions & 0 deletions posix1e_acl-formula/tests/test_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Test suite for assessing the posix1e_acl formula functionality
Copyright (C) 2024 Georg Pfuetzenreuter <mail+opensuse@georg-pfuetzenreuter.net>

This program is free software: you can redminetribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from pytest import mark

@mark.parametrize(
'pillar', [
({'/tmp/posix1e_formula_acl_test1': {'user': {'georg': {'read': True}}}}),
({'/tmp/posix1e_formula_acl_test2': {'group': {'georg': {'read': True, 'write': True}}}}),
({'/tmp/posix1e_formula_acl_test3': {'user': {'georg': {'read': True}}, 'group': {'georg': {'read': True, 'write': True}}}}),
],
indirect=['pillar']
)
@mark.parametrize('test', [True, False])
def test_acl(host, pillar, salt_apply, test):
result = salt_apply

assert len(result) > 0
assert result[2] == 0

output = result[0]
state_pkg = 'pkg_|-posix1e_packages_|-posix1e_packages_|-installed'

assert state_pkg in output
assert output[state_pkg].get('result') is True

file = list(pillar['acl'].keys())[0]
acl_types = list(pillar['acl'][file].keys())

for acl_type in acl_types:

acl_names = list(pillar['acl'][file][acl_type].keys())
for acl_name in acl_names:
state_acl_name = f'{file}_{acl_type}_{acl_name}'
state_acl = f'posix1e_acl_|-posix1e_acls_|-{state_acl_name}_|-present'
state_acl_out = output[state_acl]

assert state_acl_out.get('name') == state_acl_name

expected = {}
expected['changes'] = {'new': pillar['acl'][file][acl_type][acl_name]}

for permission in ['read', 'write', 'execute']:
if permission not in expected['changes']['new']:
expected['changes']['new'][permission] = False

if test:
expected['result'] = None
expected['comment'] = f'Would have changed ACL for {acl_type} "{acl_name}" on {file}.'

else:
expected['result'] = True
expected['comment'] = f'Changed ACL for {acl_type} "{acl_name}" on {file}.'

for return_key, return_value in expected.items():
assert state_acl_out.get(return_key) == return_value