1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/service/user_group_service.go
2026-02-05 10:15:54 +09:00

122 lines
3.8 KiB
Go

package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"context"
"log/slog"
)
type UserGroupService interface {
Create(ctx context.Context, group *domain.UserGroup) error
Update(ctx context.Context, group *domain.UserGroup) error
Delete(ctx context.Context, id string) error
Get(ctx context.Context, id string) (*domain.UserGroup, error)
List(ctx context.Context, tenantID string) ([]domain.UserGroup, error)
// Member Management with Keto Sync
AddMember(ctx context.Context, groupID, userID string) error
RemoveMember(ctx context.Context, groupID, userID string) error
}
type userGroupService struct {
repo repository.UserGroupRepository
userRepo repository.UserRepository
ketoService KetoService
}
func NewUserGroupService(repo repository.UserGroupRepository, userRepo repository.UserRepository, keto KetoService) UserGroupService {
return &userGroupService{
repo: repo,
userRepo: userRepo,
ketoService: keto,
}
}
func (s *userGroupService) Create(ctx context.Context, group *domain.UserGroup) error {
if err := s.repo.Create(ctx, group); err != nil {
return err
}
// Keto: UserGroup:<id>#parent_tenant@Tenant:<tid>
err := s.ketoService.CreateRelation(ctx, "UserGroup", group.ID, "parent_tenant", "Tenant:"+group.TenantID)
if err != nil {
slog.Error("Failed to create keto relation for user group", "error", err, "group_id", group.ID)
}
return nil
}
func (s *userGroupService) Update(ctx context.Context, group *domain.UserGroup) error {
return s.repo.Update(ctx, group)
}
func (s *userGroupService) Delete(ctx context.Context, id string) error {
// Optional: Delete relations in Keto before DB delete
return s.repo.Delete(ctx, id)
}
func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
group, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
// Fetch members from Keto
tuples, err := s.ketoService.ListRelations(ctx, "UserGroup", group.ID, "members", "")
if err != nil {
slog.Error("Failed to fetch group members from keto", "error", err, "group_id", group.ID)
// Return group without members rather than failing?
// But if we fail here, we might hide partial failure. Let's log and proceed or return error?
// For now, let's proceed with empty members to avoid blocking UI if keto is down?
// No, SSOT is Keto. If Keto is down, we can't show members.
// Returning error might be safer.
return nil, err
}
var userIDs []string
for _, t := range tuples {
// SubjectID is like "User:uuid"
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
userIDs = append(userIDs, t.SubjectID[5:])
}
}
if len(userIDs) > 0 {
members, err := s.userRepo.FindByIDs(ctx, userIDs)
if err != nil {
slog.Error("Failed to fetch member details from db", "error", err)
return nil, err
}
group.Members = members
}
return group, nil
}
func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.UserGroup, error) {
return s.repo.ListByTenantID(ctx, tenantID)
}
func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string) error {
// Keto: UserGroup:<groupID>#members@User:<userID>
err := s.ketoService.CreateRelation(ctx, "UserGroup", groupID, "members", "User:"+userID)
if err != nil {
slog.Error("Failed to sync group membership to keto", "error", err, "group", groupID, "user", userID)
return err
}
return nil
}
func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
// Keto: Delete relation
err := s.ketoService.DeleteRelation(ctx, "UserGroup", groupID, "members", "User:"+userID)
if err != nil {
slog.Error("Failed to remove group membership from keto", "error", err, "group", groupID, "user", userID)
return err
}
return nil
}