divid_teacher / admin_access_manager.py
Asem75's picture
Upload 2 files
98544d4 verified
Raw
History Blame Contribute Delete
17.2 kB
# admin_access_manager.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Divid Teacher — مدير دخول المدراء والصلاحيات.
- كلمات المرور تبقى في Hugging Face Secrets فقط.
- المدير الرئيسي: ADMIN_PANEL_KEY
- المدير 1: ADMIN_MANAGER_1_KEY
- المدير 2: ADMIN_MANAGER_2_KEY
- الصلاحيات تحفظ في system/admin_accounts.json بدون أي قيمة سرية.
- كل عملية حساسة تفحص الصلاحية على السيرفر.
"""
from __future__ import annotations
import copy
import hmac
import json
import os
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
VERSION = "admin_access_manager_v1_multi_admin_button_permissions"
SCHEMA_VERSION = "admin_access_schema_v1"
PERMISSIONS: Dict[str, str] = {
"dashboard.view": "عرض لوحة الإدارة",
"students.view": "عرض الطلاب والبحث فيهم",
"students.edit": "تعديل بيانات الطالب",
"students.status": "تغيير حالة حساب الطالب",
"students.reset_access_key": "تغيير مفتاح دخول الطالب",
"students.reset_parent_password": "إعادة ضبط كلمة مرور ولي الأمر",
"students.devices": "إدارة أجهزة الطالب",
"students.delete": "حذف حساب طالب",
"students.academic.view": "عرض الملف الأكاديمي للطالب",
"students.reports.view": "عرض تقارير الطالب",
"question_reports.view": "عرض بلاغات الأسئلة",
"question_reports.update": "تحديث حالة بلاغات الأسئلة",
"repositories.view": "عرض المستودعات وملفاتها",
"repositories.open": "فتح ومعاينة ملفات المستودع",
"repositories.download": "تنزيل ملفات المستودع",
"repositories.edit": "تحرير وحفظ الملفات النصية",
"repositories.upload": "رفع أو استبدال ملفات المستودع",
"repositories.delete": "حذف ملفات المستودع",
"questions.audit.scan": "فحص بنوك الأسئلة",
"questions.audit.create": "إنشاء أو استئناف مهمة تدقيق",
"questions.audit.run": "تشغيل Phi/Qwen لتدقيق الأسئلة",
"questions.audit.outputs": "عرض مخرجات وسجل التدقيق",
"questions.repair.view": "عرض الأسئلة المستبعدة وأسبابها",
"questions.repair.create": "إنشاء أو استئناف مهمة إصلاح",
"questions.repair.run": "تشغيل إصلاح الأسئلة وإعادة تدقيقها",
"system.snapshot": "عرض لقطة النظام",
"audit.view": "عرض سجل العمليات الإدارية",
"admins.view": "عرض المديرين والصلاحيات",
"admins.permissions.manage": "تعديل صلاحيات المديرين",
}
MANAGER_1_DEFAULT = [
"dashboard.view", "students.view", "students.academic.view",
"students.reports.view", "question_reports.view",
"repositories.view", "repositories.open", "repositories.download",
"questions.audit.scan", "questions.audit.create", "questions.audit.run",
"questions.audit.outputs", "questions.repair.view",
"questions.repair.create", "questions.repair.run", "system.snapshot",
]
MANAGER_2_DEFAULT = [
"dashboard.view", "students.view", "students.academic.view",
"students.reports.view", "question_reports.view",
]
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _text(value: Any) -> str:
return str(value if value is not None else "").strip()
class AdminAccessManager:
def __init__(self) -> None:
self.version = VERSION
self.schema_version = SCHEMA_VERSION
self._lock = threading.RLock()
system_folder = Path(os.getenv("SYSTEM_FOLDER", "system"))
system_folder.mkdir(parents=True, exist_ok=True)
self.accounts_file = system_folder / "admin_accounts.json"
self.audit_file = system_folder / "admin_access_audit.jsonl"
self._ensure_accounts_file()
def default_accounts(self) -> Dict[str, Any]:
return {
"schema_version": SCHEMA_VERSION,
"updated_at": _now(),
"accounts": [
{
"admin_id": "owner",
"display_name": "المدير الرئيسي",
"secret_name": "ADMIN_PANEL_KEY",
"enabled": True,
"permissions": ["*"],
"protected": True,
},
{
"admin_id": "manager_1",
"display_name": "مدير 1",
"secret_name": "ADMIN_MANAGER_1_KEY",
"enabled": True,
"permissions": list(MANAGER_1_DEFAULT),
"protected": False,
},
{
"admin_id": "manager_2",
"display_name": "مدير 2",
"secret_name": "ADMIN_MANAGER_2_KEY",
"enabled": True,
"permissions": list(MANAGER_2_DEFAULT),
"protected": False,
},
],
}
def _read_config(self) -> Dict[str, Any]:
try:
with self._lock:
if not self.accounts_file.exists():
return {}
value = json.loads(self.accounts_file.read_text(encoding="utf-8"))
return value if isinstance(value, dict) else {}
except Exception:
return {}
def _write_config(self, data: Dict[str, Any]) -> None:
safe = copy.deepcopy(data if isinstance(data, dict) else {})
safe["schema_version"] = SCHEMA_VERSION
safe["updated_at"] = _now()
for item in safe.get("accounts", []):
if not isinstance(item, dict):
continue
for forbidden in ("password", "key", "secret", "secret_value", "token"):
item.pop(forbidden, None)
with self._lock:
self.accounts_file.parent.mkdir(parents=True, exist_ok=True)
tmp = self.accounts_file.with_suffix(".tmp")
tmp.write_text(json.dumps(safe, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(self.accounts_file)
def _ensure_accounts_file(self) -> None:
if not self.accounts_file.exists():
self._write_config(self.default_accounts())
return
data = self._read_config()
if not isinstance(data, dict) or not isinstance(data.get("accounts"), list):
self._write_config(self.default_accounts())
return
accounts = data["accounts"]
by_id = {
_text(item.get("admin_id")): item
for item in accounts
if isinstance(item, dict) and _text(item.get("admin_id"))
}
changed = False
for default in self.default_accounts()["accounts"]:
if default["admin_id"] not in by_id:
accounts.append(copy.deepcopy(default))
changed = True
owner = next((x for x in accounts if isinstance(x, dict) and _text(x.get("admin_id")) == "owner"), None)
if owner is not None:
fixed = {
"secret_name": "ADMIN_PANEL_KEY",
"enabled": True,
"permissions": ["*"],
"protected": True,
}
for key, value in fixed.items():
if owner.get(key) != value:
owner[key] = copy.deepcopy(value)
changed = True
if changed:
data["accounts"] = accounts
self._write_config(data)
def _accounts(self) -> List[Dict[str, Any]]:
data = self._read_config()
return [
item for item in (data.get("accounts") or [])
if isinstance(item, dict) and _text(item.get("admin_id"))
]
def normalize_permissions(self, values: Iterable[Any]) -> List[str]:
result: List[str] = []
for value in values or []:
permission = _text(value)
if permission == "*":
return ["*"]
if permission in PERMISSIONS and permission not in result:
result.append(permission)
return result
def _public_account(self, account: Dict[str, Any]) -> Dict[str, Any]:
secret_name = _text(account.get("secret_name"))
return {
"admin_id": _text(account.get("admin_id")),
"display_name": _text(account.get("display_name") or account.get("admin_id")),
"secret_name": secret_name,
"secret_configured": bool(_text(os.getenv(secret_name, ""))) if secret_name else False,
"enabled": bool(account.get("enabled", True)),
"permissions": self.normalize_permissions(account.get("permissions") or []),
"protected": bool(account.get("protected", False)),
}
def public_config(self) -> Dict[str, Any]:
return {
"success": True,
"version": self.version,
"schema_version": self.schema_version,
"permissions": dict(PERMISSIONS),
"accounts": [self._public_account(x) for x in self._accounts()],
}
def authenticate(self, password: Any) -> Dict[str, Any]:
supplied = _text(password)
if not supplied:
return {"success": False, "authorized": False, "message": "أدخل كلمة مرور المدير.", "version": self.version}
configured = 0
for account in self._accounts():
if not bool(account.get("enabled", True)):
continue
secret_name = _text(account.get("secret_name"))
expected = _text(os.getenv(secret_name, "")) if secret_name else ""
if not expected:
continue
configured += 1
if hmac.compare_digest(supplied, expected):
public = self._public_account(account)
self._audit("admin.login", actor=public["admin_id"], success=True)
return {
"success": True,
"authorized": True,
"admin": public,
"admin_id": public["admin_id"],
"display_name": public["display_name"],
"permissions": public["permissions"],
"version": self.version,
}
self._audit("admin.login_failed", actor="unknown", success=False)
return {
"success": False,
"authorized": False,
"message": "كلمة مرور المدير غير صحيحة." if configured else "لا يوجد Secret إداري مضبوط. اضبط ADMIN_PANEL_KEY على الأقل.",
"version": self.version,
}
def has_permission(self, admin: Dict[str, Any], permission: str) -> bool:
permissions = admin.get("permissions") or [] if isinstance(admin, dict) else []
return "*" in permissions or _text(permission) in permissions
def guard(self, password: Any, permission: str, action: str = "", target: str = "") -> Dict[str, Any]:
auth = self.authenticate(password)
if not auth.get("success"):
return auth
admin = auth.get("admin") or {}
if not self.has_permission(admin, permission):
self._audit(action or "permission.denied", actor=_text(admin.get("admin_id")), target=target, success=False, details={"permission": permission})
return {
"success": False,
"authorized": False,
"permission": permission,
"admin_id": admin.get("admin_id", ""),
"display_name": admin.get("display_name", ""),
"message": "هذا المدير لا يملك صلاحية: " + PERMISSIONS.get(permission, permission),
"version": self.version,
}
return {
"success": True,
"authorized": True,
"permission": permission,
"admin": admin,
"admin_id": admin.get("admin_id", ""),
"display_name": admin.get("display_name", ""),
"version": self.version,
}
def list_admins(self) -> Dict[str, Any]:
data = self.public_config()
return {"success": True, "admins": data["accounts"], "permissions": data["permissions"], "version": self.version}
def get_admin(self, admin_id: Any) -> Dict[str, Any]:
wanted = _text(admin_id)
for item in self._accounts():
if _text(item.get("admin_id")) == wanted:
return {"success": True, "admin": self._public_account(item), "version": self.version}
return {"success": False, "message": "المدير غير موجود.", "version": self.version}
def update_admin_profile(
self,
actor_password: Any,
target_admin_id: Any,
display_name: Any = "",
enabled: bool = True,
permissions: Optional[Iterable[Any]] = None,
) -> Dict[str, Any]:
auth = self.guard(actor_password, "admins.permissions.manage", action="admins.permissions.update", target=_text(target_admin_id))
if not auth.get("success"):
return auth
target_id = _text(target_admin_id)
data = self._read_config()
accounts = data.get("accounts") or []
target = next((x for x in accounts if isinstance(x, dict) and _text(x.get("admin_id")) == target_id), None)
if target is None:
return {"success": False, "message": "المدير المطلوب غير موجود.", "version": self.version}
if _text(display_name):
target["display_name"] = _text(display_name)
if target_id == "owner":
target["enabled"] = True
target["permissions"] = ["*"]
target["secret_name"] = "ADMIN_PANEL_KEY"
target["protected"] = True
else:
target["enabled"] = bool(enabled)
target["permissions"] = self.normalize_permissions(permissions or [])
data["accounts"] = accounts
self._write_config(data)
actor = auth.get("admin") or {}
self._audit("admins.permissions.updated", actor=_text(actor.get("admin_id")), target=target_id, success=True, details={"enabled": target.get("enabled"), "permissions": target.get("permissions")})
return {"success": True, "message": "تم حفظ صلاحيات المدير.", "admin": self._public_account(target), "version": self.version}
def permission_choices(self) -> List[tuple]:
return [(label, permission) for permission, label in PERMISSIONS.items()]
def _audit(self, action: str, actor: str = "", target: str = "", success: bool = True, details: Optional[Dict[str, Any]] = None) -> None:
event = {"created_at": _now(), "action": _text(action), "actor": _text(actor) or "unknown", "target": _text(target), "success": bool(success), "details": details if isinstance(details, dict) else {}, "version": self.version}
try:
with self._lock:
self.audit_file.parent.mkdir(parents=True, exist_ok=True)
with self.audit_file.open("a", encoding="utf-8") as file:
file.write(json.dumps(event, ensure_ascii=False) + "\n")
except Exception:
pass
def log_authorized_action(self, password: Any, permission: str, action: str, target: str = "", details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
auth = self.guard(password, permission, action=action, target=target)
if not auth.get("success"):
return auth
admin = auth.get("admin") or {}
self._audit(action, actor=_text(admin.get("admin_id")), target=target, success=True, details=details)
return auth
def summary(self) -> Dict[str, Any]:
admins = self.list_admins().get("admins") or []
return {
"success": True,
"ready": True,
"version": self.version,
"schema_version": self.schema_version,
"admins": len(admins),
"configured_secrets": sum(1 for x in admins if x.get("secret_configured")),
"permission_count": len(PERMISSIONS),
"passwords_saved_to_file": False,
"owner_secret": "ADMIN_PANEL_KEY",
"manager_1_secret": "ADMIN_MANAGER_1_KEY",
"manager_2_secret": "ADMIN_MANAGER_2_KEY",
}
_MANAGER: Optional[AdminAccessManager] = None
_MANAGER_LOCK = threading.Lock()
def get_manager() -> AdminAccessManager:
global _MANAGER
if _MANAGER is None:
with _MANAGER_LOCK:
if _MANAGER is None:
_MANAGER = AdminAccessManager()
return _MANAGER
def authenticate_admin(password: Any) -> Dict[str, Any]:
return get_manager().authenticate(password)
def summary() -> Dict[str, Any]:
return get_manager().summary()