mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-07-02 23:22:02 +02:00
Optimized LDAP. Implemented passwordchange, usernames etc.
This commit is contained in:
parent
ff2b402ea7
commit
3ce6e958b4
@ -49,7 +49,7 @@ class InventoryManager:
|
|||||||
target.setdefault("credentials", {})["database_password"] = self.generate_value("alphanumeric")
|
target.setdefault("credentials", {})["database_password"] = self.generate_value("alphanumeric")
|
||||||
if "oauth2" in data["features"] and \
|
if "oauth2" in data["features"] and \
|
||||||
data["features"]["oauth2"]:
|
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
|
# Apply recursion only for the `credentials` section
|
||||||
self.recurse_credentials(self.schema, target)
|
self.recurse_credentials(self.schema, target)
|
||||||
@ -148,8 +148,13 @@ class InventoryManager:
|
|||||||
if algorithm == "sha1":
|
if algorithm == "sha1":
|
||||||
return hashlib.sha1(secrets.token_bytes(20)).hexdigest()
|
return hashlib.sha1(secrets.token_bytes(20)).hexdigest()
|
||||||
if algorithm == "bcrypt":
|
if algorithm == "bcrypt":
|
||||||
|
# Generate a random password and hash it with bcrypt
|
||||||
pw = secrets.token_urlsafe(16).encode()
|
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":
|
if algorithm == "alphanumeric":
|
||||||
return self.generate_secure_alphanumeric(64)
|
return self.generate_secure_alphanumeric(64)
|
||||||
if algorithm == "base64_prefixed_32":
|
if algorithm == "base64_prefixed_32":
|
||||||
|
@ -8,12 +8,13 @@
|
|||||||
# @see https://en.wikipedia.org/wiki/OpenID_Connect
|
# @see https://en.wikipedia.org/wiki/OpenID_Connect
|
||||||
|
|
||||||
## Helper Variables:
|
## Helper Variables:
|
||||||
_oidc_client_realm: "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}"
|
_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_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:
|
defaults_oidc:
|
||||||
client:
|
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
|
# 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
|
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)
|
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
|
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
|
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)
|
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
|
button_text: "SSO Login ({{primary_domain | upper}})" # Default button text
|
||||||
attributes:
|
attributes:
|
||||||
# Attribut to identify the user
|
# Attribut to identify the user
|
||||||
|
@ -25,6 +25,10 @@
|
|||||||
|
|
||||||
- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml"
|
- 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}}"
|
- name: "create directory {{ldif_host_path}}{{item}}"
|
||||||
file:
|
file:
|
||||||
path: "{{ldif_host_path}}{{item}}"
|
path: "{{ldif_host_path}}{{item}}"
|
||||||
@ -59,13 +63,13 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
- name: Ensure LDAP users exist
|
- name: Ensure LDAP users exist
|
||||||
community.general.ldap_entry:
|
community.general.ldap_entry:
|
||||||
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.users }}"
|
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.users }}"
|
||||||
server_uri: "{{ ldap_server_uri }}"
|
server_uri: "{{ ldap_server_uri }}"
|
||||||
bind_dn: "{{ ldap.dn.administrator.data }}"
|
bind_dn: "{{ ldap.dn.administrator.data }}"
|
||||||
bind_pw: "{{ ldap.bind_credential }}"
|
bind_pw: "{{ ldap.bind_credential }}"
|
||||||
objectClass: "{{ ldap.user_objects.structural }}"
|
objectClass: "{{ ldap.user_objects.structural }}"
|
||||||
attributes:
|
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) }}"
|
sn: "{{ item.value.sn | default(item.key) }}"
|
||||||
cn: "{{ item.value.cn | default(item.key) }}"
|
cn: "{{ item.value.cn | default(item.key) }}"
|
||||||
userPassword: "{SSHA}{{ item.value.password }}"
|
userPassword: "{SSHA}{{ item.value.password }}"
|
||||||
|
57
roles/docker-ldap/tasks/reset_admin_passwords.yml
Normal file
57
roles/docker-ldap/tasks/reset_admin_passwords.yml
Normal 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
|
@ -1,3 +1,6 @@
|
|||||||
titel: "Mobilizon on {{ primary_domain | upper }}"
|
titel: "Mobilizon on {{ primary_domain | upper }}"
|
||||||
images:
|
images:
|
||||||
mobilizon: "docker.io/framasoft/mobilizon"
|
mobilizon: "docker.io/framasoft/mobilizon"
|
||||||
|
features:
|
||||||
|
central_database: true
|
||||||
|
oidc: true
|
@ -78,7 +78,7 @@ class TestInventoryManager(unittest.TestCase):
|
|||||||
InventoryManager(role_dir, self.tmpdir / "inventory.yml", "pw", {}).load_application_id(role_dir)
|
InventoryManager(role_dir, self.tmpdir / "inventory.yml", "pw", {}).load_application_id(role_dir)
|
||||||
|
|
||||||
def test_generate_value_algorithms(self):
|
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
|
# Bypass __init__ to avoid YAML loading
|
||||||
im = InventoryManager.__new__(InventoryManager)
|
im = InventoryManager.__new__(InventoryManager)
|
||||||
|
|
||||||
@ -86,27 +86,40 @@ class TestInventoryManager(unittest.TestCase):
|
|||||||
hex_val = im.generate_value("random_hex")
|
hex_val = im.generate_value("random_hex")
|
||||||
self.assertEqual(len(hex_val), 128)
|
self.assertEqual(len(hex_val), 128)
|
||||||
self.assertTrue(all(c in "0123456789abcdef" for c in hex_val))
|
self.assertTrue(all(c in "0123456789abcdef" for c in hex_val))
|
||||||
|
self.assertNotIn('$', hex_val) # no dollar sign
|
||||||
|
|
||||||
# sha256 → 64 hex chars
|
# sha256 → 64 hex chars
|
||||||
sha256_val = im.generate_value("sha256")
|
sha256_val = im.generate_value("sha256")
|
||||||
self.assertEqual(len(sha256_val), 64)
|
self.assertEqual(len(sha256_val), 64)
|
||||||
|
self.assertNotIn('$', sha256_val) # no dollar sign
|
||||||
|
|
||||||
# sha1 → 40 hex chars
|
# sha1 → 40 hex chars
|
||||||
sha1_val = im.generate_value("sha1")
|
sha1_val = im.generate_value("sha1")
|
||||||
self.assertEqual(len(sha1_val), 40)
|
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")
|
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
|
# alphanumeric → 64 chars
|
||||||
alnum = im.generate_value("alphanumeric")
|
alnum = im.generate_value("alphanumeric")
|
||||||
self.assertEqual(len(alnum), 64)
|
self.assertEqual(len(alnum), 64)
|
||||||
self.assertTrue(alnum.isalnum())
|
self.assertTrue(alnum.isalnum())
|
||||||
|
self.assertNotIn('$', alnum) # no dollar sign
|
||||||
|
|
||||||
# base64_prefixed_32 → starts with "base64:"
|
# base64_prefixed_32 → starts with "base64:"
|
||||||
b64 = im.generate_value("base64_prefixed_32")
|
b64 = im.generate_value("base64_prefixed_32")
|
||||||
self.assertTrue(b64.startswith("base64:"))
|
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):
|
def test_apply_schema_and_recurse(self):
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user