forked from baron/baron-sso
조직현황 구조변경. 총괄센터삼안 실 조직 삽입확인
This commit is contained in:
255
backend/internal/handler/rp_manifest_handler.go
Normal file
255
backend/internal/handler/rp_manifest_handler.go
Normal file
@@ -0,0 +1,255 @@
|
||||
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:<client_id>]
|
||||
D -->|no| F{Route has tenant_id?}
|
||||
F -->|yes| G[obj_id = Tenant:<tenant_id>]
|
||||
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(`<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Baron RP IAM Manifest</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.6; max-width: 920px; }
|
||||
code, pre { background: #f5f5f5; border-radius: 4px; padding: .1rem .3rem; }
|
||||
pre { padding: 1rem; overflow: auto; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: .5rem; text-align: left; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Baron RP IAM Manifest</h1>
|
||||
<p>외부 RP가 Baron SSO/Ory Stack/Keto 기반 공용 IAM을 연동하기 위한 공개 규격입니다.</p>
|
||||
<ul>
|
||||
<li>Machine-readable manifest: <a href="/.well-known/baron-rp-manifest.json">/.well-known/baron-rp-manifest.json</a></li>
|
||||
<li>JSON schema: <a href="/.well-known/baron-rp-manifest.schema.json">/.well-known/baron-rp-manifest.schema.json</a></li>
|
||||
</ul>
|
||||
<h2>Issuer</h2>
|
||||
<pre>` + html.EscapeString(issuer) + `</pre>
|
||||
<h2>Identity Contract</h2>
|
||||
<table>
|
||||
<tr><th>용도</th><th>Header</th><th>정책</th></tr>
|
||||
<tr><td>Keto subject</td><td><code>X-Baron-Subject</code></td><td><code>User:<baron_identity_id></code> 전체 문자열을 opaque subject로 취급합니다.</td></tr>
|
||||
<tr><td>RP upsert key</td><td><code>X-Baron-External-Key</code></td><td>Baron-issued alias입니다. RP가 만들거나 제출하지 않고, Baron이 주입한 전체 문자열을 local user external key로 저장합니다.</td></tr>
|
||||
<tr><td>RP client</td><td><code>X-Baron-Client-ID</code></td><td>현재 접근 중인 RP client id입니다.</td></tr>
|
||||
</table>
|
||||
<h2>External Key Flow</h2>
|
||||
<p><code>X-Baron-External-Key</code>는 RP 입력값이 아니라 Baron이 인증된 subject에서 발급/조회해 주입하는 opaque alias입니다. RP upserts local user from the Baron-issued alias.</p>
|
||||
<pre>` + "```mermaid\n" + html.EscapeString(rpExternalKeyMermaid) + "\n```" + `</pre>
|
||||
<h2>Object Lookup</h2>
|
||||
<pre>check(User:abc, viewers, RelyingParty:<client_id>)
|
||||
check(User:abc, members, Tenant:<tenant_id>)
|
||||
check(User:abc, viewers, Resource:<resource_type>:<resource_id>)</pre>
|
||||
<h2>audit_contract</h2>
|
||||
<p>권한과 설정을 변경하는 command는 sync audit write에 실패하면 요청도 실패해야 합니다. Read audit은 allowlist된 조회에 한해 best effort로 취급합니다.</p>
|
||||
<pre>{
|
||||
"mutating_command_mode": "fail_closed_sync",
|
||||
"missing_audit_sink_behavior": "reject_mutation",
|
||||
"correlation_header": "X-Request-Id"
|
||||
}</pre>
|
||||
<h2>Object Lookup Flow</h2>
|
||||
<pre>` + "```mermaid\n" + html.EscapeString(rpObjectLookupMermaid) + "\n```" + `</pre>
|
||||
</body>
|
||||
</html>`)
|
||||
}
|
||||
|
||||
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:<baron_identity_id>",
|
||||
"target_object_patterns": []string{
|
||||
"RelyingParty:<client_id>",
|
||||
"Tenant:<tenant_id>",
|
||||
"Resource:<resource_type>:<resource_id>",
|
||||
},
|
||||
"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:<client_id>",
|
||||
"relations": []string{"viewers", "users", "operators", "admins"},
|
||||
"example": "check(User:abc, viewers, RelyingParty:mh-dashboard)",
|
||||
},
|
||||
"tenant_level": map[string]any{
|
||||
"object": "Tenant:<tenant_id>",
|
||||
"relations": []string{"members", "admins", "owners"},
|
||||
"example": "check(User:abc, members, Tenant:9caf62e1-297d-4e8f-870b-61780998bbe)",
|
||||
},
|
||||
"resource_level": map[string]any{
|
||||
"object": "Resource:<resource_type>:<resource_id>",
|
||||
"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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user