1
0
forked from baron/baron-sso

조직도 M2M조회 추가, 자동로그인 보완

This commit is contained in:
2026-05-13 13:44:30 +09:00
parent 72288f1d39
commit 8c2b2f71ef
29 changed files with 2985 additions and 81 deletions

View File

@@ -704,7 +704,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusInternalServerError, "Identity provider unavailable")
}
// [New Policy] Enforce Explicit Tenant Assignment (No Auto-Provisioning)
// 소속이 비어 있는 일반 가입자는 PERSONAL tenant를 자동 생성해 대표소속을 보장합니다.
companyCode := ""
var tenantID *string
@@ -765,6 +765,14 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
if tenantID == nil && req.AffiliationType == "AFFILIATE" {
return errorJSON(c, fiber.StatusBadRequest, "We couldn't verify your organization affiliation. Please check your choice.")
}
if tenantID == nil && req.AffiliationType == "GENERAL" {
tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email)
if err != nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant")
}
companyCode = tenant.Slug
tenantID = &tenant.ID
}
// Normalize Phone (E.164 형태로 보관)
normalizedPhone := strings.ReplaceAll(req.Phone, "-", "")
@@ -785,6 +793,9 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
"grade": "",
"role": domain.RoleUser,
}
if tenantID != nil {
attributes["tenant_id"] = *tenantID
}
// Sync all custom login IDs based on tenant schemas
loginIDRecords := syncCustomLoginIDs(c.Context(), h.TenantService, attributes, req.Metadata, "")
@@ -1100,6 +1111,10 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID
if traits == nil {
return claims
}
if tenantID == "" {
tenantID = representativeTenantIDFromTraits(traits)
}
includeTenantDetails := tenantClaimScopeRequested(scopes)
scopeSet := map[string]struct{}{}
for _, scope := range scopes {
@@ -1200,53 +1215,38 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID
// [New] Dynamic Claim Injection for Multi-tenancy
if tenantID != "" {
claims["tenant_id"] = tenantID
// Extract namespaced metadata if available
// The key in traits is expected to be the tenantID
if namespaced, ok := traits[tenantID].(map[string]any); ok {
for k, v := range namespaced {
claims[k] = v
}
} else if namespaced, ok := traits[tenantID].(map[string]interface{}); ok {
for k, v := range namespaced {
claims[k] = v
if includeTenantDetails {
// tenant 스코프가 있을 때만 대표소속 namespace metadata를 top-level claim으로 펼칩니다.
if namespaced, ok := traits[tenantID].(map[string]any); ok {
for k, v := range namespaced {
claims[k] = v
}
}
}
}
// [Update] Pass ALL tenants the user belongs to
allTenants := map[string]any{}
var joinedTenants []string
joinedTenants := joinedTenantIDsFromTraits(traits, tenantID)
// Heuristic: if a trait value is a map, it's treated as namespaced metadata for a tenant
for k, v := range traits {
if k == "metadata" {
continue
}
if m, ok := v.(map[string]any); ok {
allTenants[k] = m
joinedTenants = append(joinedTenants, k)
} else if m, ok := v.(map[string]interface{}); ok {
allTenants[k] = m
joinedTenants = append(joinedTenants, k)
}
}
// [Fix] Include primary tenant_id in joined_tenants if it's not already there
if primaryTenantID := getString("tenant_id"); primaryTenantID != "" {
found := false
for _, id := range joinedTenants {
if id == primaryTenantID {
found = true
break
}
}
if !found {
joinedTenants = append(joinedTenants, primaryTenantID)
}
}
if len(allTenants) > 0 || len(joinedTenants) > 0 {
claims["tenants"] = allTenants
if len(joinedTenants) > 0 {
claims["joined_tenants"] = joinedTenants
}
if includeTenantDetails && len(allTenants) > 0 {
claims["tenants"] = allTenants
}
return claims
}
@@ -1268,6 +1268,311 @@ func composeOIDCSessionClaims(client domain.HydraClient, traits map[string]any,
return withOidcSessionMetadata(claims, sessionID)
}
func (h *AuthHandler) withHanmacFamilyTenantClaims(ctx context.Context, claims map[string]any, traits map[string]any, scopes []string) map[string]any {
if claims == nil {
claims = map[string]any{}
}
if h == nil || h.TenantService == nil {
return claims
}
appointments := tenantClaimAppointmentsFromTraits(traits)
includeTenantDetails := tenantClaimScopeRequested(scopes)
tenants, hadTenantClaims := claims["tenants"].(map[string]any)
if !hadTenantClaims {
tenants = map[string]any{}
}
createdTenantClaims := map[string]bool{}
if tenantID := tenantClaimString(claims, "tenant_id"); tenantID != "" {
if _, exists := tenants[tenantID]; !exists {
tenants[tenantID] = map[string]any{}
createdTenantClaims[tenantID] = true
}
}
for _, tenantKey := range tenantClaimAppointmentPrimaryKeys(appointments) {
if _, exists := tenants[tenantKey]; !exists {
tenants[tenantKey] = map[string]any{}
createdTenantClaims[tenantKey] = true
}
}
if len(tenants) == 0 {
return claims
}
leadTenantIDs := make([]string, 0)
joinedTenantIDs := make([]string, 0)
for tenantKey, rawTenantClaim := range tenants {
tenantClaim, ok := rawTenantClaim.(map[string]any)
if !ok {
continue
}
tenant, ancestors, inHanmacFamily := h.resolveHanmacFamilyTenantClaimAncestry(ctx, tenantKey)
if !inHanmacFamily || tenant == nil {
if createdTenantClaims[tenantKey] {
delete(tenants, tenantKey)
}
continue
}
joinedTenantIDs = append(joinedTenantIDs, tenant.ID)
if !includeTenantDetails {
if createdTenantClaims[tenantKey] {
delete(tenants, tenantKey)
}
continue
}
tenantClaim["id"] = tenant.ID
tenantClaim["slug"] = tenant.Slug
tenantClaim["name"] = tenant.Name
tenantClaim["type"] = tenant.Type
tenantClaim["ancestors"] = ancestors
if len(ancestors) > 0 {
tenantClaim["parentTenantId"] = ancestors[0]["id"]
} else {
tenantClaim["parentTenantId"] = nil
}
delete(tenantClaim, "parentTenant")
if appointment := lookupTenantClaimAppointment(appointments, tenantKey, tenant); appointment != nil {
mergeTenantAppointmentClaim(tenantClaim, appointment)
}
if lead, ok := metadataBoolFromMap(tenantClaim, "lead", "isLead", "isOwner", "isManager"); ok {
tenantClaim["lead"] = lead
if lead {
leadTenantIDs = append(leadTenantIDs, tenant.ID)
}
}
if representative, ok := metadataBoolFromMap(tenantClaim, "representative", "isPrimary", "primary"); ok {
tenantClaim["representative"] = representative
tenantClaim["isPrimary"] = representative
}
tenants[tenantKey] = tenantClaim
}
if len(leadTenantIDs) > 0 {
claims["lead_tenants"] = uniqueSortedStrings(leadTenantIDs)
}
if len(joinedTenantIDs) > 0 {
claims["joined_tenants"] = mergeClaimStringList(claims["joined_tenants"], joinedTenantIDs)
}
if !includeTenantDetails {
if !hadTenantClaims {
delete(claims, "tenants")
}
delete(claims, "lead_tenants")
return claims
}
if len(tenants) > 0 {
claims["tenants"] = tenants
} else if !hadTenantClaims {
delete(claims, "tenants")
}
return claims
}
func tenantClaimScopeRequested(scopes []string) bool {
for _, scope := range scopes {
if strings.EqualFold(strings.TrimSpace(scope), "tenant") {
return true
}
}
return false
}
func mergeClaimStringList(raw any, values []string) []string {
merged := make([]string, 0, len(values))
switch current := raw.(type) {
case []string:
merged = append(merged, current...)
case []any:
for _, item := range current {
if s, ok := item.(string); ok {
merged = append(merged, s)
}
}
}
merged = append(merged, values...)
return uniqueSortedStrings(merged)
}
func tenantClaimAppointmentPrimaryKeys(appointments map[string]map[string]any) []string {
if len(appointments) == 0 {
return nil
}
seen := map[string]bool{}
keys := make([]string, 0, len(appointments))
for _, appointment := range appointments {
for _, key := range []string{"tenantId", "tenant_id", "tenantSlug", "tenant_slug"} {
value := tenantClaimString(appointment, key)
if value == "" || seen[value] {
continue
}
seen[value] = true
keys = append(keys, value)
break
}
}
sort.Strings(keys)
return keys
}
func tenantClaimAppointmentsFromTraits(traits map[string]any) map[string]map[string]any {
raw := rawAdditionalAppointments(traits)
if raw == nil {
return nil
}
items, ok := raw.([]any)
if !ok {
return nil
}
appointments := make(map[string]map[string]any)
for _, item := range items {
appointment, ok := item.(map[string]any)
if !ok {
continue
}
for _, key := range []string{"tenantId", "tenant_id", "tenantSlug", "tenant_slug"} {
if id := tenantClaimString(appointment, key); id != "" {
appointments[id] = appointment
}
}
}
return appointments
}
func rawAdditionalAppointments(traits map[string]any) any {
if traits == nil {
return nil
}
if raw, ok := traits["additionalAppointments"]; ok {
return raw
}
if metadata, ok := traits["metadata"].(map[string]any); ok {
return metadata["additionalAppointments"]
}
return nil
}
func lookupTenantClaimAppointment(appointments map[string]map[string]any, tenantKey string, tenant *domain.Tenant) map[string]any {
if len(appointments) == 0 {
return nil
}
for _, key := range []string{tenantKey, tenant.ID, tenant.Slug} {
if appointment, ok := appointments[key]; ok {
return appointment
}
}
return nil
}
func mergeTenantAppointmentClaim(tenantClaim map[string]any, appointment map[string]any) {
for _, key := range []string{"grade", "jobTitle", "job_title", "position"} {
if value := tenantClaimString(appointment, key); value != "" {
switch key {
case "job_title":
tenantClaim["jobTitle"] = value
default:
tenantClaim[key] = value
}
}
}
if lead, ok := metadataBoolFromMap(appointment, "lead", "isLead", "isOwner", "isManager"); ok {
tenantClaim["lead"] = lead
}
if representative, ok := metadataBoolFromMap(appointment, "representative", "isPrimary", "primary"); ok {
tenantClaim["representative"] = representative
tenantClaim["isPrimary"] = representative
}
}
func tenantClaimString(values map[string]any, key string) string {
raw, ok := values[key]
if !ok || raw == nil {
return ""
}
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
default:
return strings.TrimSpace(fmt.Sprint(value))
}
}
func (h *AuthHandler) resolveHanmacFamilyTenantClaimAncestry(ctx context.Context, identifier string) (*domain.Tenant, []map[string]any, bool) {
tenant, err := h.resolveTenantClaimTenant(ctx, identifier)
if err != nil || tenant == nil {
return nil, nil, false
}
if strings.EqualFold(tenant.Slug, hanmacFamilyTenantSlug) {
return tenant, []map[string]any{}, true
}
ancestors := make([]*domain.Tenant, 0)
visited := map[string]bool{tenant.ID: true}
current := tenant
for current.ParentID != nil && strings.TrimSpace(*current.ParentID) != "" {
parentID := strings.TrimSpace(*current.ParentID)
if visited[parentID] {
return tenant, tenantClaimAncestorSummaries(ancestors), false
}
visited[parentID] = true
parent, err := h.TenantService.GetTenant(ctx, parentID)
if err != nil || parent == nil {
return tenant, tenantClaimAncestorSummaries(ancestors), false
}
ancestors = append(ancestors, parent)
if strings.EqualFold(parent.Slug, hanmacFamilyTenantSlug) {
return tenant, tenantClaimAncestorSummaries(ancestors), true
}
current = parent
}
return tenant, tenantClaimAncestorSummaries(ancestors), false
}
func (h *AuthHandler) resolveTenantClaimTenant(ctx context.Context, identifier string) (*domain.Tenant, error) {
identifier = strings.TrimSpace(identifier)
if identifier == "" {
return nil, errors.New("tenant identifier is required")
}
if tenant, err := h.TenantService.GetTenant(ctx, identifier); err == nil && tenant != nil {
return tenant, nil
}
return h.TenantService.GetTenantBySlug(ctx, identifier)
}
func tenantClaimTenantSummary(tenant *domain.Tenant) map[string]any {
return map[string]any{
"id": tenant.ID,
"slug": tenant.Slug,
"name": tenant.Name,
"type": tenant.Type,
}
}
func tenantClaimAncestorSummaries(ancestors []*domain.Tenant) []map[string]any {
if len(ancestors) == 0 {
return []map[string]any{}
}
items := make([]map[string]any, 0, len(ancestors))
for i, ancestor := range ancestors {
item := tenantClaimTenantSummary(ancestor)
if i+1 < len(ancestors) {
item["parentTenantId"] = ancestors[i+1].ID
} else {
item["parentTenantId"] = nil
}
items = append(items, item)
}
return items
}
func applyConfiguredIDTokenClaims(baseClaims map[string]any, metadata map[string]interface{}) map[string]any {
if baseClaims == nil {
baseClaims = map[string]any{}
@@ -5535,19 +5840,14 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject)
if err == nil && identity != nil {
currentSessionID := h.resolveCurrentSessionID(c)
var tenantID string
if consentRequest.Client.Metadata != nil {
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
tenantID = tid
}
}
sessionClaims := composeOIDCSessionClaims(
consentRequest.Client,
identity.Traits,
consentRequest.RequestedScope,
tenantID,
representativeTenantIDFromTraits(identity.Traits),
currentSessionID,
)
sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope)
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims)
if err == nil {
@@ -5571,10 +5871,10 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
// 신원 정보를 가져오지 못하면 자동 승인을 진행할 수 없으므로 일반 흐름(UI 노출)으로 진행
} else {
currentSessionID := h.resolveCurrentSessionID(c)
var tenantID string
var clientTenantID string
if consentRequest.Client.Metadata != nil {
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
tenantID = tid
clientTenantID = tid
}
}
@@ -5582,9 +5882,10 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
consentRequest.Client,
identity.Traits,
consentRequest.RequestedScope,
tenantID,
representativeTenantIDFromTraits(identity.Traits),
currentSessionID,
)
sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope)
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용 - 자동 승인 시)
@@ -5627,7 +5928,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
EventID: GenerateSecureToken(16),
Timestamp: time.Now(),
UserID: consentRequest.Subject,
TenantID: tenantID, // Uses the tenantID extracted earlier
TenantID: clientTenantID,
SessionID: currentSessionID,
EventType: "consent.granted",
Status: "success",
@@ -5761,11 +6062,10 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
c.Locals("login_id", loginID)
}
currentSessionID := h.resolveCurrentSessionID(c)
var tenantID string
var clientTenantID string
if consentRequest.Client.Metadata != nil {
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
tenantID = tid
clientTenantID = tid
}
}
@@ -5773,9 +6073,10 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
consentRequest.Client,
identity.Traits,
consentRequest.RequestedScope,
tenantID,
representativeTenantIDFromTraits(identity.Traits),
currentSessionID,
)
sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope)
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용)
@@ -5821,7 +6122,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
EventID: GenerateSecureToken(16),
Timestamp: time.Now(),
UserID: consentRequest.Subject,
TenantID: tenantID, // [New] Add TenantID to AuditLog
TenantID: clientTenantID,
SessionID: currentSessionID,
EventType: "consent.granted",
Status: "success",