From b9ad54d459ae2be33447e1aec29c9e4e8baa5d8f Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 12 Feb 2026 11:41:01 +0900 Subject: [PATCH] usergroup --- backend/cmd/server/main.go | 17 +++++++++++- .../internal/handler/user_group_handler.go | 27 +++++++++++++++++++ .../internal/service/user_group_service.go | 26 ++++++++++++++++++ docker/ory/keto/namespaces.ts | 4 +-- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 4e8fabf7..21684322 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -246,10 +246,12 @@ func main() { // 2. Initialize Handlers tenantRepo := repository.NewTenantRepository(db) tenantGroupRepo := repository.NewTenantGroupRepository(db) + userGroupRepo := repository.NewUserGroupRepository(db) + userRepo := repository.NewUserRepository(db) tenantService := service.NewTenantService(tenantRepo) tenantGroupService := service.NewTenantGroupService(tenantGroupRepo, ketoService) + userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, ketoService) tenantService.SetKetoService(ketoService) // Keto 주입 - userRepo := repository.NewUserRepository(db) // relyingPartyRepo removed as SSOT is now Hydra+Keto hydraService := service.NewHydraAdminService() relyingPartyService := service.NewRelyingPartyService(hydraService, ketoService) @@ -265,6 +267,7 @@ func main() { devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService) tenantHandler := handler.NewTenantHandler(db, tenantService, ketoService, kratosAdminService) tenantGroupHandler := handler.NewTenantGroupHandler(tenantGroupService, kratosAdminService) + userGroupHandler := handler.NewUserGroupHandler(userGroupService) relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService) userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, ketoService, userRepo) apiKeyHandler := handler.NewApiKeyHandler(db) @@ -585,6 +588,18 @@ func main() { admin.Post("/tenant-groups/:id/admins/:userId", requireSuperAdmin, tenantGroupHandler.AddAdmin) admin.Delete("/tenant-groups/:id/admins/:userId", requireSuperAdmin, tenantGroupHandler.RemoveAdmin) + // User Group Management (Tenant Admin/Super Admin) + userGroups := admin.Group("/tenants/:tenantId/user-groups", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage")) + userGroups.Get("/", userGroupHandler.List) + userGroups.Post("/", userGroupHandler.Create) + userGroups.Get("/:id", userGroupHandler.Get) + userGroups.Put("/:id", userGroupHandler.Update) + userGroups.Delete("/:id", userGroupHandler.Delete) + userGroups.Post("/:id/members", userGroupHandler.AddMember) + userGroups.Delete("/:id/members/:userId", userGroupHandler.RemoveMember) + userGroups.Post("/:id/roles", userGroupHandler.AssignRole) + userGroups.Delete("/:id/roles/:tenantId/:relation", userGroupHandler.RemoveRole) + // Relying Party Management (Global List) admin.Get("/relying-parties", requireAdmin, relyingPartyHandler.ListAll) admin.Get("/relying-parties/:id/owners", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "RelyingParty", "manage"), relyingPartyHandler.ListOwners) diff --git a/backend/internal/handler/user_group_handler.go b/backend/internal/handler/user_group_handler.go index a3bde03b..e5e3604b 100644 --- a/backend/internal/handler/user_group_handler.go +++ b/backend/internal/handler/user_group_handler.go @@ -93,3 +93,30 @@ func (h *UserGroupHandler) RemoveMember(c *fiber.Ctx) error { } return c.SendStatus(fiber.StatusNoContent) } + +func (h *UserGroupHandler) AssignRole(c *fiber.Ctx) error { + groupID := c.Params("id") + var req struct { + TenantID string `json:"tenantId"` + Relation string `json:"relation"` + } + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"}) + } + + if err := h.Service.AssignRoleToTenant(c.Context(), groupID, req.TenantID, req.Relation); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserGroupHandler) RemoveRole(c *fiber.Ctx) error { + groupID := c.Params("id") + tenantID := c.Params("tenantId") + relation := c.Params("relation") + + if err := h.Service.RemoveRoleFromTenant(c.Context(), groupID, tenantID, relation); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index 881c2d0f..c6d8c836 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -17,6 +17,10 @@ type UserGroupService interface { // Member Management with Keto Sync AddMember(ctx context.Context, groupID, userID string) error RemoveMember(ctx context.Context, groupID, userID string) error + + // Permission Management + AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error + RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error } type userGroupService struct { @@ -119,3 +123,25 @@ func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID str return nil } + +func (s *userGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error { + // Keto: Tenant:#@UserGroup:#members + // This means all members of the group have the relation on the tenant. + subject := "UserGroup:" + groupID + "#members" + err := s.ketoService.CreateRelation(ctx, "Tenant", tenantID, relation, subject) + if err != nil { + slog.Error("Failed to assign group role to tenant in keto", "error", err, "group", groupID, "tenant", tenantID, "relation", relation) + return err + } + return nil +} + +func (s *userGroupService) RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error { + subject := "UserGroup:" + groupID + "#members" + err := s.ketoService.DeleteRelation(ctx, "Tenant", tenantID, relation, subject) + if err != nil { + slog.Error("Failed to remove group role from tenant in keto", "error", err, "group", groupID, "tenant", tenantID, "relation", relation) + return err + } + return nil +} diff --git a/docker/ory/keto/namespaces.ts b/docker/ory/keto/namespaces.ts index dc7268e1..b88bfb44 100644 --- a/docker/ory/keto/namespaces.ts +++ b/docker/ory/keto/namespaces.ts @@ -22,8 +22,8 @@ class UserGroup implements Namespace { class Tenant implements Namespace { related: { - admins: User[] - members: User[] + admins: (User | SubjectSet)[] + members: (User | SubjectSet)[] parent: Tenant[] parent_group: TenantGroup[] }