forked from baron/baron-sso
feat: 테넌트/RP 관리자 할당 UI 및 ReBAC 권한 검증 도구 구현 #244
This commit is contained in:
@@ -1,22 +1,51 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/service"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AdminHandler struct{}
|
||||
type AdminHandler struct {
|
||||
Keto service.KetoService
|
||||
}
|
||||
|
||||
func NewAdminHandler() *AdminHandler {
|
||||
return &AdminHandler{}
|
||||
func NewAdminHandler(keto service.KetoService) *AdminHandler {
|
||||
return &AdminHandler{Keto: keto}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CheckPermission(c *fiber.Ctx) error {
|
||||
namespace := c.Query("namespace")
|
||||
object := c.Query("object")
|
||||
relation := c.Query("relation")
|
||||
subject := c.Query("subject")
|
||||
|
||||
if namespace == "" || object == "" || relation == "" || subject == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "namespace, object, relation, and subject are required"})
|
||||
}
|
||||
|
||||
allowed, err := h.Keto.CheckPermission(c.Context(), subject, namespace, object, relation)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"allowed": allowed,
|
||||
"query": fiber.Map{
|
||||
"namespace": namespace,
|
||||
"object": object,
|
||||
"relation": relation,
|
||||
"subject": subject,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetSystemStats returns runtime statistics for monitoring
|
||||
func (h *AdminHandler) GetSystemStats(c *fiber.Ctx) error {
|
||||
var m runtime.MemStats
|
||||
|
||||
@@ -166,6 +166,11 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Set for audit logging
|
||||
if tid, ok := client.Metadata["tenant_id"].(string); ok {
|
||||
c.Locals("tenant_id", tid)
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*client)
|
||||
return c.JSON(clientDetailResponse{
|
||||
Client: summary,
|
||||
@@ -239,6 +244,9 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "X-Tenant-ID header is required"})
|
||||
}
|
||||
|
||||
// Set for audit logging
|
||||
c.Locals("tenant_id", targetTenantID)
|
||||
|
||||
// Validate Permission
|
||||
isAllowed := false
|
||||
if profile.Role == domain.RoleSuperAdmin {
|
||||
@@ -371,6 +379,11 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Set for audit logging
|
||||
if tid, ok := current.Metadata["tenant_id"].(string); ok {
|
||||
c.Locals("tenant_id", tid)
|
||||
}
|
||||
|
||||
clientType := ""
|
||||
if req.Type != nil {
|
||||
clientType = strings.ToLower(strings.TrimSpace(*req.Type))
|
||||
@@ -446,6 +459,14 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
||||
}
|
||||
|
||||
// Fetch first for audit log tenant_id
|
||||
client, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||
if err == nil {
|
||||
if tid, ok := client.Metadata["tenant_id"].(string); ok {
|
||||
c.Locals("tenant_id", tid)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
|
||||
if errors.Is(err, service.ErrHydraNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
||||
@@ -625,6 +646,11 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Set for audit logging
|
||||
if tid, ok := current.Metadata["tenant_id"].(string); ok {
|
||||
c.Locals("tenant_id", tid)
|
||||
}
|
||||
|
||||
// 3. Update Hydra
|
||||
current.ClientSecret = newSecret
|
||||
updated, err := h.Hydra.UpdateClient(c.Context(), clientID, *current)
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
|
||||
type RelyingPartyHandler struct {
|
||||
Service service.RelyingPartyService
|
||||
UserSvc *service.KratosAdminService
|
||||
}
|
||||
|
||||
func NewRelyingPartyHandler(s service.RelyingPartyService) *RelyingPartyHandler {
|
||||
return &RelyingPartyHandler{Service: s}
|
||||
func NewRelyingPartyHandler(s service.RelyingPartyService, userSvc *service.KratosAdminService) *RelyingPartyHandler {
|
||||
return &RelyingPartyHandler{Service: s, UserSvc: userSvc}
|
||||
}
|
||||
|
||||
func (h *RelyingPartyHandler) Create(c *fiber.Ctx) error {
|
||||
@@ -110,3 +111,58 @@ func (h *RelyingPartyHandler) Delete(c *fiber.Ctx) error {
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *RelyingPartyHandler) ListOwners(c *fiber.Ctx) error {
|
||||
clientID := c.Params("id")
|
||||
subjects, err := h.Service.ListOwners(c.Context(), clientID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
type ownerInfo struct {
|
||||
Subject string `json:"subject"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Type string `json:"type"` // "user" or "group"
|
||||
}
|
||||
|
||||
owners := make([]ownerInfo, 0, len(subjects))
|
||||
for _, s := range subjects {
|
||||
info := ownerInfo{Subject: s, Type: "unknown"}
|
||||
if len(s) > 5 && s[:5] == "User:" {
|
||||
info.Type = "user"
|
||||
userID := s[5:]
|
||||
identity, err := h.UserSvc.GetIdentity(c.Context(), userID)
|
||||
if err == nil && identity != nil {
|
||||
info.Name, _ = identity.Traits["name"].(string)
|
||||
info.Email, _ = identity.Traits["email"].(string)
|
||||
}
|
||||
} else if len(s) > 10 && s[:10] == "UserGroup:" {
|
||||
info.Type = "group"
|
||||
// Group name enrichment could be added if we have a GroupService here
|
||||
}
|
||||
owners = append(owners, info)
|
||||
}
|
||||
|
||||
return c.JSON(owners)
|
||||
}
|
||||
|
||||
func (h *RelyingPartyHandler) AddOwner(c *fiber.Ctx) error {
|
||||
clientID := c.Params("id")
|
||||
subject := c.Params("subject") // e.g. "User:uuid"
|
||||
|
||||
if err := h.Service.AddOwner(c.Context(), clientID, subject); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "owner added"})
|
||||
}
|
||||
|
||||
func (h *RelyingPartyHandler) RemoveOwner(c *fiber.Ctx) error {
|
||||
clientID := c.Params("id")
|
||||
subject := c.Params("subject")
|
||||
|
||||
if err := h.Service.RemoveOwner(c.Context(), clientID, subject); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "owner removed"})
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
)
|
||||
|
||||
type TenantGroupHandler struct {
|
||||
Service service.TenantGroupService
|
||||
Service service.TenantGroupService
|
||||
UserService *service.KratosAdminService
|
||||
}
|
||||
|
||||
func NewTenantGroupHandler(svc service.TenantGroupService) *TenantGroupHandler {
|
||||
return &TenantGroupHandler{Service: svc}
|
||||
func NewTenantGroupHandler(svc service.TenantGroupService, userSvc *service.KratosAdminService) *TenantGroupHandler {
|
||||
return &TenantGroupHandler{Service: svc, UserService: userSvc}
|
||||
}
|
||||
|
||||
type tenantGroupSummary struct {
|
||||
@@ -120,6 +121,59 @@ func (h *TenantGroupHandler) RemoveTenantFromGroup(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"message": "tenant removed from group"})
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) ListAdmins(c *fiber.Ctx) error {
|
||||
groupID := c.Params("id")
|
||||
userIDs, err := h.Service.ListGroupAdmins(c.Context(), groupID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
type adminInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
admins := make([]adminInfo, 0, len(userIDs))
|
||||
for _, uid := range userIDs {
|
||||
identity, err := h.UserService.GetIdentity(c.Context(), uid)
|
||||
if err == nil && identity != nil {
|
||||
name, _ := identity.Traits["name"].(string)
|
||||
email, _ := identity.Traits["email"].(string)
|
||||
admins = append(admins, adminInfo{
|
||||
ID: uid,
|
||||
Name: name,
|
||||
Email: email,
|
||||
})
|
||||
} else {
|
||||
// Fallback if identity not found in Kratos
|
||||
admins = append(admins, adminInfo{ID: uid})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(admins)
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) AddAdmin(c *fiber.Ctx) error {
|
||||
groupID := c.Params("id")
|
||||
userID := c.Params("userId")
|
||||
|
||||
if err := h.Service.AddGroupAdmin(c.Context(), groupID, userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "admin added to group"})
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) RemoveAdmin(c *fiber.Ctx) error {
|
||||
groupID := c.Params("id")
|
||||
userID := c.Params("userId")
|
||||
|
||||
if err := h.Service.RemoveGroupAdmin(c.Context(), groupID, userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "admin removed from group"})
|
||||
}
|
||||
|
||||
func mapTenantGroupSummary(g domain.TenantGroup) tenantGroupSummary {
|
||||
tenants := make([]tenantSummary, 0, len(g.Tenants))
|
||||
for _, t := range g.Tenants {
|
||||
|
||||
@@ -15,10 +15,11 @@ type TenantHandler struct {
|
||||
DB *gorm.DB
|
||||
Service service.TenantService
|
||||
Keto service.KetoService
|
||||
UserSvc *service.KratosAdminService
|
||||
}
|
||||
|
||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService, keto service.KetoService) *TenantHandler {
|
||||
return &TenantHandler{DB: db, Service: svc, Keto: keto}
|
||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService, keto service.KetoService, userSvc *service.KratosAdminService) *TenantHandler {
|
||||
return &TenantHandler{DB: db, Service: svc, Keto: keto, UserSvc: userSvc}
|
||||
}
|
||||
|
||||
type tenantSummary struct {
|
||||
@@ -327,6 +328,58 @@ func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ListAdmins(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
userIDs, err := h.Service.ListTenantAdmins(c.Context(), tenantID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
type adminInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
admins := make([]adminInfo, 0, len(userIDs))
|
||||
for _, uid := range userIDs {
|
||||
identity, err := h.UserSvc.GetIdentity(c.Context(), uid)
|
||||
if err == nil && identity != nil {
|
||||
name, _ := identity.Traits["name"].(string)
|
||||
email, _ := identity.Traits["email"].(string)
|
||||
admins = append(admins, adminInfo{
|
||||
ID: uid,
|
||||
Name: name,
|
||||
Email: email,
|
||||
})
|
||||
} else {
|
||||
admins = append(admins, adminInfo{ID: uid})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(admins)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
userID := c.Params("userId")
|
||||
|
||||
if err := h.Service.AddTenantAdmin(c.Context(), tenantID, userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "admin added to tenant"})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
userID := c.Params("userId")
|
||||
|
||||
if err := h.Service.RemoveTenantAdmin(c.Context(), tenantID, userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "admin removed from tenant"})
|
||||
}
|
||||
|
||||
func mapTenantSummary(t domain.Tenant) tenantSummary {
|
||||
domains := make([]string, 0, len(t.Domains))
|
||||
for _, d := range t.Domains {
|
||||
|
||||
Reference in New Issue
Block a user