Boogy 인증 (서비스 내부 인증)
Boogy의 인증은 세 계층으로 구성됩니다. 그중 어떤 것도 직접 구현하지 마십시오 — SDK는 보안 불변성을 유지하는 검증된 헬퍼를 제공합니다.
세 계층 모델
- 수신 승인(Ingress admits) — 플랫폼의 수신 모드는 요청이 서비스에 도달할지 여부를 결정합니다. 코드가 실행될 때쯤이면 호출자는 이미 알려진 주체(principal) 이거나 익명 상태입니다.
- 가드가 라우트를 보호(Guards gate routes) — 라우트별 검사가 핸들러 전에 실행됩니다: 로그인 요구 또는 요청 리소스의 소유권 요구.
- 헬퍼가 행을 스코핑(Helpers scope rows) — 핸들러는 호출자의 행만 나열/로드합니다.
호출자는 auth::current_principal() -> Option<String>로 확인합니다.
주체는 불투명합니다 — 절대 파싱, 접두사 제거, UUID 추측을 하지 말고; 소유자 열 값과 auth::*의 입력으로만 사용하십시오.
라우트별 수신: 제한된 서비스 내의 공개 라우트
1계층(수신)은 일반적으로 서비스 전체에 하나의 mode입니다. 하지만 때때로 하나의 라우트는 서비스의 나머지가 거부하는 호출자에게도 접근 가능해야 합니다 — 전형적인 예는 공개 /webhook 수신기를 나머지 서비스가 authenticated(또는 internal/mixed)인 경우입니다. 라우트별 재정의(per-route override)로 분리하십시오; 서비스의 나머지는 제한된 상태로 유지됩니다:
[ingress]
mode = "authenticated" # 서비스 전체 기본값 — 그 외 모든 것
[[ingress.routes]]
path = "/webhook" # 서비스 상대 경로; 모든 메서드
mode = "public" # 누구나 /webhook에 접근 가능
보안 계약 (읽으십시오 — 실패 시 폐쇄(fail-closed)입니다):
| 규칙 | 중요성 |
|---|---|
서비스 상대 리터럴 경로. 정확히 일치(/webhook) 또는 접두사(/stripe/*, 세그먼트 경계)여야 합니다. {param} 캡처 불가. | path는 라우터가 보는 경로이며, 핸들러와 동일한 프레임입니다. |
| 가장 구체적인 일치가 승리하며; 정확히 하나의 모드가 적용됩니다. 정확히 일치가 접두사보다 우선; 긴 접두사가 짧은 것보다 우선. | 공개 하위 라우트는 형제 라우트를 절대 넓힐 수 없습니다: /webhook 공개가 /data를 느슨하게 하지 않습니다. |
기본값 = 서비스 전체 모드. 일치하지 않는 경로(또는 .. 탐색 시도)는 제한된 기본값으로 폴백되며, 절대 가장 관대한 라우트로 폴백되지 않습니다. | 라우트 나열을 잊어도 보호된 상태로 남고, 노출되지 않습니다. |
각 재정의는 자체 모드 + 허용 목록을 가집니다. 재정의는 allowed_agents/allowed_origins를 상속하지 않습니다. | 라우트 블록에 나열하십시오; 동일한 비어 있지 않음 검증이 적용되므로 오타가 조용히 모두 거부(deny-all)할 수 없습니다. |
메서드별이 아닌 경로별입니다(현재). path = "/webhook"는 GET, POST 등에 동일하게 적용됩니다. | 공개 /webhook은 모든 동사로 접근 가능합니다 — 따라서 여전히 핸들러 내에서 요청을 검증하십시오(잘못된 GET은 아무것도 하지 않아야 합니다). |
| 에지에서 호스트가 강제합니다. 수신은 wasm이 인스턴스화되기 전에 실행됩니다. | 코드에서 공개 라우트를 자체 게이트하지 않습니다; 하지만 공개 라우트는 누구나 접근할 수 있음을 의미합니다 — 다른 방식(예: HMAC 서명, boogy:boogy-webhooks 참조)으로 인증하십시오. |
| 위임 게이트 + 속도 제한은 서비스 전체로 유지됩니다. | 공개 분할은 [ingress.delegation] 게이트를 우회할 수 없으며 속도 제한 버킷을 공유합니다. |
[[ingress.routes]]가 없는 매니페스트는 이전과 정확히 동일하게 작동합니다 — 이는 순전히 추가 기능입니다.
반대: 개방된 서비스 내의 제한된 서브트리 (소유자 전용 /admin)
반대 방향 — 기본값이 개방된(authenticated) 서비스이지만 /admin/* 서브트리는 서비스 소유자(프로비저너)만 접근할 수 있어야 합니다. 함정: 프로비저닝 가능한 모듈은 누구나 배포하므로, 매니페스트에 신원(@alice, boogy://alice/services/*)을 하드코딩해서는 안 됩니다 — 그 리터럴 소유자는 다른 모든 프로비저너에게 잘못된 값이며, 수신 허용 목록 문자열은 배포 시 대체되지 않습니다. 수신에는 "나와 같은 소유자" 매처가 없습니다.
게이트가 없는 caller_is_service_owner() 기능을 사용하십시오 — 호스트가 호출자가 이 서비스의 소유자인지 증명합니다(에이전트 토큰, 에이전트 레지스트리에 대해 호스트 측에서 확인, 또는 그들의 자신의 워크로드 중 하나). 라우트별 수신이나 하드코딩된 신원이 필요 없습니다:
[ingress]
mode = "authenticated" # 모든 라우트; 핸들러가 /admin을 자체 게이트
// require_operator(): 호스트 증명, 하드코딩 없음.
fn require_operator() -> Result<(), ApiError> {
if caller_is_service_owner() { return Ok(()); } // 소유자: 그들의 에이전트 또는 자신의 워크로드
// OBO: 사용자를 대신하는 소유자의 백엔드 — 증명된 행위자는 워크로드입니다.
let id = current_identity();
let actor_owner = id.and_then(|i| i.actor).and_then(workload_owner);
if actor_owner == Some(self_identity().owner) { return Ok(()); }
Err(ApiError::forbidden("operator only"))
}
이것이 올바른 원시 요소인 이유: 인간 소유자는 자신의 토큰으로 /admin을 직접 curl할 수 있습니다(wasm은 에이전트의 핸들을 확인할 수 없지만 호스트는 가능 — 이것이 기능이 하는 일입니다), 그리고 소유자의 백엔드는 워크로드로 작동합니다. 실패 시 폐쇄: 익명, 다른 소유자, 또는 확인 불가능한 호출자 → false. (resend-base 카탈로그 모듈 참조). 이전의 "internal + 동일 소유자 워크로드" 패턴도 작동하지만 직접 인간 curl을 제외합니다 — 소유자 전용 표면에는 caller_is_service_owner를 선호하십시오.
가드 및 헬퍼 빠른 참조 (검증됨)
| 항목 | 사용 |
|---|---|
auth::required() -> Guard | 익명이면 401. 컬렉션 라우트(list, create)에 사용. |
auth::owns_resource(table, owner_col, id_param) -> Guard | 항목 라우트(GET/DELETE /things/{id}). 행을 로드하고, 누락되었거나 당신 것이 아니면 404, req.ctx에 저장. .slot("name")으로 여러 로드. |
auth::find_owned(table, owner_col) -> Result<Vec<Row>, _> | 인덱스 엔드포인트를 위한 주체 범위 목록. 익명이면 401. |
auth::load_owned(table, owner_col, id) -> Result<Option<Row>, _> | MCP/JSON-RPC를 위한 단일 로드 + 소유권 확인(경로가 아닌 본문에 id). None = 누락 또는 사용자 소유 아님. |
auth::require_scope(scope) -> Guard | 광범위한 기능 게이트: 익명이면 401, 로그인했지만 스코프가 없으면 403. |
소유자 열은 항상 DEFAULT_OWNER_COL ("owner_principal") 입니다 — 절대 owner_id/created_by를 만들지 마십시오. 항목 라우트를 .group([auth::owns_resource("things", DEFAULT_OWNER_COL, "id")], |g| …)로 연결하십시오. 해당 가드 뒤의 핸들러는 ctx에 저장된 행을 읽습니다 — 다시 가져오지 마십시오.
토큰에서 소유자 규칙: 생성 시, 소유자 열을 auth::current_principal()에서 스탬프하십시오. 요청 본문에서 소유자를 절대 읽지 마십시오 — 클라이언트가 다른 사용자의 ID를 주장할 수 있습니다.
철칙: 존재 여부에 의한 거부 마스킹(deny-by-existence-mask)
누락된 것과 존재하지만 사용자 것이 아닌 것 모두 404를 반환 — 절대 403을 반환하지 않음. 플랫폼 보안 불변성이며, 스타일 선택이 아닙니다. 403 대 404 분할은 열거 오라클입니다: 인증된 사용자가 ID를 조사하여 어떤 것이 존재하는지(403)와 존재하지 않는지(404)를 알게 됩니다. SDK 가드가 이를 강제합니다.
"하지만 UI가 사용자에게 이유를 알려주고 싶습니다." 클라이언트 측에서 하십시오: 404를 통일적으로 "사용자에게 제공되지 않음"으로 처리하고 그 메시지를 렌더링하십시오; 와이어 상태는 404로 유지됩니다. SDK 가드를 포크하거나 패치하여 마스크를 분할하지 마십시오. (require_scope의 403은 다른 축 — 리소스 소유권이 아닌 광범위한 기능 — 이며 올바릅니다.)
프로그래매틱 호출자를 위한 API 키 (검증된 레시피)
wit_glue! 옆에 api_keys_glue!(bindings)를 호출한 다음:
init_tables에서api_key_routes::install_table().ApiKeyRoutes확장 트레이트를 통해 관리 라우트 마운트:Router::new().with_api_key_routes()(/_keys) 또는.with_api_key_routes_at("/admin/keys").- 라우트를 게이트:
.group([api_key_routes::guard], |g| ...).
| 사실 | 세부 사항 |
|---|---|
| 이중 자격 증명 | guard는 세션 베어러 또는 sk_* 키를 모두 허용; 둘 다 current_principal()로 통합되므로 owns_resource/find_owned가 변경 없이 작동. |
| 키 관리 | 세션 신원이 필요함; sk_* 키는 키를 생성할 수 없음. |
| 에스컬레이션 없음 | 키는 발급자가 이미 보유한 스코프만 전달함(그렇지 않으면 403). |
| 저장소 | 키는 서비스 자체 저장소에 해시되어 저장됨; 절대 자체 테이블을 만들지 마십시오. |
| 형식 | sk_<env>_<…>_<crc>. |
위험 신호(Red flags)
| 생각 | 현실 |
|---|---|
| "'당신 것이 아님'에 403을 반환하면 사용자에게 이유를 알려줍니다." | ID 열거 오라클입니다. 둘 다 404를 반환하고 클라이언트 측에서 설명하십시오. |
| "핸들러에서 행을 로드한 후 소유권을 확인하겠습니다." | owns_resource는 하나의 가드에서 로드 + 확인 + ctx 저장을 수행합니다. 저장된 행을 읽고 확인을 재구현하지 마십시오. |
| "사용자 정의 api_keys 테이블을 추가하겠습니다." | api_keys_glue!는 해시되고, 격리되며, 스코프 인식 테이블을 제공합니다. 사용하십시오. |
| "요청 본문에서 소유자 ID를 읽습니다." | current_principal()에서 스탬프하십시오. 본문은 공격자가 제어합니다. |
"공개 [[ingress.routes]] 라우트에는 여전히 wasm 내 인증 검사가 필요합니다." | 공개는 누구나 접근 가능함을 의미합니다 — 다른 방식(웹훅의 경우 HMAC 서명)으로 인증하십시오. 재정의가 자체 게이트하지 않습니다. |
통합
← boogy:designing-boogy-services가 계층 1을 공급하는 수신 모드를 선택합니다. boogy:boogy-account-auth는 주체가 오는 곳입니다(로그인 → 읽는 토큰). 사용자를 대신하여 서비스 간에 행동하는 방법은 boogy:boogy-obo-delegation을 참조하십시오. 서명된 콜백을 위한 공개 라우트별 분할은 boogy:boogy-webhooks의 전반부입니다.