1
0
forked from baron/baron-sso

RP 생성/삭제 운영 relation 세트 반영

This commit is contained in:
2026-04-15 15:23:50 +09:00
parent 8f7c328d22
commit 91299b1a0a
2 changed files with 79 additions and 21 deletions

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"strings"
) )
type RelyingPartyService interface { type RelyingPartyService interface {
@@ -24,6 +25,19 @@ type relyingPartyService struct {
outboxRepo repository.KetoOutboxRepository outboxRepo repository.KetoOutboxRepository
} }
var defaultRelyingPartyOperatorRelations = []string{
"admins",
"creator",
"config_editor",
"secret_rotator",
"jwks_viewer",
"jwks_operator",
"consent_viewer",
"consent_revoker",
"relationship_viewer",
"status_operator",
}
func NewRelyingPartyService( func NewRelyingPartyService(
hydraService *HydraAdminService, hydraService *HydraAdminService,
ketoService KetoService, ketoService KetoService,
@@ -36,6 +50,51 @@ func NewRelyingPartyService(
} }
} }
func extractRelyingPartyCreatorSubject(client *domain.HydraClient) string {
if client == nil || client.Metadata == nil {
return ""
}
raw, _ := client.Metadata["user_id"].(string)
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
return "User:" + raw
}
func (s *relyingPartyService) enqueueRelyingPartyTuple(ctx context.Context, action, object, relation, subject string) {
if s.outboxRepo == nil || strings.TrimSpace(object) == "" || strings.TrimSpace(relation) == "" || strings.TrimSpace(subject) == "" {
return
}
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
Namespace: "RelyingParty",
Object: object,
Relation: relation,
Subject: subject,
Action: action,
})
}
func (s *relyingPartyService) enqueueDefaultRelyingPartyRelations(ctx context.Context, action string, client *domain.HydraClient, tenantID string) {
if client == nil {
return
}
tenantID = strings.TrimSpace(tenantID)
if tenantID != "" {
s.enqueueRelyingPartyTuple(ctx, action, client.ClientID, "parents", "Tenant:"+tenantID)
}
creatorSubject := extractRelyingPartyCreatorSubject(client)
if creatorSubject == "" {
return
}
for _, relation := range defaultRelyingPartyOperatorRelations {
s.enqueueRelyingPartyTuple(ctx, action, client.ClientID, relation, creatorSubject)
}
}
func (s *relyingPartyService) Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error) { func (s *relyingPartyService) Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error) {
// 1. Create Client in Hydra // 1. Create Client in Hydra
if client.Metadata == nil { if client.Metadata == nil {
@@ -48,17 +107,8 @@ func (s *relyingPartyService) Create(ctx context.Context, tenantID string, clien
return nil, fmt.Errorf("failed to create hydra client: %w", err) return nil, fmt.Errorf("failed to create hydra client: %w", err)
} }
// 2. Create Relation in Keto via Outbox // 2. Create default relations in Keto via Outbox.
// RelyingParty:<client_id>#parents@Tenant:<tenant_id> s.enqueueDefaultRelyingPartyRelations(ctx, domain.KetoOutboxActionCreate, createdClient, tenantID)
if s.outboxRepo != nil {
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
Namespace: "RelyingParty",
Object: createdClient.ClientID,
Relation: "parents",
Subject: "Tenant:" + tenantID,
Action: domain.KetoOutboxActionCreate,
})
}
return s.mapHydraToDomain(createdClient), nil return s.mapHydraToDomain(createdClient), nil
} }
@@ -137,16 +187,8 @@ func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error
return err return err
} }
// 3. Delete from Keto via Outbox // 3. Delete default relations from Keto via Outbox.
if s.outboxRepo != nil && tenantID != "" { s.enqueueDefaultRelyingPartyRelations(ctx, domain.KetoOutboxActionDelete, client, tenantID)
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
Namespace: "RelyingParty",
Object: clientID,
Relation: "parents",
Subject: "Tenant:" + tenantID,
Action: domain.KetoOutboxActionDelete,
})
}
return nil return nil
} }

View File

@@ -52,6 +52,9 @@ func TestRelyingPartyService_Create_Success(t *testing.T) {
tenantID := "tenant-1" tenantID := "tenant-1"
inputClient := domain.HydraClient{ inputClient := domain.HydraClient{
ClientName: "Test App", ClientName: "Test App",
Metadata: map[string]interface{}{
"user_id": "creator-1",
},
} }
// Hydra Mock // Hydra Mock
@@ -81,6 +84,12 @@ func TestRelyingPartyService_Create_Success(t *testing.T) {
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool { mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "RelyingParty" && e.Object == "generated-client-id" && e.Relation == "parents" && e.Subject == "Tenant:"+tenantID return e.Namespace == "RelyingParty" && e.Object == "generated-client-id" && e.Relation == "parents" && e.Subject == "Tenant:"+tenantID
})).Return(nil) })).Return(nil)
for _, relation := range defaultRelyingPartyOperatorRelations {
rel := relation
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "RelyingParty" && e.Object == "generated-client-id" && e.Relation == rel && e.Subject == "User:creator-1"
})).Return(nil)
}
svc := NewRelyingPartyService(hydraSvc, mockKeto, mockOutbox) svc := NewRelyingPartyService(hydraSvc, mockKeto, mockOutbox)
rp, err := svc.Create(context.Background(), tenantID, inputClient) rp, err := svc.Create(context.Background(), tenantID, inputClient)
@@ -173,6 +182,7 @@ func TestRelyingPartyService_Delete_Success(t *testing.T) {
ClientID: clientID, ClientID: clientID,
Metadata: map[string]interface{}{ Metadata: map[string]interface{}{
"tenant_id": tenantID, "tenant_id": tenantID,
"user_id": "creator-1",
}, },
}) })
return return
@@ -192,6 +202,12 @@ func TestRelyingPartyService_Delete_Success(t *testing.T) {
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool { mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "RelyingParty" && e.Object == clientID && e.Relation == "parents" && e.Subject == "Tenant:"+tenantID return e.Namespace == "RelyingParty" && e.Object == clientID && e.Relation == "parents" && e.Subject == "Tenant:"+tenantID
})).Return(nil) })).Return(nil)
for _, relation := range defaultRelyingPartyOperatorRelations {
rel := relation
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "RelyingParty" && e.Object == clientID && e.Relation == rel && e.Subject == "User:creator-1"
})).Return(nil)
}
svc := NewRelyingPartyService(hydraSvc, mockKeto, mockOutbox) svc := NewRelyingPartyService(hydraSvc, mockKeto, mockOutbox)
err := svc.Delete(context.Background(), clientID) err := svc.Delete(context.Background(), clientID)