forked from baron/baron-sso
256 lines
9.6 KiB
Go
256 lines
9.6 KiB
Go
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"},
|
|
},
|
|
}
|
|
}
|