forked from baron/baron-sso
236 lines
7.0 KiB
Go
236 lines
7.0 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
|
|
|
|
// 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:<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 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:<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
|
|
}
|
|
|
|
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:<tenantID>#<relation>@UserGroup:<groupID>#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
|
|
}
|