Optimized LDAP. Implemented passwordchange, usernames etc.

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-01 16:45:03 +02:00
parent ff2b402ea7
commit 3ce6e958b4
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
6 changed files with 99 additions and 15 deletions

View File

@ -49,7 +49,7 @@ class InventoryManager:
target.setdefault("credentials", {})["database_password"] = self.generate_value("alphanumeric")
if "oauth2" in data["features"] and \
data["features"]["oauth2"]:
target.setdefault("credentials", {})["oauth2"] = self.generate_value("random_hex_16")
target.setdefault("credentials", {})["oauth2_proxy_cookie_secret"] = self.generate_value("random_hex_16")
# Apply recursion only for the `credentials` section
self.recurse_credentials(self.schema, target)
@ -148,8 +148,13 @@ class InventoryManager:
if algorithm == "sha1":
return hashlib.sha1(secrets.token_bytes(20)).hexdigest()
if algorithm == "bcrypt":
# Generate a random password and hash it with bcrypt
pw = secrets.token_urlsafe(16).encode()
return bcrypt.hashpw(pw, bcrypt.gensalt()).decode()
raw_hash = bcrypt.hashpw(pw, bcrypt.gensalt()).decode()
# Replace every '$' with a random lowercase alphanumeric character
alnum = string.digits + string.ascii_lowercase
escaped = "".join(secrets.choice(alnum) if ch == '$' else ch for ch in raw_hash)
return escaped
if algorithm == "alphanumeric":
return self.generate_secure_alphanumeric(64)
if algorithm == "base64_prefixed_32":

View File

@ -8,12 +8,13 @@
# @see https://en.wikipedia.org/wiki/OpenID_Connect
## Helper Variables:
_oidc_client_realm: "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}"
_oidc_client_issuer_url: "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/realms/{{_oidc_client_realm}}"
_oidc_client_realm: "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}"
_oidc_client_issuer_url: "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/realms/{{_oidc_client_realm}}"
_oidc_client_id: "{{ oidc.client.id if oidc.client is defined and oidc.client.id is defined else primary_domain }}"
defaults_oidc:
client:
id: "{{primary_domain}}" # Client identifier, typically matching your primary domain
id: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain
# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters
realm: "{{_oidc_client_realm}}" # The realm to which the client belongs in the OIDC provider
issuer_url: "{{_oidc_client_issuer_url}}" # Base URL of the OIDC provider (issuer)
@ -24,6 +25,7 @@ defaults_oidc:
logout_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/logout" # Endpoint to log out the user
change_credentials: "{{_oidc_client_issuer_url}}account/account-security/signing-in" # URL for managing or changing user credentials
certs: "{{_oidc_client_issuer_url}}/protocol/openid-connect/certs" # JSON Web Key Set (JWKS)
reset_credentials: "{{_oidc_client_issuer_url}}/login-actions/reset-credentials?client_id={{ _oidc_client_id }}" # Password reset url
button_text: "SSO Login ({{primary_domain | upper}})" # Default button text
attributes:
# Attribut to identify the user

View File

@ -25,6 +25,10 @@
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
- name: "Reset LDAP admin passwords"
include_tasks: reset_admin_passwords.yml
when: applications[application_id].network.local
- name: "create directory {{ldif_host_path}}{{item}}"
file:
path: "{{ldif_host_path}}{{item}}"
@ -59,13 +63,13 @@
###############################################################################
- name: Ensure LDAP users exist
community.general.ldap_entry:
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.users }}"
server_uri: "{{ ldap_server_uri }}"
bind_dn: "{{ ldap.dn.administrator.data }}"
bind_pw: "{{ ldap.bind_credential }}"
objectClass: "{{ ldap.user_objects.structural }}"
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.users }}"
server_uri: "{{ ldap_server_uri }}"
bind_dn: "{{ ldap.dn.administrator.data }}"
bind_pw: "{{ ldap.bind_credential }}"
objectClass: "{{ ldap.user_objects.structural }}"
attributes:
uid: "{{ item.key }}" # {{ ldap.attributes.user_id }} can't be used as key here, dynamic key generation isn't possible
uid: "{{ item.value.username }}"
sn: "{{ item.value.sn | default(item.key) }}"
cn: "{{ item.value.cn | default(item.key) }}"
userPassword: "{SSHA}{{ item.value.password }}"

View File

