콘텐츠로 이동

Principal 기반 권한 관리

SchemaRouter는 신뢰할 수 있도록 검증된 RBAC/ABAC 속성·권한 정보를 기능 검색 전에 적용하고, 실행 직전에 다시 검증할 수 있습니다.

SchemaRouter 자체가 identity provider가 되는 것은 아닙니다. 로그인, SSO, token 검증, group membership, HR directory의 정본은 host application이 담당하고, 검증된 결과만 PrincipalContext로 전달합니다.

사원, 관리자, 임원

from schemarouter import (
    AuthorizationPolicy,
    AuthorizationRule,
    PrincipalContext,
    RunConfig,
    SchemaRouter,
)

policy = AuthorizationPolicy(
    rules=(
        AuthorizationRule(
            name="employee-data",
            effect="allow",
            operation="employee_records.*",
            roles_any=("employee", "manager", "executive"),
        ),
        AuthorizationRule(
            name="manager-data",
            effect="allow",
            operation="management_metrics.*",
            roles_any=("manager", "executive"),
        ),
        AuthorizationRule(
            name="executive-data",
            effect="allow",
            operation="board_financials.*",
            roles_any=("executive",),
        ),
    )
)

router = SchemaRouter(authorization_policy=policy)

employee = PrincipalContext(subject="alice", roles=("employee",))
manager = PrincipalContext(subject="bob", roles=("manager",))
executive = PrincipalContext(subject="ceo", roles=("executive",))

Authorization policy를 설정한 router에서 principal 없이 planning/retrieval을 호출하면 fail-closed합니다.

employee_candidates = router.retrieve_authorized(
    "quarterly finance",
    principal=employee,
    k=5,
)

executive_candidates = router.retrieve_authorized(
    "quarterly finance",
    principal=executive,
    k=5,
)

권한이 없는 endpoint는 ranking 전에 제외되므로 downstream model에게 금지된 tool을 보여 준 뒤 실행 단계에서만 막는 구조가 아닙니다.

실행 직전에도 다시 확인합니다.

result = await router.execute(
    plan,
    config=RunConfig(principal=executive),
)

더 높은 권한의 principal로 만든 plan을 더 낮은 권한의 principal이 재사용할 수 없습니다.

부서, 팀, attribute

RBAC와 ABAC를 함께 사용할 수 있습니다.

AuthorizationRule(
    name="sales-apac-enterprise",
    effect="allow",
    operation="sales_pipeline.*",
    departments_any=("sales",),
    teams_any=("enterprise",),
    attributes=(("region", "apac"),),
)

서로 다른 선택 조건 범주는 AND(모두 충족), 동일한 *_any 선택 조건에 속한 값들은 OR(하나 이상 충족)로 평가합니다. 규칙은 선언 순서대로 평가하며 처음 일치한 규칙을 적용합니다.

First-match 우선순위와 policy lint

Runtime 평가는 선언 순서를 유지하며 첫 번째로 match한 authorization/data-scope rule이 적용됩니다. 따라서 configuration loader는 뒤의 rule이 명백히 도달 불가능한 broad-before- narrow 순서나 중복 matcher를 기본적으로 lint 오류로 처리합니다.

from schemarouter import parse_authorization_policy

policy = parse_authorization_policy(policy_json)  # 기본 lint=True

Lint는 중복 rule name, 동일 match condition, 안전하게 증명할 수 있는 shadowing을 진단합니다. 임의 wildcard predicate의 의미를 과도하게 추론하지 않으며, rule 이름/위치는 trusted configuration surface에만 제공합니다. Runtime의 authorization denial message는 계속 의도적으로 일반적인 형태를 유지합니다. lint=False는 기존 ordered policy를 마이그레이션할 때 host가 순서를 별도로 검토한 경우에만 사용해야 합니다.

Compiled lookup partition은 성능을 위해 무관한 rule을 건너뛸 수 있지만 원래 선언 순서와 first-match 결과는 바꾸지 않습니다.

기본 정책

AuthorizationPolicy는 기본 거부(deny-by-default) 방식입니다. 어떤 규칙에도 일치하지 않으면 해당 경로는 검색 결과에 노출되지 않으며 실행할 수도 없습니다.

authorization_policy를 설정하지 않은 기존 router는 principal 없이도 이전 동작을 그대로 유지합니다.

Non-disclosure 경계

권한 정책은 planning/retrieval의 추가 local availability predicate로 적용됩니다. 보이지 않는 endpoint는 "거절된 후보"로도 반환하지 않습니다. 기존 capability eligibility API 역시 host-invisible capability에는 설명을 반환하지 않습니다.

실행 단계의 authorization 오류도 어떤 role/department/team/attribute에서 실패했는지를 노출하지 않는 일반적인 denial 메시지를 사용합니다.

Database 권한

Database onboarding은 이 principal boundary 위에 구성합니다. DB adapter는 선택된 access scope에 허용된 table/column schema만 노출하고, row predicate는 trusted database invoker 내부에서 강제해야 합니다. Model이 전달하는 argument가 row predicate를 제거하거나 약화할 수 없어야 합니다.

