diff --git a/Makefile b/Makefile index 66a85870..1db4cffe 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,5 @@ install: build @echo "⚙️ Install complete." test: - @echo "🧪 Running Unit Tests..." - python -m unittest discover -s tests/unit - @echo "🔬 Running Integration Tests..." - python -m unittest discover -s tests/integration \ No newline at end of file + @echo "🧪 Running Tests..." + python -m unittest discover -s tests \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cli/generate_applications.py b/cli/generate_applications.py index 4693e5bb..8a8ac97c 100644 --- a/cli/generate_applications.py +++ b/cli/generate_applications.py @@ -6,6 +6,11 @@ import yaml import sys from pathlib import Path +plugin_path = Path(__file__).resolve().parent / ".." / "lookup_plugins" +sys.path.insert(0, str(plugin_path)) + +from application_gid import LookupModule + def load_yaml_file(path): """Load a YAML file if it exists, otherwise return an empty dict.""" if not path.exists(): @@ -37,6 +42,7 @@ def main(): # Initialize result structure result = {"defaults_applications": {}} + gid_lookup = LookupModule() # Process each role for application configs for role_dir in sorted(roles_dir.iterdir()): role_name = role_dir.name @@ -67,6 +73,12 @@ def main(): config_data = load_yaml_file(config_file) if config_data: + try: + gid_number = gid_lookup.run([application_id], roles_dir=str(roles_dir))[0] + except Exception as e: + print(f"Warning: failed to determine gid for '{application_id}': {e}", file=sys.stderr) + sys.exit(1) + config_data["group_id"] = gid_number result["defaults_applications"][application_id] = config_data users_meta_file = role_dir / "meta" / "users.yml" transformed_users = {} diff --git a/lookup_plugins/application_gid.py b/lookup_plugins/application_gid.py new file mode 100644 index 00000000..2f7f5066 --- /dev/null +++ b/lookup_plugins/application_gid.py @@ -0,0 +1,42 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import yaml + +from ansible.plugins.lookup import LookupBase +from ansible.errors import AnsibleError + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + application_id = terms[0] + base_gid = kwargs.get('base_gid', 10000) + roles_dir = kwargs.get('roles_dir', 'roles') + + if not os.path.isdir(roles_dir): + raise AnsibleError(f"Roles directory '{roles_dir}' not found") + + matched_roles = [] + + for root, dirs, files in os.walk(roles_dir): + if os.path.basename(root) == "vars" and "main.yml" in files: + vars_path = os.path.join(root, "main.yml") + try: + with open(vars_path, 'r') as f: + data = yaml.safe_load(f) or {} + app_id = data.get('application_id') + if app_id: + matched_roles.append((app_id, vars_path)) + except Exception as e: + raise AnsibleError(f"Error parsing {vars_path}: {e}") + + # sort alphabetically by application_id + sorted_ids = sorted(app_id for app_id, _ in matched_roles) + + try: + index = sorted_ids.index(application_id) + except ValueError: + raise AnsibleError(f"Application ID '{application_id}' not found in any role") + + return [base_gid + index] diff --git a/roles/docker-ldap/docs/Administration.md b/roles/docker-ldap/docs/Administration.md index a5d94288..44140661 100644 --- a/roles/docker-ldap/docs/Administration.md +++ b/roles/docker-ldap/docs/Administration.md @@ -46,6 +46,10 @@ docker exec -it ldap bash -c "ldapsearch -LLL -o ldif-wrap=no -x -D \"\$LDAP_ADM ### Delete Groups and Subgroup To delete the group inclusive all subgroups use: ```bash -docker exec -it ldap bash -c "ldapsearch -LLL -o ldif-wrap=no -x -D \"\$LDAP_ADMIN_DN\" -w \"\$LDAP_ADMIN_PASSWORD\" -b \"ou=applications,ou=groups,\$LDAP_ROOT\" dn | sed -n 's/^dn: //p' | tac | while read -r dn; do echo \"Deleting \$dn\"; ldapdelete -x -D \"\$LDAP_ADMIN_DN\" -w \"\$LDAP_ADMIN_PASSWORD\" \"\$dn\"; done" - +docker exec -it ldap \ + ldapdelete -x \ + -D "$LDAP_ADMIN_DN" \ + -w "$LDAP_ADMIN_PASSWORD" \ + -r \ + "ou=groups,dc=veen,dc=world" ``` \ No newline at end of file diff --git a/roles/docker-ldap/templates/ldif/data/01_application_roles.ldif.j2 b/roles/docker-ldap/templates/ldif/data/01_application_roles.ldif.j2 index fb8dc432..1c5e4cc0 100644 --- a/roles/docker-ldap/templates/ldif/data/01_application_roles.ldif.j2 +++ b/roles/docker-ldap/templates/ldif/data/01_application_roles.ldif.j2 @@ -1,34 +1,30 @@ -{% for application_id, application_config in applications.items() %} - - {# 1. Build up roles dict, defaulting to {} if rbac oder roles fehlt, then ensure administrator immer dabei ist #} - {% set base_roles = application_config.rbac.roles | default({}) %} - {% set roles = base_roles | combine({ - 'administrator': { - 'description': 'Has full administrative access: manage themes, plugins, settings, and users' - } - }) +{%- for application_id, application_config in applications.items() %} + {%- set base_roles = application_config.rbac.roles | default({}) %} + {%- set roles = base_roles | combine({ + 'administrator': { + 'description': 'Has full administrative access: manage themes, plugins, settings, and users' + } + }) %} - {# 2. Emit role definitions #} - {% for role_name, role_conf in roles.items() %} + {%- for role_name, role_conf in roles.items() %} dn: cn={{ application_id }}-{{ role_name }},{{ ldap.dn.ou.roles }} objectClass: top objectClass: organizationalRole +objectClass: posixGroup +gidNumber: {{ application_config['group_id'] }} cn: {{ application_id }}-{{ role_name }} description: {{ role_conf.description }} - {# 3. Assign only if user has that role #} - {% for username, user_config in users.items() %} - {% set user_roles = user_config.roles | default([]) %} - {% if role_name in user_roles %} + {%- for username, user_config in users.items() %} + {%- set user_roles = user_config.roles | default([]) %} + {%- if role_name in user_roles %} dn: cn={{ application_id }}-{{ role_name }},{{ ldap.dn.ou.roles }} changetype: modify add: roleOccupant roleOccupant: {{ ldap.attributes.user_id }}={{ username }},{{ ldap.dn.ou.users }} - {% endif %} - {% endfor %} - - {% endfor %} - -{% endfor %} + {%- endif %} + {%- endfor %} + {%- endfor %} +{%- endfor %} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_application_gid.py b/tests/unit/test_application_gid.py new file mode 100644 index 00000000..532b4efe --- /dev/null +++ b/tests/unit/test_application_gid.py @@ -0,0 +1,62 @@ +import os +import sys +import tempfile +import shutil +import unittest +import yaml + +dir_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../lookup_plugins') +) +sys.path.insert(0, dir_path) + +from application_gid import LookupModule + + +class TestApplicationGidLookup(unittest.TestCase): + + def setUp(self): + # Create a temporary roles directory + self.temp_dir = tempfile.mkdtemp() + self.roles_dir = os.path.join(self.temp_dir, "roles") + os.mkdir(self.roles_dir) + + # Define mock application_ids + self.applications = { + "nextcloud": "docker-nextcloud", + "moodle": "docker-moodle", + "wordpress": "docker-wordpress", + "taiga": "docker-taiga" + } + + # Create fake role dirs and vars/main.yml + for app_id, dirname in self.applications.items(): + role_path = os.path.join(self.roles_dir, dirname, "vars") + os.makedirs(role_path) + with open(os.path.join(role_path, "main.yml"), "w") as f: + yaml.dump({"application_id": app_id}, f) + + self.lookup = LookupModule() + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_gid_lookup(self): + # The sorted application_ids: [moodle, nextcloud, taiga, wordpress] + expected_order = ["moodle", "nextcloud", "taiga", "wordpress"] + for i, app_id in enumerate(expected_order): + result = self.lookup.run([app_id], roles_dir=self.roles_dir) + self.assertEqual(result, [10000 + i]) + + def test_custom_base_gid(self): + result = self.lookup.run(["taiga"], roles_dir=self.roles_dir, base_gid=20000) + self.assertEqual(result, [20002]) # 2nd index in sorted list + + def test_application_id_not_found(self): + with self.assertRaises(Exception) as context: + self.lookup.run(["unknownapp"], roles_dir=self.roles_dir) + self.assertIn("Application ID 'unknownapp' not found", str(context.exception)) + + +if __name__ == '__main__': + unittest.main()