1
0
forked from baron/baron-sso

ReBAC 고도화 및 애플리케이션 관리 시스템 통합 구현

This commit is contained in:
2026-02-04 15:01:13 +09:00
parent 066ea86f46
commit 7e09764ad9
21 changed files with 1532 additions and 62 deletions

View File

@@ -0,0 +1,155 @@
package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"context"
"fmt"
"log/slog"
)
type RelyingPartyService interface {
Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error)
Get(ctx context.Context, clientID string) (*domain.RelyingParty, *domain.HydraClient, error)
List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error)
ListAll(ctx context.Context) ([]domain.RelyingParty, error)
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
}
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,
}
}
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
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
// 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).
}
return rp, 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
}
func (s *relyingPartyService) List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error) {
return s.repo.ListByTenantID(ctx, tenantID)
}
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
}
func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error {
// Delete from DB
if err := s.repo.Delete(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...
}
// 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
}