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_MIGRATIONS 에 ALTER 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/a | visible=True, raw |
| caller 매치 grant 0건 | n/a | visible=False (deny) |
| 매치 grant + condition False | 모두 False | visible=False (deny) |
| 매치 grant + condition True | 하나 True | visible=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 | 의미 |
|---|---|
hide | row visibility deny (condition 위반 또는 caller 매치 grant 없음) |
mask | row 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.py | M3 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 적용 순서)