forked from baron/baron-sso
Merge branch 'feature/user-group' into dev
This commit is contained in:
@@ -169,7 +169,7 @@ func (s *ketoService) CreateRelation(ctx context.Context, namespace, object, rel
|
||||
}
|
||||
|
||||
func (s *ketoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||
u, _ := url.Parse(fmt.Sprintf("%s/relation-tuples", s.writeURL))
|
||||
u, _ := url.Parse(fmt.Sprintf("%s/admin/relation-tuples", s.writeURL))
|
||||
q := u.Query()
|
||||
q.Set("namespace", namespace)
|
||||
q.Set("object", object)
|
||||
|
||||
@@ -39,6 +39,74 @@ func (s *tenantService) GetTenant(ctx context.Context, id string) (*domain.Tenan
|
||||
return s.repo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
if s.keto == nil {
|
||||
return nil, errors.New("keto service not initialized")
|
||||
}
|
||||
|
||||
// 1. 직접 관리자인 테넌트 ID 목록 (Tenant:ID#admins@User:ID)
|
||||
directTenantIDs, err := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list direct tenants", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 2. 관리 권한이 있는 유저 그룹 목록 (UserGroup:ID#owners@User:ID)
|
||||
// 정책: 그룹장은 해당 그룹(테넌트)의 어드민이 된다.
|
||||
ownedGroupIDs, err := s.keto.ListObjects(ctx, "UserGroup", "owners", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list owned groups", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 3. 멤버로 속한 유저 그룹 목록 (UserGroup:ID#members@User:ID)
|
||||
memberGroupIDs, err := s.keto.ListObjects(ctx, "UserGroup", "members", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list group memberships", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 4. 유저 그룹을 통해 상속받은 테넌트 목록 조회 (Tenant:ID#manage@UserGroup:ID#members)
|
||||
var inheritedTenantIDs []string
|
||||
allMyGroups := append(ownedGroupIDs, memberGroupIDs...)
|
||||
for _, groupID := range allMyGroups {
|
||||
// 해당 그룹에 부여된 테넌트 관리 권한 역추적
|
||||
relations, err := s.keto.ListRelations(ctx, "Tenant", "", "manage", "UserGroup:"+groupID+"#members")
|
||||
if err == nil {
|
||||
for _, r := range relations {
|
||||
inheritedTenantIDs = append(inheritedTenantIDs, r.Object)
|
||||
}
|
||||
}
|
||||
// view 권한도 관리 가능 목록에 포함 (필요 시)
|
||||
relationsView, err := s.keto.ListRelations(ctx, "Tenant", "", "view", "UserGroup:"+groupID+"#members")
|
||||
if err == nil {
|
||||
for _, r := range relationsView {
|
||||
inheritedTenantIDs = append(inheritedTenantIDs, r.Object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 합산 및 중복 제거
|
||||
allIDsMap := make(map[string]bool)
|
||||
for _, id := range directTenantIDs {
|
||||
allIDsMap[id] = true
|
||||
}
|
||||
for _, id := range ownedGroupIDs {
|
||||
allIDsMap[id] = true // 그룹 자체도 테넌트이므로 포함
|
||||
}
|
||||
for _, id := range inheritedTenantIDs {
|
||||
allIDsMap[id] = true
|
||||
}
|
||||
|
||||
allIDs := make([]string, 0, len(allIDsMap))
|
||||
for id := range allIDsMap {
|
||||
allIDs = append(allIDs, id)
|
||||
}
|
||||
|
||||
if len(allIDs) == 0 {
|
||||
return []domain.Tenant{}, nil
|
||||
}
|
||||
|
||||
return s.repo.FindByIDs(ctx, allIDs)
|
||||
}
|
||||
|
||||
func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error) {
|
||||
// Validate Slug
|
||||
if ok, msg := utils.ValidateSlug(slug); !ok {
|
||||
|
||||
@@ -17,19 +17,34 @@ type UserGroupService interface {
|
||||
// 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, keto KetoService) UserGroupService {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,36 +81,75 @@ func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGrou
|
||||
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:])
|
||||
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)
|
||||
return nil, err
|
||||
}
|
||||
group.Members = members
|
||||
|
||||
// 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) {
|
||||
return s.repo.ListByTenantID(ctx, tenantID)
|
||||
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 {
|
||||
@@ -119,3 +173,63 @@ func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID str
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user