1
0
forked from baron/baron-sso

관리자 비밀번호 변경을 Kratos 해시 업데이트 방식으로 수정

This commit is contained in:
2026-03-31 10:28:27 +09:00
parent 4d8b9d9f87
commit 2364ff59d2
6 changed files with 335 additions and 42 deletions

View File

@@ -1203,12 +1203,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
}
finalLoginID := extractTraitString(traits, "id")
explicitLoginID := strings.TrimSpace(extractTraitString(traits, "id"))
userEmail := extractTraitString(traits, "email")
userPhone := extractTraitString(traits, "phone")
if err := domain.ValidateLoginID(finalLoginID, userEmail, userPhone); err != nil {
userPhone := extractTraitString(traits, "phone_number")
if err := domain.ValidateLoginID(explicitLoginID, userEmail, userPhone); err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
finalLoginID := resolvePasswordLoginID(traits)
state := normalizeKratosState(req.Status)
@@ -1234,7 +1235,10 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
if req.Password != nil && *req.Password != "" {
if err := h.KratosAdmin.UpdateIdentityPassword(c.Context(), userID, *req.Password); err != nil {
if h.OryProvider == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "password provider not available")
}
if err := h.OryProvider.UpdateUserPassword(finalLoginID, *req.Password, nil); err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
}
@@ -1508,6 +1512,16 @@ func extractTraitString(traits map[string]interface{}, key string) string {
return ""
}
func resolvePasswordLoginID(traits map[string]interface{}) string {
if loginID := strings.TrimSpace(extractTraitString(traits, "id")); loginID != "" {
return loginID
}
if email := strings.TrimSpace(extractTraitString(traits, "email")); email != "" {
return email
}
return strings.TrimSpace(extractTraitString(traits, "phone_number"))
}
// syncLoginID ensures that the 'id' trait (used as Kratos identifier) is in sync with the configured custom field.
func syncLoginID(traits map[string]interface{}, metadata map[string]any, tenantID string, loginIDField string) {
if loginIDField == "" || loginIDField == "id" {

View File

@@ -488,6 +488,117 @@ func TestUserHandler_UpdateUser_LoginIDSync(t *testing.T) {
})
}
func TestUserHandler_UpdateUser_PasswordUsesProvider(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockOry := new(MockOryProvider)
mockTenant := new(MockTenantServiceForUser)
h := &UserHandler{
KratosAdmin: mockKratos,
OryProvider: mockOry,
TenantService: mockTenant,
}
app.Put("/users/:id", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
return h.UpdateUser(c)
})
userID := "u-1"
mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"id": "dyddus1210",
"email": "dyddus1210@gmail.com",
"companyCode": "test-tenant",
},
}, nil).Once()
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil)
mockTenant.On("ListManageableTenants", mock.Anything, userID).Return([]domain.Tenant{}, nil).Once()
mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool {
return traits["id"] == "dyddus1210"
}), "").Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"id": "dyddus1210",
"email": "dyddus1210@gmail.com",
},
}, nil).Once()
mockOry.On("UpdateUserPassword", "dyddus1210", "asdfzxcv1234!", (*http.Request)(nil)).Return(nil).Once()
payload := map[string]interface{}{
"password": "asdfzxcv1234!",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("PUT", "/users/"+userID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
mockOry.AssertExpectations(t)
mockKratos.AssertNotCalled(t, "UpdateIdentityPassword", mock.Anything, mock.Anything, mock.Anything)
}
func TestUserHandler_UpdateUser_PasswordFallsBackToEmail(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockOry := new(MockOryProvider)
mockTenant := new(MockTenantServiceForUser)
h := &UserHandler{
KratosAdmin: mockKratos,
OryProvider: mockOry,
TenantService: mockTenant,
}
app.Put("/users/:id", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
return h.UpdateUser(c)
})
userID := "u-2"
mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "dyddus1210@gmail.com",
"companyCode": "test-tenant",
},
}, nil).Once()
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil)
mockTenant.On("ListManageableTenants", mock.Anything, userID).Return([]domain.Tenant{}, nil).Once()
mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool {
return traits["email"] == "dyddus1210@gmail.com"
}), "").Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "dyddus1210@gmail.com",
},
}, nil).Once()
mockOry.On("UpdateUserPassword", "dyddus1210@gmail.com", "asdfzxcv1234!", (*http.Request)(nil)).Return(nil).Once()
payload := map[string]interface{}{
"password": "asdfzxcv1234!",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("PUT", "/users/"+userID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
mockOry.AssertExpectations(t)
}
func TestUserHandler_CreateUser_LoginIDSync(t *testing.T) {
t.Run("Success - Sync LoginID from namespaced metadata", func(t *testing.T) {
app := fiber.New()