From a6c552236f579d392faea6af94d185e80b74056e Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 19 Mar 2026 11:12:42 +0900 Subject: [PATCH] feat: prevent self-removal and last admin/owner removal in tenant handler --- backend/internal/handler/tenant_handler.go | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 17cd07ae..1b29de6b 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -539,6 +539,30 @@ func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error { return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required") } + if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok { + if profile.ID == userID { + return errorJSON(c, fiber.StatusBadRequest, "cannot remove yourself from admin role") + } + } + + if h.Keto != nil { + if relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", ""); err == nil { + adminCount := 0 + isTargetAdmin := false + for _, rel := range relations { + if strings.HasPrefix(rel.SubjectID, "User:") { + adminCount++ + if rel.SubjectID == "User:"+userID { + isTargetAdmin = true + } + } + } + if isTargetAdmin && adminCount <= 1 { + return errorJSON(c, fiber.StatusBadRequest, "cannot remove the last admin") + } + } + } + if h.KetoOutbox != nil { _ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{ Namespace: "Tenant", @@ -646,6 +670,30 @@ func (h *TenantHandler) RemoveOwner(c *fiber.Ctx) error { return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required") } + if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok { + if profile.ID == userID { + return errorJSON(c, fiber.StatusBadRequest, "cannot remove yourself from owner role") + } + } + + if h.Keto != nil { + if relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "owners", ""); err == nil { + ownerCount := 0 + isTargetOwner := false + for _, rel := range relations { + if strings.HasPrefix(rel.SubjectID, "User:") { + ownerCount++ + if rel.SubjectID == "User:"+userID { + isTargetOwner = true + } + } + } + if isTargetOwner && ownerCount <= 1 { + return errorJSON(c, fiber.StatusBadRequest, "cannot remove the last owner") + } + } + } + if h.KetoOutbox != nil { _ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{ Namespace: "Tenant",