mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-07-05 08:23:08 +02:00
Optimized RBAC via LDAP
This commit is contained in:
parent
a9f55579a2
commit
ee0561db72
@ -7,26 +7,44 @@ import glob
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def represent_str(dumper, data):
|
||||
"""
|
||||
Custom YAML string representer that forces double quotes around any string
|
||||
containing a Jinja2 placeholder ({{ ... }}).
|
||||
"""
|
||||
if isinstance(data, str) and '{{' in data:
|
||||
return dumper.represent_scalar(
|
||||
'tag:yaml.org,2002:str',
|
||||
data,
|
||||
style='"'
|
||||
)
|
||||
return dumper.represent_scalar(
|
||||
'tag:yaml.org,2002:str',
|
||||
data
|
||||
)
|
||||
|
||||
|
||||
def build_users(defs, primary_domain, start_id, become_pwd):
|
||||
"""
|
||||
Build user entries with auto-incremented uid/gid, default username/email, and optional description.
|
||||
Construct user entries with auto-incremented UID/GID, default username/email,
|
||||
and optional description.
|
||||
|
||||
Args:
|
||||
defs (OrderedDict): Keys are user IDs, values are dicts with optional overrides.
|
||||
primary_domain (str): e.g., 'example.com'.
|
||||
start_id (int): Starting uid/gid (e.g., 1001).
|
||||
become_pwd (str): Password string for all users.
|
||||
defs (OrderedDict): Mapping of user keys to their override settings.
|
||||
primary_domain (str): The primary domain for email addresses (e.g. 'example.com').
|
||||
start_id (int): Starting number for UID/GID allocation (e.g. 1001).
|
||||
become_pwd (str): Default password string for users without an override.
|
||||
|
||||
Returns:
|
||||
OrderedDict: Merged user definitions with full fields.
|
||||
OrderedDict: Complete user definitions with all required fields filled in.
|
||||
|
||||
Raises:
|
||||
ValueError: If duplicate override uids/gids or conflicts in generated values.
|
||||
ValueError: If there are duplicate UIDs, usernames, or emails.
|
||||
"""
|
||||
users = OrderedDict()
|
||||
used_uids = set()
|
||||
|
||||
# Pre-collect any provided uids/gids and check for duplicates
|
||||
|
||||
# Collect any preset UIDs to avoid collisions
|
||||
for key, overrides in defs.items():
|
||||
if 'uid' in overrides:
|
||||
uid = overrides['uid']
|
||||
@ -34,79 +52,76 @@ def build_users(defs, primary_domain, start_id, become_pwd):
|
||||
raise ValueError(f"Duplicate uid {uid} for user '{key}'")
|
||||
used_uids.add(uid)
|
||||
|
||||
next_free = start_id
|
||||
def allocate_free_id():
|
||||
nonlocal next_free
|
||||
# find next free id not in used_uids
|
||||
while next_free in used_uids:
|
||||
next_free += 1
|
||||
free = next_free
|
||||
used_uids.add(free)
|
||||
next_free += 1
|
||||
return free
|
||||
next_uid = start_id
|
||||
def allocate_uid():
|
||||
nonlocal next_uid
|
||||
# Find the next free UID not already used
|
||||
while next_uid in used_uids:
|
||||
next_uid += 1
|
||||
free_uid = next_uid
|
||||
used_uids.add(free_uid)
|
||||
next_uid += 1
|
||||
return free_uid
|
||||
|
||||
# Build entries
|
||||
# Build each user entry
|
||||
for key, overrides in defs.items():
|
||||
username = overrides.get('username', key)
|
||||
email = overrides.get('email', f"{username}@{primary_domain}")
|
||||
description = overrides.get('description')
|
||||
roles = overrides.get('roles',[])
|
||||
password = overrides.get('password',become_pwd)
|
||||
# UID assignment
|
||||
roles = overrides.get('roles', [])
|
||||
password = overrides.get('password', become_pwd)
|
||||
|
||||
# Determine UID and GID
|
||||
if 'uid' in overrides:
|
||||
uid = overrides['uid']
|
||||
else:
|
||||
uid = allocate_free_id()
|
||||
gid = overrides.get('gid',uid)
|
||||
uid = allocate_uid()
|
||||
gid = overrides.get('gid', uid)
|
||||
|
||||
entry = {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'uid': uid,
|
||||
'gid': gid,
|
||||
'roles': roles
|
||||
'uid': uid,
|
||||
'gid': gid,
|
||||
'roles': roles
|
||||
}
|
||||
if description is not None:
|
||||
entry['description'] = description
|
||||
|
||||
users[key] = entry
|
||||
|
||||
# Validate uniqueness of username, email, and gid
|
||||
# Ensure uniqueness of usernames and emails
|
||||
seen_usernames = set()
|
||||
seen_emails = set()
|
||||
seen_gids = set()
|
||||
|
||||
for key, entry in users.items():
|
||||
un = entry['username']
|
||||
em = entry['email']
|
||||
gd = entry['gid']
|
||||
if un in seen_usernames:
|
||||
raise ValueError(f"Duplicate username '{un}' in merged users")
|
||||
if em in seen_emails:
|
||||
raise ValueError(f"Duplicate email '{em}' in merged users")
|
||||
seen_usernames.add(un)
|
||||
seen_emails.add(em)
|
||||
seen_gids.add(gd)
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def load_user_defs(roles_dir):
|
||||
def load_user_defs(roles_directory):
|
||||
"""
|
||||
Scan all roles/*/meta/users.yml files and extract 'users:' sections.
|
||||
|
||||
Raises an exception if conflicting definitions are found.
|
||||
Scan all roles/*/meta/users.yml files and merge any 'users:' sections.
|
||||
|
||||
Args:
|
||||
roles_dir (str): Path to the directory containing role subdirectories.
|
||||
roles_directory (str): Path to the directory containing role subdirectories.
|
||||
|
||||
Returns:
|
||||
OrderedDict: Merged user definitions.
|
||||
OrderedDict: Merged user definitions from all roles.
|
||||
|
||||
Raises:
|
||||
ValueError: On invalid format or conflicting field values.
|
||||
ValueError: On invalid format or conflicting override values.
|
||||
"""
|
||||
pattern = os.path.join(roles_dir, '*/meta/users.yml')
|
||||
pattern = os.path.join(roles_directory, '*/meta/users.yml')
|
||||
files = sorted(glob.glob(pattern))
|
||||
merged = OrderedDict()
|
||||
|
||||
@ -128,8 +143,7 @@ def load_user_defs(roles_dir):
|
||||
for field, value in overrides.items():
|
||||
if field in existing and existing[field] != value:
|
||||
raise ValueError(
|
||||
f"Conflict for user '{key}': field '{field}' has existing value "
|
||||
f"'{existing[field]}', tried to set '{value}' in {filepath}"
|
||||
f"Conflict for user '{key}': field '{field}' has existing value '{existing[field]}', tried to set '{value}' in {filepath}"
|
||||
)
|
||||
existing.update(overrides)
|
||||
|
||||
@ -138,7 +152,7 @@ def load_user_defs(roles_dir):
|
||||
|
||||
def dictify(data):
|
||||
"""
|
||||
Recursively convert OrderedDict to regular dict before YAML dump.
|
||||
Recursively convert OrderedDict to regular dict for YAML dumping.
|
||||
"""
|
||||
if isinstance(data, OrderedDict):
|
||||
return {k: dictify(v) for k, v in data.items()}
|
||||
@ -151,7 +165,7 @@ def dictify(data):
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate a users.yml by merging all roles/*/meta/users.yml users sections.'
|
||||
description='Generate a users.yml by merging all roles/*/meta/users.yml definitions.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--roles-dir', '-r', required=True,
|
||||
@ -163,7 +177,7 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
'--start-id', '-s', type=int, default=1001,
|
||||
help='Starting uid/gid number (default: 1001).'
|
||||
help='Starting UID/GID number (default: 1001).'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--extra-users', '-e',
|
||||
@ -179,42 +193,41 @@ def main():
|
||||
become_pwd = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
|
||||
|
||||
try:
|
||||
user_defs = load_user_defs(args.roles_dir)
|
||||
definitions = load_user_defs(args.roles_dir)
|
||||
except ValueError as e:
|
||||
print(f"Error merging user definitions: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Add extra users if any
|
||||
# Add extra users if specified
|
||||
if args.extra_users:
|
||||
for name in args.extra_users.split(','):
|
||||
user = name.strip()
|
||||
if not user:
|
||||
user_key = name.strip()
|
||||
if not user_key:
|
||||
continue
|
||||
if user in user_defs:
|
||||
print(f"Warning: extra user '{user}' already defined; skipping.", file=sys.stderr)
|
||||
if user_key in definitions:
|
||||
print(f"Warning: extra user '{user_key}' already defined; skipping.", file=sys.stderr)
|
||||
else:
|
||||
user_defs[user] = {}
|
||||
definitions[user_key] = {}
|
||||
|
||||
try:
|
||||
users = build_users(
|
||||
defs=user_defs,
|
||||
primary_domain=primary_domain,
|
||||
start_id=args.start_id,
|
||||
become_pwd=become_pwd
|
||||
definitions,
|
||||
primary_domain,
|
||||
args.start_id,
|
||||
become_pwd
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"Error building user entries: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Convert OrderedDict into plain dict for YAML
|
||||
default_users = {'default_users': users}
|
||||
plain_data = dictify(default_users)
|
||||
|
||||
# Ensure strings are represented without Python-specific tags
|
||||
yaml.SafeDumper.add_representer(
|
||||
str,
|
||||
lambda dumper, data: dumper.represent_scalar('tag:yaml.org,2002:str', data)
|
||||
)
|
||||
# Register custom string representer
|
||||
yaml.SafeDumper.add_representer(str, represent_str)
|
||||
|
||||
# Dump the YAML file
|
||||
with open(args.output, 'w') as f:
|
||||
yaml.safe_dump(
|
||||
plain_data,
|
||||
|
@ -89,4 +89,9 @@ ldap:
|
||||
filters:
|
||||
users:
|
||||
login: "(&{{ _ldap_filters_users_all }}({{_ldap_user_id}}=%{{_ldap_user_id}}))"
|
||||
all: "{{ _ldap_filters_users_all }}"
|
||||
all: "{{ _ldap_filters_users_all }}"
|
||||
rbac:
|
||||
flavors:
|
||||
# Valid values posixGroup, groupOfNames
|
||||
- groupOfNames
|
||||
# - posixGroup
|
||||
|
6
main.py
6
main.py
@ -147,9 +147,13 @@ if __name__ == "__main__":
|
||||
|
||||
log_file = None
|
||||
if log_enabled:
|
||||
log_file_path = os.path.join(script_dir, 'logfile.log')
|
||||
log_dir = os.path.join(script_dir, 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
|
||||
log_file_path = os.path.join(log_dir, f'{timestamp}.log')
|
||||
log_file = open(log_file_path, 'a', encoding='utf-8')
|
||||
|
||||
|
||||
try:
|
||||
if log_enabled:
|
||||
# Use a pseudo-terminal to preserve color formatting
|
||||
|
0
roles/__init__.py
Normal file
0
roles/__init__.py
Normal file
@ -1,7 +1,8 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
||||
username: "administrator"
|
||||
contact:
|
||||
description: "General contact account"
|
||||
username: "contact"
|
||||
mailu_token_enabled: true
|
||||
roles:
|
||||
- mail-bot
|
@ -24,4 +24,6 @@ csp:
|
||||
- https://s.espocrm.com/
|
||||
domains:
|
||||
aliases:
|
||||
- "crm.{{ primary_domain }}"
|
||||
- "crm.{{ primary_domain }}"
|
||||
email:
|
||||
from_name: "Customer Relationship Management ({{ primary_domain }})"
|
@ -1,5 +1,3 @@
|
||||
application_id: "espocrm"
|
||||
# EspoCRM uses MySQL/MariaDB
|
||||
database_type: "mariadb"
|
||||
email:
|
||||
from_name: "Customer Relationship Management ({{ primary_domain }})"
|
||||
database_type: "mariadb"
|
0
roles/docker-ldap/__init__.py
Normal file
0
roles/docker-ldap/__init__.py
Normal file
@ -46,10 +46,23 @@ 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"
|
||||
|
||||
# Works
|
||||
docker exec -it ldap \
|
||||
ldapdelete -x \
|
||||
-D "$LDAP_ADMIN_DN" \
|
||||
-w "$LDAP_ADMIN_PASSWORD" \
|
||||
-r \
|
||||
"ou=groups,dc=veen,dc=world"
|
||||
"ou=groups,$LDAP_ROOT"
|
||||
```
|
||||
|
||||
## Import RBAC
|
||||
```bash
|
||||
docker exec -i ldap \
|
||||
ldapadd -x \
|
||||
-D "$LDAP_ADMIN_DN" \
|
||||
-w "$LDAP_ADMIN_PASSWORD" \
|
||||
-c \
|
||||
-f "/tmp/ldif/data/01_rbac_roles.ldif"
|
||||
```
|
0
roles/docker-ldap/filter_plugins/__init__.py
Normal file
0
roles/docker-ldap/filter_plugins/__init__.py
Normal file
64
roles/docker-ldap/filter_plugins/build_ldap_role_entries.py
Normal file
64
roles/docker-ldap/filter_plugins/build_ldap_role_entries.py
Normal file
@ -0,0 +1,64 @@
|
||||
def build_ldap_role_entries(applications, users, ldap):
|
||||
"""
|
||||
Builds structured LDAP role entries using the global `ldap` configuration.
|
||||
Supports objectClasses: posixGroup (adds gidNumber, memberUid), groupOfNames (adds member).
|
||||
"""
|
||||
|
||||
result = {}
|
||||
|
||||
for application_id, application_config in applications.items():
|
||||
base_roles = application_config.get("rbac", {}).get("roles", {})
|
||||
roles = {
|
||||
**base_roles,
|
||||
"administrator": {
|
||||
"description": "Has full administrative access: manage themes, plugins, settings, and users"
|
||||
}
|
||||
}
|
||||
|
||||
group_id = application_config.get("group_id")
|
||||
user_dn_base = ldap["dn"]["ou"]["users"]
|
||||
ldap_user_attr = ldap["attributes"]["user_id"]
|
||||
role_dn_base = ldap["dn"]["ou"]["roles"]
|
||||
flavors = ldap.get("rbac", {}).get("flavors", [])
|
||||
|
||||
for role_name, role_conf in roles.items():
|
||||
group_cn = f"{application_id}-{role_name}"
|
||||
dn = f"cn={group_cn},{role_dn_base}"
|
||||
|
||||
entry = {
|
||||
"dn": dn,
|
||||
"cn": group_cn,
|
||||
"description": role_conf.get("description", ""),
|
||||
"objectClass": ["top"] + flavors,
|
||||
}
|
||||
|
||||
# Initialize member lists
|
||||
member_dns = []
|
||||
member_uids = []
|
||||
|
||||
for username, user_config in users.items():
|
||||
if role_name in user_config.get("roles", []):
|
||||
user_dn = f"{ldap_user_attr}={username},{user_dn_base}"
|
||||
member_dns.append(user_dn)
|
||||
member_uids.append(username)
|
||||
|
||||
# Add gidNumber for posixGroup
|
||||
if "posixGroup" in flavors:
|
||||
entry["gidNumber"] = group_id
|
||||
if member_uids:
|
||||
entry["memberUid"] = member_uids
|
||||
|
||||
# Add members for groupOfNames
|
||||
if "groupOfNames" in flavors and member_dns:
|
||||
entry["member"] = member_dns
|
||||
|
||||
result[dn] = entry
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
"build_ldap_role_entries": build_ldap_role_entries
|
||||
}
|
@ -52,4 +52,4 @@
|
||||
listen:
|
||||
- "Import data LDIF files"
|
||||
- "Import all LDIF files"
|
||||
loop: "{{ lookup('fileglob', role_path ~ '/templates/ldif/data/*.j2', wantlist=True) }}"
|
||||
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/data/*.j2') | sort }}"
|
@ -21,6 +21,8 @@
|
||||
attributes:
|
||||
objectClass: "{{ missing_auxiliary }}"
|
||||
state: present
|
||||
async: 60
|
||||
poll: 0
|
||||
loop: "{{ ldap_users_with_classes.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.dn }}"
|
||||
|
@ -1,9 +1,11 @@
|
||||
# In own task file for easier looping
|
||||
|
||||
- name: "Create LDIF files at {{ ldif_host_path }}{{ folder }}"
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "{{ ldif_host_path }}{{ folder }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
|
||||
mode: '770'
|
||||
loop: "{{ lookup('fileglob', role_path ~ '/templates/ldif/' ~ folder ~ '/*.j2', wantlist=True) }}"
|
||||
loop: >-
|
||||
{{
|
||||
lookup('fileglob', role_path ~ '/templates/ldif/' ~ folder ~ '/*.j2', wantlist=True)
|
||||
| sort
|
||||
}}
|
||||
notify: "Import {{ folder }} LDIF files"
|
||||
|
@ -78,6 +78,8 @@
|
||||
uidNumber: "{{ item.value.uid | int }}"
|
||||
gidNumber: "{{ item.value.gid | int }}"
|
||||
state: present # ↳ creates but never updates
|
||||
async: 60
|
||||
poll: 0
|
||||
loop: "{{ users | dict2items }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
@ -95,6 +97,8 @@
|
||||
objectClass: "{{ ldap.user_objects.structural }}"
|
||||
mail: "{{ item.value.email }}"
|
||||
state: exact
|
||||
async: 60
|
||||
poll: 0
|
||||
loop: "{{ users | dict2items }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
|
@ -1,30 +0,0 @@
|
||||
{%- 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'
|
||||
}
|
||||
})
|
||||
%}
|
||||
|
||||
{%- 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 }}
|
||||
|
||||
{%- 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 %}
|
23
roles/docker-ldap/templates/ldif/data/01_rbac.ldif.j2
Normal file
23
roles/docker-ldap/templates/ldif/data/01_rbac.ldif.j2
Normal file
@ -0,0 +1,23 @@
|
||||
{% for dn, entry in (applications | build_ldap_role_entries(users, ldap)).items() %}
|
||||
|
||||
dn: {{ dn }}
|
||||
{% for oc in entry.objectClass %}
|
||||
objectClass: {{ oc }}
|
||||
{% endfor %}
|
||||
{% if entry.gidNumber is defined %}
|
||||
gidNumber: {{ entry.gidNumber }}
|
||||
{% endif %}
|
||||
cn: {{ entry.cn }}
|
||||
description: {{ entry.description }}
|
||||
{% if entry.memberUid is defined %}
|
||||
{% for uid in entry.memberUid %}
|
||||
memberUid: {{ uid }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if entry.member is defined %}
|
||||
{% for m in entry.member %}
|
||||
member: {{ m }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
@ -3,7 +3,9 @@ users:
|
||||
username: "administrator"
|
||||
bounce:
|
||||
username: "bounce"
|
||||
mailu_token_enabled: true
|
||||
roles:
|
||||
- mail-bot
|
||||
newsletter:
|
||||
username: "newsletter"
|
||||
mailu_token_enabled: true
|
||||
roles:
|
||||
- mail-bot
|
@ -12,6 +12,7 @@
|
||||
"Duplicate entry" not in mailu_user_result.stderr
|
||||
)
|
||||
changed_when: mailu_user_result.rc == 0
|
||||
when: "'mail-bot' in item.value.roles or 'administrator' in item.value.roles"
|
||||
|
||||
- name: "Change password for user '{{ mailu_user_key }};{{ mailu_user_name }}@{{ mailu_domain }}'"
|
||||
command: >
|
||||
@ -19,7 +20,8 @@
|
||||
{{ mailu_user_name }} {{ mailu_domain }} '{{ mailu_password }}'
|
||||
args:
|
||||
chdir: "{{ mailu_compose_dir }}"
|
||||
when: "'mail-bot' in item.value.roles or 'administrator' in item.value.roles"
|
||||
|
||||
- name: "Create Mailu API Token for {{ mailu_user_name }}"
|
||||
include_tasks: create-mailu-token.yml
|
||||
when: mailu_token_enabled
|
||||
when: "{{ 'mail-bot' in item.value.roles }}"
|
@ -42,7 +42,6 @@
|
||||
mailu_user_key: "{{ item.key }}"
|
||||
mailu_user_name: "{{ item.value.username }}"
|
||||
mailu_password: "{{ item.value.password }}"
|
||||
mailu_token_enabled: "{{ item.value.mailu_token_enabled | default(false)}}"
|
||||
mailu_token_ip: "{{ item.value.ip | default('') }}"
|
||||
loop: "{{ users | dict2items }}"
|
||||
loop_control:
|
||||
|
@ -22,4 +22,8 @@ csp:
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
rbac:
|
||||
roles:
|
||||
mail-bot:
|
||||
description: "Has an token to send and recieve emails"
|
||||
|
@ -3,4 +3,5 @@ users:
|
||||
username: "administrator"
|
||||
no-reply:
|
||||
username: "no-reply"
|
||||
mailu_token_enabled: true
|
||||
roles:
|
||||
- mail-bot
|
@ -46,7 +46,7 @@ accounts:
|
||||
iframe: {{ applications | is_feature_enabled('portfolio_iframe','pixelfed') }}
|
||||
{% endif %}
|
||||
{% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %}
|
||||
- name: Peertube
|
||||
- name: Videos
|
||||
description: Discover {{ 'our' if service_provider.type == 'legal' else 'my' }} videos on Peertube.
|
||||
icon:
|
||||
class: fa-solid fa-video
|
||||
|
0
tests/unit/roles/docker-ldap/__init__.py
Normal file
0
tests/unit/roles/docker-ldap/__init__.py
Normal file
96
tests/unit/roles/docker-ldap/test_build_ldap_role_entries.py
Normal file
96
tests/unit/roles/docker-ldap/test_build_ldap_role_entries.py
Normal file
@ -0,0 +1,96 @@
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
import importlib.util
|
||||
|
||||
# Dynamisch den Filter-Plugin Pfad hinzufügen
|
||||
current_dir = os.path.dirname(__file__)
|
||||
filter_plugin_path = os.path.abspath(os.path.join(current_dir, "../../../../roles/docker-ldap/filter_plugins"))
|
||||
|
||||
# Modul dynamisch laden
|
||||
spec = importlib.util.spec_from_file_location("build_ldap_role_entries", os.path.join(filter_plugin_path, "build_ldap_role_entries.py"))
|
||||
ble_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ble_module)
|
||||
|
||||
build_ldap_role_entries = ble_module.build_ldap_role_entries
|
||||
|
||||
|
||||
class TestBuildLdapRoleEntries(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.applications = {
|
||||
"app1": {
|
||||
"group_id": 10000,
|
||||
"rbac": {
|
||||
"roles": {
|
||||
"editor": {"description": "Can edit content"},
|
||||
"viewer": {"description": "Can view content"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.users = {
|
||||
"alice": {
|
||||
"roles": ["editor", "administrator"]
|
||||
},
|
||||
"bob": {
|
||||
"roles": ["viewer"]
|
||||
},
|
||||
"carol": {
|
||||
"roles": []
|
||||
}
|
||||
}
|
||||
|
||||
self.ldap = {
|
||||
"dn": {
|
||||
"ou": {
|
||||
"users": "ou=users,dc=example,dc=org",
|
||||
"roles": "ou=roles,dc=example,dc=org"
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"user_id": "uid"
|
||||
},
|
||||
"rbac": {
|
||||
"flavors": ["posixGroup", "groupOfNames"]
|
||||
}
|
||||
}
|
||||
|
||||
def test_entries_structure(self):
|
||||
entries = build_ldap_role_entries(self.applications, self.users, self.ldap)
|
||||
expected_dns = {
|
||||
"cn=app1-editor,ou=roles,dc=example,dc=org",
|
||||
"cn=app1-viewer,ou=roles,dc=example,dc=org",
|
||||
"cn=app1-administrator,ou=roles,dc=example,dc=org"
|
||||
}
|
||||
self.assertEqual(set(entries.keys()), expected_dns)
|
||||
|
||||
def test_posix_group_members(self):
|
||||
entries = build_ldap_role_entries(self.applications, self.users, self.ldap)
|
||||
editor = entries["cn=app1-editor,ou=roles,dc=example,dc=org"]
|
||||
self.assertEqual(editor["gidNumber"], 10000)
|
||||
self.assertIn("memberUid", editor)
|
||||
self.assertIn("alice", editor["memberUid"])
|
||||
|
||||
def test_group_of_names_members(self):
|
||||
entries = build_ldap_role_entries(self.applications, self.users, self.ldap)
|
||||
viewer = entries["cn=app1-viewer,ou=roles,dc=example,dc=org"]
|
||||
expected_dn = "uid=bob,ou=users,dc=example,dc=org"
|
||||
self.assertIn("member", viewer)
|
||||
self.assertIn(expected_dn, viewer["member"])
|
||||
|
||||
def test_administrator_auto_included(self):
|
||||
entries = build_ldap_role_entries(self.applications, self.users, self.ldap)
|
||||
admin = entries["cn=app1-administrator,ou=roles,dc=example,dc=org"]
|
||||
self.assertEqual(admin["description"], "Has full administrative access: manage themes, plugins, settings, and users")
|
||||
self.assertIn("alice", admin.get("memberUid", []))
|
||||
|
||||
def test_empty_roles_are_skipped(self):
|
||||
entries = build_ldap_role_entries(self.applications, self.users, self.ldap)
|
||||
for entry in entries.values():
|
||||
if entry["cn"].endswith("-viewer"):
|
||||
self.assertNotIn("carol", entry.get("memberUid", []))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user