Labels came from a fixed table matched by subset, so Storage (no bits) matched any level the table lacked, and a level was named after the first entry it contained: the server's Brain/MINDS 2.0 folders (0x10000) showed as Storage and 0x10104 as CBS or PW Open. Labels are now composed from the bits. Projects (bit 16 and up) take the names the server reports to their members through users/current/, and otherwise Project<n>; restrict slot 1 is CBS and any other Restrict<n>. A level with no meaning is shown as Unknown (0x...). chacl reads keys built the same way, refuses public and storage before any request, and lists the user's projects with their keys when given an invalid level.
150 lines
5.4 KiB
Python
150 lines
5.4 KiB
Python
import re
|
|
from typing import Any, Final, Mapping
|
|
from unicodedata import normalize
|
|
|
|
from pydantic.dataclasses import dataclass
|
|
|
|
from mdrsclient.models.utils import iso8601_to_user_friendly
|
|
|
|
|
|
class FolderAccessLevel:
|
|
# Bit Mask
|
|
# - bit 0: Is Private
|
|
# - bit 1: Is Public
|
|
# - bit 2: With Password
|
|
# - bit 3-7: (Reserved)
|
|
# - bit 8-15: Restricted Open, one bit per restrict network slot (slot 1 is bit 8)
|
|
# - bit 16-: Project Open, one bit per project bit_position (bit_position 0 is bit 16)
|
|
STORAGE: Final[int] = 0x0000
|
|
PRIVATE: Final[int] = 0x0001
|
|
PUBLIC: Final[int] = 0x0002
|
|
PASSWORD: Final[int] = 0x0004
|
|
RESERVED: Final[int] = 0x00F8
|
|
RESTRICT_SHIFT: Final[int] = 8
|
|
RESTRICT_SLOTS: Final[int] = 8
|
|
PROJECT_SHIFT: Final[int] = 16
|
|
# The server keeps no names for its restrict networks, so the one in use is named here.
|
|
# Any other slot is named after its number.
|
|
RESTRICT_NAMES: Final[dict[int, str]] = {1: "CBS"}
|
|
LEVEL_NAMES: Final[dict[int, str]] = {STORAGE: "Storage", PRIVATE: "Private", PUBLIC: "Public"}
|
|
|
|
@staticmethod
|
|
def restrict_name(slot: int) -> str:
|
|
return FolderAccessLevel.RESTRICT_NAMES.get(slot, f"Restrict{slot}")
|
|
|
|
@staticmethod
|
|
def project_name(bit_position: int, projects: Mapping[int, str] | None = None) -> str:
|
|
name = (projects or {}).get(bit_position)
|
|
return name if name else f"Project{bit_position}"
|
|
|
|
@staticmethod
|
|
def id2label(id: int, projects: Mapping[int, str] | None = None) -> str | None:
|
|
"""
|
|
Label an access level from its bits, e.g. 0x0104 is "CBS or PW Open".
|
|
|
|
`projects` names the projects by bit_position, as the server tells their members;
|
|
any other project gets a name made from its bit_position. None for a level that
|
|
has no meaning: Private or Public with any other bit, or a reserved bit.
|
|
"""
|
|
if id in FolderAccessLevel.LEVEL_NAMES:
|
|
return FolderAccessLevel.LEVEL_NAMES[id]
|
|
if id < 0 or id & (FolderAccessLevel.PRIVATE | FolderAccessLevel.PUBLIC | FolderAccessLevel.RESERVED):
|
|
return None
|
|
names = [
|
|
FolderAccessLevel.restrict_name(slot)
|
|
for slot in range(1, FolderAccessLevel.RESTRICT_SLOTS + 1)
|
|
if id & (1 << (FolderAccessLevel.RESTRICT_SHIFT + slot - 1))
|
|
]
|
|
project_bits = id >> FolderAccessLevel.PROJECT_SHIFT
|
|
names += [
|
|
FolderAccessLevel.project_name(bit, projects)
|
|
for bit in range(project_bits.bit_length())
|
|
if project_bits & (1 << bit)
|
|
]
|
|
if id & FolderAccessLevel.PASSWORD:
|
|
names.append("PW")
|
|
return " or ".join(names) + " Open"
|
|
|
|
@staticmethod
|
|
def key2id(key: str) -> int | None:
|
|
"""
|
|
The access level a key names: `private`, `public`, `storage`, or the parts of an
|
|
open level joined by `_or_` and ending in `_open`, e.g. `cbs_or_pw_open`. A part
|
|
is `pw`, `cbs`, `restrict<slot>` or `project<bit_position>`.
|
|
"""
|
|
fixed = {label.lower(): id for id, label in FolderAccessLevel.LEVEL_NAMES.items()}
|
|
if key in fixed:
|
|
return fixed[key]
|
|
if not key.endswith("_open"):
|
|
return None
|
|
id = 0
|
|
for part in key.removesuffix("_open").split("_or_"):
|
|
bit = FolderAccessLevel._part2bit(part)
|
|
if bit is None:
|
|
return None
|
|
id |= bit
|
|
return id
|
|
|
|
@staticmethod
|
|
def _part2bit(part: str) -> int | None:
|
|
if part == "pw":
|
|
return FolderAccessLevel.PASSWORD
|
|
slots = {name.lower(): slot for slot, name in FolderAccessLevel.RESTRICT_NAMES.items()}
|
|
match = re.fullmatch(r"restrict([1-9][0-9]*)", part)
|
|
slot = slots.get(part, int(match.group(1)) if match else None)
|
|
if slot is not None:
|
|
if slot > FolderAccessLevel.RESTRICT_SLOTS:
|
|
return None
|
|
return 1 << (FolderAccessLevel.RESTRICT_SHIFT + slot - 1)
|
|
match = re.fullmatch(r"project(0|[1-9][0-9]*)", part)
|
|
if match:
|
|
return 1 << (FolderAccessLevel.PROJECT_SHIFT + int(match.group(1)))
|
|
return None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FolderSimple:
|
|
id: str
|
|
pid: str | None
|
|
name: str
|
|
access_level: int
|
|
lock: bool
|
|
size: int
|
|
laboratory_id: int
|
|
description: str
|
|
created_at: str
|
|
updated_at: str
|
|
restrict_opened_at: str | None
|
|
|
|
def access_level_label(self, projects: Mapping[int, str] | None = None) -> str:
|
|
"""The access level as shown to a user, naming the projects given by bit_position."""
|
|
label = FolderAccessLevel.id2label(self.access_level, projects)
|
|
return label if label is not None else f"Unknown (0x{self.access_level:x})"
|
|
|
|
@property
|
|
def access_level_name(self) -> str:
|
|
return self.access_level_label()
|
|
|
|
@property
|
|
def lock_name(self) -> str:
|
|
return "locked" if self.lock else "unlocked"
|
|
|
|
@property
|
|
def created_at_name(self) -> str:
|
|
return iso8601_to_user_friendly(self.created_at)
|
|
|
|
@property
|
|
def updated_at_name(self) -> str:
|
|
return iso8601_to_user_friendly(self.updated_at)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Folder(FolderSimple):
|
|
metadata: list[dict[str, Any]]
|
|
sub_folders: list[FolderSimple]
|
|
path: str
|
|
|
|
def find_sub_folder(self, name: str) -> FolderSimple | None:
|
|
_name = normalize("NFC", name).lower()
|
|
return next((x for x in self.sub_folders if x.name.lower() == _name), None)
|