forked from baron/baron-sso
Merge pull request 'feat/issue-204-kratos-sot' (#209) from feat/issue-204-kratos-sot into main
Reviewed-on: ai-team/baron-sso#209
This commit is contained in:
@@ -35,6 +35,9 @@ CORS_ALLOWED_ORIGINS=http://localhost:5000 # 쿠키 인증 사용 시 정확한
|
|||||||
AUDIT_WORKER_COUNT=5 # 비동기 감사 로그 처리를 위한 고루틴 워커 수
|
AUDIT_WORKER_COUNT=5 # 비동기 감사 로그 처리를 위한 고루틴 워커 수
|
||||||
AUDIT_QUEUE_SIZE=2000 # 감사 로그 대기열(채널) 버퍼 크기
|
AUDIT_QUEUE_SIZE=2000 # 감사 로그 대기열(채널) 버퍼 크기
|
||||||
|
|
||||||
|
# Redis Cache Configuration
|
||||||
|
PROFILE_CACHE_TTL=30m # User Profile Redis 캐시 만료 시간
|
||||||
|
|
||||||
# Descope Project ID (Required for Auth)
|
# Descope Project ID (Required for Auth)
|
||||||
DESCOPE_PROJECT_ID=P2t...your_descope_project_id
|
DESCOPE_PROJECT_ID=P2t...your_descope_project_id
|
||||||
DESCOPE_MANAGEMENT_KEY=your_descope_management_key_here
|
DESCOPE_MANAGEMENT_KEY=your_descope_management_key_here
|
||||||
|
|||||||
@@ -3748,10 +3748,30 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
token := h.getBearerToken(c)
|
token := h.getBearerToken(c)
|
||||||
|
cookie := c.Get("Cookie")
|
||||||
|
cacheKey := ""
|
||||||
|
|
||||||
|
// 1. Try Redis Cache
|
||||||
|
if h.RedisService != nil {
|
||||||
|
if token != "" {
|
||||||
|
cacheKey = "cache:profile:token:" + token
|
||||||
|
}
|
||||||
|
// Cookie based caching skipped for simplicity/safety
|
||||||
|
|
||||||
|
if cacheKey != "" {
|
||||||
|
cached, _ := h.RedisService.Get(cacheKey)
|
||||||
|
if cached != "" {
|
||||||
|
if json.Unmarshal([]byte(cached), &profile) == nil {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch from Kratos (SoT)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
profile, err = h.getKratosProfile(token)
|
profile, err = h.getKratosProfile(token)
|
||||||
} else {
|
} else {
|
||||||
cookie := c.Get("Cookie")
|
|
||||||
if cookie != "" {
|
if cookie != "" {
|
||||||
profile, err = h.getKratosProfileWithCookie(cookie)
|
profile, err = h.getKratosProfileWithCookie(cookie)
|
||||||
}
|
}
|
||||||
@@ -3761,26 +3781,40 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
return nil, errors.New("invalid session (trace:resolve_profile)")
|
return nil, errors.New("invalid session (trace:resolve_profile)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// [New] Enrich with Local DB (Roles, TenantID, etc.)
|
// 3. Post-Process (Defaults & Metadata Enrichment)
|
||||||
if h.UserRepo != nil {
|
// Default Role if missing (migration safety)
|
||||||
localUser, err := h.UserRepo.FindByID(c.Context(), profile.ID)
|
if profile.Role == "" {
|
||||||
if err == nil && localUser != nil {
|
profile.Role = domain.RoleUser
|
||||||
profile.Role = localUser.Role
|
|
||||||
profile.TenantID = localUser.TenantID
|
|
||||||
profile.RelyingPartyID = localUser.RelyingPartyID
|
|
||||||
if profile.Tenant == nil && localUser.Tenant != nil {
|
|
||||||
profile.Tenant = localUser.Tenant
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 로컬 DB에 없으면 기본 권한 부여
|
|
||||||
profile.Role = domain.RoleUser
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로컬 DB에 Tenant 정보가 없더라도 companyCode(slug)가 있으면 조회 시도
|
// Fetch Tenant Metadata if missing
|
||||||
|
// Case A: Have TenantID from Kratos -> Fetch by ID
|
||||||
|
if profile.Tenant == nil && profile.TenantID != nil && *profile.TenantID != "" {
|
||||||
|
if tenant, err := h.TenantService.GetTenant(c.Context(), *profile.TenantID); err == nil {
|
||||||
|
profile.Tenant = tenant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case B: Have CompanyCode but no TenantID -> Fetch by Slug
|
||||||
if profile.Tenant == nil && profile.CompanyCode != "" {
|
if profile.Tenant == nil && profile.CompanyCode != "" {
|
||||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), profile.CompanyCode); err == nil && tenant != nil {
|
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), profile.CompanyCode); err == nil && tenant != nil {
|
||||||
profile.Tenant = tenant
|
profile.Tenant = tenant
|
||||||
|
if profile.TenantID == nil || *profile.TenantID == "" {
|
||||||
|
profile.TenantID = &tenant.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save to Redis Cache (Short TTL)
|
||||||
|
if h.RedisService != nil && cacheKey != "" {
|
||||||
|
if data, err := json.Marshal(profile); err == nil {
|
||||||
|
ttlStr := os.Getenv("PROFILE_CACHE_TTL")
|
||||||
|
ttl := 30 * time.Minute // Default TTL
|
||||||
|
if ttlStr != "" {
|
||||||
|
if parsed, err := time.ParseDuration(ttlStr); err == nil {
|
||||||
|
ttl = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = h.RedisService.Set(cacheKey, string(data), ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4809,6 +4843,8 @@ func (h *AuthHandler) getKratosProfile(sessionToken string) (*domain.UserProfile
|
|||||||
dept, _ := traits["department"].(string)
|
dept, _ := traits["department"].(string)
|
||||||
affType, _ := traits["affiliationType"].(string)
|
affType, _ := traits["affiliationType"].(string)
|
||||||
compCode, _ := traits["companyCode"].(string)
|
compCode, _ := traits["companyCode"].(string)
|
||||||
|
role, _ := traits["role"].(string)
|
||||||
|
tenantID, _ := traits["tenant_id"].(string)
|
||||||
|
|
||||||
profile := &domain.UserProfileResponse{
|
profile := &domain.UserProfileResponse{
|
||||||
ID: identityID,
|
ID: identityID,
|
||||||
@@ -4818,13 +4854,18 @@ func (h *AuthHandler) getKratosProfile(sessionToken string) (*domain.UserProfile
|
|||||||
Department: dept,
|
Department: dept,
|
||||||
AffiliationType: affType,
|
AffiliationType: affType,
|
||||||
CompanyCode: compCode,
|
CompanyCode: compCode,
|
||||||
|
Role: role,
|
||||||
Metadata: make(map[string]any),
|
Metadata: make(map[string]any),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tenantID != "" {
|
||||||
|
profile.TenantID = &tenantID
|
||||||
|
}
|
||||||
|
|
||||||
coreTraits := map[string]bool{
|
coreTraits := map[string]bool{
|
||||||
"email": true, "name": true, "phone_number": true,
|
"email": true, "name": true, "phone_number": true,
|
||||||
"grade": true, "companyCode": true, "department": true,
|
"grade": true, "companyCode": true, "department": true,
|
||||||
"affiliationType": true,
|
"affiliationType": true, "role": true, "tenant_id": true,
|
||||||
}
|
}
|
||||||
for k, v := range traits {
|
for k, v := range traits {
|
||||||
if !coreTraits[k] {
|
if !coreTraits[k] {
|
||||||
@@ -4847,6 +4888,8 @@ func (h *AuthHandler) getKratosProfileWithCookie(cookie string) (*domain.UserPro
|
|||||||
dept, _ := traits["department"].(string)
|
dept, _ := traits["department"].(string)
|
||||||
affType, _ := traits["affiliationType"].(string)
|
affType, _ := traits["affiliationType"].(string)
|
||||||
compCode, _ := traits["companyCode"].(string)
|
compCode, _ := traits["companyCode"].(string)
|
||||||
|
role, _ := traits["role"].(string)
|
||||||
|
tenantID, _ := traits["tenant_id"].(string)
|
||||||
|
|
||||||
profile := &domain.UserProfileResponse{
|
profile := &domain.UserProfileResponse{
|
||||||
ID: identityID,
|
ID: identityID,
|
||||||
@@ -4856,13 +4899,18 @@ func (h *AuthHandler) getKratosProfileWithCookie(cookie string) (*domain.UserPro
|
|||||||
Department: dept,
|
Department: dept,
|
||||||
AffiliationType: affType,
|
AffiliationType: affType,
|
||||||
CompanyCode: compCode,
|
CompanyCode: compCode,
|
||||||
|
Role: role,
|
||||||
Metadata: make(map[string]any),
|
Metadata: make(map[string]any),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tenantID != "" {
|
||||||
|
profile.TenantID = &tenantID
|
||||||
|
}
|
||||||
|
|
||||||
coreTraits := map[string]bool{
|
coreTraits := map[string]bool{
|
||||||
"email": true, "name": true, "phone_number": true,
|
"email": true, "name": true, "phone_number": true,
|
||||||
"grade": true, "companyCode": true, "department": true,
|
"grade": true, "companyCode": true, "department": true,
|
||||||
"affiliationType": true,
|
"affiliationType": true, "role": true, "tenant_id": true,
|
||||||
}
|
}
|
||||||
for k, v := range traits {
|
for k, v := range traits {
|
||||||
if !coreTraits[k] {
|
if !coreTraits[k] {
|
||||||
|
|||||||
@@ -252,6 +252,18 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
"grade": role,
|
"grade": role,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Resolve TenantID before Kratos creation]
|
||||||
|
var tenantID string
|
||||||
|
if req.CompanyCode != "" && h.TenantService != nil {
|
||||||
|
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode); err == nil && tenant != nil {
|
||||||
|
tenantID = tenant.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributes["role"] = role
|
||||||
|
if tenantID != "" {
|
||||||
|
attributes["tenant_id"] = tenantID
|
||||||
|
}
|
||||||
|
|
||||||
// Merge custom metadata into attributes
|
// Merge custom metadata into attributes
|
||||||
for k, v := range req.Metadata {
|
for k, v := range req.Metadata {
|
||||||
// Don't overwrite core fields
|
// Don't overwrite core fields
|
||||||
@@ -288,11 +300,8 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
Status: "active",
|
Status: "active",
|
||||||
Metadata: req.Metadata,
|
Metadata: req.Metadata,
|
||||||
}
|
}
|
||||||
|
if tenantID != "" {
|
||||||
if req.CompanyCode != "" && h.TenantService != nil {
|
localUser.TenantID = &tenantID
|
||||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode); err == nil && tenant != nil {
|
|
||||||
localUser.TenantID = &tenant.ID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.UserRepo != nil {
|
if h.UserRepo != nil {
|
||||||
@@ -392,7 +401,14 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
|||||||
traits["phone_number"] = normalizePhoneNumber(strings.TrimSpace(*req.Phone))
|
traits["phone_number"] = normalizePhoneNumber(strings.TrimSpace(*req.Phone))
|
||||||
}
|
}
|
||||||
if req.CompanyCode != nil {
|
if req.CompanyCode != nil {
|
||||||
traits["companyCode"] = strings.TrimSpace(*req.CompanyCode)
|
code := strings.TrimSpace(*req.CompanyCode)
|
||||||
|
traits["companyCode"] = code
|
||||||
|
// Resolve TenantID for Kratos Trait
|
||||||
|
if h.TenantService != nil && code != "" {
|
||||||
|
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil {
|
||||||
|
traits["tenant_id"] = tenant.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if req.Department != nil {
|
if req.Department != nil {
|
||||||
traits["department"] = strings.TrimSpace(*req.Department)
|
traits["department"] = strings.TrimSpace(*req.Department)
|
||||||
@@ -403,13 +419,14 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
|||||||
role = "user"
|
role = "user"
|
||||||
}
|
}
|
||||||
traits["grade"] = role
|
traits["grade"] = role
|
||||||
|
traits["role"] = role
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Refined] Metadata synchronization: replace non-core traits with new Metadata
|
// [Refined] Metadata synchronization: replace non-core traits with new Metadata
|
||||||
coreTraits := map[string]bool{
|
coreTraits := map[string]bool{
|
||||||
"email": true, "name": true, "phone_number": true,
|
"email": true, "name": true, "phone_number": true,
|
||||||
"grade": true, "companyCode": true, "department": true,
|
"grade": true, "companyCode": true, "department": true,
|
||||||
"affiliationType": true,
|
"affiliationType": true, "role": true, "tenant_id": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Remove existing non-core traits to handle deletions
|
// 1. Remove existing non-core traits to handle deletions
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type TenantService interface {
|
|||||||
RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error)
|
RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error)
|
||||||
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
|
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
|
||||||
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
||||||
|
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
||||||
ApproveTenant(ctx context.Context, id string) error
|
ApproveTenant(ctx context.Context, id string) error
|
||||||
SetKetoService(keto KetoService) // 추가
|
SetKetoService(keto KetoService) // 추가
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,10 @@ func (s *tenantService) SetKetoService(keto KetoService) {
|
|||||||
s.keto = keto
|
s.keto = keto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *tenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||||
|
return s.repo.FindByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error) {
|
func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error) {
|
||||||
// Validate Slug
|
// Validate Slug
|
||||||
if ok, msg := utils.ValidateSlug(slug); !ok {
|
if ok, msg := utils.ValidateSlug(slug); !ok {
|
||||||
|
|||||||
@@ -62,6 +62,14 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Company Code"
|
"title": "Company Code"
|
||||||
},
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Role"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Tenant ID"
|
||||||
|
},
|
||||||
"displayname": {
|
"displayname": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Display Name"
|
"title": "Display Name"
|
||||||
|
|||||||
Reference in New Issue
Block a user