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:#parent_tenant@Tenant: 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:#members@User: 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 }