"""M02 층 권한 한 벌 — 나무 · 항목(`Router_Items`) · 양식 · 남의 층 목록(`Router_Layers`)이 함께 씀 (PLAN 27-1c). 화면 쪽 짝 = `M02_MasterTemplete_Layer.ts` 의 `accessFor`. 「회사 C 를 맡음」(`manages` · 39-6) = SYSTEM_ADMIN · 또는 C 의 회사 관리자(ADMIN · 마스터). system 읽기 = 로그인 누구나 · 수정 = SYSTEM_ADMIN company {C} 읽기 = C 사람 · SYSTEM_ADMIN · 수정 = C 를 맡은 사람 personal {C}/{U} 본인 = 읽기 · 수정 · 남 = C 를 맡은 사람만 읽기 · 수정(일반 = 403) owner 글 없음 = 자기 층 · 남의 층 읽기는 그 층 폴더가 있을 때만(없으면 404) · 읽기로 남의 층에 뼈대를 안 만듦(쓰기는 부르는 쪽이 소속을 본 뒤 `folder(…, create=True)`). """ from __future__ import annotations import re from dataclasses import dataclass from pathlib import Path from typing import Any from fastapi import HTTPException from M02_MasterTemplete import M02_Template_Layers as layers NO_COMPANY = "회사부터 등록 — 회사 없이는 회사 · 개인 구조물 집계표를 만들 수 없음" _SHAPE = {"company": re.compile(r"^\d+$"), "personal": re.compile(r"^\d+/\d+$")} def is_system_admin(session: dict[str, Any]) -> bool: return session.get("role") == "SYSTEM_ADMIN" def is_company_admin(session: dict[str, Any]) -> bool: return session.get("role") in ("ADMIN", "SYSTEM_ADMIN") or bool(session.get("is_master")) def manages(session: dict[str, Any], company: Any) -> bool: """회사 `company` 를 맡음 — 그 회사 층 · 그 회사 모든 개인 층을 보고 고침.""" if is_system_admin(session): return True own = session.get("company_id") return own is not None and str(own) == str(company) and is_company_admin(session) @dataclass(frozen=True) class Grant: """층 한 칸의 권한 — `owner` = 층 주인 글(시스템 None) · `mine` = 자기 층(뼈대 만들어도 됨).""" layer: str owner: str | None write: bool mine: bool def own_owner(session: dict[str, Any], layer: str, company: Any = None) -> str | None: """자기 층 주인 글 — 회사 없으면 None(`company` = 프로젝트 회사로 바꿔 끼울 때).""" company = session.get("company_id") if company is None else company if company is None: return None return str(company) if layer == "company" else f"{company}/{session.get('user_id')}" def grant(session: dict[str, Any], layer: str, owner: str | None = None) -> Grant: """읽기 권한이 없으면 403 · 회사 없는 사람의 자기 층은 409 · 모양이 틀리면 400.""" if layer == "system": return Grant(layer, None, is_system_admin(session), True) if layer not in _SHAPE: raise HTTPException(status_code=404, detail="없는 층입니다.") own = own_owner(session, layer) who = own if owner in (None, "") else str(owner) if who is None: raise HTTPException(status_code=409, detail=NO_COMPANY) if not _SHAPE[layer].match(who): raise HTTPException(status_code=400, detail=f"층 주인 모양이 틀림 「{who}」") company = who.split("/")[0] if who == own: return Grant(layer, who, layer == "personal" or manages(session, company), True) if not manages(session, company): raise HTTPException( status_code=403, detail="다른 회사 · 다른 사람의 구조물 집계표는 볼 수 없음" ) return Grant(layer, who, True, False) def require( session: dict[str, Any], layer: str, owner: str | None = None, *, write: bool = False ) -> Grant: """`grant` + 쓰기 권한까지 — 까닭은 층마다.""" got = grant(session, layer, owner) if write and not got.write: if layer == "system": detail = "마스터 템플릿은 시스템 관리자만 고침." else: detail = "회사용은 회사 관리자만 고침 — 개인용으로 복사해서 씀." raise HTTPException(status_code=403, detail=detail) return got def folder(got: Grant, create: bool = False) -> Path: """회사 · 개인 층 폴더 — 남의 층은 그 층 폴더가 있어야 함 · `create` = 쓰기(부르는 쪽이 소속을 본 뒤 뼈대를 만듦).""" if got.owner is None: raise HTTPException(status_code=409, detail=NO_COMPANY) try: if got.layer == "company": path = layers.company_dir(got.owner) else: company, user = got.owner.split("/", 1) path = layers.personal_dir(company, user) except ValueError as error: raise HTTPException(status_code=400, detail=f"층 주인 모양 — {error}") from error if not (got.mine or create) and not path.is_dir(): raise HTTPException(status_code=404, detail="그 층은 아직 만든 것이 없음") return path