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 }