forked from baron/baron-sso
feat: 테넌트 그룹 기반 권한 상속 고도화 및 개발자 포털 보안 강화 #239
This commit is contained in:
@@ -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 {
|
||||
@@ -192,3 +193,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
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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)
|
||||
}
|
||||
|
||||
type relyingPartyService struct {
|
||||
@@ -158,6 +159,10 @@ 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) mapHydraToDomain(client *domain.HydraClient) *domain.RelyingParty {
|
||||
if client == nil {
|
||||
return nil
|
||||
|
||||
@@ -18,6 +18,7 @@ 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) // 추가
|
||||
}
|
||||
@@ -39,6 +40,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 {
|
||||
|
||||
Reference in New Issue
Block a user