From f34e90b3035ea02e49bdf201b98e1a1681ec3d8e Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Tue, 17 Mar 2026 10:12:10 +0100 Subject: [PATCH] better folder/share names --- README.md | 19 ++++++++++--------- app/reconcile_shares.py | 28 ++++++++++++++++++++++++++-- etc/samba/smb.conf | 2 +- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 62ee4f1..35a71c7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # AD-Integrated Containerized Samba File Server -This repository provides a production-oriented Samba file server container that joins an existing Active Directory domain and exposes three SMB shares: `Privat`, `Data`, and `FSLogix`. +This repository provides a production-oriented Samba file server container that joins an existing Active Directory domain and exposes three SMB shares: `Private`, `Data`, and `FSLogix`. ## Architecture - Samba runs in ADS mode with `winbind` identity mapping. - Static shares: - - `\\server\Privat` -> `/data/private` + - `\\server\Private` -> `/data/private` - `\\server\Data` -> `/data/groups/data` - `\\server\FSLogix` -> `/data/fslogix` - FS_* groups are projected as folders inside the Data share (`/data/groups/data/`). @@ -62,8 +62,9 @@ CREATE TABLE shares ( - Initial admin credentials with rights to create/reset `FileShare_ServiceAccount` during `./setup`. - `FileShare_ServiceAccount` must be allowed to join computers to the domain (`net ads join`) in your AD policy. - Dynamic group discovery primarily uses machine-account LDAP (`net ads search -P`); join credentials are only used as a fallback LDAP bind path. -- Group naming convention for Data folders: - - `FS_` +- Group naming convention for Data folder eligibility: + - `FS_` +- Folder names use AD group display names (`displayName`, then `name`/`cn` fallback), not pre-2000 (`sAMAccountName`) names. ## DNS Requirements @@ -137,14 +138,14 @@ Kerberos requires close time alignment. ## SMB Shares -### Privat +### Private -- Share: `\\server\Privat` +- Share: `\\server\Private` - Root path: `/data/private` - Per-user path: `/data/private/` - Script ensures user directories exist and assigns ownership through winbind identity resolution. -- Root `/data/private` is enforced read/execute-only (`0555`) to prevent folder creation directly under `\\server\Privat`. -- SMB-side ACL changes on `\\server\Privat` are blocked (`nt acl support = no`). +- Root `/data/private` is enforced read/execute-only (`0555`) to prevent folder creation directly under `\\server\Private`. +- SMB-side ACL changes on `\\server\Private` are blocked (`nt acl support = no`). - Auto-creation skips well-known/service/non-login accounts (disabled, locked, or expired). - Each private user tree is reconciled recursively to homogeneous permissions (dirs `0700`, files `0600`, user/admin ACLs). - Permissions: @@ -279,7 +280,7 @@ docker compose exec samba sh -lc 'tail -n 200 /var/log/backup.log' docker compose exec samba sh -lc 'mods="$(smbd -b | sed -n "s/^ *MODULESDIR: //p" | head -n1)/vfs"; ls -1 "$mods"/acl_xattr.so "$mods"/full_audit.so' ``` -### Permissions in Privat share are incorrect +### Permissions in Private share are incorrect - Re-run reconciliation to rebuild private directories and ACLs: diff --git a/app/reconcile_shares.py b/app/reconcile_shares.py index faf206b..330c8e1 100755 --- a/app/reconcile_shares.py +++ b/app/reconcile_shares.py @@ -24,6 +24,7 @@ FSLOGIX_ROOT = "/data/fslogix" LDAP_FILTER = "(&(objectClass=group)(sAMAccountName=FS_*))" GROUP_PREFIXES = ("FS_",) +GROUP_TITLE_ATTRS = ("displayname", "name", "cn") USER_STATUS_FILTER = "(&(objectClass=user)(!(objectClass=computer))(sAMAccountName=*))" REQUIRED_ENV = ["REALM", "WORKGROUP", "DOMAIN"] @@ -122,6 +123,15 @@ def derive_share_name(sam_account_name: str) -> Optional[str]: return None +def derive_group_title(entry: Dict[str, Tuple[str, bool]]) -> Optional[str]: + for attr in GROUP_TITLE_ATTRS: + if attr in entry: + value = entry[attr][0].strip() + if value: + return value + return None + + def parse_groups_from_ldap_output(output: str) -> List[Dict[str, str]]: entries = parse_ldap_entries(output) @@ -132,7 +142,7 @@ def parse_groups_from_ldap_output(output: str) -> List[Dict[str, str]]: sam_value, _ = entry["samaccountname"] sam = sam_value.strip() - share_name = derive_share_name(sam) + share_name = derive_group_title(entry) or derive_share_name(sam) if not share_name: continue @@ -223,7 +233,18 @@ def next_available_path(path: str) -> str: def fetch_groups_via_net_ads() -> List[Dict[str, str]]: result = run_command( - ["net", "ads", "search", "-P", LDAP_FILTER, "objectGUID", "sAMAccountName"], + [ + "net", + "ads", + "search", + "-P", + LDAP_FILTER, + "objectGUID", + "sAMAccountName", + "displayName", + "name", + "cn", + ], check=False, ) if result.returncode != 0: @@ -270,6 +291,9 @@ def fetch_groups_via_ldap_bind() -> List[Dict[str, str]]: LDAP_FILTER, "objectGUID", "sAMAccountName", + "displayName", + "name", + "cn", ] ) return parse_groups_from_ldap_output(result.stdout) diff --git a/etc/samba/smb.conf b/etc/samba/smb.conf index b941d98..82a02b1 100644 --- a/etc/samba/smb.conf +++ b/etc/samba/smb.conf @@ -37,7 +37,7 @@ logging = file log level = 1 auth:5 passdb:5 winbind:3 -[Privat] +[Private] path = /data/private read only = no browseable = yes