From a52ec3b9f8141bce79104ab4e082f5a850292f8f Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 28 Jan 2026 17:47:56 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=B8=EB=B6=80=20IdP=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20API=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/bootstrap/bootstrap.go | 1 + backend/internal/domain/federation_models.go | 51 +++++++ .../internal/handler/federation_handler.go | 126 ++++++++++++++++++ .../repository/federation_repository.go | 54 ++++++++ 4 files changed, 232 insertions(+) create mode 100644 backend/internal/domain/federation_models.go create mode 100644 backend/internal/handler/federation_handler.go create mode 100644 backend/internal/repository/federation_repository.go diff --git a/backend/internal/bootstrap/bootstrap.go b/backend/internal/bootstrap/bootstrap.go index c6f6993a..35b83e87 100644 --- a/backend/internal/bootstrap/bootstrap.go +++ b/backend/internal/bootstrap/bootstrap.go @@ -36,6 +36,7 @@ func migrateSchemas(db *gorm.DB) error { &domain.User{}, &domain.Tenant{}, &domain.ApiKey{}, + &domain.IdentityProviderConfig{}, // &domain.RelyingParty{}, // TODO: Uncomment when model is ready // &domain.UserConsent{}, // TODO: Uncomment when model is ready ) diff --git a/backend/internal/domain/federation_models.go b/backend/internal/domain/federation_models.go new file mode 100644 index 00000000..0dbcbe21 --- /dev/null +++ b/backend/internal/domain/federation_models.go @@ -0,0 +1,51 @@ +package domain + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// ProviderType defines the type of the identity provider. +type ProviderType string + +const ( + ProviderTypeOIDC ProviderType = "oidc" + ProviderTypeSAML ProviderType = "saml" +) + +// IdentityProviderConfig stores the configuration for an external Identity Provider. +type IdentityProviderConfig struct { + ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"` + TenantID string `gorm:"type:uuid;not null;index" json:"tenant_id"` + Tenant Tenant `gorm:"foreignKey:TenantID" json:"-"` // Belongs to Tenant + ProviderType ProviderType `gorm:"type:varchar(10);not null" json:"provider_type"` + DisplayName string `gorm:"not null" json:"display_name"` + Status string `gorm:"default:'active'" json:"status"` + + // OIDC Specific Fields + IssuerURL *string `gorm:"null" json:"issuer_url,omitempty"` + ClientID *string `gorm:"null" json:"client_id,omitempty"` + ClientSecret *string `gorm:"null" json:"client_secret,omitempty"` + // Scopes are space-separated + Scopes *string `gorm:"null" json:"scopes,omitempty"` + + // SAML Specific Fields + MetadataURL *string `gorm:"null" json:"metadata_url,omitempty"` + MetadataXML *string `gorm:"type:text;null" json:"metadata_xml,omitempty"` + EntityID *string `gorm:"null" json:"entity_id,omitempty"` + AcsURL *string `gorm:"null" json:"acs_url,omitempty"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +// BeforeCreate hook to generate UUID if not present. +func (idc *IdentityProviderConfig) BeforeCreate(tx *gorm.DB) (err error) { + if idc.ID == "" { + idc.ID = uuid.NewString() + } + return +} diff --git a/backend/internal/handler/federation_handler.go b/backend/internal/handler/federation_handler.go new file mode 100644 index 00000000..7d264b67 --- /dev/null +++ b/backend/internal/handler/federation_handler.go @@ -0,0 +1,126 @@ +package handler + +import ( + "baron-sso-backend/internal/domain" + "baron-sso-backend/internal/repository" + "errors" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +// FederationHandler handles API requests for IdP federation configurations. +type FederationHandler struct { + Repo *repository.FederationRepository + DB *gorm.DB // For tenant existence checks +} + +// NewFederationHandler creates a new FederationHandler. +func NewFederationHandler(repo *repository.FederationRepository, db *gorm.DB) *FederationHandler { + return &FederationHandler{Repo: repo, DB: db} +} + +// CreateIdpConfig handles the creation of a new IdP configuration. +func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error { + var req domain.IdentityProviderConfig + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + } + + // Basic validation + if req.TenantID == "" || req.DisplayName == "" || req.ProviderType == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id, display_name, and provider_type are required"}) + } + + // Check if tenant exists + var tenant domain.Tenant + if err := h.DB.First(&tenant, "id = ?", req.TenantID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant not found"}) + } + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + if err := h.Repo.Create(&req); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.Status(fiber.StatusCreated).JSON(req) +} + +// GetIdpConfigByID handles retrieving a single IdP configuration. +func (h *FederationHandler) GetIdpConfigByID(c *fiber.Ctx) error { + id := c.Params("id") + config, err := h.Repo.GetByID(id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + if config == nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "identity provider configuration not found"}) + } + + return c.JSON(config) +} + +// ListIdpConfigsForTenant handles listing all IdP configurations for a tenant. +func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error { + tenantID := c.Params("tenantId") + if tenantID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"}) + } + + configs, err := h.Repo.ListByTenantID(tenantID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(configs) +} + +// UpdateIdpConfig handles updating an IdP configuration. +func (h *FederationHandler) UpdateIdpConfig(c *fiber.Ctx) error { + id := c.Params("id") + + existingConfig, err := h.Repo.GetByID(id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + if existingConfig == nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "identity provider configuration not found"}) + } + + var req domain.IdentityProviderConfig + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + } + + // Overwrite fields + existingConfig.DisplayName = req.DisplayName + existingConfig.Status = req.Status + existingConfig.IssuerURL = req.IssuerURL + existingConfig.ClientID = req.ClientID + existingConfig.ClientSecret = req.ClientSecret + existingConfig.Scopes = req.Scopes + existingConfig.MetadataURL = req.MetadataURL + existingConfig.MetadataXML = req.MetadataXML + existingConfig.EntityID = req.EntityID + existingConfig.AcsURL = req.AcsURL + + + if err := h.Repo.Update(existingConfig); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(existingConfig) +} + +// DeleteIdpConfig handles deleting an IdP configuration. +func (h *FederationHandler) DeleteIdpConfig(c *fiber.Ctx) error { + id := c.Params("id") + + if err := h.Repo.Delete(id); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/internal/repository/federation_repository.go b/backend/internal/repository/federation_repository.go new file mode 100644 index 00000000..09003069 --- /dev/null +++ b/backend/internal/repository/federation_repository.go @@ -0,0 +1,54 @@ +package repository + +import ( + "baron-sso-backend/internal/domain" + "errors" + + "gorm.io/gorm" +) + +// FederationRepository handles database operations for IdentityProviderConfig. +type FederationRepository struct { + DB *gorm.DB +} + +// NewFederationRepository creates a new FederationRepository. +func NewFederationRepository(db *gorm.DB) *FederationRepository { + return &FederationRepository{DB: db} +} + +// Create creates a new identity provider configuration. +func (r *FederationRepository) Create(config *domain.IdentityProviderConfig) error { + return r.DB.Create(config).Error +} + +// GetByID retrieves an identity provider configuration by its ID. +func (r *FederationRepository) GetByID(id string) (*domain.IdentityProviderConfig, error) { + var config domain.IdentityProviderConfig + if err := r.DB.First(&config, "id = ?", id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // Return nil, nil for not found + } + return nil, err + } + return &config, nil +} + +// ListByTenantID retrieves all identity provider configurations for a given tenant. +func (r *FederationRepository) ListByTenantID(tenantID string) ([]domain.IdentityProviderConfig, error) { + var configs []domain.IdentityProviderConfig + if err := r.DB.Where("tenant_id = ?", tenantID).Find(&configs).Error; err != nil { + return nil, err + } + return configs, nil +} + +// Update updates an existing identity provider configuration. +func (r *FederationRepository) Update(config *domain.IdentityProviderConfig) error { + return r.DB.Save(config).Error +} + +// Delete removes an identity provider configuration by its ID. +func (r *FederationRepository) Delete(id string) error { + return r.DB.Delete(&domain.IdentityProviderConfig{}, "id = ?", id).Error +}