forked from baron/baron-sso
custom claim 권한체크 확인
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user