@ -0,0 +1,57 @@
---
# Reset both Database and Configuration Admin passwords in LDAP via LDAPI
# roles/docker-ldap/tasks/reset_admin_passwords.yml
- name: "Query available LDAP databases"
shell: |
docker exec {{ applications[application_id].hostname }} \
ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config "(olcDatabase=*)" dn
register: ldap_databases
- name: "Determine data backend DN (mdb)"
set_fact:
data_backend_dn: >-
{{ ldap_databases.stdout_lines
| select('search','^dn: olcDatabase=.*mdb')
| map('regex_replace','^dn: ','')
| list
| first }}
- name: "Determine config backend DN"
set_fact:
config_backend_dn: >-
{{ ldap_databases.stdout_lines
| select('search','^dn: olcDatabase=\{[0-9]+\}config,cn=config$')
| map('regex_replace','^dn: ','')
| list
| first }}
- name: "Generate hash for Database Admin password"
shell: |
docker exec {{ applications[application_id].hostname }} \
slappasswd -s "{{ ldap.bind_credential }}"
register: database_admin_pw_hash
- name: "Reset Database Admin password in LDAP (olcRootPW)"
shell: |
docker exec -i {{ applications[application_id].hostname }} ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
dn: {{ data_backend_dn }}
changetype: modify
replace: olcRootPW
olcRootPW: {{ database_admin_pw_hash.stdout }}
EOF
- name: "Generate hash for Configuration Admin password"
shell: |
docker exec {{ applications[application_id].hostname }} \
slappasswd -s "{{ applications[application_id].credentials.administrator_password }}"
register: config_admin_pw_hash
- name: "Reset Configuration Admin password in LDAP (olcRootPW)"
shell: |
docker exec -i {{ applications[application_id].hostname }} ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
dn: {{ config_backend_dn }}
changetype: modify
replace: olcRootPW
olcRootPW: {{ config_admin_pw_hash.stdout }}
EOF

View File

@ -1,3 +1,6 @@
titel: "Mobilizon on {{ primary_domain | upper }}"
images:
mobilizon: "docker.io/framasoft/mobilizon"
features:
central_database: true
oidc: true

View File

@ -78,7 +78,7 @@ class TestInventoryManager(unittest.TestCase):
InventoryManager(role_dir, self.tmpdir / "inventory.yml", "pw", {}).load_application_id(role_dir)
def test_generate_value_algorithms(self):
"""Verify generate_value produces outputs of the expected form."""
"""Verify generate_value produces outputs of the expected form and contains no dollar signs."""
# Bypass __init__ to avoid YAML loading
im = InventoryManager.__new__(InventoryManager)
@ -86,27 +86,40 @@ class TestInventoryManager(unittest.TestCase):
hex_val = im.generate_value("random_hex")
self.assertEqual(len(hex_val), 128)
self.assertTrue(all(c in "0123456789abcdef" for c in hex_val))
self.assertNotIn('$', hex_val) # no dollar sign
# sha256 → 64 hex chars
sha256_val = im.generate_value("sha256")
self.assertEqual(len(sha256_val), 64)
self.assertNotIn('$', sha256_val) # no dollar sign
# sha1 → 40 hex chars
sha1_val = im.generate_value("sha1")
self.assertEqual(len(sha1_val), 40)
self.assertNotIn('$', sha1_val) # no dollar sign
# bcrypt → starts with bcrypt prefix
# bcrypt → should *not* start with '$2' after escaping, and contain no '$'
bcrypt_val = im.generate_value("bcrypt")
self.assertTrue(bcrypt_val.startswith("$2"))
self.assertFalse(bcrypt_val.startswith("$2"))
self.assertNotIn('$', bcrypt_val) # no dollar sign
# alphanumeric → 64 chars
alnum = im.generate_value("alphanumeric")
self.assertEqual(len(alnum), 64)
self.assertTrue(alnum.isalnum())
self.assertNotIn('$', alnum) # no dollar sign
# base64_prefixed_32 → starts with "base64:"
b64 = im.generate_value("base64_prefixed_32")
self.assertTrue(b64.startswith("base64:"))
self.assertNotIn('$', b64) # no dollar sign
# random_hex_16 → 32 hex chars
hex16 = im.generate_value("random_hex_16")
self.assertEqual(len(hex16), 32)
self.assertTrue(all(c in "0123456789abcdef" for c in hex16))
self.assertNotIn('$', hex16) # no dollar sign
def test_apply_schema_and_recurse(self):
"""