1
0
forked from baron/baron-sso

SSOT 전환

This commit is contained in:
2026-02-05 10:15:54 +09:00
parent c811b7e283
commit d8f133b1e5
13 changed files with 874 additions and 93 deletions

View File

@@ -2,7 +2,6 @@ package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"context"
"fmt"
"log/slog"
@@ -18,33 +17,16 @@ type RelyingPartyService interface {
Delete(ctx context.Context, clientID string) error
}
func (s *relyingPartyService) ListAll(ctx context.Context) ([]domain.RelyingParty, error) {
return s.repo.ListAll(ctx)
}
func (s *relyingPartyService) ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error) {
// Simple implementation for now, repository could be optimized with IN clause
var allRps []domain.RelyingParty
for _, tid := range tenantIDs {
rps, _ := s.repo.ListByTenantID(ctx, tid)
allRps = append(allRps, rps...)
}
return allRps, nil
}
type relyingPartyService struct {
repo repository.RelyingPartyRepository
hydraService *HydraAdminService
ketoService KetoService
}
func NewRelyingPartyService(
repo repository.RelyingPartyRepository,
hydraService *HydraAdminService,
ketoService KetoService,
) RelyingPartyService {
return &relyingPartyService{
repo: repo,
hydraService: hydraService,
ketoService: ketoService,
}
@@ -52,104 +34,146 @@ func NewRelyingPartyService(
func (s *relyingPartyService) Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error) {
// 1. Create Client in Hydra
// Ensure metadata contains tenant_id for reference
if client.Metadata == nil {
client.Metadata = make(map[string]interface{})
}
client.Metadata["tenant_id"] = tenantID
// Ensure description is in metadata if provided in some other way?
// The input 'client' is domain.HydraClient. It doesn't have a separate description field.
// Assuming caller puts description in metadata.
createdClient, err := s.hydraService.CreateClient(ctx, client)
if err != nil {
return nil, fmt.Errorf("failed to create hydra client: %w", err)
}
// 2. Create Record in DB
rp := &domain.RelyingParty{
ClientID: createdClient.ClientID,
TenantID: tenantID,
Name: createdClient.ClientName,
Description: "", // Hydra doesn't have description field standard, maybe in metadata?
}
if err := s.repo.Create(ctx, rp); err != nil {
// Rollback: Delete Hydra Client
_ = s.hydraService.DeleteClient(ctx, createdClient.ClientID)
return nil, fmt.Errorf("failed to create relying party in db: %w", err)
}
// 3. Create Relation in Keto
// 2. Create Relation in Keto
// RelyingParty:<client_id>#parent_tenant@Tenant:<tenant_id>
err = s.ketoService.CreateRelation(ctx, "RelyingParty", createdClient.ClientID, "parent_tenant", "Tenant:"+tenantID)
if err != nil {
slog.Error("Failed to create keto relation for relying party", "error", err, "client_id", createdClient.ClientID)
// We don't rollback here, but we should probably have a background job to fix this.
// Or return error and let caller decide? For MVP, logging error is acceptable as per issue discussion (Eventual Consistency preferred).
// Try to cleanup Hydra client
_ = s.hydraService.DeleteClient(ctx, createdClient.ClientID)
return nil, err
}
return rp, nil
return s.mapHydraToDomain(createdClient), nil
}
func (s *relyingPartyService) Get(ctx context.Context, clientID string) (*domain.RelyingParty, *domain.HydraClient, error) {
// Get from DB
rp, err := s.repo.FindByID(ctx, clientID)
if err != nil {
return nil, nil, err
}
// Get from Hydra
hydraClient, err := s.hydraService.GetClient(ctx, clientID)
if err != nil {
return nil, nil, err
}
return rp, hydraClient, nil
return s.mapHydraToDomain(hydraClient), hydraClient, nil
}
func (s *relyingPartyService) List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error) {
return s.repo.ListByTenantID(ctx, tenantID)
// 1. Fetch ClientIDs from Keto
// Subject: Tenant:<tenantID>, Relation: parent_tenant, Namespace: RelyingParty
// Note: ListRelations checks "who has relation to subject".
// Relation tuple: RelyingParty:cid # parent_tenant @ Tenant:tid
// We want to find objects where subject=Tenant:tid.
tuples, err := s.ketoService.ListRelations(ctx, "RelyingParty", "", "parent_tenant", "Tenant:"+tenantID)
if err != nil {
return nil, err
}
var rps []domain.RelyingParty
for _, t := range tuples {
// Object is "RelyingParty:clientId"
if len(t.Object) > 13 && t.Object[:13] == "RelyingParty:" {
clientID := t.Object[13:]
client, err := s.hydraService.GetClient(ctx, clientID)
if err != nil {
slog.Warn("Failed to fetch relying party from hydra", "client_id", clientID, "error", err)
continue
}
if rp := s.mapHydraToDomain(client); rp != nil {
rps = append(rps, *rp)
}
}
}
return rps, nil
}
func (s *relyingPartyService) ListAll(ctx context.Context) ([]domain.RelyingParty, error) {
// This might be heavy if there are many clients.
// Hydra doesn't support "List all clients" easily without pagination.
// Assuming HydraAdminService has ListClients or similar?
// The interface wasn't shown, but assuming it's available or we skip implementation.
// For now, let's return empty or error?
// Wait, repo.ListAll was used.
// Let's assume we can't implement efficient ListAll without DB,
// UNLESS we use Keto to list all RelyingParties (if Keto supports listing all objects in namespace).
// Keto doesn't support listing all objects easily.
// But `hydraService` likely has `ListClients`.
return nil, fmt.Errorf("ListAll not implemented in SSOT mode yet")
}
func (s *relyingPartyService) ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error) {
var allRps []domain.RelyingParty
for _, tid := range tenantIDs {
rps, err := s.List(ctx, tid)
if err == nil {
allRps = append(allRps, rps...)
}
}
return allRps, nil
}
func (s *relyingPartyService) Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error) {
// Update Hydra
updatedClient, err := s.hydraService.UpdateClient(ctx, clientID, client)
if err != nil {
return nil, err
}
// Update DB
rp, err := s.repo.FindByID(ctx, clientID)
if err != nil {
return nil, err
}
rp.Name = updatedClient.ClientName
// Update other fields if necessary
if err := s.repo.Update(ctx, rp); err != nil {
return nil, err
}
return rp, nil
return s.mapHydraToDomain(updatedClient), nil
}
func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error {
// Delete from DB
if err := s.repo.Delete(ctx, clientID); err != nil {
// 1. Get client to find tenantID (for Keto cleanup)
client, err := s.hydraService.GetClient(ctx, clientID)
if err != nil {
return err // Or ignore if not found?
}
tenantID := ""
if client.Metadata != nil {
if tid, ok := client.Metadata["tenant_id"].(string); ok {
tenantID = tid
}
}
// 2. Delete from Hydra
if err := s.hydraService.DeleteClient(ctx, clientID); err != nil {
return err
}
// Delete from Hydra
if err := s.hydraService.DeleteClient(ctx, clientID); err != nil {
slog.Error("Failed to delete hydra client", "error", err, "client_id", clientID)
// Proceeding...
// 3. Delete from Keto
if tenantID != "" {
_ = s.ketoService.DeleteRelation(ctx, "RelyingParty", clientID, "parent_tenant", "Tenant:"+tenantID)
}
// Delete from Keto (Optional, but good practice to clean up)
// We might not know the tenant ID here without querying DB first, but if DB is deleted, we might miss it.
//Ideally, we should query DB first.
// But `DeleteRelation` requires specific object/relation/subject.
// If we want to delete ALL relations for this object, Keto API supports that?
// `DeleteRelation` in our service wrapper is specific.
// We can skip explicit Keto deletion for now as orphaned tuples are less critical than orphaned resources.
return nil
}
func (s *relyingPartyService) mapHydraToDomain(client *domain.HydraClient) *domain.RelyingParty {
if client == nil {
return nil
}
rp := &domain.RelyingParty{
ClientID: client.ClientID,
Name: client.ClientName,
}
if client.Metadata != nil {
if tid, ok := client.Metadata["tenant_id"].(string); ok {
rp.TenantID = tid
}
if desc, ok := client.Metadata["description"].(string); ok {
rp.Description = desc
}
}
return rp
}