From a79931b8bb1bf1840e8964545f5bd41f59e3da35 Mon Sep 17 00:00:00 2001
From: transcaffeine <transcaffeine@finally.coffee>
Date: Sat, 3 May 2025 23:48:16 +0200
Subject: [PATCH] feat(openldap): add ansible role for deployment

---
 galaxy.yml                                 |  1 +
 playbooks/openldap.yml                     |  7 +++
 roles/openldap/README.md                   |  3 +
 roles/openldap/defaults/main/container.yml | 61 +++++++++++++++++++++
 roles/openldap/defaults/main/main.yml      | 20 +++++++
 roles/openldap/defaults/main/openldap.yml  | 53 ++++++++++++++++++
 roles/openldap/tasks/configure.yml         |  0
 roles/openldap/tasks/deploy-docker.yml     | 25 +++++++++
 roles/openldap/tasks/initialize-docker.yml | 37 +++++++++++++
 roles/openldap/tasks/initialize.yml        | 47 ++++++++++++++++
 roles/openldap/tasks/main.yml              | 26 +++++++++
 roles/openldap/tasks/prepare-docker.yml    |  7 +++
 roles/openldap/templates/slapd.ldif.j2     | 64 ++++++++++++++++++++++
 roles/openldap/vars/main.yml               |  6 ++
 14 files changed, 357 insertions(+)
 create mode 100644 playbooks/openldap.yml
 create mode 100644 roles/openldap/README.md
 create mode 100644 roles/openldap/defaults/main/container.yml
 create mode 100644 roles/openldap/defaults/main/main.yml
 create mode 100644 roles/openldap/defaults/main/openldap.yml
 create mode 100644 roles/openldap/tasks/configure.yml
 create mode 100644 roles/openldap/tasks/deploy-docker.yml
 create mode 100644 roles/openldap/tasks/initialize-docker.yml
 create mode 100644 roles/openldap/tasks/initialize.yml
 create mode 100644 roles/openldap/tasks/main.yml
 create mode 100644 roles/openldap/tasks/prepare-docker.yml
 create mode 100644 roles/openldap/templates/slapd.ldif.j2
 create mode 100644 roles/openldap/vars/main.yml

diff --git a/galaxy.yml b/galaxy.yml
index ae7193a..26e1d92 100644
--- a/galaxy.yml
+++ b/galaxy.yml
@@ -25,3 +25,4 @@ tags:
   - nginx
   - restic
   - user_management
