forked from baron/baron-sso
refactor: backend tenant_group 제거 및 관련 정리
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TenantGroup represents a collection of tenants.
|
||||
type TenantGroup struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Slug string `gorm:"uniqueIndex;not null" json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Tenants []Tenant `gorm:"foreignKey:TenantGroupID" json:"tenants,omitempty"`
|
||||
Config JSONMap `gorm:"type:jsonb" json:"config,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (tg *TenantGroup) TableName() string {
|
||||
return "tenant_groups"
|
||||
}
|
||||
|
||||
func (tg *TenantGroup) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if tg.ID == "" {
|
||||
tg.ID = uuid.NewString()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type TenantGroupHandler struct {
|
||||
Service service.TenantGroupService
|
||||
UserService *service.KratosAdminService
|
||||
}
|
||||
|
||||
func NewTenantGroupHandler(svc service.TenantGroupService, userSvc *service.KratosAdminService) *TenantGroupHandler {
|
||||
return &TenantGroupHandler{Service: svc, UserService: userSvc}
|
||||
}
|
||||
|
||||
type tenantGroupSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Tenants []tenantSummary `json:"tenants,omitempty"`
|
||||
Config domain.JSONMap `json:"config,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) ListGroups(c *fiber.Ctx) error {
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
groups, total, err := h.Service.ListGroups(c.Context(), limit, offset)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
items := make([]tenantGroupSummary, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
items = append(items, mapTenantGroupSummary(g))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) GetGroup(c *fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
group, err := h.Service.GetGroup(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "group not found"})
|
||||
}
|
||||
return c.JSON(mapTenantGroupSummary(*group))
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) CreateGroup(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
group, err := h.Service.CreateGroup(c.Context(), req.Name, req.Slug, req.Description)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(mapTenantGroupSummary(*group))
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) UpdateGroup(c *fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
group, err := h.Service.UpdateGroup(c.Context(), id, req.Name, req.Description)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(mapTenantGroupSummary(*group))
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) DeleteGroup(c *fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := h.Service.DeleteGroup(c.Context(), id); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) AddTenantToGroup(c *fiber.Ctx) error {
|
||||
groupID := c.Params("id")
|
||||
tenantID := c.Params("tenantId")
|
||||
|
||||
if err := h.Service.AddTenantToGroup(c.Context(), groupID, tenantID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "tenant added to group"})
|
||||
}
|
||||
|
||||
func (h *TenantGroupHandler) RemoveTenantFromGroup(c *fiber.Ctx) error {
|
||||
groupID := c.Params("id")
|
||||
tenantID := c.Params("tenantId")
|
||||
|
||||
if err := h.Service.RemoveTenantFromGroup(c.Context(), groupID, tenantID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.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 {
|
||||
tenants = append(tenants, mapTenantSummary(t))
|
||||
}
|
||||
|
||||
return tenantGroupSummary{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
Slug: g.Slug,
|
||||
Description: g.Description,
|
||||
Tenants: tenants,
|
||||
Config: g.Config,
|
||||
CreatedAt: g.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: g.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/middleware"
|
||||
"baron-sso-backend/internal/service"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Reusing MockKetoService from previous step or defining here if needed
|
||||
type MockKetoService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockKetoService) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
|
||||
args := m.Called(ctx, subject, namespace, object, relation)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
|
||||
args := m.Called(ctx, namespace, object, relation, subject)
|
||||
return args.Get(0).([]service.RelationTuple), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||
args := m.Called(ctx, namespace, relation, subject)
|
||||
return args.Get(0).([]string), args.Error(1)
|
||||
}
|
||||
|
||||
// MockAuthHandler implements middleware.AuthProfileProvider
|
||||
type MockAuthHandler struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
args := m.Called(c)
|
||||
return args.Get(0).(*domain.UserProfileResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func TestRequireKetoPermission_Tenant_AuditContext(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKeto := new(MockKetoService)
|
||||
mockAuth := new(MockAuthHandler)
|
||||
|
||||
config := middleware.RBACConfig{
|
||||
AuthHandler: mockAuth,
|
||||
KetoService: mockKeto,
|
||||
}
|
||||
|
||||
userID := "user-1"
|
||||
tenantID := "tenant-abc"
|
||||
|
||||
// Mock user profile
|
||||
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
||||
ID: userID,
|
||||
Role: domain.RoleTenantAdmin,
|
||||
}, nil)
|
||||
|
||||
// Mock Keto: Allow access
|
||||
mockKeto.On("CheckPermission", mock.Anything, userID, "Tenant", tenantID, "manage").Return(true, nil)
|
||||
|
||||
// Route with middleware
|
||||
app.Get("/test/tenants/:id", middleware.RequireKetoPermission(config, "Tenant", "manage"), func(c *fiber.Ctx) error {
|
||||
// Verify that tenant_id was injected into Locals for audit log
|
||||
assert.Equal(t, tenantID, c.Locals("tenant_id"))
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
// Execute
|
||||
req := httptest.NewRequest("GET", "/test/tenants/"+tenantID, nil)
|
||||
resp, _ := app.Test(req)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
mockKeto.AssertExpectations(t)
|
||||
mockAuth.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestRequireKetoPermission_Deny(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKeto := new(MockKetoService)
|
||||
mockAuth := new(MockAuthHandler)
|
||||
|
||||
config := middleware.RBACConfig{
|
||||
AuthHandler: mockAuth,
|
||||
KetoService: mockKeto,
|
||||
}
|
||||
|
||||
userID := "user-bad"
|
||||
tenantID := "tenant-secret"
|
||||
|
||||
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
||||
ID: userID,
|
||||
Role: domain.RoleUser,
|
||||
}, nil)
|
||||
|
||||
// Mock Keto: Deny access
|
||||
mockKeto.On("CheckPermission", mock.Anything, userID, "Tenant", tenantID, "view").Return(false, nil)
|
||||
|
||||
app.Get("/test/tenants/:id", middleware.RequireKetoPermission(config, "Tenant", "view"), func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test/tenants/"+tenantID, nil)
|
||||
resp, _ := app.Test(req)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TenantGroupRepository interface {
|
||||
Create(ctx context.Context, group *domain.TenantGroup) error
|
||||
Update(ctx context.Context, group *domain.TenantGroup) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
FindByID(ctx context.Context, id string) (*domain.TenantGroup, error)
|
||||
List(ctx context.Context, limit, offset int) ([]domain.TenantGroup, int64, error)
|
||||
AddTenant(ctx context.Context, groupID, tenantID string) error
|
||||
RemoveTenant(ctx context.Context, groupID, tenantID string) error
|
||||
}
|
||||
|
||||
type tenantGroupRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTenantGroupRepository(db *gorm.DB) TenantGroupRepository {
|
||||
return &tenantGroupRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) Create(ctx context.Context, group *domain.TenantGroup) error {
|
||||
return r.db.WithContext(ctx).Create(group).Error
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) Update(ctx context.Context, group *domain.TenantGroup) error {
|
||||
return r.db.WithContext(ctx).Save(group).Error
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Delete(&domain.TenantGroup{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) FindByID(ctx context.Context, id string) (*domain.TenantGroup, error) {
|
||||
var group domain.TenantGroup
|
||||
if err := r.db.WithContext(ctx).Preload("Tenants").First(&group, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) List(ctx context.Context, limit, offset int) ([]domain.TenantGroup, int64, error) {
|
||||
var groups []domain.TenantGroup
|
||||
var total int64
|
||||
db := r.db.WithContext(ctx).Model(&domain.TenantGroup{})
|
||||
db.Count(&total)
|
||||
if err := db.Limit(limit).Offset(offset).Find(&groups).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return groups, total, nil
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) AddTenant(ctx context.Context, groupID, tenantID string) error {
|
||||
return r.db.WithContext(ctx).Model(&domain.Tenant{}).Where("id = ?", tenantID).Update("tenant_group_id", groupID).Error
|
||||
}
|
||||
|
||||
func (r *tenantGroupRepository) RemoveTenant(ctx context.Context, groupID, tenantID string) error {
|
||||
return r.db.WithContext(ctx).Model(&domain.Tenant{}).Where("id = ? AND tenant_group_id = ?", tenantID, groupID).Update("tenant_group_id", nil).Error
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/repository"
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type TenantGroupService interface {
|
||||
CreateGroup(ctx context.Context, name, slug, description string) (*domain.TenantGroup, error)
|
||||
GetGroup(ctx context.Context, id string) (*domain.TenantGroup, error)
|
||||
ListGroups(ctx context.Context, limit, offset int) ([]domain.TenantGroup, int64, error)
|
||||
UpdateGroup(ctx context.Context, id string, name, description string) (*domain.TenantGroup, error)
|
||||
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 {
|
||||
repo repository.TenantGroupRepository
|
||||
keto KetoService
|
||||
}
|
||||
|
||||
func NewTenantGroupService(repo repository.TenantGroupRepository, keto KetoService) TenantGroupService {
|
||||
return &tenantGroupService{repo: repo, keto: keto}
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) CreateGroup(ctx context.Context, name, slug, description string) (*domain.TenantGroup, error) {
|
||||
group := &domain.TenantGroup{
|
||||
Name: name,
|
||||
Slug: slug,
|
||||
Description: description,
|
||||
}
|
||||
if err := s.repo.Create(ctx, group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) GetGroup(ctx context.Context, id string) (*domain.TenantGroup, error) {
|
||||
return s.repo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) ListGroups(ctx context.Context, limit, offset int) ([]domain.TenantGroup, int64, error) {
|
||||
return s.repo.List(ctx, limit, offset)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) UpdateGroup(ctx context.Context, id string, name, description string) (*domain.TenantGroup, error) {
|
||||
group, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
group.Name = name
|
||||
group.Description = description
|
||||
if err := s.repo.Update(ctx, group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) DeleteGroup(ctx context.Context, id string) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) AddTenantToGroup(ctx context.Context, groupID, tenantID string) error {
|
||||
if err := s.repo.AddTenant(ctx, groupID, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// [Keto] ReBAC: Tenant -> Group membership
|
||||
if s.keto != nil {
|
||||
err := s.keto.CreateRelation(ctx, "Tenant", tenantID, "parent_group", groupID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to sync Keto relation for tenant group", "tenantID", tenantID, "groupID", groupID, "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) RemoveTenantFromGroup(ctx context.Context, groupID, tenantID string) error {
|
||||
if err := s.repo.RemoveTenant(ctx, groupID, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// [Keto] ReBAC: Remove Tenant -> Group membership
|
||||
if s.keto != nil {
|
||||
err := s.keto.DeleteRelation(ctx, "Tenant", tenantID, "parent_group", groupID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to remove Keto relation for tenant group", "tenantID", tenantID, "groupID", groupID, "error", err)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockTenantRepository is a mock implementation of repository.TenantRepository
|
||||
type MockTenantRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error {
|
||||
return m.Called(ctx, tenant).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) Update(ctx context.Context, tenant *domain.Tenant) error {
|
||||
return m.Called(ctx, tenant).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, slug)
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByName(ctx context.Context, name string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, name)
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, domainName)
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) {
|
||||
args := m.Called(ctx, ids)
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string) error {
|
||||
return m.Called(ctx, tenantID, domainName).Error(0)
|
||||
}
|
||||
|
||||
func TestTenantService_ListManageableTenants_Inheritance(t *testing.T) {
|
||||
mockRepo := new(MockTenantRepository)
|
||||
mockKeto := new(MockKetoService)
|
||||
svc := &tenantService{
|
||||
repo: mockRepo,
|
||||
keto: mockKeto,
|
||||
}
|
||||
|
||||
userID := "user-123"
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. Mock direct tenant management (admins relation)
|
||||
mockKeto.On("ListObjects", ctx, "Tenant", "admins", userID).Return([]string{"t-direct-1"}, nil)
|
||||
|
||||
// 2. Mock group management (admins of a group)
|
||||
mockKeto.On("ListObjects", ctx, "TenantGroup", "admins", userID).Return([]string{"g-1"}, nil)
|
||||
|
||||
// 3. Mock tenants belonging to group g-1
|
||||
mockKeto.On("ListRelations", ctx, "Tenant", "", "parent_group", "TenantGroup:g-1").Return([]RelationTuple{
|
||||
{Object: "t-inherited-1", Relation: "parent_group", SubjectID: "TenantGroup:g-1"},
|
||||
{Object: "t-inherited-2", Relation: "parent_group", SubjectID: "TenantGroup:g-1"},
|
||||
}, nil)
|
||||
|
||||
// 4. Expect repository to fetch all unique IDs: t-direct-1, t-inherited-1, t-inherited-2
|
||||
expectedIDs := []string{"t-direct-1", "t-inherited-1", "t-inherited-2"}
|
||||
mockRepo.On("FindByIDs", ctx, mock.MatchedBy(func(ids []string) bool {
|
||||
// Check if all expected IDs are present (order doesn't matter since we dedup via map)
|
||||
foundCount := 0
|
||||
for _, eid := range expectedIDs {
|
||||
for _, id := range ids {
|
||||
if id == eid {
|
||||
foundCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundCount == len(expectedIDs) && len(ids) == len(expectedIDs)
|
||||
})).Return([]domain.Tenant{
|
||||
{ID: "t-direct-1", Name: "Direct Tenant"},
|
||||
{ID: "t-inherited-1", Name: "Inherited Tenant 1"},
|
||||
{ID: "t-inherited-2", Name: "Inherited Tenant 2"},
|
||||
}, nil)
|
||||
|
||||
// Execute
|
||||
tenants, err := svc.ListManageableTenants(ctx, userID)
|
||||
|
||||
// Verify
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tenants, 3)
|
||||
mockKeto.AssertExpectations(t)
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetEnv retrieves the value of the environment variable named by the key.
|
||||
// It returns the value if it exists, otherwise it returns the fallback value.
|
||||
// It automatically strips surrounding double quotes from the value.
|
||||
func GetEnv(key, fallback string) string {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
// Strip surrounding double quotes if present
|
||||
v = strings.TrimSpace(v)
|
||||
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||
return v[1 : len(v)-1]
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user