forked from baron/baron-sso
156 lines
5.1 KiB
Go
156 lines
5.1 KiB
Go
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
|
|
}
|