- 층 권한 한 벌 M02_Layer_Access — 나무 · 항목(Router_Items) · 양식(Router_Layers) 같은 규칙 · owner 인자 · 남의 층은 읽기만 · 읽기로 남의 층에 뼈대 안 씀
- 남의 층 목록 GET /api/m02/layers/{company|personal}/owners — 시스템 관리자 회사 목록 · 회사 관리자 같은 회사 사람
- ?layer= 페이지 분리 폐기 → 한 화면 뿌리 셋(마스터 · 회사 · 개인) · 뿌리 부품 TreePanel_Roots(펼칠 때 불러옴) · TreePanel 은 owner · api 를 받는 공용 부품
- 진입 해시 ?open=구조물조합|일위대가|옵션|도면&root=&owner= · 항목 고르기 [관리 화면에서 고치기]도 이 해시
- 「항목 나무」 → 「구조물 라이브러리」
- 시험 test_m02_layer_access 새로 · layer_screen · template_layers 새 규칙으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
4.6 KiB
Python
108 lines
4.6 KiB
Python
"""M02 층 권한 한 벌 — 나무 · 항목(`Router_Items`) · 양식 · 남의 층 목록(`Router_Layers`)이 함께 씀
|
|
(PLAN 27-1c). 화면 쪽 짝 = `M02_MasterTemplete_Layer.ts` 의 `accessFor`.
|
|
|
|
system 읽기 = 로그인 누구나 · 수정 = SYSTEM_ADMIN
|
|
company {C} 자기 회사 = 읽기 누구나 · 수정 = 회사 관리자(ADMIN · 마스터 · SYSTEM_ADMIN) ·
|
|
남의 회사 = SYSTEM_ADMIN 읽기만
|
|
personal {C}/{U} 본인 = 읽기 · 수정 · 같은 회사 남 = 회사 관리자 읽기만 ·
|
|
다른 회사 = SYSTEM_ADMIN 읽기만
|
|
owner 글 없음 = 자기 층 · 남의 층은 그 층 폴더가 있을 때만(없으면 404) ·
|
|
읽기로 남의 층에 뼈대를 안 만듦.
|
|
"""
|
|
|
|
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"))
|
|
|
|
|
|
@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}」")
|
|
if who == own:
|
|
return Grant(layer, who, layer == "personal" or is_company_admin(session), True)
|
|
same_company = who.split("/")[0] == str(session.get("company_id"))
|
|
colleague = layer == "personal" and same_company and is_company_admin(session)
|
|
if not (is_system_admin(session) or colleague):
|
|
raise HTTPException(
|
|
status_code=403, detail="다른 회사 · 다른 사람의 구조물 집계표는 볼 수 없음"
|
|
)
|
|
return Grant(layer, who, False, 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 = "마스터 템플릿은 시스템 관리자만 고침."
|
|
elif got.mine:
|
|
detail = "회사용은 회사 관리자만 고침 — 개인용으로 복사해서 씀."
|
|
else:
|
|
detail = "남의 층은 읽기만."
|
|
raise HTTPException(status_code=403, detail=detail)
|
|
return got
|
|
|
|
|
|
def folder(got: Grant) -> Path:
|
|
"""회사 · 개인 층 폴더 — 남의 층은 그 층 폴더가 있어야 함(부르는 쪽은 자기 층만 뼈대를 만듦)."""
|
|
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 and not path.is_dir():
|
|
raise HTTPException(status_code=404, detail="그 층은 아직 만든 것이 없음")
|
|
return path
|