+  - openldap
diff --git a/playbooks/openldap.yml b/playbooks/openldap.yml
new file mode 100644
index 0000000..40f25f7
--- /dev/null
+++ b/playbooks/openldap.yml
@@ -0,0 +1,7 @@
+---
+- name: Deploy and configure openLDAP
+  hosts: "{{ openldap_hosts | default('openldap', true) }}"
+  become: "{{ openldap_become | default(true) }}"
+  gather_facts: "{{ openldap_playbook_gather_facts | default(false) }}"
+  roles:
+    - role: finallycoffee.base.openldap
diff --git a/roles/openldap/README.md b/roles/openldap/README.md
new file mode 100644
index 0000000..85ad817
--- /dev/null
+++ b/roles/openldap/README.md
@@ -0,0 +1,3 @@
+# `finallycoffee.base.openldap` ansible role
+
+Deploy and configure [OpenLDAP](https://www.openldap.org/).
diff --git a/roles/openldap/defaults/main/container.yml b/roles/openldap/defaults/main/container.yml
new file mode 100644
index 0000000..419513f
--- /dev/null
+++ b/roles/openldap/defaults/main/container.yml
@@ -0,0 +1,61 @@
+---
+openldap_container_name: "openldap"
+openldap_container_image_registry: docker.finally.coffee
+openldap_container_image_namespace: containers
+openldap_container_image_name: "openldap"
+openldap_container_image_tag: ~
+openldap_container_image_source: "pull"
+openldap_container_image_force_source: >-2
+  {{ openldap_container_image_tag | default(false, true) }}
+openldap_container_image_repository: >-2
+  {{
+    [
+      openldap_container_image_registry | default([], true),
+      openldap_container_image_namespace | default([], true),
+      openldap_container_image_name
+    ] | flatten | join('/')
+  }}
+openldap_container_image: >-2
+  {{
+    [
+      openldap_container_image_repository,
+      openldap_container_image_tag
+        | default(openldap_alpine_package_version, true),
+    ] | join(':')
+  }}
+openldap_container_env: ~
+openldap_container_user: ~
+openldap_container_ports: ~
+openldap_container_labels: ~
+openldap_container_volumes: ~
+openldap_container_networks: ~
+openldap_container_network_mode: ~
+openldap_container_dns_servers: ~
+openldap_container_etc_hosts: ~
+openldap_container_ulimits:
+  - "nofile:{{ openldap_fd_soft_limit }}:{{ openldap_fd_hard_limit }}"
+openldap_container_memory: "256M"
+openldap_container_memory_swap: ~
+openldap_container_memory_reservation: "128M"
+openldap_container_restart_policy: "on-failure"
+openldap_container_state: >-2
+  {{ (openldap_state == 'present') | ternary('started', 'absent') }}
+
+openldap_container_data_path: "{{ openldap_data_path }}"
+openldap_container_config_path: "{{ openldap_config_path }}"
+openldap_container_socket_path: "{{ openldap_socket_path }}"
+openldap_container_base_volumes:
+  - "{{ openldap_config_path }}:{{ openldap_container_config_path }}:Z"
+  - "{{ openldap_data_path }}:{{ openldap_container_data_path }}:rw"
+  - "{{ openldap_socket_path }}:{{ openldap_container_socket_path }}:rw"
+openldap_container_all_volumes: >-2
+  {{ openldap_container_base_volumes | default([], true)
+    + openldap_container_volumes | default([], true) }}
+openldap_init_container_volumes:
+  - "{{ [openldap_slapd_path, openldap_slapd_path, 'ro'] | join(':') }}"
+
+openldap_container_healthcheck:
+  test: >-2
+    [[ $(netstat -plnte | grep slapd | wc -l) -ge 1 ]]
+    && [[ $(ps aux | grep slapd | wc -l) -ge 1 ]]
+    || exit 1
diff --git a/roles/openldap/defaults/main/main.yml b/roles/openldap/defaults/main/main.yml
new file mode 100644
index 0000000..28bc8f7
--- /dev/null
+++ b/roles/openldap/defaults/main/main.yml
@@ -0,0 +1,20 @@
+---
+openldap_version: "2.6.8"
+openldap_alpine_revision: "0"
+openldap_alpine_package_version: >-2
+  v{{ openldap_version }}-r{{ openldap_alpine_revision | string }}
+
+openldap_domain: ~
+
+openldap_config_path: "/etc/openldap/"
+openldap_olc_path: "{{ openldap_config_path }}/{0}config"
+openldap_slapd_path: "{{ openldap_config_path }}/slapd.ldif"
+openldap_schema_path: "{{ openldap_config_path }}/schema"
+openldap_data_path: "/var/lib/openldap"
+openldap_socket_path: "/run/openldap"
+
+openldap_state: "present"
+openldap_deployment_method: "docker"
+
+openldap_slapadd_init_command: >-2
+  slapadd -v -F {{ openldap_olc_path }} -n 0 -l {{ openldap_slapd_path }}
diff --git a/roles/openldap/defaults/main/openldap.yml b/roles/openldap/defaults/main/openldap.yml
new file mode 100644
index 0000000..89aa4d4
--- /dev/null
+++ b/roles/openldap/defaults/main/openldap.yml
@@ -0,0 +1,53 @@
+---
+openldap_dn: >-2
+  dc={{ openldap_domain | regex_replace('\\.', ',dc=') }}
+openldap_root_username: "admin"
+openldap_root_pw: ~
+
+openldap_fd_soft_limit: "8192"
+openldap_fd_hard_limit: "8192"
+
+openldap_module_path: "/usr/lib/openldap"
+openldap_modules:
+  - "mdb"
+  - "hdb"
+
+openldap_core_schema_path: "{{ openldap_schema_path }}/core.ldif"
+openldap_enabled_schemas:
+  - name: "cosine"
+  - name: "inetorgperson"
+openldap_additional_schemas: []
+openldap_schemas: >-2
+  {{ openldap_enabled_schemas + openldap_additional_schemas }}
+
+openldap_config_db_olc_access: >-2
+  to *
+    by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
+    by * none
+openldap_config_db_attributes:
+  olcAccess: "{{ openldap_config_db_olc_access }}"
+
+openldap_default_indices:
+  - "objectClass eq"
+openldap_indices: []
+
+openldap_default_database_name: "mdb"
+openldap_default_database_object_class: "olcMdbConfig"
+openldap_default_database_suffix: "{{ openldap_dn }}"
+openldap_default_database_root_dn: >-2
+  cn={{ openldap_root_username }},{{ openldap_default_database_suffix }}
+openldap_default_database_root_pw: "{{ openldap_root_pw }}"
+openldap_default_database_directory: >-2
+  {{ openldap_data_path }}/{{ openldap_default_database_name }}
+openldap_default_database_indices: >-2
+  {{ openldap_default_indices + openldap_indices }}
+openldap_default_database:
+  name: "{{ openldap_default_database_name }}"
+  object_class: "{{ openldap_default_database_object_class }}"
+  suffix: "{{ openldap_default_database_suffix }}"
+  root_dn: "{{ openldap_default_database_root_dn }}"
+  root_pw: "{{ openldap_default_database_root_pw }}"
+  directory: "{{ openldap_default_database_directory }}"
+  indices: "{{ openldap_default_database_indices }}"
+openldap_databases:
+  - "{{ openldap_default_database }}" 
diff --git a/roles/openldap/tasks/configure.yml b/roles/openldap/tasks/configure.yml
new file mode 100644
index 0000000..e69de29
diff --git a/roles/openldap/tasks/deploy-docker.yml b/roles/openldap/tasks/deploy-docker.yml
new file mode 100644
index 0000000..1ef6c40
--- /dev/null
+++ b/roles/openldap/tasks/deploy-docker.yml
@@ -0,0 +1,25 @@
+---
+- name: Ensure container '{{ openldap_container_name }}' is {{ openldap_container_state }}
+  community.docker.docker_container:
+    name: "{{ openldap_container_name }}"
+    image: "{{ openldap_container_image }}"
+    env: "{{ openldap_container_env | default(omit, true) }}"
+    user: "{{ openldap_container_user | default(omit, true) }}"
+    ports: "{{ openldap_container_ports | default(omit, true) }}"
+    labels: "{{ openldap_container_labels | default(omit, true) }}"
+    volumes: "{{ openldap_container_all_volumes | default(omit, true) }}"
+    networks: "{{ openldap_container_networks | default(omit, true) }}"
+    network_mode: "{{ openldap_container_network_mode | default(omit, true) }}"
+    dns_servers: "{{ openldap_container_dns_servers | default(omit, true) }}"
+    etc_hosts: "{{ openldap_container_etc_hosts | default(omit, true) }}"
+    command: "{{ openldap_container_command | default(omit, true) }}"
+    ulimits: "{{ openldap_container_ulimits | default(omit, true) }}"
+    memory: "{{ openldap_container_memory | default(omit, true) }}"
+    memory_swap: "{{ openldap_container_memory_swap | default(omit, true) }}"
+    memory_reservation: >-2
+      {{ openldap_container_memory_reservation | default(omit, true) }}
+    restart_policy: >-2
+      {{ openldap_container_restart_policy | default(omit, true) }}
+    healthcheck: "{{ openldap_container_healthcheck | default(omit, true) }}"
+    state: "{{ openldap_container_state }}"
+
diff --git a/roles/openldap/tasks/initialize-docker.yml b/roles/openldap/tasks/initialize-docker.yml
new file mode 100644
index 0000000..60aca1f
--- /dev/null
+++ b/roles/openldap/tasks/initialize-docker.yml
@@ -0,0 +1,37 @@
+---
+- name: Ensure additional schemas are mapped to container
+  ansible.builtin.set_fact:
+    openldap_init_container_volumes: >-2
+      {{ openldap_init_container_volumes + [ schema_mount ] }}
+  vars:
+    schema_file: "{{ openldap_schema_path }}/{{ schema.name }}.ldif"
+    schema_mount: >-2
+      {{ schema_file }}:{{ schema_file }}:ro
+  loop: "{{ openldap_additional_schemas }}"
+  loop_control:
+    loop_var: "schema"
+    label: "{{ schema.name }}"
+
+- name: Ensure ldap container is initialized
+  community.docker.docker_container:
+    name: "{{ openldap_container_name }}"
+    image: "{{ openldap_container_image }}"
+    env: "{{ openldap_container_env | default(omit, true) }}"
+    user: "{{ openldap_container_user | default(omit, true) }}"
+    ports: "{{ openldap_container_ports | default(omit, true) }}"
+    labels: "{{ openldap_container_labels | default(omit, true) }}"
+    volumes: "{{ openldap_container_merged_volumes | default(omit, true) }}"
+    networks: "{{ openldap_container_networks | default(omit, true) }}"
+    network_mode: "{{ openldap_container_network_mode | default(omit, true) }}"
+    dns_servers: "{{ openldap_container_dns_servers | default(omit, true) }}"
+    etc_hosts: "{{ openldap_container_etc_hosts | default(omit, true) }}"
+    memory: "{{ openldap_container_memory | default(omit, true) }}"
+    memory_swap: "{{ openldap_container_memory_swap | default(omit, true) }}"
+    memory_reservation: >-2
+      {{ openldap_container_memory_reservation | default(omit, true) }}
+    command: "{{ openldap_slapadd_init_command }}"
+    detach: false
+    cleanup: true
+  vars:
+    openldap_container_merged_volumes: >-2
+      {{ openldap_container_all_volumes + openldap_init_container_volumes }}
diff --git a/roles/openldap/tasks/initialize.yml b/roles/openldap/tasks/initialize.yml
new file mode 100644
index 0000000..b910d45
--- /dev/null
+++ b/roles/openldap/tasks/initialize.yml
@@ -0,0 +1,47 @@
+---
+- name: Determine if persisted OLC config exists
+  ansible.builtin.stat:
+    path: "{{ openldap_olc_path }}/cn=config"
+  register: openldap_olc_stat_info
+
+- name: Ensure openldap databases are initialized
+  when: not openldap_olc_stat_info.stat.exists
+  block:
+    - name: Ensure initial slapd.ldif is templated
+      ansible.builtin.template:
+        src: "slapd.ldif.j2"
+        dest: "{{ openldap_slapd_path }}"
+        mode: "0644"
+    - name: Ensure additional schemas to install are present
+      ansible.builtin.copy:
+        content: "{{ schema.content }}"
+        dest: "{{ openldap_schema_path }}/{{ schema.name }}.ldif"
+        mode: "0644"
+      loop: "{{ openldap_additional_schemas }}"
+      loop_control:
+        loop_var: "schema"
+        label: "{{ schema.name }}"
+    - name: Ensure db data directory exists
+      ansible.builtin.file:
+        path: "{{ openldap_default_database_directory }}"
+        state: directory
+        mode: "0750"
+    - name: Ensure container is initialized using {{ openldap_deployment_method }}
+      ansible.builtin.include_tasks:
+        file: "initialize-{{ openldap_deployment_method }}.yml"
+  rescue:
+    - name: Ensure temporary schema files are absent
+      ansible.builtin.file:
+        path: "{{ openldap_schema_path }}/{{ file.name }}.ldif"
+        state: absent
+      loop: >-2
+        {{ openldap_additional_schemas }}
+      loop_control:
+        loop_var: "file"
+        label: "{{ file.name }}"
+      ignore_errors: true
+    - name: Ensure intial slapd.ldif file is absent
+      ansible.builtin.file:
+        path: "{{ openldap_slapd_path }}"
+        state: absent
+      ignore_errors: true
diff --git a/roles/openldap/tasks/main.yml b/roles/openldap/tasks/main.yml
new file mode 100644
index 0000000..520ae35
--- /dev/null
+++ b/roles/openldap/tasks/main.yml
@@ -0,0 +1,26 @@
+---
+- name: Check if 'openldap_state' is valid
+  ansible.builtin.fail:
+    msg: >-2
+      Invalid state '{{ openldap_state }}'!
+      Supported states are {{ openldap_states | join(', ') }}.
+  when: openldap_state not in openldap_states
+
+- name: Check if 'openldap_deployment_method' is valid
+  ansible.builtin.fail:
+    msg: >-2
+      Invalid state '{{ openldap_deployment_method }}'!
+      Supported states are {{ openldap_deployment_methods | join(', ') }}.
+  when: openldap_deployment_method not in openldap_deployment_methods
+
+- name: Ensure openldap deployment is prepared
+  ansible.builtin.include_tasks:
+    file: "prepare-{{ openldap_deployment_method }}.yml"
+
+- name: Ensure openldap is configured
+  ansible.builtin.include_tasks:
+    file: "configure.yml"
+
+- name: Ensure openldap is deployed using {{ openldap_deployment_method }}
+  ansible.builtin.include_tasks:
+    file: "deploy-{{ openldap_deployment_method }}.yml"
diff --git a/roles/openldap/tasks/prepare-docker.yml b/roles/openldap/tasks/prepare-docker.yml
new file mode 100644
index 0000000..24ddd73
--- /dev/null
+++ b/roles/openldap/tasks/prepare-docker.yml
@@ -0,0 +1,7 @@
+---
+- name: Ensure container image '{{ openldap_container_image }}' is {{ openldap_state }}
+  community.docker.docker_image:
+    name: "{{ openldap_container_image }}"
+    state: "{{ openldap_state }}"
+    source: "{{ openldap_container_image_source }}"
+    force_source: "{{ openldap_container_image_force_source }}"
diff --git a/roles/openldap/templates/slapd.ldif.j2 b/roles/openldap/templates/slapd.ldif.j2
new file mode 100644
index 0000000..d038b73
--- /dev/null
+++ b/roles/openldap/templates/slapd.ldif.j2
@@ -0,0 +1,64 @@
+dn: cn=config
+objectClass: olcGlobal
+cn: config
+olcPidFile: /run/openldap/slapd.pid
+olcArgsFile: /run/openldap/slapd.args
+
+
+# Dynamic backend modules
+dn: cn=module,cn=config
+objectClass: olcModuleList
+cn: module
+olcModulepath: {{ openldap_module_path }}
+{% for mod in openldap_modules | default([]) %}
+olcModuleload: back_{{ mod }}.so
+{% endfor %}
+
+# Schema config
+dn: cn=schema,cn=config
+objectClass: olcSchemaConfig
+cn: schema
+
+include: file://{{ openldap_core_schema_path }}
+{% for schema in openldap_schemas %}
+include: file://{{ openldap_schema_path }}/{{ schema.name }}.ldif
+{% endfor %}
+
+# Frontend settings
+dn: olcDatabase=frontend,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcFrontendConfig
+olcDatabase: frontend
+
+
+# Config-DB settings
+dn: olcDatabase=config,cn=config
+objectClass: olcDatabaseConfig
+olcDatabase: config
+{% for attr in openldap_config_db_attributes | dict2items %}
+{% if attr is string %}
+{{ attr.key }}: {{ attr.value }}
+{% else %}
+{% for val in attr.value %}
+{{ attr.key }}: {{ val }}
+{% endfor %}
+{% endif %}
+{% endfor %}
+
+
+# database settings
+{% for db in openldap_databases %}
+dn: olcDatabase={{ db.name }},cn=config
+objectClass: olcDatabaseConfig
+objectClass: {{ db.object_class }}
+olcDatabase: {{ db.name }}
+olcSuffix: {{ db.suffix }}
+olcRootDN: {{ db.root_dn }}
+olcRootPW: {{ db.root_pw }}
+olcDbDirectory: {{ db.directory }}
+{% for idx in db.indices %}
+olcDbIndex: {{ idx }}
+{% endfor %}
+
+
+{% endfor %}
diff --git a/roles/openldap/vars/main.yml b/roles/openldap/vars/main.yml
new file mode 100644
index 0000000..d2e6bff
--- /dev/null
+++ b/roles/openldap/vars/main.yml
@@ -0,0 +1,6 @@
+---
+openldap_states:
+  - "present"
+  - "absent"
+openldap_deployment_methods:
+  - "docker"