forked from baron/baron-sso
Merge branch 'feature/tenant-group-239' into dev
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -27,8 +28,8 @@ type HydraAdminService struct {
|
||||
|
||||
func NewHydraAdminService() *HydraAdminService {
|
||||
return &HydraAdminService{
|
||||
AdminURL: getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
|
||||
PublicURL: getenv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
|
||||
AdminURL: utils.GetEnv("HYDRA_ADMIN_URL", "http://hydra:4445"),
|
||||
PublicURL: utils.GetEnv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ type KetoService interface {
|
||||
CreateRelation(ctx context.Context, namespace, object, relation, subject string) error
|
||||
DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error
|
||||
ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]RelationTuple, error)
|
||||
ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error)
|
||||
}
|
||||
|
||||
type ketoService struct {
|
||||
@@ -27,14 +28,8 @@ type ketoService struct {
|
||||
}
|
||||
|
||||
func NewKetoService() KetoService {
|
||||
readURL := os.Getenv("KETO_READ_URL")
|
||||
if readURL == "" {
|
||||
readURL = "http://keto:4466"
|
||||
}
|
||||
writeURL := os.Getenv("KETO_WRITE_URL")
|
||||
if writeURL == "" {
|
||||
writeURL = "http://keto:4467"
|
||||
}
|
||||
readURL := utils.GetEnv("KETO_READ_URL", "http://keto:4466")
|
||||
writeURL := utils.GetEnv("KETO_WRITE_URL", "http://keto:4467")
|
||||
|
||||
return &ketoService{
|
||||
readURL: readURL,
|
||||
@@ -192,3 +187,40 @@ func (s *ketoService) DeleteRelation(ctx context.Context, namespace, object, rel
|
||||
slog.Info("Keto relation deleted", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ketoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||
u, _ := url.Parse(fmt.Sprintf("%s/relation-tuples", s.readURL))
|
||||
q := u.Query()
|
||||
q.Set("namespace", namespace)
|
||||
q.Set("relation", relation)
|
||||
q.Set("subject_id", subject)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("keto returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var res relationTuplesResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects := make([]string, 0, len(res.RelationTuples))
|
||||
seen := make(map[string]bool)
|
||||
for _, rt := range res.RelationTuples {
|
||||
if !seen[rt.Object] {
|
||||
objects = append(objects, rt.Object)
|
||||
seen[rt.Object] = true
|
||||
}
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -28,7 +29,7 @@ type KratosAdminService struct {
|
||||
|
||||
func NewKratosAdminService() *KratosAdminService {
|
||||
return &KratosAdminService{
|
||||
AdminURL: getenvKratos("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
||||
AdminURL: utils.GetEnv("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,8 +228,9 @@ func (s *KratosAdminService) httpClient() *http.Client {
|
||||
}
|
||||
|
||||
func getenvKratos(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
return fallback
|
||||
return strings.Trim(v, "\"")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -27,9 +28,9 @@ type OryProvider struct {
|
||||
|
||||
func NewOryProvider() *OryProvider {
|
||||
return &OryProvider{
|
||||
KratosAdminURL: getenv("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
||||
KratosPublicURL: getenv("KRATOS_PUBLIC_URL", "http://kratos:4433"),
|
||||
HydraAdminURL: getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
|
||||
KratosAdminURL: utils.GetEnv("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
||||
KratosPublicURL: utils.GetEnv("KRATOS_PUBLIC_URL", "http://kratos:4433"),
|
||||
HydraAdminURL: utils.GetEnv("HYDRA_ADMIN_URL", "http://hydra:4445"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,10 +729,12 @@ func (o *OryProvider) UpdateUserPassword(loginID, newPassword string, r *http.Re
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
return fallback
|
||||
// Strip surrounding double quotes if present
|
||||
return strings.Trim(v, "\"")
|
||||
}
|
||||
|
||||
// findIdentityID: Kratos Admin API에서 credentials_identifier로 검색 후 첫 번째 identity id 반환
|
||||
|
||||
@@ -15,6 +15,10 @@ type RelyingPartyService interface {
|
||||
ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error)
|
||||
Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error)
|
||||
Delete(ctx context.Context, clientID string) error
|
||||
CheckPermission(ctx context.Context, userID, clientID, relation string) (bool, error)
|
||||
AddOwner(ctx context.Context, clientID, subject string) error
|
||||
RemoveOwner(ctx context.Context, clientID, subject string) error
|
||||
ListOwners(ctx context.Context, clientID string) ([]string, error)
|
||||
}
|
||||
|
||||
type relyingPartyService struct {
|
||||
@@ -158,6 +162,31 @@ func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) CheckPermission(ctx context.Context, userID, clientID, relation string) (bool, error) {
|
||||
return s.ketoService.CheckPermission(ctx, userID, "RelyingParty", clientID, relation)
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) AddOwner(ctx context.Context, clientID, subject string) error {
|
||||
return s.ketoService.CreateRelation(ctx, "RelyingParty", clientID, "owners", subject)
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) RemoveOwner(ctx context.Context, clientID, subject string) error {
|
||||
return s.ketoService.DeleteRelation(ctx, "RelyingParty", clientID, "owners", subject)
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) ListOwners(ctx context.Context, clientID string) ([]string, error) {
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "RelyingParty", clientID, "owners", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subjects := make([]string, 0, len(tuples))
|
||||
for _, t := range tuples {
|
||||
subjects = append(subjects, t.SubjectID)
|
||||
}
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func (s *relyingPartyService) mapHydraToDomain(client *domain.HydraClient) *domain.RelyingParty {
|
||||
if client == nil {
|
||||
return nil
|
||||
|
||||
@@ -54,6 +54,14 @@ func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object,
|
||||
return args.Get(0).([]RelationTuple), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||
args := m.Called(ctx, namespace, relation, subject)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]string), args.Error(1)
|
||||
}
|
||||
|
||||
// --- Test Helpers ---
|
||||
|
||||
type hydraRoundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
130
backend/internal/service/tenant_group_service.go
Normal file
130
backend/internal/service/tenant_group_service.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/repository"
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type TenantGroupService interface {
|
||||
CreateGroup(ctx context.Context, name, slug, description string) (*domain.TenantGroup, error)
|
||||
GetGroup(ctx context.Context, id string) (*domain.TenantGroup, error)
|
||||
ListGroups(ctx context.Context, limit, offset int) ([]domain.TenantGroup, int64, error)
|
||||
UpdateGroup(ctx context.Context, id string, name, description string) (*domain.TenantGroup, error)
|
||||
DeleteGroup(ctx context.Context, id string) error
|
||||
AddTenantToGroup(ctx context.Context, groupID, tenantID string) error
|
||||
RemoveTenantFromGroup(ctx context.Context, groupID, tenantID string) error
|
||||
AddGroupAdmin(ctx context.Context, groupID, userID string) error
|
||||
RemoveGroupAdmin(ctx context.Context, groupID, userID string) error
|
||||
ListGroupAdmins(ctx context.Context, groupID string) ([]string, error)
|
||||
}
|
||||
|
||||
type tenantGroupService struct {
|
||||
repo repository.TenantGroupRepository
|
||||
keto KetoService
|
||||
}
|
||||
|
||||
func NewTenantGroupService(repo repository.TenantGroupRepository, keto KetoService) TenantGroupService {
|
||||
return &tenantGroupService{repo: repo, keto: keto}
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) CreateGroup(ctx context.Context, name, slug, description string) (*domain.TenantGroup, error) {
|
||||
group := &domain.TenantGroup{
|
||||
Name: name,
|
||||
Slug: slug,
|
||||
Description: description,
|
||||
}
|
||||
if err := s.repo.Create(ctx, group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) GetGroup(ctx context.Context, id string) (*domain.TenantGroup, error) {
|
||||
return s.repo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) ListGroups(ctx context.Context, limit, offset int) ([]domain.TenantGroup, int64, error) {
|
||||
return s.repo.List(ctx, limit, offset)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) UpdateGroup(ctx context.Context, id string, name, description string) (*domain.TenantGroup, error) {
|
||||
group, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
group.Name = name
|
||||
group.Description = description
|
||||
if err := s.repo.Update(ctx, group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) DeleteGroup(ctx context.Context, id string) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) AddTenantToGroup(ctx context.Context, groupID, tenantID string) error {
|
||||
if err := s.repo.AddTenant(ctx, groupID, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// [Keto] ReBAC: Tenant -> Group membership
|
||||
if s.keto != nil {
|
||||
err := s.keto.CreateRelation(ctx, "Tenant", tenantID, "parent_group", groupID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to sync Keto relation for tenant group", "tenantID", tenantID, "groupID", groupID, "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) RemoveTenantFromGroup(ctx context.Context, groupID, tenantID string) error {
|
||||
if err := s.repo.RemoveTenant(ctx, groupID, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// [Keto] ReBAC: Remove Tenant -> Group membership
|
||||
if s.keto != nil {
|
||||
err := s.keto.DeleteRelation(ctx, "Tenant", tenantID, "parent_group", groupID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to remove Keto relation for tenant group", "tenantID", tenantID, "groupID", groupID, "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) AddGroupAdmin(ctx context.Context, groupID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return nil
|
||||
}
|
||||
return s.keto.CreateRelation(ctx, "TenantGroup", groupID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) RemoveGroupAdmin(ctx context.Context, groupID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return nil
|
||||
}
|
||||
return s.keto.DeleteRelation(ctx, "TenantGroup", groupID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantGroupService) ListGroupAdmins(ctx context.Context, groupID string) ([]string, error) {
|
||||
if s.keto == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
tuples, err := s.keto.ListRelations(ctx, "TenantGroup", groupID, "admins", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDs := make([]string, 0, len(tuples))
|
||||
for _, t := range tuples {
|
||||
// subject_id is "User:uuid"
|
||||
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
|
||||
userIDs = append(userIDs, t.SubjectID[5:])
|
||||
}
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
108
backend/internal/service/tenant_rebac_service_test.go
Normal file
108
backend/internal/service/tenant_rebac_service_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockTenantRepository is a mock implementation of repository.TenantRepository
|
||||
type MockTenantRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error {
|
||||
return m.Called(ctx, tenant).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) Update(ctx context.Context, tenant *domain.Tenant) error {
|
||||
return m.Called(ctx, tenant).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, slug)
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByName(ctx context.Context, name string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, name)
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, domainName)
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) {
|
||||
args := m.Called(ctx, ids)
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string) error {
|
||||
return m.Called(ctx, tenantID, domainName).Error(0)
|
||||
}
|
||||
|
||||
func TestTenantService_ListManageableTenants_Inheritance(t *testing.T) {
|
||||
mockRepo := new(MockTenantRepository)
|
||||
mockKeto := new(MockKetoService)
|
||||
svc := &tenantService{
|
||||
repo: mockRepo,
|
||||
keto: mockKeto,
|
||||
}
|
||||
|
||||
userID := "user-123"
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. Mock direct tenant management (admins relation)
|
||||
mockKeto.On("ListObjects", ctx, "Tenant", "admins", userID).Return([]string{"t-direct-1"}, nil)
|
||||
|
||||
// 2. Mock group management (admins of a group)
|
||||
mockKeto.On("ListObjects", ctx, "TenantGroup", "admins", userID).Return([]string{"g-1"}, nil)
|
||||
|
||||
// 3. Mock tenants belonging to group g-1
|
||||
mockKeto.On("ListRelations", ctx, "Tenant", "", "parent_group", "TenantGroup:g-1").Return([]RelationTuple{
|
||||
{Object: "t-inherited-1", Relation: "parent_group", SubjectID: "TenantGroup:g-1"},
|
||||
{Object: "t-inherited-2", Relation: "parent_group", SubjectID: "TenantGroup:g-1"},
|
||||
}, nil)
|
||||
|
||||
// 4. Expect repository to fetch all unique IDs: t-direct-1, t-inherited-1, t-inherited-2
|
||||
expectedIDs := []string{"t-direct-1", "t-inherited-1", "t-inherited-2"}
|
||||
mockRepo.On("FindByIDs", ctx, mock.MatchedBy(func(ids []string) bool {
|
||||
// Check if all expected IDs are present (order doesn't matter since we dedup via map)
|
||||
foundCount := 0
|
||||
for _, eid := range expectedIDs {
|
||||
for _, id := range ids {
|
||||
if id == eid {
|
||||
foundCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundCount == len(expectedIDs) && len(ids) == len(expectedIDs)
|
||||
})).Return([]domain.Tenant{
|
||||
{ID: "t-direct-1", Name: "Direct Tenant"},
|
||||
{ID: "t-inherited-1", Name: "Inherited Tenant 1"},
|
||||
{ID: "t-inherited-2", Name: "Inherited Tenant 2"},
|
||||
}, nil)
|
||||
|
||||
// Execute
|
||||
tenants, err := svc.ListManageableTenants(ctx, userID)
|
||||
|
||||
// Verify
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tenants, 3)
|
||||
mockKeto.AssertExpectations(t)
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
@@ -18,8 +18,12 @@ type TenantService interface {
|
||||
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
|
||||
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
||||
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
||||
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
||||
ApproveTenant(ctx context.Context, id string) error
|
||||
SetKetoService(keto KetoService) // 추가
|
||||
AddTenantAdmin(ctx context.Context, tenantID, userID string) error
|
||||
RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error
|
||||
ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error)
|
||||
}
|
||||
|
||||
type tenantService struct {
|
||||
@@ -39,6 +43,60 @@ 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. Get directly managed tenants
|
||||
directTenantIDs, err := s.keto.ListObjects(ctx, "Tenant", "admins", userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list directly managed tenants from Keto", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 2. Get managed tenant groups
|
||||
groupIDs, err := s.keto.ListObjects(ctx, "TenantGroup", "admins", userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list managed tenant groups from Keto", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 3. Get tenants belonging to those groups
|
||||
var groupInheritedTenantIDs []string
|
||||
for _, groupID := range groupIDs {
|
||||
// In Keto, we defined: Tenant#parent_group@TenantGroup:GroupID#_
|
||||
// To find tenants in a group, we look for relations where namespace=Tenant, relation=parent_group, subject=TenantGroup:GroupID#_
|
||||
// Wait, my ListObjects lists objects given a subject.
|
||||
// So subject="TenantGroup:"+groupID+"#_"
|
||||
// Object is Tenant ID.
|
||||
ts, err := s.keto.ListRelations(ctx, "Tenant", "", "parent_group", "TenantGroup:"+groupID)
|
||||
if err == nil {
|
||||
for _, t := range ts {
|
||||
groupInheritedTenantIDs = append(groupInheritedTenantIDs, t.Object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combine and deduplicate IDs
|
||||
allIDsMap := make(map[string]bool)
|
||||
for _, id := range directTenantIDs {
|
||||
allIDsMap[id] = true
|
||||
}
|
||||
for _, id := range groupInheritedTenantIDs {
|
||||
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 {
|
||||
@@ -153,3 +211,35 @@ func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain strin
|
||||
func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
return s.repo.FindBySlug(ctx, slug)
|
||||
}
|
||||
|
||||
func (s *tenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return errors.New("keto service not initialized")
|
||||
}
|
||||
return s.keto.CreateRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
||||
if s.keto == nil {
|
||||
return errors.New("keto service not initialized")
|
||||
}
|
||||
return s.keto.DeleteRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
|
||||
}
|
||||
|
||||
func (s *tenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
|
||||
if s.keto == nil {
|
||||
return nil, errors.New("keto service not initialized")
|
||||
}
|
||||
tuples, err := s.keto.ListRelations(ctx, "Tenant", tenantID, "admins", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDs := make([]string, 0, len(tuples))
|
||||
for _, t := range tuples {
|
||||
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
|
||||
userIDs = append(userIDs, t.SubjectID[5:])
|
||||
}
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user