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 {
|
||||
|
||||
@@ -16,6 +16,9 @@ type RelyingPartyService interface {
|
||||
Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error)
|
||||
Delete(ctx context.Context, clientID string) error
|
||||
CheckPermission(ctx context.Context, userID, clientID, relation string) (bool, error)
|
||||
AddOwner(ctx context.Context, clientID, subject string) error
|
||||
RemoveOwner(ctx context.Context, clientID, subject string) error
|
||||
ListOwners(ctx context.Context, clientID string) ([]string, error)
|
||||
}
|
||||
|
||||
type relyingPartyService struct {
|
||||
@@ -163,6 +166,27 @@ func (s *relyingPartyService) CheckPermission(ctx context.Context, userID, clien
|
||||
return s.ketoService.CheckPermission(ctx, userID, "RelyingParty", clientID, relation)
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) AddOwner(ctx context.Context, clientID, subject string) error {
|
||||
return s.ketoService.CreateRelation(ctx, "RelyingParty", clientID, "owners", subject)
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) RemoveOwner(ctx context.Context, clientID, subject string) error {
|
||||
return s.ketoService.DeleteRelation(ctx, "RelyingParty", clientID, "owners", subject)
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) ListOwners(ctx context.Context, clientID string) ([]string, error) {
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "RelyingParty", clientID, "owners", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subjects := make([]string, 0, len(tuples))
|
||||
for _, t := range tuples {
|
||||
subjects = append(subjects, t.SubjectID)
|
||||
}
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) mapHydraToDomain(client *domain.HydraClient) *domain.RelyingParty {
|
||||
if client == nil {
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,9 @@ type TenantGroupService interface {
|
||||
DeleteGroup(ctx context.Context, id string) error
|
||||
AddTenantToGroup(ctx context.Context, groupID, tenantID string) error
|
||||
RemoveTenantFromGroup(ctx context.Context, groupID, tenantID string) error
|
||||
AddGroupAdmin(ctx context.Context, groupID, userID string) error
|
||||
RemoveGroupAdmin(ctx context.Context, groupID, userID string) error
|
||||
ListGroupAdmins(ctx context.Context, groupID string) ([]string, error)
|
||||
}
|
||||
|
||||
type tenantGroupService struct {
|
||||
@@ -92,3 +95,36 @@ func (s *tenantGroupService) RemoveTenantFromGroup(ctx context.Context, groupID,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) AddGroupAdmin(ctx context.Context, groupID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return nil
|
||||
}
|
||||
return s.keto.CreateRelation(ctx, "TenantGroup", groupID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) RemoveGroupAdmin(ctx context.Context, groupID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return nil
|
||||
}
|
||||
return s.keto.DeleteRelation(ctx, "TenantGroup", groupID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) ListGroupAdmins(ctx context.Context, groupID string) ([]string, error) {
|
||||
if s.keto == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
tuples, err := s.keto.ListRelations(ctx, "TenantGroup", groupID, "admins", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDs := make([]string, 0, len(tuples))
|
||||
for _, t := range tuples {
|
||||
// subject_id is "User:uuid"
|
||||
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
|
||||
userIDs = append(userIDs, t.SubjectID[5:])
|
||||
}
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ type TenantService interface {
|
||||
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
||||
ApproveTenant(ctx context.Context, id string) error
|
||||
SetKetoService(keto KetoService) // 추가
|
||||
AddTenantAdmin(ctx context.Context, tenantID, userID string) error
|
||||
RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error
|
||||
ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error)
|
||||
}
|
||||
|
||||
type tenantService struct {
|
||||
@@ -208,3 +211,35 @@ func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain strin
|
||||
func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
return s.repo.FindBySlug(ctx, slug)
|
||||
}
|
||||
|
||||
func (s *tenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return errors.New("keto service not initialized")
|
||||
}
|
||||
return s.keto.CreateRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return errors.New("keto service not initialized")
|
||||
}
|
||||
return s.keto.DeleteRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
|
||||
if s.keto == nil {
|
||||
return nil, errors.New("keto service not initialized")
|
||||
}
|
||||
tuples, err := s.keto.ListRelations(ctx, "Tenant", tenantID, "admins", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDs := make([]string, 0, len(tuples))
|
||||
for _, t := range tuples {
|
||||
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
|
||||
userIDs = append(userIDs, t.SubjectID[5:])
|
||||
}
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user