1
0
forked from baron/baron-sso

custom claim 권한체크 확인

This commit is contained in:
2026-06-11 08:29:25 +09:00
parent 839ca9d407
commit 4d77060b5d
79 changed files with 4268 additions and 670 deletions

View File

@@ -1400,6 +1400,84 @@ func TestUserHandler_BulkUpdateUsers(t *testing.T) {
})
}
func TestUserHandler_BulkUpdateUsersAddTenantMembership(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockTenant := new(MockTenantServiceForUser)
mockOutbox := new(userHandlerMockKetoOutboxRepository)
h := &UserHandler{
KratosAdmin: mockKratos,
TenantService: mockTenant,
KetoOutboxRepo: mockOutbox,
}
app.Put("/users/bulk", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
return h.BulkUpdateUsers(c)
})
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{
ID: "u-1",
State: "active",
Traits: map[string]any{
"email": "u1@test.com",
"name": "Bulk User",
"tenant_id": "primary-tenant-id",
},
}, nil).Once()
mockTenant.On("GetTenantBySlug", mock.Anything, "team-a").Return(&domain.Tenant{
ID: "team-a-id",
Name: "Team A",
Slug: "team-a",
}, nil).Once()
mockKratos.On(
"UpdateIdentity",
mock.Anything,
"u-1",
mock.MatchedBy(func(traits map[string]any) bool {
if extractTraitString(traits, "tenant_id") != "primary-tenant-id" {
return false
}
appointments, ok := traits["additionalAppointments"].([]any)
if !ok || len(appointments) != 1 {
return false
}
appointment, ok := appointments[0].(map[string]any)
return ok &&
appointment["tenantId"] == "team-a-id" &&
appointment["tenantSlug"] == "team-a" &&
appointment["tenantName"] == "Team A"
}),
mock.Anything,
).Return(&service.KratosIdentity{
ID: "u-1",
State: "active",
Traits: map[string]any{
"email": "u1@test.com",
"name": "Bulk User",
"tenant_id": "primary-tenant-id",
"additionalAppointments": []any{map[string]any{"tenantId": "team-a-id", "tenantSlug": "team-a", "tenantName": "Team A"}},
},
}, nil).Once()
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
return entry.Namespace == "Tenant" &&
entry.Object == "team-a-id" &&
entry.Relation == "members" &&
entry.Subject == "User:u-1" &&
entry.Action == domain.KetoOutboxActionCreate
})).Return(nil).Once()
body := `{"userIds":["u-1"],"tenantSlug":"team-a","isAddTenant":true}`
req := httptest.NewRequest(http.MethodPut, "/users/bulk", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
mockKratos.AssertExpectations(t)
mockTenant.AssertExpectations(t)
mockOutbox.AssertExpectations(t)
}
func TestUserHandler_BulkDeleteUsers(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
@@ -1702,6 +1780,137 @@ func TestUserHandler_UpdateUser_AdminOnlyField(t *testing.T) {
})
}
func TestUserHandler_UpdateUser_GlobalCustomClaimWritePermission(t *testing.T) {
newApp := func(t *testing.T, existingPermission string, updateIdentity bool) (*fiber.App, *MockKratosAdmin, *MockTenantServiceForUser, *map[string]any) {
t.Helper()
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockTenant := new(MockTenantServiceForUser)
capturedTraits := map[string]any(nil)
h := &UserHandler{
KratosAdmin: mockKratos,
TenantService: mockTenant,
}
tenantID := "t-123"
app.Put("/users/:id", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "requester-1",
Role: domain.RoleUser,
TenantID: &tenantID,
ManageableTenants: []domain.Tenant{
{ID: tenantID, Slug: "test-tenant"},
},
})
return h.UpdateUser(c)
})
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{
ID: "u-1",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Custom Claim User",
"tenant_id": tenantID,
"global_custom_claims": map[string]any{
"contract_date": "2026-06-09",
},
"global_custom_claim_types": map[string]any{
"contract_date": "date",
},
"global_custom_claim_permissions": map[string]any{
"contract_date": map[string]any{
"readPermission": "user_and_admin",
"writePermission": existingPermission,
},
},
},
}, nil).Once()
mockTenant.On("GetTenant", mock.Anything, tenantID).Return(&domain.Tenant{
ID: tenantID,
Slug: "test-tenant",
Config: domain.JSONMap{
"userSchema": []any{},
},
}, nil).Maybe()
if updateIdentity {
mockKratos.On("UpdateIdentity", mock.Anything, "u-1", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
capturedTraits = args.Get(2).(map[string]any)
}).Return(&service.KratosIdentity{
ID: "u-1",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Custom Claim User",
},
}, nil).Once()
} else {
mockKratos.On("UpdateIdentity", mock.Anything, "u-1", mock.Anything, mock.Anything).Return(&service.KratosIdentity{
ID: "u-1",
State: "active",
Traits: map[string]any{},
}, nil).Maybe()
}
return app, mockKratos, mockTenant, &capturedTraits
}
requestBody := func(nextValue string) *bytes.Reader {
body, _ := json.Marshal(map[string]any{
"metadata": map[string]any{
"global_custom_claims": map[string]any{
"contract_date": nextValue,
},
"global_custom_claim_types": map[string]any{
"contract_date": "date",
},
"global_custom_claim_permissions": map[string]any{
"contract_date": map[string]any{
"readPermission": "user_and_admin",
"writePermission": "user_and_admin",
},
},
},
})
return bytes.NewReader(body)
}
t.Run("regular user cannot change admin_only global custom claim value", func(t *testing.T) {
app, mockKratos, mockTenant, _ := newApp(t, "admin_only", false)
req := httptest.NewRequest(http.MethodPut, "/users/u-1", requestBody("2026-07-01"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
mockKratos.AssertNotCalled(t, "UpdateIdentity", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockKratos.AssertExpectations(t)
mockTenant.AssertExpectations(t)
})
t.Run("regular user can change user_and_admin global custom claim value", func(t *testing.T) {
app, mockKratos, mockTenant, capturedTraits := newApp(t, "user_and_admin", true)
req := httptest.NewRequest(http.MethodPut, "/users/u-1", requestBody("2026-07-01"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.NotNil(t, *capturedTraits)
claims := (*capturedTraits)["global_custom_claims"].(map[string]any)
require.Equal(t, "2026-07-01", claims["contract_date"])
permissions := (*capturedTraits)["global_custom_claim_permissions"].(map[string]any)
require.Equal(t, map[string]any{
"readPermission": "user_and_admin",
"writePermission": "user_and_admin",
}, permissions["contract_date"])
mockKratos.AssertExpectations(t)
mockTenant.AssertExpectations(t)
})
}
func TestUserHandler_UpdateUser_AcceptsDeprecatedAdminRolesAsUser(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
@@ -2569,6 +2778,117 @@ func TestUserHandler_UpdateUserAddTenantKeepsPrimaryAndAddsAppointment(t *testin
require.Equal(t, false, added["isPrimary"])
}
func TestUserHandler_UpdateUserRemoveTenantDropsAdditionalAppointment(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockTenant := new(MockTenantServiceForUser)
mockOutbox := new(userHandlerMockKetoOutboxRepository)
h := &UserHandler{
KratosAdmin: mockKratos,
TenantService: mockTenant,
KetoOutboxRepo: mockOutbox,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "admin-id",
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Put("/users/:id", h.UpdateUser)
mockKratos.On("GetIdentity", mock.Anything, "user-id").Return(&service.KratosIdentity{
ID: "user-id",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Test User",
"tenant_id": "primary-tenant-id",
"role": domain.RoleUser,
"additionalAppointments": []any{
map[string]any{
"tenantId": "primary-tenant-id",
"tenantSlug": "primary-tenant",
"tenantName": "대표 조직",
"isPrimary": true,
},
map[string]any{
"tenantId": "private-team-id",
"tenantSlug": "private-team",
"tenantName": "비공개 팀",
"isPrimary": false,
},
},
},
}, nil)
mockTenant.On("GetTenantBySlug", mock.Anything, "private-team").Return(&domain.Tenant{
ID: "private-team-id",
Name: "비공개 팀",
Slug: "private-team",
Config: domain.JSONMap{
"visibility": "private",
},
}, nil)
mockTenant.On("GetTenant", mock.Anything, "private-team-id").Return(&domain.Tenant{
ID: "private-team-id",
Name: "비공개 팀",
Slug: "private-team",
Config: domain.JSONMap{
"visibility": "private",
},
}, nil).Maybe()
mockTenant.On("GetTenant", mock.Anything, "primary-tenant-id").Return(&domain.Tenant{
ID: "primary-tenant-id",
Name: "대표 조직",
Slug: "primary-tenant",
}, nil).Maybe()
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
return entry.Namespace == "Tenant" &&
entry.Object == "private-team-id" &&
entry.Relation == "members" &&
entry.Subject == "User:user-id" &&
entry.Action == domain.KetoOutboxActionDelete
})).Return(nil).Maybe()
var capturedTraits map[string]any
mockKratos.On("UpdateIdentity", mock.Anything, "user-id", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
capturedTraits = args.Get(2).(map[string]any)
}).Return(&service.KratosIdentity{
ID: "user-id",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Test User",
"tenant_id": "primary-tenant-id",
"role": domain.RoleUser,
"additionalAppointments": []any{
map[string]any{
"tenantId": "primary-tenant-id",
"tenantSlug": "primary-tenant",
"tenantName": "대표 조직",
"isPrimary": true,
},
},
},
}, nil)
body := `{"tenantSlug":"private-team","isRemoveTenant":true}`
req := httptest.NewRequest(http.MethodPut, "/users/user-id", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "primary-tenant-id", capturedTraits["tenant_id"])
appointments, ok := capturedTraits["additionalAppointments"].([]any)
require.True(t, ok)
require.Len(t, appointments, 1)
remaining := appointments[0].(map[string]any)
require.Equal(t, "primary-tenant-id", remaining["tenantId"])
require.Equal(t, "primary-tenant", remaining["tenantSlug"])
mockOutbox.AssertExpectations(t)
}
func TestUserHandler_UpdateUserAddTenantRejectsUnmanageableTenantForTenantAdmin(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)