Field, row, tenant, traversal 세부 scope

Capability authorization은 해당 principal이 endpoint 자체를 사용할 수 있는가를 결정합니다. Data-scope rule은 이미 허용된 endpoint가 실제로 보여 주거나 실행할 수 있는 데이터 범위를 더 좁힙니다.

from schemarouter import DataScopeRule, TrustedFilterBinding

policy = AuthorizationPolicy(
    rules=(
        AuthorizationRule(
            effect="allow",
            operation="company.employees.select",
            roles_any=("employee", "manager", "executive"),
        ),
    ),
    data_rules=(
        DataScopeRule(
            name="employee-row-scope",
            operation="company.employees.select",
            roles_any=("employee",),
            visible_fields=("id", "name"),
            trusted_filters=(
                TrustedFilterBinding(
                    field="department",
                    principal_value="attribute:department",
                ),
            ),
        ),
        DataScopeRule(
            name="manager-row-scope",
            operation="company.employees.select",
            roles_any=("manager",),
            visible_fields=("id", "name", "department", "salary"),
            trusted_filters=(
                TrustedFilterBinding(
                    field="department",
                    principal_value="attribute:department",
                ),
            ),
        ),
        DataScopeRule(
            name="executive-scope",
            operation="company.employees.select",
            roles_any=("executive",),
            visible_fields=("id", "name", "department", "salary"),
        ),
    ),
)

위 employee rule에서는 다음이 적용됩니다.

  • salary와 department는 scoring 전에 model-visible retrieval schema에서 제거됩니다.
  • trusted department predicate는 검증된 principal context에서 결정됩니다.
  • predicate는 trusted DB invoker 내부에서 강제되며 model argument가 제거하거나 약화할 수 없습니다.
  • 숨긴 field를 직접 요청하도록 위조한 stale plan도 실행 단계에서 fail-closed합니다.

TrustedFilterBinding은 subject, role, department, team, attribute:<name>에서 값을 가져올 수 있습니다. 필요한 attribute가 없으면 fail-closed합니다.

동일한 scope contract를 DB 계열별로 다음처럼 적용합니다.

데이터 계열 Scope 강제 방식
SQLite / SQLAlchemy RDB visible column + trusted equality/IN row predicate
Vector store visible result/metadata field + trusted filterable metadata predicate
Document/search/KV/time-series visible field + trusted exact-match predicate
Graph / RDF visible result field + 허용 relationship/predicate + 최대 hop depth

벡터 저장소 백엔드에서 신뢰된 테넌트 필터를 강제하려면 명시적인 filters 인자를 지원해야 합니다. 해당 필터를 실제로 강제할 수 없다면 필터 없는 검색으로 우회하지 않고 호출을 거부합니다.

Graph/RDF에서 relationship_types를 생략해도 접근 범위가 넓어지지 않습니다. Invoker는 principal에 허용된 relationship 집합을 기본값으로 사용하고, max_hops도 정책 상한으로 제한합니다.

보안 경계

데이터 범위 제한 규칙은 애플리케이션에서 볼 수 있는 범위를 더 좁힐 뿐, 데이터베이스 권한을 새로 부여하지 않습니다. DB-native role/grant/RLS/ACL, tenant credential, network boundary는 계속 최종 권한 경계이며 별도로 구성해야 합니다.

Data-scope rule도 선언 순서대로 첫 번째 match를 적용합니다. 넓은 rule은 사원/팀/부서별 구체적인 rule 뒤에 두는 것이 안전합니다.

Authorization audit event

Enterprise host는 opt-in trusted callback을 통해 authorization 결정을 관측할 수 있습니다. 기본 이벤트에는 raw principal claim이나 trusted-filter 값이 저장되지 않습니다.

from schemarouter import RunConfig, SchemaRouter

audit_events = []

router = SchemaRouter(
    authorization_policy=policy,
    authorization_audit_hook=audit_events.append,
)

result = await router.execute(
    plan,
    config=RunConfig(
        principal=employee,
        principal_audit_id="directory-user-7f3a",
    ),
)

AuthorizationAuditEvent에는 allow/deny 결과, rule/default 결정 출처, match한 rule 이름, data-scope rule 이름, visible field 개수, trusted-filter의 field 이름, graph scope 요약, tool/endpoint identity, phase, run ID가 포함됩니다. Host가 principal_audit_id를 제공하면 그 opaque identifier도 같은 이벤트에 포함됩니다.

Host가 run ID를 지정하지 않으면 runtime이 생성합니다. LangChain/LlamaIndex export는 export 결정과 실제 execution 결정에 같은 run ID를 재사용하므로, trusted audit sink에서 두 경계를 연결해 볼 수 있고 raw PrincipalContext를 넘길 필요는 없습니다.

기본값에서는 audit hook이 꺼져 있습니다. SchemaRouter는 subject, role, department, team, principal attribute, resolved trusted-filter 값을 자동 저장하지 않습니다. Host가 별도 audit sink에 추가 identity 정보를 기록한다면 그 sink 자체가 host의 trusted security boundary가 됩니다.