Files
SaraView/Backend/Locks.py
T

124 lines
3.9 KiB
Python

from __future__ import annotations
import tomllib
from typing import Any
import tomli_w
from fastapi import HTTPException
from Settings import GetDataDirectory
def GetLocksPath():
return GetDataDirectory() / "LockedDirectories.toml"
def LoadLocks() -> list[dict[str, Any]]:
LocksPath = GetLocksPath()
if not LocksPath.is_file():
return []
with LocksPath.open("rb") as LocksFile:
Loaded = tomllib.load(LocksFile)
return Loaded.get("Locks", [])
def SaveLocks(Locks: list[dict[str, Any]]) -> None:
GetDataDirectory().mkdir(parents=True, exist_ok=True)
with GetLocksPath().open("wb") as LocksFile:
tomli_w.dump({"Locks": Locks}, LocksFile)
def GetLockForPath(RootName: str, RelativePath: str) -> dict[str, Any] | None:
for Lock in LoadLocks():
if Lock.get("Root") == RootName and Lock.get("Path") == RelativePath:
return Lock
return None
def SetLock(RootName: str, RelativePath: str, Usernames: list[str], GroupNames: list[str]) -> None:
"""Restrict a file or directory to specific users and/or groups. Passing
both empty lists is the same as removing the lock entirely.
"""
if not RelativePath:
raise ValueError("Can't lock an entire root — pick a specific file or directory.")
if not Usernames and not GroupNames:
RemoveLock(RootName, RelativePath)
return
Locks = LoadLocks()
for Lock in Locks:
if Lock.get("Root") == RootName and Lock.get("Path") == RelativePath:
Lock["Users"] = Usernames
Lock["Groups"] = GroupNames
SaveLocks(Locks)
return
Locks.append({"Root": RootName, "Path": RelativePath, "Users": Usernames, "Groups": GroupNames})
SaveLocks(Locks)
def RemoveLock(RootName: str, RelativePath: str) -> None:
Locks = [
Lock
for Lock in LoadLocks()
if not (Lock.get("Root") == RootName and Lock.get("Path") == RelativePath)
]
SaveLocks(Locks)
def IsPathAccessible(
RootName: str, RelativePath: str, Username: str, UserGroups: list[str]
) -> bool:
"""A path is inaccessible if it IS a locked file/directory, or lies
anywhere underneath a locked directory, and the user isn't permitted
(by username or group) on every applicable lock. Locks don't propagate
to subdirectories as separate records — there's only ever one lock
entry per locked directory — but access to anything inside still
requires being permitted on that ancestor lock, so you always have to
"enter the parent first." Admins do NOT bypass locks - Admin is a
management group (create users, manage groups, and so on), not a way
to view everyone's private directories. Only direct server/filesystem
access can see everything.
"""
for Lock in LoadLocks():
if Lock.get("Root") != RootName:
continue
LockPath = Lock.get("Path", "")
if not LockPath:
continue
if RelativePath == LockPath or RelativePath.startswith(f"{LockPath}/"):
PermittedUsers = Lock.get("Users", [])
PermittedGroups = Lock.get("Groups", [])
IsPermittedByUser = Username in PermittedUsers
IsPermittedByGroup = any(Group in PermittedGroups for Group in UserGroups)
if not IsPermittedByUser and not IsPermittedByGroup:
return False
return True
def RequirePathAccessible(RootName: str, RelativePath: str, CurrentUser: dict[str, Any]) -> None:
"""Raises a 404 (never a distinguishing "access denied") for a path the
current user isn't permitted to see, so a locked directory's existence
isn't revealed to anyone not on its list.
"""
if not IsPathAccessible(
RootName,
RelativePath,
CurrentUser["Username"],
CurrentUser.get("Groups", []),
):
raise HTTPException(status_code=404, detail="Not found.")