본문으로 건너뛰기

Ontology Instance Row-Level Mask — M4 Step 2

본 가이드는 Epic #1246 M4 Step 2 에서 도입된 인스턴스 단위 ABAC 를 다룹니다. M3 Step 1 은 Class 단위 allow/deny 만 평가했지만, 본 Step 에서 list endpoint 결과 row 마다 condition_expr 평가 + mask_columns 컬럼 redact.

Schema 변경

OntologyClassGrant 에 신규 컬럼:

class OntologyClassGrant(Base):
# ... 기존 필드 ...
condition_expr: Mapped[str | None] = mapped_column(Text, nullable=True)
# M4 Step 2 (#1248)
mask_columns: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)

_COLUMN_MIGRATIONSALTER TABLE ontology_class_grants ADD COLUMN mask_columns JSONB 추가 — 기존 prod DB 도 idempotent 마이그레이션.

NULL 또는 빈 list [] 면 mask 없음 (backwards compat 보장).

Engine

evaluate_instance_visibility(db, class_row, caller, instance, *, action='read')

단일 instance:

grant 상태condition_expr결과
0건n/avisible=True, raw
caller 매치 grant 0건n/avisible=False (deny)
매치 grant + condition False모두 Falsevisible=False (deny)
매치 grant + condition True하나 Truevisible=True + mask_columns 적용

filter_instances(db, class_row, instances, caller, *, action='read')

list endpoint 용 — 단일 grant 쿼리 + 메모리 평가 (N+1 회피).

evaluate_instance_visibility 가 instance 마다 grant 쿼리하는 N+1 회피.

라우터 통합

  • GET /api/v1/ontology/instances/{class_id} (list) — 결과 row 마다 filter_instances 적용 → 통과 행만 + masked 컬럼 응답
  • GET /api/v1/ontology/instances/{class_id}/{key} (single) — evaluate_instance_visibility 적용 → 통과 시 masked_instance 반환, deny 시 403

Class-level ABAC (_enforce_class_abac) 는 instance context 없으므로 condition_expr 평가 deferred — subject+action 매치만으로 통과, row 단위 평가는 위 함수가 책임. M3 Step 1 의 evaluate_class_access(instance=None) 동작 정정.

Mask placeholder

MASK_PLACEHOLDER = "***MASKED***"

UI 가 redact 표시. 컬럼이 instance dict 에 존재하지 않으면 mask 시도 skip (원래 없는 필드를 추가하지 않음).

메트릭

gend_ontology_row_mask_total{class_name, action} (Counter):

action의미
hiderow visibility deny (condition 위반 또는 caller 매치 grant 없음)
maskrow visible + 1개 이상 컬럼 redact

silent emit — prometheus_client import 실패해도 핸들러 정상 동작.

예시

Grant 정의

-- viewer 는 INFO/WARN severity 만 보고, pii_email 은 redact.
INSERT INTO ontology_class_grants
(layer, class_id, subject_id, action, condition_expr, mask_columns)
VALUES (
'L2', '<equipment-uuid>', 'viewer', 'read',
'instance.severity in [''INFO'', ''WARN'']',
'["pii_email"]'::jsonb
);

viewer 응답

[
{"_key": "T-001", "severity": "INFO", "pii_email": "***MASKED***", "tag": "valve-1"},
{"_key": "T-003", "severity": "WARN", "pii_email": "***MASKED***", "tag": "valve-2"}
]

severity=CRITICAL row 는 결과에서 제외 (hide). 모든 visible row 의 pii_email 은 redact.

회귀 가드

파일범위
apps/api/tests/test_ontology_row_mask.py_COLUMN_MIGRATIONS 등록 + ORM 직렬화 + evaluate_instance_visibility (no_grant/condition pass+mask/condition fail/other subject) + filter_instances (no_grant/mixed/empty) + 라우터 통합 (list + single) — 11
apps/api/tests/test_ontology_abac.pyM3 Step 1 회귀 유지 — Class-level instance=None 호출 deferred 정합

관련

  • Epic: #1246 M4
  • 의존: M3 Step 1 (#1233) ABAC engine
  • 후속: M4 Step 3 (LNG e2e — row mask 동작 검증), M4 Step 4 (combined fall-through)
  • 메모리: [[feedback_abac_column_mask_order]] (column 적용 순서)