mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Optimized RBAC via LDAP
This commit is contained in:
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
|
||||
|
Reference in New Issue
Block a user