package handler import ( "html" "os" "strings" "github.com/gofiber/fiber/v2" ) type RPManifestHandler struct{} const rpObjectLookupMermaid = `flowchart TD A[RP request] --> B{obj_id supplied?} B -->|yes| C[Normalize object type and obj_id] B -->|no| D{Route has client_id?} D -->|yes| E[obj_id = RelyingParty:] D -->|no| F{Route has tenant_id?} F -->|yes| G[obj_id = Tenant:] F -->|no| H[Reject: explicit obj_id required] C --> I[Check Keto relation] E --> I G --> I I --> J{allowed?} J -->|yes| K[Inject trusted Baron headers] J -->|no| L[Reject request] K --> M[Write audit with obj_id, relation, client_id, X-Request-Id]` const rpExternalKeyMermaid = `flowchart TD A[User authenticates through Baron SSO] --> B[Baron resolves internal identity] B --> C[Baron derives or loads Baron-issued alias] C --> D[Baron injects X-Baron-External-Key] D --> E[Baron injects X-Baron-Subject] E --> I[RP receives trusted headers from Baron gateway] I --> F[RP upserts local user with provider + X-Baron-External-Key] F --> G[RP stores the full external key as opaque value] G --> H[RP never parses or stores raw kratos_identity_id]` func NewRPManifestHandler() *RPManifestHandler { return &RPManifestHandler{} } func (h *RPManifestHandler) GetJSON(c *fiber.Ctx) error { c.Set(fiber.HeaderCacheControl, "public, max-age=300") return c.JSON(buildRPManifest(c)) } func (h *RPManifestHandler) GetSchema(c *fiber.Ctx) error { c.Set(fiber.HeaderCacheControl, "public, max-age=300") return c.JSON(rpManifestSchema()) } func (h *RPManifestHandler) GetHTML(c *fiber.Ctx) error { manifest := buildRPManifest(c) issuer, _ := manifest["issuer"].(string) c.Set(fiber.HeaderCacheControl, "public, max-age=300") c.Type("html", "utf-8") return c.SendString(` Baron RP IAM Manifest

Baron RP IAM Manifest

외부 RP가 Baron SSO/Ory Stack/Keto 기반 공용 IAM을 연동하기 위한 공개 규격입니다.

Issuer

` + html.EscapeString(issuer) + `

Identity Contract

용도Header정책
Keto subjectX-Baron-SubjectUser:<baron_identity_id> 전체 문자열을 opaque subject로 취급합니다.
RP upsert keyX-Baron-External-KeyBaron-issued alias입니다. RP가 만들거나 제출하지 않고, Baron이 주입한 전체 문자열을 local user external key로 저장합니다.
RP clientX-Baron-Client-ID현재 접근 중인 RP client id입니다.

External Key Flow

X-Baron-External-Key는 RP 입력값이 아니라 Baron이 인증된 subject에서 발급/조회해 주입하는 opaque alias입니다. RP upserts local user from the Baron-issued alias.

` + "```mermaid\n" + html.EscapeString(rpExternalKeyMermaid) + "\n```" + `

Object Lookup

check(User:abc, viewers, RelyingParty:<client_id>)
check(User:abc, members, Tenant:<tenant_id>)
check(User:abc, viewers, Resource:<resource_type>:<resource_id>)

audit_contract

권한과 설정을 변경하는 command는 sync audit write에 실패하면 요청도 실패해야 합니다. Read audit은 allowlist된 조회에 한해 best effort로 취급합니다.

{
  "mutating_command_mode": "fail_closed_sync",
  "missing_audit_sink_behavior": "reject_mutation",
  "correlation_header": "X-Request-Id"
}

Object Lookup Flow

` + "```mermaid\n" + html.EscapeString(rpObjectLookupMermaid) + "\n```" + `
`) } func buildRPManifest(c *fiber.Ctx) map[string]any { issuer := resolvePublicRequestBaseURL(c, os.Getenv("BACKEND_PUBLIC_URL")) if issuer == "" { issuer = strings.TrimRight(os.Getenv("USERFRONT_URL"), "/") } if issuer == "" { issuer = "https://sso.hmac.kr" } issuer = strings.TrimRight(issuer, "/") return map[string]any{ "version": "2026-05-11", "issuer": issuer, "oidc": map[string]any{ "discovery_url": issuer + "/.well-known/openid-configuration", "jwks_url": issuer + "/.well-known/jwks.json", "supported_flows": []string{"authorization_code_pkce"}, "required_scopes": []string{"openid", "profile", "email"}, }, "iam": map[string]any{ "authorization_engine": "ory-keto", "subject_format": "User:", "target_object_patterns": []string{ "RelyingParty:", "Tenant:", "Resource::", }, "supported_relations": []string{ "admins", "users", "viewers", "operators", "members", "owners", "editors", }, }, "identity_contract": map[string]any{ "subject_header": "X-Baron-Subject", "external_key_header": "X-Baron-External-Key", "external_key_is_opaque": true, "external_key_issuer": "baron", "external_key_delivery": "baron_injected_header", "external_key_lifecycle": "issued_or_loaded_after_successful_authentication_before_rp_request", "rp_supplied_external_key_allowed": false, "rp_user_upsert_source": "rp_must_upsert_from_header_value", "raw_kratos_identity_id_exposed": false, "rp_user_upsert_key": "provider + external_key", "email_is_stable_primary_key": false, "initial_external_key_expression": "X-Baron-External-Key", "fallback_to_subject_allowed": false, }, "trusted_headers": map[string]any{ "subject": "X-Baron-Subject", "external_key": "X-Baron-External-Key", "email": "X-Baron-Email", "tenant": "X-Baron-Tenant", "relations": "X-Baron-Relations", "client_id": "X-Baron-Client-ID", }, "object_lookup": map[string]any{ "rp_level": map[string]any{ "object": "RelyingParty:", "relations": []string{"viewers", "users", "operators", "admins"}, "example": "check(User:abc, viewers, RelyingParty:mh-dashboard)", }, "tenant_level": map[string]any{ "object": "Tenant:", "relations": []string{"members", "admins", "owners"}, "example": "check(User:abc, members, Tenant:9caf62e1-297d-4e8f-870b-61780998bbe)", }, "resource_level": map[string]any{ "object": "Resource::", "relations": []string{"viewers", "editors", "owners"}, "example": "check(User:abc, viewers, Resource:dashboard:mh-monthly-2026-05)", }, "recommended_order": []string{ "authenticated", "rp_level", "tenant_or_resource_level", "trusted_header_injection", }, }, "object_lookup_flow": map[string]any{ "format": "mermaid", "mermaid": rpObjectLookupMermaid, }, "external_key_flow": map[string]any{ "format": "mermaid", "mermaid": rpExternalKeyMermaid, }, "audit_contract": map[string]any{ "mutating_command_mode": "fail_closed_sync", "missing_audit_sink_behavior": "reject_mutation", "read_audit_mode": "best_effort_allowlisted", "correlation_header": "X-Request-Id", "rp_business_audit_required": true, "baron_gateway_audit_required": true, "required_detail_fields": []string{ "obj_id", "relation", "client_id", "subject", "decision", }, "guarantee_scope": "Baron-mediated IAM mutations fail closed on audit write failure; RP-owned business events must be emitted by the RP with the same correlation header.", }, "security_requirements": map[string]any{ "strip_external_identity_headers": true, "backend_direct_exposure_allowed": false, "static_snapshot_requires_auth": true, "email_as_primary_key_allowed": false, }, } } func rpManifestSchema() map[string]any { return map[string]any{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Baron RP IAM Manifest", "type": "object", "required": []string{ "version", "issuer", "oidc", "iam", "trusted_headers", "identity_contract", "object_lookup", "object_lookup_flow", "external_key_flow", "audit_contract", "security_requirements", }, "properties": map[string]any{ "version": map[string]any{"type": "string"}, "issuer": map[string]any{"type": "string", "format": "uri"}, "oidc": map[string]any{"type": "object"}, "iam": map[string]any{"type": "object"}, "trusted_headers": map[string]any{"type": "object"}, "identity_contract": map[string]any{"type": "object"}, "object_lookup": map[string]any{"type": "object"}, "object_lookup_flow": map[string]any{"type": "object"}, "external_key_flow": map[string]any{"type": "object"}, "audit_contract": map[string]any{"type": "object"}, "security_requirements": map[string]any{"type": "object"}, }, } }