Bot & Automation
jorgasisten
/root/hermes-projects/jorgasisten
apps/jorgasisten/services/access_control.py
text
import json
import re
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Literal
from jorgasisten.core.config import Settings, get_settings
from jorgasisten.services.text import safe_string
AccessRole = Literal["guest", "viewer", "operator", "admin"]
ROLE_GUEST: AccessRole = "guest"
ROLE_VIEWER: AccessRole = "viewer"
ROLE_OPERATOR: AccessRole = "operator"
ROLE_ADMIN: AccessRole = "admin"
PERSISTED_ROLES = {ROLE_VIEWER, ROLE_OPERATOR}
PASSWORD_HINT_RE = re.compile(
r"^(?:/login(?:@\w+)?|login|masuk|password(?:\s+akses)?|akses)\s*[:=]?\s+(.+)$",
re.IGNORECASE,
)
@dataclass(frozen=True)
class AccessSession:
role: AccessRole
authenticated_at: str
class AccessControlService:
def __init__(self, settings: Settings | None = None) -> None:
self.settings = settings or get_settings()
self.path = Path(self.settings.user_access_registry_path)
def get_role(self, user_id: int | str | None) -> AccessRole:
if user_id is None:
return ROLE_GUEST
# If user_id is a string that is a numeric admin ID, handle it
if isinstance(user_id, int) and user_id in self.settings.admin_ids:
return ROLE_ADMIN
if isinstance(user_id, str) and user_id.isdigit() and int(user_id) in self.settings.admin_ids:
return ROLE_ADMIN
state = self._load_state()
session = state["users"].get(str(user_id), {})
role = safe_string(session.get("role")).lower()
if role in PERSISTED_ROLES:
return role # type: ignore[return-value]
return ROLE_GUEST
def get_session(self, user_id: int | str | None) -> AccessSession | None:
role = self.get_role(user_id)
if role == ROLE_GUEST:
return None
if role == ROLE_ADMIN:
return AccessSession(role=ROLE_ADMIN, authenticated_at="-")
state = self._load_state()
payload = state["users"].get(str(user_id), {})
return AccessSession(
role=role,
authenticated_at=safe_string(payload.get("authenticated_at")) or current_timestamp(),
)
def authenticate(self, user_id: int | str | None, password: str) -> AccessRole:
if user_id is None:
return ROLE_GUEST
normalized_password = safe_string(password)
if not normalized_password:
return ROLE_GUEST
matched_role: AccessRole = ROLE_GUEST
if normalized_password == self.settings.operator_access_password and self.settings.operator_access_password:
matched_role = ROLE_OPERATOR
elif normalized_password == self.settings.viewer_access_password and self.settings.viewer_access_password:
matched_role = ROLE_VIEWER
if matched_role == ROLE_GUEST:
return ROLE_GUEST
state = self._load_state()
state["users"][str(user_id)] = {
"role": matched_role,
"authenticated_at": current_timestamp(),
}
self._save_state(state)
return matched_role
def logout(self, user_id: int | str | None) -> None:
if user_id is None:
return
state = self._load_state()
state["users"].pop(str(user_id), None)
self._save_state(state)
def _load_state(self) -> dict[str, Any]:
state: dict[str, Any] = {"users": {}}
if self.path.exists():
loaded = json.loads(self.path.read_text(encoding="utf-8"))
if isinstance(loaded, dict):
state.update(loaded)
users = state.get("users")
if not isinstance(users, dict):
state["users"] = {}
return state
def _save_state(self, state: dict[str, Any]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
payload = {"users": state.get("users", {})}
tmp_path = self.path.with_suffix(self.path.suffix + ".tmp")
tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
tmp_path.replace(self.path)
def extract_password_candidate(text: str) -> str:
normalized = safe_string(text)
if not normalized:
return ""
matched = PASSWORD_HINT_RE.match(normalized)
if matched:
return safe_string(matched.group(1))
return normalized
def is_explicit_access_attempt(text: str, settings: Settings | None = None) -> bool:
normalized = safe_string(text)
if not normalized:
return False
if PASSWORD_HINT_RE.match(normalized):
return True
active_settings = settings or get_settings()
configured_passwords = {
safe_string(active_settings.operator_access_password),
safe_string(active_settings.viewer_access_password),
}
configured_passwords.discard("")
return normalized in configured_passwords
def current_timestamp() -> str:
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
def is_authenticated_role(role: AccessRole) -> bool:
return role != ROLE_GUEST
def can_read_data(role: AccessRole) -> bool:
return role in {ROLE_VIEWER, ROLE_OPERATOR, ROLE_ADMIN}
def can_write_data(role: AccessRole) -> bool:
return role in {ROLE_OPERATOR, ROLE_ADMIN}
def can_delete_data(role: AccessRole) -> bool:
return role in {ROLE_OPERATOR, ROLE_ADMIN}
def can_manage_structure(role: AccessRole) -> bool:
return role in {ROLE_OPERATOR, ROLE_ADMIN}
def can_manage_spreadsheets(role: AccessRole) -> bool:
return role in {ROLE_OPERATOR, ROLE_ADMIN}