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 // Permission Management ListRoles(ctx context.Context, groupID string) ([]domain.GroupRole, error) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error } type userGroupService struct { repo repository.UserGroupRepository userRepo repository.UserRepository tenantRepo repository.TenantRepository ketoService KetoService kratos *KratosAdminService } func NewUserGroupService( repo repository.UserGroupRepository, userRepo repository.UserRepository, tenantRepo repository.TenantRepository, keto KetoService, kratos *KratosAdminService, ) UserGroupService { return &userGroupService{ repo: repo, userRepo: userRepo, tenantRepo: tenantRepo, ketoService: keto, kratos: kratos, } } 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 nil, err } var userIDs []string for _, t := range tuples { sid := t.SubjectID if len(sid) > 5 && sid[:5] == "User:" { userIDs = append(userIDs, sid[5:]) } else { userIDs = append(userIDs, sid) } } if len(userIDs) > 0 { // 1. Try to find in local DB members, err := s.userRepo.FindByIDs(ctx, userIDs) if err != nil { slog.Error("Failed to fetch member details from db", "error", err) } // 2. Map existing DB members memberMap := make(map[string]domain.User) for _, m := range members { memberMap[m.ID] = m } // 3. For IDs not in DB, fetch from Kratos var finalMembers []domain.User for _, uid := range userIDs { if m, ok := memberMap[uid]; ok { finalMembers = append(finalMembers, m) } else if s.kratos != nil { // Fallback to Kratos identity, err := s.kratos.GetIdentity(ctx, uid) if err == nil && identity != nil { name, _ := identity.Traits["name"].(string) email, _ := identity.Traits["email"].(string) finalMembers = append(finalMembers, domain.User{ ID: uid, Name: name, Email: email, }) } } } group.Members = finalMembers } else { group.Members = []domain.User{} } return group, nil } func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.UserGroup, error) { groups, err := s.repo.ListByTenantID(ctx, tenantID) if err != nil { return nil, err } // For each group, fetch member count from Keto for i := range groups { tuples, err := s.ketoService.ListRelations(ctx, "UserGroup", groups[i].ID, "members", "") if err == nil { // Create dummy members just to carry the count for the JSON response groups[i].Members = make([]domain.User, len(tuples)) } } return groups, nil } 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 } func (s *userGroupService) ListRoles(ctx context.Context, groupID string) ([]domain.GroupRole, error) { // Query: namespace=Tenant, subject=UserGroup:groupID#members subject := "UserGroup:" + groupID + "#members" tuples, err := s.ketoService.ListRelations(ctx, "Tenant", "", "", subject) if err != nil { slog.Error("Failed to fetch group roles from keto", "error", err, "group_id", groupID) return nil, err } var roles []domain.GroupRole tenantIDs := make([]string, 0, len(tuples)) for _, t := range tuples { tenantIDs = append(tenantIDs, t.Object) } if len(tenantIDs) > 0 { tenantList, err := s.tenantRepo.FindByIDs(ctx, tenantIDs) if err != nil { slog.Error("Failed to fetch tenant details for roles", "error", err) } tenantMap := make(map[string]string) for _, t := range tenantList { tenantMap[t.ID] = t.Name } for _, t := range tuples { roles = append(roles, domain.GroupRole{ TenantID: t.Object, TenantName: tenantMap[t.Object], Relation: t.Relation, }) } } return roles, nil } func (s *userGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error { // Keto: Tenant:#@UserGroup:#members // This means all members of the group have the relation on the tenant. subject := "UserGroup:" + groupID + "#members" err := s.ketoService.CreateRelation(ctx, "Tenant", tenantID, relation, subject) if err != nil { slog.Error("Failed to assign group role to tenant in keto", "error", err, "group", groupID, "tenant", tenantID, "relation", relation) return err } return nil } func (s *userGroupService) RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error { subject := "UserGroup:" + groupID + "#members" err := s.ketoService.DeleteRelation(ctx, "Tenant", tenantID, relation, subject) if err != nil { slog.Error("Failed to remove group role from tenant in keto", "error", err, "group", groupID, "tenant", tenantID, "relation", relation) return err } return nil }