better folder/share names

This commit is contained in:
Ludwig Lehnert
2026-03-17 10:12:10 +01:00
parent 972c1a649f
commit f34e90b303
3 changed files with 37 additions and 12 deletions

View File

@@ -1,12 +1,12 @@
# AD-Integrated Containerized Samba File Server # 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 ## Architecture
- Samba runs in ADS mode with `winbind` identity mapping. - Samba runs in ADS mode with `winbind` identity mapping.
- Static shares: - Static shares:
- `\\server\Privat` -> `/data/private` - `\\server\Private` -> `/data/private`
- `\\server\Data` -> `/data/groups/data` - `\\server\Data` -> `/data/groups/data`
- `\\server\FSLogix` -> `/data/fslogix` - `\\server\FSLogix` -> `/data/fslogix`
- FS_* groups are projected as folders inside the Data share (`/data/groups/data/<groupName>`). - FS_* groups are projected as folders inside the Data share (`/data/groups/data/<groupName>`).
@@ -62,8 +62,9 @@ CREATE TABLE shares (
- Initial admin credentials with rights to create/reset `FileShare_ServiceAccount` during `./setup`. - 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. - `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. - 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: - Group naming convention for Data folder eligibility:
- `FS_<FolderName>` - `FS_<Anything>`
- Folder names use AD group display names (`displayName`, then `name`/`cn` fallback), not pre-2000 (`sAMAccountName`) names.
## DNS Requirements ## DNS Requirements
@@ -137,14 +138,14 @@ Kerberos requires close time alignment.
## SMB Shares ## SMB Shares
### Privat ### Private
- Share: `\\server\Privat` - Share: `\\server\Private`
- Root path: `/data/private` - Root path: `/data/private`
- Per-user path: `/data/private/<samAccountName>` - Per-user path: `/data/private/<samAccountName>`
- Script ensures user directories exist and assigns ownership through winbind identity resolution. - 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`. - Root `/data/private` is enforced read/execute-only (`0555`) to prevent folder creation directly under `\\server\Private`.
- SMB-side ACL changes on `\\server\Privat` are blocked (`nt acl support = no`). - 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). - 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). - Each private user tree is reconciled recursively to homogeneous permissions (dirs `0700`, files `0600`, user/admin ACLs).
- Permissions: - 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' 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: - Re-run reconciliation to rebuild private directories and ACLs:

View File

@@ -24,6 +24,7 @@ FSLOGIX_ROOT = "/data/fslogix"
LDAP_FILTER = "(&(objectClass=group)(sAMAccountName=FS_*))" LDAP_FILTER = "(&(objectClass=group)(sAMAccountName=FS_*))"
GROUP_PREFIXES = ("FS_",) GROUP_PREFIXES = ("FS_",)
GROUP_TITLE_ATTRS = ("displayname", "name", "cn")
USER_STATUS_FILTER = "(&(objectClass=user)(!(objectClass=computer))(sAMAccountName=*))" USER_STATUS_FILTER = "(&(objectClass=user)(!(objectClass=computer))(sAMAccountName=*))"
REQUIRED_ENV = ["REALM", "WORKGROUP", "DOMAIN"] REQUIRED_ENV = ["REALM", "WORKGROUP", "DOMAIN"]
@@ -122,6 +123,15 @@ def derive_share_name(sam_account_name: str) -> Optional[str]:
return None 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]]: def parse_groups_from_ldap_output(output: str) -> List[Dict[str, str]]:
entries = parse_ldap_entries(output) 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_value, _ = entry["samaccountname"]
sam = sam_value.strip() 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: if not share_name:
continue continue
@@ -223,7 +233,18 @@ def next_available_path(path: str) -> str:
def fetch_groups_via_net_ads() -> List[Dict[str, str]]: def fetch_groups_via_net_ads() -> List[Dict[str, str]]:
result = run_command( result = run_command(
["net", "ads", "search", "-P", LDAP_FILTER, "objectGUID", "sAMAccountName"], [
"net",
"ads",
"search",
"-P",
LDAP_FILTER,
"objectGUID",
"sAMAccountName",
"displayName",
"name",
"cn",
],
check=False, check=False,
) )
if result.returncode != 0: if result.returncode != 0:
@@ -270,6 +291,9 @@ def fetch_groups_via_ldap_bind() -> List[Dict[str, str]]:
LDAP_FILTER, LDAP_FILTER,
"objectGUID", "objectGUID",
"sAMAccountName", "sAMAccountName",
"displayName",
"name",
"cn",
] ]
) )
return parse_groups_from_ldap_output(result.stdout) return parse_groups_from_ldap_output(result.stdout)

View File

@@ -37,7 +37,7 @@
logging = file logging = file
log level = 1 auth:5 passdb:5 winbind:3 log level = 1 auth:5 passdb:5 winbind:3
[Privat] [Private]
path = /data/private path = /data/private
read only = no read only = no
browseable = yes browseable = yes