better folder/share names
This commit is contained in:
19
README.md
19
README.md
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user