From 91299b1a0a0b1f95243e419f5f87337c9e0e3c70 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 15 Apr 2026 15:23:50 +0900 Subject: [PATCH] =?UTF-8?q?RP=20=EC=83=9D=EC=84=B1/=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20relation=20=EC=84=B8=ED=8A=B8=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/service/relying_party_service.go | 84 ++++++++++++++----- .../service/relying_party_service_test.go | 16 ++++ 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/backend/internal/service/relying_party_service.go b/backend/internal/service/relying_party_service.go index c2fa0bc1..26b0ef02 100644 --- a/backend/internal/service/relying_party_service.go +++ b/backend/internal/service/relying_party_service.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "log/slog" + "strings" ) type RelyingPartyService interface { @@ -24,6 +25,19 @@ type relyingPartyService struct { 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( hydraService *HydraAdminService, 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) { // 1. Create Client in Hydra 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) } - // 2. Create Relation in Keto via Outbox - // RelyingParty:#parents@Tenant: - if s.outboxRepo != nil { - _ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{ - Namespace: "RelyingParty", - Object: createdClient.ClientID, - Relation: "parents", - Subject: "Tenant:" + tenantID, - Action: domain.KetoOutboxActionCreate, - }) - } + // 2. Create default relations in Keto via Outbox. + s.enqueueDefaultRelyingPartyRelations(ctx, domain.KetoOutboxActionCreate, createdClient, tenantID) return s.mapHydraToDomain(createdClient), nil } @@ -137,16 +187,8 @@ func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error return err } - // 3. Delete from Keto via Outbox - if s.outboxRepo != nil && tenantID != "" { - _ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{ - Namespace: "RelyingParty", - Object: clientID, - Relation: "parents", - Subject: "Tenant:" + tenantID, - Action: domain.KetoOutboxActionDelete, - }) - } + // 3. Delete default relations from Keto via Outbox. + s.enqueueDefaultRelyingPartyRelations(ctx, domain.KetoOutboxActionDelete, client, tenantID) return nil } diff --git a/backend/internal/service/relying_party_service_test.go b/backend/internal/service/relying_party_service_test.go index e0b26c76..510b5802 100644 --- a/backend/internal/service/relying_party_service_test.go +++ b/backend/internal/service/relying_party_service_test.go @@ -52,6 +52,9 @@ func TestRelyingPartyService_Create_Success(t *testing.T) { tenantID := "tenant-1" inputClient := domain.HydraClient{ ClientName: "Test App", + Metadata: map[string]interface{}{ + "user_id": "creator-1", + }, } // 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 { return e.Namespace == "RelyingParty" && e.Object == "generated-client-id" && e.Relation == "parents" && e.Subject == "Tenant:"+tenantID })).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) rp, err := svc.Create(context.Background(), tenantID, inputClient) @@ -173,6 +182,7 @@ func TestRelyingPartyService_Delete_Success(t *testing.T) { ClientID: clientID, Metadata: map[string]interface{}{ "tenant_id": tenantID, + "user_id": "creator-1", }, }) return @@ -192,6 +202,12 @@ func TestRelyingPartyService_Delete_Success(t *testing.T) { 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(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) err := svc.Delete(context.Background(), clientID)