finalizing permissions; renamed Public -> Shared
This commit is contained in:
11
README.md
11
README.md
@@ -7,7 +7,7 @@ This repository provides a production-oriented Samba file server container that
|
|||||||
- Samba runs in ADS mode with `winbind` identity mapping.
|
- Samba runs in ADS mode with `winbind` identity mapping.
|
||||||
- Static shares:
|
- Static shares:
|
||||||
- `\\server\Private` -> `/data/private`
|
- `\\server\Private` -> `/data/private`
|
||||||
- `\\server\Public` -> `/data/public`
|
- `\\server\Shared` -> `/data/public`
|
||||||
- Dynamic shares are generated from AD groups matching `FileShare_*` or `FS_*` and written to `/etc/samba/generated/shares.conf`.
|
- Dynamic shares are generated from AD groups matching `FileShare_*` or `FS_*` and written to `/etc/samba/generated/shares.conf`.
|
||||||
- Dynamic share records are persisted in SQLite at `/state/shares.db`.
|
- Dynamic share records are persisted in SQLite at `/state/shares.db`.
|
||||||
- Backing storage is GUID-based and stable across group rename:
|
- Backing storage is GUID-based and stable across group rename:
|
||||||
@@ -132,20 +132,22 @@ Kerberos requires close time alignment.
|
|||||||
- 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\Private`.
|
- Root `/data/private` is enforced read/execute-only (`0555`) to prevent folder creation directly under `\\server\Private`.
|
||||||
- SMB-side permission changes on `\\server\Private` are blocked (`nt acl support = no` and security masks set to `0000`).
|
- 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).
|
||||||
- Permissions:
|
- Permissions:
|
||||||
- owner user: full control
|
- owner user: full control
|
||||||
- Domain Admins: ACL full control
|
- Domain Admins: ACL full control
|
||||||
- mode: `700`
|
- mode: `700`
|
||||||
- `hide unreadable = yes` + ACLs enforce that users only see their own folder.
|
- `hide unreadable = yes` + ACLs enforce that users only see their own folder.
|
||||||
|
|
||||||
### Public
|
### Shared
|
||||||
|
|
||||||
- Share: `\\server\Public`
|
- Share: `\\server\Shared`
|
||||||
- Path: `/data/public`
|
- Path: `/data/public`
|
||||||
- Read/write for authenticated users in configurable `PUBLIC_GROUP_SID` (default: `DOMAIN_USERS_SID`, resolved through winbind).
|
- Read/write for authenticated users in configurable `PUBLIC_GROUP_SID` (default: `DOMAIN_USERS_SID`, resolved through winbind).
|
||||||
- No guest access.
|
- No guest access.
|
||||||
|
- Permissions are reconciled recursively so all descendants remain homogeneous (dirs `2770`, files `0660`, shared group/admin ACLs).
|
||||||
|
|
||||||
### Dynamic Group Shares
|
### Dynamic Group Shares
|
||||||
|
|
||||||
@@ -155,6 +157,7 @@ Kerberos requires close time alignment.
|
|||||||
- Share exposure generated in `/etc/samba/generated/shares.conf`
|
- Share exposure generated in `/etc/samba/generated/shares.conf`
|
||||||
- Dynamic share names are validated for SMB compatibility and deduplicated case-insensitively.
|
- Dynamic share names are validated for SMB compatibility and deduplicated case-insensitively.
|
||||||
- Group membership changes are refreshed continuously via winbind cache updates (`winbind cache time = 60`) and Samba config reload during reconciliation.
|
- Group membership changes are refreshed continuously via winbind cache updates (`winbind cache time = 60`) and Samba config reload during reconciliation.
|
||||||
|
- Dynamic share trees are reconciled recursively so all descendants keep homogeneous permissions.
|
||||||
|
|
||||||
## Useful Commands
|
## Useful Commands
|
||||||
|
|
||||||
|
|||||||
@@ -540,38 +540,28 @@ def resolve_gid_from_sid(sid: str) -> Optional[int]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def set_acl(path: str, user_uid: int, admin_gid: Optional[int]) -> None:
|
def apply_group_permissions(
|
||||||
run_command(["setfacl", "-b", path], check=False)
|
path: str, group_gid: int, admin_gid: Optional[int], is_dir: bool
|
||||||
acl_entries = [f"u:{user_uid}:rwx", f"d:u:{user_uid}:rwx"]
|
|
||||||
if admin_gid is not None:
|
|
||||||
acl_entries.extend([f"g:{admin_gid}:rwx", f"d:g:{admin_gid}:rwx"])
|
|
||||||
|
|
||||||
result = run_command(
|
|
||||||
["setfacl", "-m", ",".join(acl_entries), path],
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
log(
|
|
||||||
f"setfacl failed for {path}: {result.stderr.strip() or result.stdout.strip()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_group_acl(path: str, group_gid: int) -> None:
|
|
||||||
acl_entries = [f"g:{group_gid}:rwx", f"d:g:{group_gid}:rwx"]
|
|
||||||
result = run_command(["setfacl", "-m", ",".join(acl_entries), path], check=False)
|
|
||||||
if result.returncode != 0:
|
|
||||||
log(
|
|
||||||
f"setfacl failed for {path}: {result.stderr.strip() or result.stdout.strip()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_group_acl_with_admin(
|
|
||||||
path: str, group_gid: int, admin_gid: Optional[int]
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if os.path.islink(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
mode = 0o2770 if is_dir else 0o660
|
||||||
|
group_perms = "rwx" if is_dir else "rw-"
|
||||||
|
|
||||||
|
os.chown(path, 0, group_gid)
|
||||||
|
os.chmod(path, mode)
|
||||||
run_command(["setfacl", "-b", path], check=False)
|
run_command(["setfacl", "-b", path], check=False)
|
||||||
acl_entries = [f"g:{group_gid}:rwx", f"d:g:{group_gid}:rwx"]
|
|
||||||
|
acl_entries = [f"g:{group_gid}:{group_perms}"]
|
||||||
if admin_gid is not None:
|
if admin_gid is not None:
|
||||||
acl_entries.extend([f"g:{admin_gid}:rwx", f"d:g:{admin_gid}:rwx"])
|
acl_entries.append(f"g:{admin_gid}:{group_perms}")
|
||||||
|
|
||||||
|
if is_dir:
|
||||||
|
acl_entries.append(f"d:g:{group_gid}:rwx")
|
||||||
|
if admin_gid is not None:
|
||||||
|
acl_entries.append(f"d:g:{admin_gid}:rwx")
|
||||||
|
|
||||||
result = run_command(["setfacl", "-m", ",".join(acl_entries), path], check=False)
|
result = run_command(["setfacl", "-m", ",".join(acl_entries), path], check=False)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
log(
|
log(
|
||||||
@@ -579,6 +569,80 @@ def set_group_acl_with_admin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_private_permissions(
|
||||||
|
path: str, user_uid: int, user_gid: int, admin_gid: Optional[int], is_dir: bool
|
||||||
|
) -> None:
|
||||||
|
if os.path.islink(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
mode = 0o700 if is_dir else 0o600
|
||||||
|
user_perms = "rwx" if is_dir else "rw-"
|
||||||
|
|
||||||
|
os.chown(path, user_uid, user_gid)
|
||||||
|
os.chmod(path, mode)
|
||||||
|
run_command(["setfacl", "-b", path], check=False)
|
||||||
|
|
||||||
|
acl_entries = [f"u:{user_uid}:{user_perms}"]
|
||||||
|
if admin_gid is not None:
|
||||||
|
acl_entries.append(f"g:{admin_gid}:{user_perms}")
|
||||||
|
|
||||||
|
if is_dir:
|
||||||
|
acl_entries.append(f"d:u:{user_uid}:rwx")
|
||||||
|
if admin_gid is not None:
|
||||||
|
acl_entries.append(f"d:g:{admin_gid}:rwx")
|
||||||
|
|
||||||
|
result = run_command(["setfacl", "-m", ",".join(acl_entries), path], check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
log(
|
||||||
|
f"setfacl failed for {path}: {result.stderr.strip() or result.stdout.strip()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_group_tree_permissions(
|
||||||
|
root_path: str, group_gid: int, admin_gid: Optional[int]
|
||||||
|
) -> None:
|
||||||
|
apply_group_permissions(root_path, group_gid, admin_gid, is_dir=True)
|
||||||
|
for current_root, dirnames, filenames in os.walk(root_path):
|
||||||
|
for dirname in dirnames:
|
||||||
|
apply_group_permissions(
|
||||||
|
os.path.join(current_root, dirname), group_gid, admin_gid, is_dir=True
|
||||||
|
)
|
||||||
|
for filename in filenames:
|
||||||
|
apply_group_permissions(
|
||||||
|
os.path.join(current_root, filename), group_gid, admin_gid, is_dir=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_user_primary_gid(uid: int) -> Optional[int]:
|
||||||
|
try:
|
||||||
|
return pwd.getpwuid(uid).pw_gid
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_private_tree_permissions(
|
||||||
|
root_path: str, user_uid: int, user_gid: int, admin_gid: Optional[int]
|
||||||
|
) -> None:
|
||||||
|
apply_private_permissions(root_path, user_uid, user_gid, admin_gid, is_dir=True)
|
||||||
|
for current_root, dirnames, filenames in os.walk(root_path):
|
||||||
|
for dirname in dirnames:
|
||||||
|
apply_private_permissions(
|
||||||
|
os.path.join(current_root, dirname),
|
||||||
|
user_uid,
|
||||||
|
user_gid,
|
||||||
|
admin_gid,
|
||||||
|
is_dir=True,
|
||||||
|
)
|
||||||
|
for filename in filenames:
|
||||||
|
apply_private_permissions(
|
||||||
|
os.path.join(current_root, filename),
|
||||||
|
user_uid,
|
||||||
|
user_gid,
|
||||||
|
admin_gid,
|
||||||
|
is_dir=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_domain_users(non_login_users: set) -> List[str]:
|
def list_domain_users(non_login_users: set) -> List[str]:
|
||||||
result = run_command(["wbinfo", "-u"], check=False)
|
result = run_command(["wbinfo", "-u"], check=False)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
@@ -644,15 +708,12 @@ def sync_public_directory() -> None:
|
|||||||
gid = resolve_gid_from_sid(public_group_sid)
|
gid = resolve_gid_from_sid(public_group_sid)
|
||||||
|
|
||||||
if gid is not None:
|
if gid is not None:
|
||||||
os.chown(PUBLIC_ROOT, 0, gid)
|
admin_gid = resolve_gid_from_sid(os.getenv("DOMAIN_ADMINS_SID", ""))
|
||||||
run_command(["setfacl", "-b", PUBLIC_ROOT], check=False)
|
enforce_group_tree_permissions(PUBLIC_ROOT, gid, admin_gid)
|
||||||
set_group_acl(PUBLIC_ROOT, gid)
|
|
||||||
else:
|
else:
|
||||||
group_display = qualified_group or public_group_sid or "<unset>"
|
group_display = qualified_group or public_group_sid or "<unset>"
|
||||||
log(f"Unable to resolve GID for {group_display}; public ACLs unchanged")
|
log(f"Unable to resolve GID for {group_display}; public ACLs unchanged")
|
||||||
|
|
||||||
os.chmod(PUBLIC_ROOT, 0o2770)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_private_directories() -> None:
|
def sync_private_directories() -> None:
|
||||||
workgroup = os.environ["WORKGROUP"]
|
workgroup = os.environ["WORKGROUP"]
|
||||||
@@ -676,11 +737,16 @@ def sync_private_directories() -> None:
|
|||||||
log(f"Unable to resolve UID for {username}, skipping private folder")
|
log(f"Unable to resolve UID for {username}, skipping private folder")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
user_gid = resolve_user_primary_gid(uid)
|
||||||
|
if user_gid is None:
|
||||||
|
log(
|
||||||
|
f"Unable to resolve primary GID for {username}, skipping private folder"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
user_path = os.path.join(PRIVATE_ROOT, username)
|
user_path = os.path.join(PRIVATE_ROOT, username)
|
||||||
os.makedirs(user_path, exist_ok=True)
|
os.makedirs(user_path, exist_ok=True)
|
||||||
os.chown(user_path, uid, -1)
|
enforce_private_tree_permissions(user_path, uid, user_gid, admin_gid)
|
||||||
os.chmod(user_path, 0o700)
|
|
||||||
set_acl(user_path, uid, admin_gid)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_dynamic_directory_permissions(conn: sqlite3.Connection) -> None:
|
def sync_dynamic_directory_permissions(conn: sqlite3.Connection) -> None:
|
||||||
@@ -706,8 +772,7 @@ def sync_dynamic_directory_permissions(conn: sqlite3.Connection) -> None:
|
|||||||
log(f"Unable to resolve GID for {sam}; leaving existing ACLs")
|
log(f"Unable to resolve GID for {sam}; leaving existing ACLs")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
os.chown(path, 0, gid)
|
enforce_group_tree_permissions(path, gid, admin_gid)
|
||||||
set_group_acl_with_admin(path, gid, admin_gid)
|
|
||||||
|
|
||||||
|
|
||||||
def with_lock() -> bool:
|
def with_lock() -> bool:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
ea support = yes
|
ea support = yes
|
||||||
nt acl support = no
|
nt acl support = no
|
||||||
|
|
||||||
[Public]
|
[Shared]
|
||||||
path = /data/public
|
path = /data/public
|
||||||
read only = no
|
read only = no
|
||||||
browseable = yes
|
browseable = yes
|
||||||
|
|||||||
Reference in New Issue
Block a user