forked from baron/baron-sso
multi IDP 모델 적용 scaffolding
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/handler"
|
||||
"baron-sso-backend/internal/logger"
|
||||
"baron-sso-backend/internal/repository"
|
||||
"baron-sso-backend/internal/service"
|
||||
"baron-sso-backend/internal/validator"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
@@ -59,6 +61,27 @@ func main() {
|
||||
"redis_addr", getEnv("REDIS_ADDR", "redis:6379"),
|
||||
)
|
||||
|
||||
// --- Fail-Fast Schema Validation ---
|
||||
// Initialize the IDP Provider (Descope)
|
||||
descopeProjectID := getEnv("DESCOPE_PROJECT_ID", "")
|
||||
descopeManagementKey := getEnv("DESCOPE_MANAGEMENT_KEY", "")
|
||||
|
||||
// We create a provider instance to check schema compatibility.
|
||||
// This ensures that our BrokerUser model requirements (e.g. custom attributes)
|
||||
// are supported by the configured IDP.
|
||||
idpProvider := service.NewDescopeProvider(descopeProjectID, descopeManagementKey)
|
||||
|
||||
if err := validator.ValidateIDPCompatibility(domain.BrokerUser{}, idpProvider); err != nil {
|
||||
slog.Error("❌ [CRITICAL] Broker Schema Mismatch",
|
||||
"idp", idpProvider.Name(),
|
||||
"error", err,
|
||||
)
|
||||
fmt.Printf("\n!!! CRITICAL ERROR: IDP Schema Mismatch !!!\n%v\n\n", err)
|
||||
os.Exit(1) // Break the build/deployment
|
||||
}
|
||||
slog.Info("✅ IDP Schema Validation Passed", "idp", idpProvider.Name())
|
||||
// -----------------------------------
|
||||
|
||||
// 2. Initialize DB Connections
|
||||
chHost := getEnv("CLICKHOUSE_HOST", "localhost")
|
||||
chPort, _ := strconv.Atoi(getEnv("CLICKHOUSE_PORT_NATIVE", "9000"))
|
||||
|
||||
28
backend/internal/domain/idp_models.go
Normal file
28
backend/internal/domain/idp_models.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package domain
|
||||
|
||||
// BrokerUser is the standard user model used within Baron SSO business logic.
|
||||
// It defines the canonical set of fields that must be supported by any underlying IDP.
|
||||
type BrokerUser struct {
|
||||
ID string `json:"id" required:"true"`
|
||||
Email string `json:"email" required:"true"`
|
||||
Name string `json:"name"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
// Attributes stores custom user attributes.
|
||||
// The "required_keys" tag specifies which keys MUST be present in the IDP's schema support.
|
||||
Attributes map[string]interface{} `json:"attributes" required_keys:"grade,department"`
|
||||
}
|
||||
|
||||
// IDPMetadata represents the schema capabilities of an Identity Provider.
|
||||
type IDPMetadata struct {
|
||||
// SupportedFields lists the BrokerUser fields (json tag names) that the IDP supports.
|
||||
// For custom attributes, use the key name directly (e.g., "grade").
|
||||
SupportedFields []string
|
||||
}
|
||||
|
||||
// IdentityProvider is the interface that all IDP adapters must implement.
|
||||
type IdentityProvider interface {
|
||||
Name() string
|
||||
// GetMetadata returns the schema support information for this IDP.
|
||||
// This is used for startup-time validation.
|
||||
GetMetadata() (*IDPMetadata, error)
|
||||
}
|
||||
62
backend/internal/service/descope_service.go
Normal file
62
backend/internal/service/descope_service.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"log/slog"
|
||||
|
||||
"github.com/descope/go-sdk/descope/client"
|
||||
)
|
||||
|
||||
type DescopeProvider struct {
|
||||
Client *client.DescopeClient
|
||||
fieldMapping map[string]string // Key: Broker Field Name, Value: Descope Attribute Key
|
||||
}
|
||||
|
||||
func NewDescopeProvider(projectID, managementKey string) *DescopeProvider {
|
||||
var descopeClient *client.DescopeClient
|
||||
var err error
|
||||
if projectID != "" {
|
||||
descopeClient, err = client.NewWithConfig(&client.Config{
|
||||
ProjectID: projectID,
|
||||
ManagementKey: managementKey,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("Failed to initialize Descope Client in Provider", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Define the mapping between BrokerUser fields and Descope attributes.
|
||||
// In a real scenario, this could be loaded from a config file.
|
||||
// For this implementation, we hardcode the support to demonstrate the validation.
|
||||
// We map the Broker's required custom attributes to Descope's keys.
|
||||
mapping := map[string]string{
|
||||
"grade": "customAttributes.userRank", // Broker 'grade' maps to Descope 'userRank'
|
||||
"department": "customAttributes.dept", // Broker 'department' maps to Descope 'dept'
|
||||
}
|
||||
|
||||
return &DescopeProvider{
|
||||
Client: descopeClient,
|
||||
fieldMapping: mapping,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DescopeProvider) Name() string {
|
||||
return "Descope"
|
||||
}
|
||||
|
||||
// GetMetadata returns the schema support information.
|
||||
// Currently, it returns the standard fields Descope supports + the mapped custom attributes.
|
||||
func (d *DescopeProvider) GetMetadata() (*domain.IDPMetadata, error) {
|
||||
// 1. Standard Fields supported by Descope
|
||||
supported := []string{"id", "email", "name", "phone_number"}
|
||||
|
||||
// 2. Add mapped custom attributes
|
||||
// The Validator checks if the Broker's required keys (e.g., "grade") are present in this list.
|
||||
for brokerKey := range d.fieldMapping {
|
||||
supported = append(supported, brokerKey)
|
||||
}
|
||||
|
||||
return &domain.IDPMetadata{
|
||||
SupportedFields: supported,
|
||||
}, nil
|
||||
}
|
||||
62
backend/internal/validator/schema_validator.go
Normal file
62
backend/internal/validator/schema_validator.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateIDPCompatibility checks if the provided IDP supports all required fields defined in the BrokerUser model.
|
||||
func ValidateIDPCompatibility(brokerModel interface{}, idp domain.IdentityProvider) error {
|
||||
metadata, err := idp.GetMetadata()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch metadata from IDP %s: %w", idp.Name(), err)
|
||||
}
|
||||
|
||||
supportedMap := make(map[string]bool)
|
||||
for _, f := range metadata.SupportedFields {
|
||||
supportedMap[f] = true
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(brokerModel)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
// Check "required" tag
|
||||
isRequired := field.Tag.Get("required") == "true"
|
||||
jsonTag := field.Tag.Get("json")
|
||||
fieldName := strings.Split(jsonTag, ",")[0]
|
||||
|
||||
// Skip if fieldName is empty or if it's the Attributes map (handled separately)
|
||||
if fieldName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldName != "attributes" {
|
||||
if isRequired && !supportedMap[fieldName] {
|
||||
return fmt.Errorf("IDP %s does not support required field: %s", idp.Name(), fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
// Check "required_keys" tag for map types (Custom Attributes)
|
||||
if fieldName == "attributes" {
|
||||
reqKeys := field.Tag.Get("required_keys")
|
||||
if reqKeys != "" {
|
||||
keys := strings.Split(reqKeys, ",")
|
||||
for _, key := range keys {
|
||||
key = strings.TrimSpace(key)
|
||||
if !supportedMap[key] {
|
||||
return fmt.Errorf("IDP %s does not support required custom attribute: %s", idp.Name(), key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
78
backend/internal/validator/schema_validator_test.go
Normal file
78
backend/internal/validator/schema_validator_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// MockProvider는 IdentityProvider 인터페이스를 구현하는 테스트용 구조체입니다.
|
||||
type MockProvider struct {
|
||||
Supported []string
|
||||
}
|
||||
|
||||
func (m *MockProvider) Name() string {
|
||||
return "MockIDP"
|
||||
}
|
||||
|
||||
func (m *MockProvider) GetMetadata() (*domain.IDPMetadata, error) {
|
||||
return &domain.IDPMetadata{
|
||||
SupportedFields: m.Supported,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestValidateIDPCompatibility(t *testing.T) {
|
||||
// BrokerUser 모델은 다음과 같이 정의되어 있다고 가정합니다 (idp_models.go 참조):
|
||||
// ID (required), Email (required), Name, PhoneNumber
|
||||
// Attributes (required_keys: "grade", "department")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
supported []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "성공: 모든 필수 필드 지원",
|
||||
supported: []string{"id", "email", "name", "grade", "department"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "성공: 선택 필드(name) 누락이어도 성공",
|
||||
supported: []string{"id", "email", "grade", "department"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "실패: 필수 필드(id) 누락",
|
||||
supported: []string{"email", "grade", "department"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "실패: 필수 필드(email) 누락",
|
||||
supported: []string{"id", "grade", "department"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "실패: 커스텀 속성(grade) 누락",
|
||||
supported: []string{"id", "email", "department"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "실패: 커스텀 속성(department) 누락",
|
||||
supported: []string{"id", "email", "grade"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 테스트용 IDP Provider 생성
|
||||
mockIDP := &MockProvider{Supported: tt.supported}
|
||||
|
||||
// 검증 수행
|
||||
err := ValidateIDPCompatibility(domain.BrokerUser{}, mockIDP)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateIDPCompatibility() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user