#!/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()