Imported sources.
This commit is contained in:
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM ubuntu:26.04
|
||||
|
||||
RUN apt-get update && \
|
||||
echo "slapd slapd/no_configuration boolean true" | debconf-set-selections && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
slapd \
|
||||
ldap-utils \
|
||||
python3-jinja2 \
|
||||
python3-ldap3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY entrypoint.sh /entrypoint
|
||||
COPY bootstrap/ /bootstrap/
|
||||
|
||||
RUN chmod +x /entrypoint
|
||||
|
||||
EXPOSE 389 636
|
||||
|
||||
ENTRYPOINT ["/entrypoint"]
|
||||
212
README.md
Normal file
212
README.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# OpenLDAP
|
||||
|
||||
OpenLDAP 2.6 container running on Ubuntu 26.04 with cn=config (slapd-config) database.
|
||||
|
||||
## Optional Bootstrap Accounts CSV Files
|
||||
|
||||
Bootstrap reads account files from `/bootstrap/accounts` inside the container.
|
||||
|
||||
With the current compose setup, this maps to:
|
||||
|
||||
`~/app-data/openldap/accounts`
|
||||
|
||||
Optional files:
|
||||
|
||||
- `users.csv`: `uid,gn,sn,mail`
|
||||
- `admins.csv`: `uid,gn,sn,mail`
|
||||
- `posix-users.csv`: `uid,gn,sn,mail,uidNumber,gidNumber`
|
||||
|
||||
You can provide any subset of these files; missing files are skipped.
|
||||
|
||||
Rows starting with `#` are ignored.
|
||||
|
||||
## Changing the password
|
||||
|
||||
Use `ldappasswd` to change the password:
|
||||
|
||||
```bash
|
||||
BASE_DN="dc=koszewscy,dc=waw,dc=pl"
|
||||
USER_DN="cn=admin,$BASE_DN"
|
||||
ldappasswd -x -D "$USER_DN" -W -S "$USER_DN"
|
||||
```
|
||||
|
||||
or use a oneliner:
|
||||
|
||||
```shell
|
||||
DN="cn=admin,dc=koszewscy,dc=waw,dc=pl" ldappasswd -x -D "$DN" -W -S "$DN"
|
||||
```
|
||||
|
||||
Change the password for the Admin:
|
||||
|
||||
`change_password.ldif`:
|
||||
|
||||
```ldif
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcRootPW
|
||||
olcRootPW: {SSHA}newhashedpassword
|
||||
```
|
||||
|
||||
```bash
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f change_password.ldif
|
||||
```
|
||||
|
||||
|
||||
## LDIF file format
|
||||
|
||||
The basic form of an entry is:
|
||||
|
||||
```
|
||||
# comment
|
||||
dn: <distinguished name>
|
||||
<attrdesc>: <attrvalue>
|
||||
<attrdesc>: <attrvalue>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Lines may be continued by starting the next line with a single space or tab.
|
||||
|
||||
```
|
||||
dn: cn=Barbara J Jensen,dc=example,dc=
|
||||
com
|
||||
cn: Barbara J
|
||||
Jensen
|
||||
```
|
||||
|
||||
is equivalent to:
|
||||
|
||||
```
|
||||
dn: cn=Barbara J Jensen,dc=example,dc=com
|
||||
cn: Barbara J Jensen
|
||||
```
|
||||
|
||||
Multiple values for the same attribute are represented by repeating the attribute description:
|
||||
|
||||
```
|
||||
dn: cn=Barbara J Jensen,dc=example,dc=com
|
||||
cn: Barbara J Jensen
|
||||
cn: Babs Jensen
|
||||
```
|
||||
|
||||
If an attribute value contains a non-printable character, it must be base64-encoded and prefixed with a single colon:
|
||||
|
||||
```
|
||||
dn: cn=Barbara J Jensen,dc=example,dc=com
|
||||
cn:: QmFyYmFyYSBKIEplbnNlbgo=
|
||||
```
|
||||
|
||||
Binary files (e.g. images) can be included in the LDIF file by using the "file:" prefix:
|
||||
|
||||
```
|
||||
dn: cn=Barbara J Jensen,dc=example,dc=com
|
||||
jpegPhoto:< file:///home/bjensen/photo.jpg
|
||||
```
|
||||
|
||||
Multiple entries are separated by a blank line. Binary files like the one above may also be included in as Base64-encoded values.
|
||||
|
||||
The full specification is available at https://datatracker.ietf.org/doc/html/rfc2849.
|
||||
|
||||
## Accessing cn=config
|
||||
|
||||
SASL EXTERNAL authenticates via the Unix socket — uid=0 maps to the cn=config superuser.
|
||||
The commands must run inside the container where the socket is accessible.
|
||||
|
||||
### Browse the entire cn=config tree
|
||||
|
||||
```bash
|
||||
ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b cn=config
|
||||
```
|
||||
|
||||
### Browse a specific database entry
|
||||
|
||||
```bash
|
||||
ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "olcDatabase={1}mdb,cn=config"
|
||||
```
|
||||
|
||||
### Modify cn=config
|
||||
|
||||
```bash
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// <<'EOF'
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
replace: olcLogLevel
|
||||
olcLogLevel: stats
|
||||
EOF
|
||||
```
|
||||
|
||||
### Verify EXTERNAL identity
|
||||
|
||||
```bash
|
||||
ldapwhoami -Q -Y EXTERNAL -H ldapi:///
|
||||
```
|
||||
|
||||
Expected: `dn:gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth`
|
||||
|
||||
## Editing Access Control Rules
|
||||
|
||||
1. Inserting a New Rule at the Top
|
||||
|
||||
```ldif
|
||||
# filename: insert_rule.ldif
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
add: olcAccess
|
||||
olcAccess: {0}to *
|
||||
by dn.exact="cn=security-scanner,dc=example,dc=com" read break
|
||||
```
|
||||
|
||||
2. Deleting a Specific Rule
|
||||
|
||||
```ldif
|
||||
# filename: delete_rule.ldif
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
delete: olcAccess
|
||||
olcAccess: {2}
|
||||
```
|
||||
|
||||
3. Updating an Existing Rule (In-Place)
|
||||
|
||||
```ldif
|
||||
# filename: update_rule.ldif
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcAccess
|
||||
olcAccess: {1}to attrs=userPassword
|
||||
by self write
|
||||
by anonymous auth
|
||||
by group.exact="cn=it-admins,dc=example,dc=com" write
|
||||
```
|
||||
|
||||
4. Reordering the Entire Stack
|
||||
|
||||
```ldif
|
||||
# filename: reorder_rules.ldif
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcAccess
|
||||
olcAccess: {0}to *
|
||||
by dn.exact="cn=security-scanner,dc=example,dc=com" read break
|
||||
olcAccess: {1}to attrs=userPassword
|
||||
by self write
|
||||
by anonymous auth
|
||||
by group.exact="cn=it-admins,dc=example,dc=com" write
|
||||
olcAccess: {2}to *
|
||||
by self read
|
||||
```
|
||||
|
||||
## Accessing the DIT
|
||||
|
||||
```bash
|
||||
ldapsearch -x -H ldap://localhost \
|
||||
-D "cn=admin,dc=koszewscy,dc=waw,dc=pl" -W \
|
||||
-b "dc=koszewscy,dc=waw,dc=pl"
|
||||
```
|
||||
|
||||
### Verify readonly service account bind
|
||||
|
||||
```bash
|
||||
ldapwhoami -x -H ldap://localhost \
|
||||
-D "cn=readonly,ou=service-accounts,dc=koszewscy,dc=waw,dc=pl" -W
|
||||
```
|
||||
2
admins-example.csv
Normal file
2
admins-example.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
# uid,gn,sn,mail
|
||||
admin,Admin,Example,admin@example.com
|
||||
|
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 }}
|
||||
78
entrypoint.sh
Normal file
78
entrypoint.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CERTS_DIR="/etc/ldap/certs"
|
||||
DATA_DIR="/var/lib/ldap"
|
||||
SLAPD_D="/etc/ldap/slapd.d"
|
||||
INITIALIZED_FLAG="$DATA_DIR/.initialized"
|
||||
CA_CERT_NAME="ca_cert.pem"
|
||||
SERVER_CERT_NAME="server_cert.pem"
|
||||
SERVER_KEY_NAME="server_key.pem"
|
||||
|
||||
echo "Starting OpenLDAP entrypoint..."
|
||||
|
||||
base_dn="${LDAP_BASE_DN:-dc=example,dc=org}"
|
||||
domain="${LDAP_DOMAIN:-example.org}"
|
||||
org="${LDAP_ORG:-Example Org}"
|
||||
password="${LDAP_PASSWORD:-changeit}"
|
||||
admin_password="${LDAP_ADMIN_PASSWORD:-$password}"
|
||||
|
||||
echo "Base DN : $base_dn"
|
||||
echo "Domain : $domain"
|
||||
echo "Org : $org"
|
||||
|
||||
tls_enabled="0"
|
||||
if [ -f "$CERTS_DIR/$CA_CERT_NAME" ] && [ -f "$CERTS_DIR/$SERVER_CERT_NAME" ] && [ -f "$CERTS_DIR/$SERVER_KEY_NAME" ]; then
|
||||
tls_enabled="1"
|
||||
fi
|
||||
if [ "$tls_enabled" = "1" ]; then
|
||||
echo "TLS : enabled"
|
||||
else
|
||||
echo "TLS : disabled"
|
||||
fi
|
||||
|
||||
echo "Ensuring slapd runtime directory..."
|
||||
mkdir -p /var/run/slapd
|
||||
chown openldap:openldap /var/run/slapd
|
||||
|
||||
if [ ! -f "$INITIALIZED_FLAG" ]; then
|
||||
echo "First run - configuring slapd via debconf..."
|
||||
cat <<EOF | debconf-set-selections
|
||||
slapd slapd/no_configuration boolean false
|
||||
slapd slapd/dump_database select when needed
|
||||
slapd slapd/dump_database_destdir string /var/backups/slapd-VERSION
|
||||
slapd slapd/move_old_database boolean false
|
||||
slapd slapd/domain string $domain
|
||||
slapd shared/organization string $org
|
||||
slapd slapd/password1 password $admin_password
|
||||
slapd slapd/password2 password $admin_password
|
||||
slapd slapd/purge_database boolean false
|
||||
slapd slapd/internal/adminpw1 password $admin_password
|
||||
slapd slapd/internal/generated_adminpw password $admin_password
|
||||
EOF
|
||||
|
||||
echo "Running dpkg-reconfigure slapd..."
|
||||
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive slapd
|
||||
echo "dpkg-reconfigure complete."
|
||||
|
||||
echo "Running bootstrap init..."
|
||||
LDAP_BASE_DN="$base_dn" \
|
||||
LDAP_PASSWORD="$password" \
|
||||
TLS_ENABLED="$tls_enabled" \
|
||||
python3 -u /bootstrap/init.py
|
||||
else
|
||||
echo "Already initialised - skipping bootstrap."
|
||||
fi
|
||||
|
||||
slapd_url="ldapi:/// ldap://:389/"
|
||||
if [ "$tls_enabled" = "1" ]; then
|
||||
slapd_url="$slapd_url ldaps://:636/"
|
||||
fi
|
||||
|
||||
echo "Launching slapd (URLs: $slapd_url)..."
|
||||
exec slapd \
|
||||
-F "$SLAPD_D" \
|
||||
-u openldap \
|
||||
-g openldap \
|
||||
-d 0 \
|
||||
-h "$slapd_url"
|
||||
5
env.example
Normal file
5
env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
LDAP_DOMAIN=example.com
|
||||
LDAP_BASE_DN=dc=example,dc=com
|
||||
LDAP_ORG=Example Organization
|
||||
LDAP_PASSWORD=ChangeMe123!
|
||||
LDAP_ADMIN_PASSWORD=ChangeMe123!
|
||||
11587
local-guide/guide.html
Normal file
11587
local-guide/guide.html
Normal file
File diff suppressed because it is too large
Load Diff
3
posix-users-example.csv
Normal file
3
posix-users-example.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
# uid,gn,sn,mail,uidNumber,gidNumber
|
||||
alice,Alice,Example,alice@example.com,10000,10000
|
||||
bob,Bob,Example,bob@example.com,10001,10000
|
||||
|
132
scripts/accounts_editor.py
Normal file
132
scripts/accounts_editor.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
|
||||
|
||||
APP_TITLE = "OpenLDAP Accounts CSV Editor"
|
||||
|
||||
|
||||
FILES = {
|
||||
"Users": {
|
||||
"filename": "users.csv",
|
||||
"keys": ["uid", "gn", "sn", "mail"],
|
||||
"labels": {
|
||||
"uid": "UID",
|
||||
"gn": "Given Name",
|
||||
"sn": "Surname",
|
||||
"mail": "Email",
|
||||
},
|
||||
},
|
||||
"Admins": {
|
||||
"filename": "admins.csv",
|
||||
"keys": ["uid", "gn", "sn", "mail"],
|
||||
"labels": {
|
||||
"uid": "UID",
|
||||
"gn": "Given Name",
|
||||
"sn": "Surname",
|
||||
"mail": "Email",
|
||||
},
|
||||
},
|
||||
"POSIX Users": {
|
||||
"filename": "posix-users.csv",
|
||||
"keys": ["uid", "gn", "sn", "mail", "uidNumber", "gidNumber"],
|
||||
"labels": {
|
||||
"uid": "UID",
|
||||
"gn": "Given Name",
|
||||
"sn": "Surname",
|
||||
"mail": "Email",
|
||||
"uidNumber": "POSIX UID",
|
||||
"gidNumber": "POSIX GID",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def read_csv(path: Path, keys: list[str]) -> pd.DataFrame:
|
||||
if not path.exists():
|
||||
return pd.DataFrame(columns=keys)
|
||||
|
||||
df = pd.read_csv(
|
||||
path,
|
||||
header=None,
|
||||
names=keys,
|
||||
comment="#",
|
||||
skip_blank_lines=True,
|
||||
dtype=str,
|
||||
keep_default_na=False,
|
||||
usecols=range(len(keys)),
|
||||
)
|
||||
return df.apply(lambda col: col.str.strip())
|
||||
|
||||
|
||||
def write_csv(path: Path, keys: list[str], df: pd.DataFrame) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cleaned = df.fillna("").astype(str)
|
||||
cleaned = cleaned.apply(lambda col: col.str.strip())
|
||||
cleaned = cleaned[cleaned.ne("").any(axis=1)]
|
||||
|
||||
with path.open("w", newline="", encoding="utf-8") as f:
|
||||
f.write(f"# {','.join(keys)}\n")
|
||||
cleaned.to_csv(f, index=False, header=False)
|
||||
|
||||
|
||||
def render_page(accounts_dir: Path, page_name: str) -> None:
|
||||
spec = FILES[page_name]
|
||||
csv_path = accounts_dir / spec["filename"]
|
||||
|
||||
st.subheader(page_name)
|
||||
st.caption(f"File: {csv_path}")
|
||||
|
||||
df = read_csv(csv_path, spec["keys"])
|
||||
|
||||
column_config = {
|
||||
key: st.column_config.TextColumn(label=spec["labels"].get(key, key), width="medium")
|
||||
for key in spec["keys"]
|
||||
}
|
||||
|
||||
edited = st.data_editor(
|
||||
df,
|
||||
width="stretch",
|
||||
num_rows="dynamic",
|
||||
column_config=column_config,
|
||||
hide_index=True,
|
||||
key=f"editor-{page_name}",
|
||||
)
|
||||
|
||||
with st.container(horizontal=True):
|
||||
if st.button("Save", type="primary", width=120, key=f"save-{page_name}"):
|
||||
write_csv(csv_path, spec["keys"], edited)
|
||||
st.success(f"Saved {spec['filename']}")
|
||||
|
||||
if st.button("Reload", width=120, key=f"reload-{page_name}"):
|
||||
st.rerun()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
st.set_page_config(page_title=APP_TITLE, layout="wide")
|
||||
st.title(APP_TITLE)
|
||||
|
||||
default_dir = os.environ.get("OPENLDAP_ACCOUNTS_DIR", "~/app-data/openldap/accounts")
|
||||
|
||||
with st.sidebar:
|
||||
st.header("Settings")
|
||||
accounts_dir_raw = st.text_input("Accounts directory", value=default_dir)
|
||||
page_name = st.radio("Page", list(FILES.keys()))
|
||||
|
||||
accounts_dir = Path(accounts_dir_raw).expanduser()
|
||||
st.caption("This editor manages the CSV files used by OpenLDAP bootstrap.")
|
||||
|
||||
if not accounts_dir.exists():
|
||||
st.warning(f"Directory does not exist yet: {accounts_dir}")
|
||||
|
||||
render_page(accounts_dir, page_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
scripts/login.py
Normal file
26
scripts/login.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from getpass import getpass
|
||||
from ldap3 import Server, Connection, ALL
|
||||
from ldap3 import ObjectDef, AttrDef, Reader, Writer, Entry, Attribute, OperationalAttribute
|
||||
import keyring
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="LDAP Test Script")
|
||||
|
||||
# Generic LDAP connection parameters
|
||||
parser.add_argument("--host", "-H", default="gra-01.koszewscy.waw.pl", help="LDAP server host")
|
||||
parser.add_argument("--port", "-p", type=int, default=389, help="LDAP server port")
|
||||
parser.add_argument("--base-dn", "-b", default="dc=koszewscy,dc=waw,dc=pl", help="Base DN for LDAP operations")
|
||||
parser.add_argument("--user", "-u", default="cn=admin,dc=koszewscy,dc=waw,dc=pl", help="Bind DN for LDAP connection")
|
||||
|
||||
# Specific parameters
|
||||
parser.add_argument("--object-class", "-c", nargs='*', default=["inetOrgPerson"], help="Object class(es) to work with")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
server = Server(args.host, port=args.port, get_info=ALL)
|
||||
password = keyring.get_password("Home Lab Password", "admin") or getpass("Password: ")
|
||||
|
||||
conn = Connection(server, user=args.user, password=password, auto_bind=True)
|
||||
person = ObjectDef(args.object_class, conn)
|
||||
print(person)
|
||||
conn.unbind()
|
||||
31
scripts/search.py
Normal file
31
scripts/search.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from ldap3 import Server, Connection, ALL
|
||||
import streamlit as st
|
||||
|
||||
BASE_DN = "dc=koszewscy,dc=waw,dc=pl"
|
||||
LDAP_HOST = "gra-01.koszewscy.waw.pl"
|
||||
LDAP_PORT = 389
|
||||
|
||||
st.set_page_config(page_title="LDAP Search", layout="wide")
|
||||
|
||||
host = st.text_input("Host", LDAP_HOST)
|
||||
port = st.number_input("Port", value=LDAP_PORT, step=1)
|
||||
bind_dn = st.text_input("Bind DN", f"cn=admin,{BASE_DN}")
|
||||
password = st.text_input("Password", type="password")
|
||||
base_dn = st.text_input("Base DN", BASE_DN)
|
||||
search_filter = st.text_input("Filter", "(objectClass=*)")
|
||||
|
||||
if st.button("Search"):
|
||||
server = Server(host, port=port, get_info=ALL)
|
||||
conn = Connection(server, user=bind_dn, password=password, auto_bind=True)
|
||||
ok = conn.search(
|
||||
search_base=base_dn,
|
||||
search_filter=search_filter,
|
||||
attributes=["*"]
|
||||
)
|
||||
if ok:
|
||||
st.write(f"Entries: {len(conn.entries)}")
|
||||
for e in conn.entries:
|
||||
st.code(e.entry_to_ldif())
|
||||
else:
|
||||
st.error(conn.result)
|
||||
conn.unbind()
|
||||
4
users-example.csv
Normal file
4
users-example.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
# uid,gn,sn,mail
|
||||
admin,Admin,Example,admin@example.com
|
||||
alice,Alice,Example,alice@example.com
|
||||
bob,Bob,Example,bob@example.com
|
||||
|
Reference in New Issue
Block a user