forked from baron/baron-sso
Ory Keto ReBAC Policy & Relation Tuple Architecture
This commit is contained in:
@@ -23,13 +23,18 @@ type TenantService interface {
|
||||
}
|
||||
|
||||
type tenantService struct {
|
||||
repo repository.TenantRepository
|
||||
userRepo repository.UserRepository
|
||||
keto KetoService
|
||||
repo repository.TenantRepository
|
||||
userRepo repository.UserRepository
|
||||
keto KetoService
|
||||
outboxRepo repository.KetoOutboxRepository
|
||||
}
|
||||
|
||||
func NewTenantService(repo repository.TenantRepository, userRepo repository.UserRepository) TenantService {
|
||||
return &tenantService{repo: repo, userRepo: userRepo}
|
||||
func NewTenantService(repo repository.TenantRepository, userRepo repository.UserRepository, outboxRepo repository.KetoOutboxRepository) TenantService {
|
||||
return &tenantService{
|
||||
repo: repo,
|
||||
userRepo: userRepo,
|
||||
outboxRepo: outboxRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tenantService) SetKetoService(keto KetoService) {
|
||||
@@ -46,56 +51,32 @@ func (s *tenantService) ListManageableTenants(ctx context.Context, userID string
|
||||
}
|
||||
|
||||
// 1. 직접 관리자인 테넌트 ID 목록 (Tenant:ID#admins@User:ID)
|
||||
directTenantIDs, err := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||
directAdminIDs, err := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list direct tenants", "userID", userID, "error", err)
|
||||
slog.Error("Failed to list direct admin tenants", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 2. 관리 권한이 있는 유저 그룹 목록 (UserGroup:ID#owners@User:ID)
|
||||
// 정책: 그룹장은 해당 그룹(테넌트)의 어드민이 된다.
|
||||
ownedGroupIDs, err := s.keto.ListObjects(ctx, "UserGroup", "owners", "User:"+userID)
|
||||
// 2. 직접 소유자(조직장)인 테넌트 ID 목록 (Tenant:ID#owners@User:ID)
|
||||
directOwnerIDs, err := s.keto.ListObjects(ctx, "Tenant", "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)
|
||||
}
|
||||
}
|
||||
slog.Error("Failed to list owned tenants", "userID", userID, "error", err)
|
||||
}
|
||||
|
||||
// 합산 및 중복 제거
|
||||
allIDsMap := make(map[string]bool)
|
||||
for _, id := range directTenantIDs {
|
||||
for _, id := range directAdminIDs {
|
||||
allIDsMap[id] = true
|
||||
}
|
||||
for _, id := range ownedGroupIDs {
|
||||
allIDsMap[id] = true // 그룹 자체도 테넌트이므로 포함
|
||||
}
|
||||
for _, id := range inheritedTenantIDs {
|
||||
for _, id := range directOwnerIDs {
|
||||
allIDsMap[id] = true
|
||||
}
|
||||
|
||||
// Note: 상속된 권한(부모의 어드민이 자식의 어드민)은 Keto의 OPL에서 처리되므로,
|
||||
// 특정 유저가 'view' 또는 'manage' 권한을 가진 테넌트를 모두 찾으려면
|
||||
// Keto의 'expand' 또는 'list objects' 기능을 더 고도화하거나,
|
||||
// 여기서는 직접 할당된 부모 테넌트를 기준으로 하위 테넌트 정보를 추가 조회하는 로직이 필요할 수 있습니다.
|
||||
// 우선 직접 할당된 테넌트들만 반환합니다.
|
||||
|
||||
allIDs := make([]string, 0, len(allIDsMap))
|
||||
for id := range allIDsMap {
|
||||
allIDs = append(allIDs, id)
|
||||
@@ -125,6 +106,7 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, descript
|
||||
|
||||
// 2. Create Tenant
|
||||
tenant := &domain.Tenant{
|
||||
Type: domain.TenantTypeCompany, // Default to COMPANY for manual registration
|
||||
Name: name,
|
||||
Slug: slug,
|
||||
Description: description,
|
||||
@@ -135,6 +117,17 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, descript
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// [Keto] Sync hierarchy via Outbox if ParentID exists
|
||||
if s.outboxRepo != nil && tenant.ParentID != nil {
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
Relation: "parents",
|
||||
Subject: "Tenant:" + *tenant.ParentID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Add Domains (Auto-verify for manual admin registration)
|
||||
for _, d := range domains {
|
||||
if err := s.repo.AddDomain(ctx, tenant.ID, d, true); err != nil {
|
||||
@@ -158,6 +151,7 @@ func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, des
|
||||
}
|
||||
|
||||
tenant := &domain.Tenant{
|
||||
Type: domain.TenantTypeCompany,
|
||||
Name: name,
|
||||
Slug: slug,
|
||||
Description: description,
|
||||
@@ -188,21 +182,22 @@ func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// [Keto] Sync relation
|
||||
if s.keto != nil {
|
||||
// [Keto] Sync relation via Outbox
|
||||
if s.outboxRepo != nil {
|
||||
if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
|
||||
slog.Info("Syncing tenant admin to Keto", "tenant", tenant.Slug, "adminEmail", adminEmail)
|
||||
slog.Info("Queueing tenant admin sync to Keto", "tenant", tenant.Slug, "adminEmail", adminEmail)
|
||||
// Check if user already exists in our Read-Model
|
||||
if s.userRepo != nil {
|
||||
user, err := s.userRepo.FindByEmail(ctx, adminEmail)
|
||||
if err == nil && user != nil {
|
||||
// User exists, assign Admin role in Keto
|
||||
err = s.keto.CreateRelation(ctx, "Tenant", tenant.ID, "admin", "User:"+user.ID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to assign tenant admin in Keto", "tenant", tenant.ID, "user", user.ID, "error", err)
|
||||
} else {
|
||||
slog.Info("Assigned tenant admin in Keto", "tenant", tenant.ID, "user", user.ID)
|
||||
}
|
||||
// User exists, assign Admin role in Keto via Outbox
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
Relation: "admins",
|
||||
Subject: "User:" + user.ID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
} else {
|
||||
slog.Info("Tenant admin user not found in local DB, will need manual sync or sync on signup", "email", adminEmail)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user