Imported sources.

This commit is contained in:
2026-05-04 07:07:52 +02:00
commit a3f3105081
26 changed files with 12475 additions and 0 deletions

19
Dockerfile Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
# uid,gn,sn,mail
admin,Admin,Example,admin@example.com
1 # uid gn sn mail
2 admin Admin Example admin@example.com

205
bootstrap/init.py Normal file
View 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()

View 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 %}

View 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
View 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

View 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

View File

@@ -0,0 +1,7 @@
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: mail eq,pres
-
add: olcDbIndex
olcDbIndex: memberOf eq

View File

@@ -0,0 +1,4 @@
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof

View 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

View 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

View 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

View 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 %}

View 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

View 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

View 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 }}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

3
posix-users-example.csv Normal file
View 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
1 # uid gn sn mail uidNumber gidNumber
2 alice Alice Example alice@example.com 10000 10000
3 bob Bob Example bob@example.com 10001 10000

132
scripts/accounts_editor.py Normal file
View 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
View 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
View 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
View 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
1 # uid gn sn mail
2 admin Admin Example admin@example.com
3 alice Alice Example alice@example.com
4 bob Bob Example bob@example.com