Imported sources.
This commit is contained in:
205
bootstrap/init.py
Normal file
205
bootstrap/init.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
import csv
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
LDAPI_SOCKET = "/var/run/slapd/ldapi"
|
||||
LDIF_DIR = Path("/bootstrap/ldif")
|
||||
CSV_DIR = Path("/bootstrap/accounts")
|
||||
INITIALIZED_FLAG = Path("/var/lib/ldap/.initialized")
|
||||
SLAPD_D = Path("/etc/ldap/slapd.d")
|
||||
|
||||
|
||||
base_dn = os.environ["LDAP_BASE_DN"]
|
||||
password = os.environ["LDAP_PASSWORD"]
|
||||
tls_enabled = os.environ.get("TLS_ENABLED") == "1"
|
||||
admin_dn = f"cn=admin,{base_dn}"
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return subprocess.check_output(
|
||||
["slappasswd", "-h", "{SSHA}", "-s", password],
|
||||
text=True,
|
||||
).strip()
|
||||
|
||||
|
||||
def run_command(cmd: list, input: str) -> None:
|
||||
proc = subprocess.run(cmd, input=input, text=True, capture_output=True, check=False)
|
||||
if proc.stdout:
|
||||
print(proc.stdout, end="")
|
||||
if proc.stderr:
|
||||
print(proc.stderr, end="")
|
||||
if proc.returncode != 0:
|
||||
raise subprocess.CalledProcessError(proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr)
|
||||
|
||||
|
||||
def apply_ldif(path: Path, env: Environment, command: str = "ldapadd", **ctx) -> None:
|
||||
print(f"Applying LDIF: {path.name}")
|
||||
rendered = env.get_template(path.name).render(**ctx)
|
||||
run_command([command, "-Q", "-Y", "EXTERNAL", "-H", "ldapi:///"], rendered)
|
||||
print(f"Finished LDIF: {path.name}")
|
||||
|
||||
|
||||
def wait_for_slapd(timeout: int = 30) -> None:
|
||||
for _ in range(timeout):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(LDAPI_SOCKET)
|
||||
sock.close()
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(1)
|
||||
raise RuntimeError("slapd did not become ready in time")
|
||||
|
||||
|
||||
def stop_slapd(proc: subprocess.Popen, timeout: int = 30) -> None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
def group_exists(name: str, group_ou: str) -> bool:
|
||||
# fmt: off
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ldapsearch", "-Q", "-Y", "EXTERNAL", "-H", "ldapi:///",
|
||||
"-b", f"ou={group_ou},{base_dn}", "-s", "one",
|
||||
f"(&(objectClass=groupOfNames)(cn={name}))",
|
||||
"cn",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
# fmt: on
|
||||
return "numEntries: 1" in result.stdout
|
||||
|
||||
|
||||
def read_users(path: Path, fields: list[str]) -> list[dict]:
|
||||
users = []
|
||||
with open(path, newline="") as f:
|
||||
for row in csv.reader(f):
|
||||
if not row or row[0].strip().startswith("#"):
|
||||
continue
|
||||
users.append(dict(zip(fields, (c.strip() for c in row))))
|
||||
return users
|
||||
|
||||
|
||||
def main():
|
||||
print("First run — starting initialisation...")
|
||||
env = Environment(loader=FileSystemLoader(str(LDIF_DIR)), keep_trailing_newline=True)
|
||||
|
||||
# fmt: off
|
||||
proc = subprocess.Popen([
|
||||
"slapd",
|
||||
"-d", "0",
|
||||
"-F", str(SLAPD_D),
|
||||
"-u", "openldap",
|
||||
"-g", "openldap",
|
||||
"-h", "ldapi:///",
|
||||
])
|
||||
# fmt: on
|
||||
|
||||
wait_for_slapd()
|
||||
print("slapd is ready.")
|
||||
|
||||
apply_ldif(LDIF_DIR / "config-memberof-module.ldif", env)
|
||||
apply_ldif(LDIF_DIR / "config-memberof-overlay.ldif", env)
|
||||
apply_ldif(LDIF_DIR / "config-indexes.ldif", env)
|
||||
apply_ldif(LDIF_DIR / "config-performance.ldif", env)
|
||||
apply_ldif(LDIF_DIR / "config-acl.ldif", env, base_dn=base_dn, admin_dn=admin_dn)
|
||||
if tls_enabled:
|
||||
apply_ldif(LDIF_DIR / "config-tls.ldif", env)
|
||||
|
||||
print("cn=config updated.")
|
||||
|
||||
password_hash = hash_password(password)
|
||||
|
||||
ctx = dict(
|
||||
base_dn=base_dn,
|
||||
password_hash=password_hash,
|
||||
)
|
||||
apply_ldif(LDIF_DIR / "base.ldif", env, **ctx)
|
||||
apply_ldif(LDIF_DIR / "service-accounts.ldif", env, **ctx)
|
||||
|
||||
# Process optional account CSV files.
|
||||
user_files = {
|
||||
"users.csv": {
|
||||
"name": "users",
|
||||
"description": "Regular users",
|
||||
"fields": ["uid", "gn", "sn", "mail"],
|
||||
"group_ou": "groups",
|
||||
"member_ou": "users",
|
||||
},
|
||||
"admins.csv": {
|
||||
"name": "admins",
|
||||
"description": "Administrators",
|
||||
"fields": ["uid", "gn", "sn", "mail"],
|
||||
"group_ou": "privileged-groups",
|
||||
"member_ou": "users",
|
||||
},
|
||||
"posix-users.csv": {
|
||||
"name": "users",
|
||||
"description": "POSIX users",
|
||||
"fields": ["uid", "gn", "sn", "mail", "uidNumber", "gidNumber"],
|
||||
"group_ou": "groups",
|
||||
"member_ou": "users",
|
||||
},
|
||||
}
|
||||
|
||||
for filename, group in user_files.items():
|
||||
path = CSV_DIR / filename
|
||||
if not path.exists():
|
||||
# Backward-compatible fallback for older bind-mount layout.
|
||||
path = Path("/bootstrap") / filename
|
||||
if path.exists():
|
||||
print(f' Reading {group["name"]} from {filename}...')
|
||||
users = read_users(path, group["fields"])
|
||||
if users:
|
||||
ldif = Path(filename).stem + ".ldif"
|
||||
for user in users:
|
||||
print(f' Adding user: {user["uid"]}')
|
||||
apply_ldif(
|
||||
LDIF_DIR / ldif,
|
||||
env,
|
||||
base_dn=base_dn,
|
||||
password_hash=password_hash,
|
||||
**user,
|
||||
)
|
||||
|
||||
uids = [u["uid"] for u in users]
|
||||
if group_exists(group["name"], group["group_ou"]):
|
||||
apply_ldif(
|
||||
LDIF_DIR / "add-member.ldif",
|
||||
env,
|
||||
base_dn=base_dn,
|
||||
name=group["name"],
|
||||
group_ou=group["group_ou"],
|
||||
member_ou=group["member_ou"],
|
||||
uids=uids,
|
||||
)
|
||||
else:
|
||||
apply_ldif(
|
||||
LDIF_DIR / "group.ldif",
|
||||
env,
|
||||
base_dn=base_dn,
|
||||
name=group["name"],
|
||||
description=group["description"],
|
||||
group_ou=group["group_ou"],
|
||||
member_ou=group["member_ou"],
|
||||
uids=uids,
|
||||
)
|
||||
|
||||
INITIALIZED_FLAG.touch()
|
||||
stop_slapd(proc)
|
||||
print("Bootstrap complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
bootstrap/ldif/add-member.ldif
Normal file
6
bootstrap/ldif/add-member.ldif
Normal file
@@ -0,0 +1,6 @@
|
||||
dn: cn={{ name }},ou={{ group_ou }},{{ base_dn }}
|
||||
changeType: modify
|
||||
add: member
|
||||
{% for uid in uids -%}
|
||||
member: uid={{ uid }},ou={{ member_ou }},{{ base_dn }}
|
||||
{% endfor %}
|
||||
9
bootstrap/ldif/admins.ldif
Normal file
9
bootstrap/ldif/admins.ldif
Normal file
@@ -0,0 +1,9 @@
|
||||
dn: uid={{ uid }},ou=users,{{ base_dn }}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: shadowAccount
|
||||
uid: {{ uid }}
|
||||
givenName: {{ gn }}
|
||||
sn: {{ sn }}
|
||||
cn: {{ gn }} {{ sn }}
|
||||
mail: {{ mail }}
|
||||
userPassword: {{ password_hash }}
|
||||
19
bootstrap/ldif/base.ldif
Normal file
19
bootstrap/ldif/base.ldif
Normal file
@@ -0,0 +1,19 @@
|
||||
dn: ou=users,{{ base_dn }}
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
description: All users
|
||||
|
||||
dn: ou=groups,{{ base_dn }}
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
description: Regular groups
|
||||
|
||||
dn: ou=privileged-groups,{{ base_dn }}
|
||||
objectClass: organizationalUnit
|
||||
ou: privileged-groups
|
||||
description: Privileged groups
|
||||
|
||||
dn: ou=service-accounts,{{ base_dn }}
|
||||
objectClass: organizationalUnit
|
||||
ou: service-accounts
|
||||
description: Service accounts
|
||||
44
bootstrap/ldif/config-acl.ldif
Normal file
44
bootstrap/ldif/config-acl.ldif
Normal file
@@ -0,0 +1,44 @@
|
||||
dn: olcDatabase={-1}frontend,cn=config
|
||||
changetype: modify
|
||||
replace: olcAccess
|
||||
olcAccess: {0}to *
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
|
||||
by dn.exact="cn=admin,{{ base_dn }}" manage
|
||||
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
|
||||
by * break
|
||||
olcAccess: {1}to dn.exact=""
|
||||
by * read
|
||||
olcAccess: {2}to dn.base="cn=Subschema"
|
||||
by * read
|
||||
|
||||
dn: olcDatabase={0}config,cn=config
|
||||
changetype: modify
|
||||
replace: olcAccess
|
||||
olcAccess: {0}to *
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
|
||||
by dn.exact="cn=admin,{{ base_dn }}" manage
|
||||
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
|
||||
by * break
|
||||
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcAccess
|
||||
olcAccess: {0}to attrs=userPassword
|
||||
by self write
|
||||
by anonymous auth
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
|
||||
by dn.exact="{{ admin_dn }}" manage
|
||||
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
|
||||
by * none
|
||||
olcAccess: {1}to attrs=shadowLastChange
|
||||
by self write
|
||||
by * read
|
||||
olcAccess: {2}to dn.base=""
|
||||
by * read
|
||||
olcAccess: {3}to *
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
|
||||
by dn.exact="{{ admin_dn }}" manage
|
||||
by dn.exact="cn=readonly,ou=service-accounts,{{ base_dn }}" read
|
||||
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
|
||||
by self read
|
||||
by * none
|
||||
7
bootstrap/ldif/config-indexes.ldif
Normal file
7
bootstrap/ldif/config-indexes.ldif
Normal file
@@ -0,0 +1,7 @@
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
add: olcDbIndex
|
||||
olcDbIndex: mail eq,pres
|
||||
-
|
||||
add: olcDbIndex
|
||||
olcDbIndex: memberOf eq
|
||||
4
bootstrap/ldif/config-memberof-module.ldif
Normal file
4
bootstrap/ldif/config-memberof-module.ldif
Normal file
@@ -0,0 +1,4 @@
|
||||
dn: cn=module{0},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: memberof
|
||||
10
bootstrap/ldif/config-memberof-overlay.ldif
Normal file
10
bootstrap/ldif/config-memberof-overlay.ldif
Normal file
@@ -0,0 +1,10 @@
|
||||
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
|
||||
changetype: add
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: olcMemberOf
|
||||
olcOverlay: memberof
|
||||
olcMemberOfDangling: error
|
||||
olcMemberOfRefint: TRUE
|
||||
olcMemberOfGroupOC: groupOfNames
|
||||
olcMemberOfMemberAD: member
|
||||
olcMemberOfMemberOfAD: memberOf
|
||||
9
bootstrap/ldif/config-performance.ldif
Normal file
9
bootstrap/ldif/config-performance.ldif
Normal file
@@ -0,0 +1,9 @@
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
replace: olcThreads
|
||||
olcThreads: 4
|
||||
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcDbMaxSize
|
||||
olcDbMaxSize: 134217728
|
||||
13
bootstrap/ldif/config-tls.ldif
Normal file
13
bootstrap/ldif/config-tls.ldif
Normal file
@@ -0,0 +1,13 @@
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
add: olcTLSCACertificateFile
|
||||
olcTLSCACertificateFile: /etc/ldap/certs/ca_cert.pem
|
||||
-
|
||||
add: olcTLSCertificateFile
|
||||
olcTLSCertificateFile: /etc/ldap/certs/server_cert.pem
|
||||
-
|
||||
add: olcTLSCertificateKeyFile
|
||||
olcTLSCertificateKeyFile: /etc/ldap/certs/server_key.pem
|
||||
-
|
||||
add: olcTLSVerifyClient
|
||||
olcTLSVerifyClient: never
|
||||
7
bootstrap/ldif/group.ldif
Normal file
7
bootstrap/ldif/group.ldif
Normal file
@@ -0,0 +1,7 @@
|
||||
dn: cn={{ name }},ou={{ group_ou }},{{ base_dn }}
|
||||
objectClass: groupOfNames
|
||||
cn: {{ name }}
|
||||
description: {{ description }}
|
||||
{% for uid in uids -%}
|
||||
member: uid={{ uid }},ou={{ member_ou }},{{ base_dn }}
|
||||
{% endfor %}
|
||||
14
bootstrap/ldif/posix-user.ldif
Normal file
14
bootstrap/ldif/posix-user.ldif
Normal file
@@ -0,0 +1,14 @@
|
||||
dn: uid={{ uid }},ou=users,{{ base_dn }}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
uid: {{ uid }}
|
||||
givenName: {{ gn }}
|
||||
sn: {{ sn }}
|
||||
cn: {{ gn }} {{ sn }}
|
||||
mail: {{ mail }}
|
||||
userPassword: {{ password_hash }}
|
||||
uidNumber: {{ uidNumber }}
|
||||
gidNumber: {{ gidNumber }}
|
||||
homeDirectory: /home/{{ uid }}
|
||||
loginShell: /usr/bin/bash
|
||||
14
bootstrap/ldif/posix-users.ldif
Normal file
14
bootstrap/ldif/posix-users.ldif
Normal file
@@ -0,0 +1,14 @@
|
||||
dn: uid={{ uid }},ou=users,{{ base_dn }}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
uid: {{ uid }}
|
||||
givenName: {{ gn }}
|
||||
sn: {{ sn }}
|
||||
cn: {{ gn }} {{ sn }}
|
||||
mail: {{ mail }}
|
||||
userPassword: {{ password_hash }}
|
||||
uidNumber: {{ uidNumber }}
|
||||
gidNumber: {{ gidNumber }}
|
||||
homeDirectory: /home/{{ uid }}
|
||||
loginShell: /usr/bin/bash
|
||||
6
bootstrap/ldif/service-accounts.ldif
Normal file
6
bootstrap/ldif/service-accounts.ldif
Normal file
@@ -0,0 +1,6 @@
|
||||
dn: cn=readonly,ou=service-accounts,{{ base_dn }}
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: organizationalRole
|
||||
cn: readonly
|
||||
description: Read-only service account
|
||||
userPassword: {{ password_hash }}
|
||||
9
bootstrap/ldif/users.ldif
Normal file
9
bootstrap/ldif/users.ldif
Normal file
@@ -0,0 +1,9 @@
|
||||
dn: uid={{ uid }},ou=users,{{ base_dn }}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: shadowAccount
|
||||
uid: {{ uid }}
|
||||
givenName: {{ gn }}
|
||||
sn: {{ sn }}
|
||||
cn: {{ gn }} {{ sn }}
|
||||
mail: {{ mail }}
|
||||
userPassword: {{ password_hash }}
|
||||
Reference in New Issue
Block a user