1
0
forked from baron/baron-sso

golangci lint 적용

This commit is contained in:
2026-02-06 16:25:50 +09:00
parent 568af8f90e
commit 5294066de6
33 changed files with 143 additions and 128 deletions

View File

@@ -12,8 +12,8 @@ func main() {
// KETO_READ_URL과 KETO_WRITE_URL은 컨테이너 외부 포트 또는 내부 주소에 맞게 설정 필요
os.Setenv("KETO_READ_URL", "http://keto:4466")
os.Setenv("KETO_WRITE_URL", "http://keto:4467")
keto := service.NewKetoService()
keto := service.NewKetoService()
ctx := context.Background()
userID := "test-user-id"

View File

@@ -35,15 +35,25 @@ func main() {
godotenv.Load("backend/.env")
pgHost := os.Getenv("DB_HOST")
if pgHost == "" { pgHost = "localhost" }
if pgHost == "" {
pgHost = "localhost"
}
pgPort := os.Getenv("DB_PORT")
if pgPort == "" { pgPort = "5432" }
if pgPort == "" {
pgPort = "5432"
}
pgUser := os.Getenv("DB_USER")
if pgUser == "" { pgUser = "baron" }
if pgUser == "" {
pgUser = "baron"
}
pgPass := os.Getenv("DB_PASSWORD")
if pgPass == "" { pgPass = "password" }
if pgPass == "" {
pgPass = "password"
}
pgName := os.Getenv("DB_NAME")
if pgName == "" { pgName = "baron_sso" }
if pgName == "" {
pgName = "baron_sso"
}
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
pgHost, pgUser, pgPass, pgName, pgPort)

View File

@@ -1,6 +1,7 @@
package main
import (
"baron-sso-backend/internal/bootstrap"
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/handler"
"baron-sso-backend/internal/idp"
@@ -28,8 +29,6 @@ import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
"baron-sso-backend/internal/bootstrap"
)
func getEnv(key, fallback string) string {
@@ -492,10 +491,10 @@ func main() {
auth.Post("/login/code/verify", authHandler.VerifyLoginCode)
auth.Post("/login/code/verify-short", authHandler.VerifyLoginShortCode)
auth.Post("/password/login", authHandler.PasswordLogin)
auth.Get("/consent", authHandler.GetConsentRequest)
auth.Post("/consent/accept", authHandler.AcceptConsentRequest)
auth.Post("/consent/reject", authHandler.RejectConsentRequest)
auth.Get("/consent", authHandler.GetConsentRequest)
auth.Post("/consent/accept", authHandler.AcceptConsentRequest)
auth.Post("/consent/reject", authHandler.RejectConsentRequest)
auth.Post("/oidc/login/accept", authHandler.AcceptOidcLoginRequest)
auth.Post("/enchanted-link/init", authHandler.InitEnchantedLink)

View File

@@ -1,12 +1,12 @@
package domain
type EnchantedLinkInitRequest struct {
LoginID string `json:"loginId"`
URI string `json:"uri,omitempty"` // Redirect URI (optional for polling flow)
Method string `json:"method,omitempty"` // "email" or "sms"
LoginID string `json:"loginId"`
URI string `json:"uri,omitempty"` // Redirect URI (optional for polling flow)
Method string `json:"method,omitempty"` // "email" or "sms"
CodeOnly bool `json:"codeOnly,omitempty"`
DryRun bool `json:"dryRun,omitempty"`
DrySend bool `json:"drySend,omitempty"`
DryRun bool `json:"dryRun,omitempty"`
DrySend bool `json:"drySend,omitempty"`
}
type EnchantedLinkInitResponse struct {
@@ -68,15 +68,15 @@ type SignupRequest struct {
// User Profile Models
type UserProfileResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"` // 추가
Department string `json:"department"`
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"` // 추가
Department string `json:"department"`
AffiliationType string `json:"affiliationType"`
CompanyCode string `json:"companyCode,omitempty"`
TenantID *string `json:"tenantId,omitempty"` // 추가
TenantID *string `json:"tenantId,omitempty"` // 추가
RelyingPartyID *string `json:"relyingPartyId,omitempty"` // 추가
Metadata map[string]any `json:"metadata,omitempty"`
Tenant *Tenant `json:"tenant,omitempty"`

View File

@@ -17,15 +17,15 @@ const (
// IdentityProviderConfig stores the configuration for an external Identity Provider.
type IdentityProviderConfig struct {
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
ClientID string `gorm:"type:uuid;not null;index" json:"client_id"` // Replaces TenantID
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"`
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
ClientID string `gorm:"type:uuid;not null;index" json:"client_id"` // Replaces TenantID
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"`
OIDCClientID *string `gorm:"null" json:"oidc_client_id,omitempty"` // Renamed from ClientID
IssuerURL *string `gorm:"null" json:"issuer_url,omitempty"`
OIDCClientID *string `gorm:"null" json:"oidc_client_id,omitempty"` // Renamed from ClientID
OIDCClientSecret *string `gorm:"null" json:"oidc_client_secret,omitempty"` // Renamed from ClientSecret
// Scopes are space-separated
Scopes *string `gorm:"null" json:"scopes,omitempty"`

View File

@@ -7,17 +7,17 @@ import (
// AuditLog represents a single audit event
type AuditLog struct {
EventID string `json:"event_id"`
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id"`
SessionID string `json:"session_id,omitempty"`
EventType string `json:"event_type"` // e.g., "login_success", "login_failed", "otp_sent"
Status string `json:"status"` // e.g., "success", "failure"
AuthMethod string `json:"auth_method,omitempty"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
DeviceID string `json:"device_id,omitempty"`
Details string `json:"details,omitempty"` // JSON string or simple text
EventID string `json:"event_id"`
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id"`
SessionID string `json:"session_id,omitempty"`
EventType string `json:"event_type"` // e.g., "login_success", "login_failed", "otp_sent"
Status string `json:"status"` // e.g., "success", "failure"
AuthMethod string `json:"auth_method,omitempty"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
DeviceID string `json:"device_id,omitempty"`
Details string `json:"details,omitempty"` // JSON string or simple text
}
// AuditRepository defines interface for storing logs

View File

@@ -31,4 +31,3 @@ func (ug *UserGroup) BeforeCreate(tx *gorm.DB) (err error) {
}
return
}

View File

@@ -13,7 +13,7 @@ import (
)
// Mock DB for ApiKey tests using a real GORM instance but with a hijacked connection
// or just a simple mock if we only check nil.
// or just a simple mock if we only check nil.
// For ApiKeyHandler, it uses DB for Create/List/Delete.
func TestApiKeyHandler_CreateApiKey(t *testing.T) {
@@ -22,11 +22,11 @@ func TestApiKeyHandler_CreateApiKey(t *testing.T) {
// Since we don't have a real DB here, we'll check if it fails gracefully
// or we can use sqlite in-memory for a more realistic test.
h := &ApiKeyHandler{DB: nil} // Testing ServiceUnavailable
app.Post("/api-keys", h.CreateApiKey)
input := map[string]interface{}{
"name": "M2M Test",
"name": "M2M Test",
"scopes": []string{"read", "write"},
}
body, _ := json.Marshal(input)
@@ -41,8 +41,8 @@ func TestApiKeyHandler_CreateApiKey(t *testing.T) {
func TestApiKeyHandler_Validation(t *testing.T) {
app := fiber.New()
// Using a dummy DB pointer to pass the nil check
h := &ApiKeyHandler{DB: &gorm.DB{}}
h := &ApiKeyHandler{DB: &gorm.DB{}}
app.Post("/api-keys", h.CreateApiKey)
// Missing name

View File

@@ -3820,6 +3820,7 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
return profile, nil
}
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
token := h.getBearerToken(c)
if token != "" {

View File

@@ -31,12 +31,15 @@ type MockIdentityProvider struct {
func (m *MockIdentityProvider) Name() string {
return "mock-idp"
}
func (m *MockIdentityProvider) GetMetadata() (*domain.IDPMetadata, error) {
return nil, nil
}
func (m *MockIdentityProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
return "", nil
}
func (m *MockIdentityProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
args := m.Called(loginID, password)
if args.Get(0) == nil {
@@ -44,27 +47,35 @@ func (m *MockIdentityProvider) SignIn(loginID, password string) (*domain.AuthInf
}
return args.Get(0).(*domain.AuthInfo), args.Error(1)
}
func (m *MockIdentityProvider) UserExists(loginID string) (bool, error) {
return true, nil
}
func (m *MockIdentityProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *MockIdentityProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
return nil, nil
}
func (m *MockIdentityProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *MockIdentityProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
return nil, nil
}
func (m *MockIdentityProvider) InitiatePasswordReset(loginID, redirectUrl string) error {
return nil
}
func (m *MockIdentityProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *MockIdentityProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
return nil
}
@@ -102,7 +113,7 @@ func mockHydraTransport(handler http.Handler) http.RoundTripper {
func TestPasswordLogin_OIDC_Success(t *testing.T) {
mockIdp := new(MockIdentityProvider)
// Mock IDP SignIn Success
mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
SessionToken: &domain.Token{JWT: "valid-jwt"},
@@ -142,7 +153,7 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) {
// Inject Mock Kratos (Hack: overwrite the service field if it was an interface, but it's a struct pointer)
// AuthHandler uses *service.KratosAdminService struct pointer.
// KratosAdminService methods are real. We need to mock HTTP client inside KratosAdminService too.
kratosHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Mock FindIdentityIDByIdentifier response
if strings.Contains(r.URL.Path, "/identities") {
@@ -159,8 +170,8 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) {
app := newAuthLoginTestApp(h)
body, _ := json.Marshal(map[string]string{
"loginId": "user@example.com",
"password": "password",
"loginId": "user@example.com",
"password": "password",
"login_challenge": "challenge-123",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
@@ -209,7 +220,7 @@ func TestPasswordLogin_OIDC_InactiveClient(t *testing.T) {
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
},
}
kratosHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{{"id": "kratos-identity-id"}})
})
@@ -219,8 +230,8 @@ func TestPasswordLogin_OIDC_InactiveClient(t *testing.T) {
app := newAuthLoginTestApp(h)
body, _ := json.Marshal(map[string]string{
"loginId": "user@example.com",
"password": "password",
"loginId": "user@example.com",
"password": "password",
"login_challenge": "challenge-inactive",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
@@ -250,7 +261,7 @@ func TestPasswordLogin_NoOIDC_Success(t *testing.T) {
KratosAdmin: service.NewKratosAdminService(),
Hydra: service.NewHydraAdminService(),
}
kratosHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{{"id": "kratos-identity-id"}})
})

View File

@@ -1,6 +1,7 @@
package handler
import (
"baron-sso-backend/internal/service"
"bytes"
"encoding/json"
"io"
@@ -9,8 +10,6 @@ import (
"testing"
"github.com/gofiber/fiber/v2"
"baron-sso-backend/internal/service"
)
func newOidcLoginTestApp(h *AuthHandler) *fiber.App {

View File

@@ -12,17 +12,17 @@ import (
// FederationHandler handles API requests for IdP federation.
type FederationHandler struct {
fedSvc *service.FederationService
repo repository.FederationRepository // For IdP Config CRUD
db *gorm.DB // For tenant existence checks, etc. in CRUD
fedSvc *service.FederationService
repo repository.FederationRepository // For IdP Config CRUD
db *gorm.DB // For tenant existence checks, etc. in CRUD
}
// NewFederationHandler creates a new FederationHandler.
func NewFederationHandler(fedSvc *service.FederationService, repo repository.FederationRepository, db *gorm.DB) *FederationHandler {
return &FederationHandler{
fedSvc: fedSvc,
repo: repo,
db: db,
fedSvc: fedSvc,
repo: repo,
db: db,
}
}
@@ -98,7 +98,7 @@ func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error {
if req.DisplayName == "" || req.ProviderType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "display_name and provider_type are required"})
}
// TODO: Optionally, validate if the clientID exists in Hydra
// Create in DB
@@ -108,8 +108,6 @@ func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(req)
}
// --- Deprecated Tenant-based IdP Config Methods ---
// ListIdpConfigsForTenant handles listing all IdP configurations for a tenant.
@@ -150,7 +148,7 @@ func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error {
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// Create in DB
if err := h.db.Create(&req).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
@@ -158,4 +156,5 @@ func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(req)
}
// TODO: Re-implement Update, Delete handlers for IdP Configs for Clients

View File

@@ -3,8 +3,9 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"github.com/gofiber/fiber/v2"
"log/slog"
"github.com/gofiber/fiber/v2"
)
type RelyingPartyHandler struct {

View File

@@ -139,8 +139,8 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
}
var req struct {
Name string `json:"name"`
Slug string `json:"slug"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Status string `json:"status"`
Domains []string `json:"domains"`
@@ -204,8 +204,8 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
}
var req struct {
Name *string `json:"name"`
Slug *string `json:"slug"`
Name *string `json:"name"`
Slug *string `json:"slug"`
Description *string `json:"description"`
Status *string `json:"status"`
Domains []string `json:"domains"`

View File

@@ -74,13 +74,13 @@ func TestTenantHandler_CreateTenant(t *testing.T) {
app := fiber.New()
mockSvc := new(MockTenantService)
// CreateTenant checks h.DB != nil
h := &TenantHandler{Service: mockSvc, DB: &gorm.DB{}}
h := &TenantHandler{Service: mockSvc, DB: &gorm.DB{}}
app.Post("/tenants", h.CreateTenant)
input := map[string]interface{}{
"name": "Test Tenant",
"slug": "test-tenant",
"name": "Test Tenant",
"slug": "test-tenant",
"domains": []string{"test.com"},
}
body, _ := json.Marshal(input)
@@ -102,7 +102,7 @@ func TestTenantHandler_ApproveTenant(t *testing.T) {
app := fiber.New()
mockSvc := new(MockTenantService)
h := &TenantHandler{Service: mockSvc}
app.Post("/tenants/:id/approve", h.ApproveTenant)
mockSvc.On("ApproveTenant", mock.Anything, "t1").Return(nil)

View File

@@ -3,6 +3,7 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"github.com/gofiber/fiber/v2"
)

View File

@@ -548,7 +548,7 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
ctx := context.Background()
// Fetch user from DB before cleanup if needed, but here we cleanup common namespaces
_ = h.KetoService.DeleteRelation(ctx, "System", "global", "super_admins", uID)
// If we had more complex relations, we would query Keto first or use user metadata
slog.Info("Keto relations cleaned up for user", "userID", uID)
}(userID)

View File

@@ -99,7 +99,7 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
// 4. Gather Metrics & Context
latency := time.Since(start)
status := c.Response().StatusCode()
// If Fiber handler returned an error, status might default to 500 or be in the error
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
@@ -120,7 +120,7 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
tenantID, _ := c.Locals("tenant_id").(string)
sessionID, _ := c.Locals("session_id").(string)
clientIP := extractClientIP(c)
// 6. Capture & Mask Body
var maskedBody string
if config.BodyDump {
@@ -187,7 +187,7 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
// 9. Store Log (Policy Enforcement)
_, isWrite := writeMethods[c.Method()]
if isNil(config.Repo) {
if isWrite {
slog.Error("Audit repository missing for command", "req_id", reqID)

View File

@@ -43,7 +43,7 @@ func TestAuditMiddleware(t *testing.T) {
t.Run("POST request - Sync Success", func(t *testing.T) {
app := fiber.New()
mockRepo := new(MockAuditRepository)
app.Use(AuditMiddleware(AuditConfig{
Repo: mockRepo,
BodyDump: true,
@@ -56,14 +56,14 @@ func TestAuditMiddleware(t *testing.T) {
mockRepo.On("Create", mock.MatchedBy(func(log *domain.AuditLog) bool {
var details map[string]any
json.Unmarshal([]byte(log.Details), &details)
return log.Status == "success" &&
details["method"] == "POST" &&
return log.Status == "success" &&
details["method"] == "POST" &&
details["request_body"] == `{"password":"*****","user":"test"}`
})).Return(nil)
req := httptest.NewRequest("POST", "/test", strings.NewReader(`{"user": "test", "password": "mypassword"}`))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
mockRepo.AssertExpectations(t)
@@ -72,7 +72,7 @@ func TestAuditMiddleware(t *testing.T) {
t.Run("POST request - Sync Failure (Strict Mode)", func(t *testing.T) {
app := fiber.New()
mockRepo := new(MockAuditRepository)
app.Use(AuditMiddleware(AuditConfig{
Repo: mockRepo,
}))
@@ -85,7 +85,7 @@ func TestAuditMiddleware(t *testing.T) {
req := httptest.NewRequest("POST", "/test", nil)
resp, _ := app.Test(req)
// Should return 503 because Audit failed on a Write method
assert.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)
})
@@ -93,7 +93,7 @@ func TestAuditMiddleware(t *testing.T) {
t.Run("GET request - Async Load Shedding", func(t *testing.T) {
app := fiber.New()
mockRepo := new(MockAuditRepository)
// Set very small queue and no workers to force load shedding
app.Use(AuditMiddleware(AuditConfig{
Repo: mockRepo,
@@ -107,16 +107,16 @@ func TestAuditMiddleware(t *testing.T) {
// 1. First request fills the queue
mockRepo.On("Create", mock.Anything).Return(nil)
req1 := httptest.NewRequest("GET", "/test", nil)
resp1, _ := app.Test(req1)
assert.Equal(t, fiber.StatusOK, resp1.StatusCode)
// 2. Second request should be dropped (load shedding) if workers are slow
// Since we can't easily pause workers without modifying code,
// Since we can't easily pause workers without modifying code,
// this test mostly ensures the non-blocking send doesn't hang.
req2 := httptest.NewRequest("GET", "/test", nil)
resp2, _ := app.Test(req2)
assert.Equal(t, fiber.StatusOK, resp2.StatusCode)
})
}
}

View File

@@ -3,8 +3,9 @@ package middleware
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"github.com/gofiber/fiber/v2"
"log/slog"
"github.com/gofiber/fiber/v2"
)
// RBACConfig defines the configuration for RBAC middleware
@@ -89,9 +90,9 @@ func RequireRole(config RBACConfig) fiber.Handler {
}
if !roleAllowed {
slog.Warn("RBAC access denied",
"userID", profile.ID,
"userRole", profile.Role,
slog.Warn("RBAC access denied",
"userID", profile.ID,
"userRole", profile.Role,
"allowedRoles", config.AllowedRoles,
"path", c.Path(),
)

View File

@@ -7,4 +7,4 @@ import (
type FederationRepository interface {
FindProviderByID(ctx context.Context, providerID string) (*domain.IdentityProviderConfig, error)
}
}

View File

@@ -3,6 +3,7 @@ package repository
import (
"baron-sso-backend/internal/domain"
"context"
"gorm.io/gorm"
)

View File

@@ -62,7 +62,7 @@ func (r *tenantRepository) FindByDomain(ctx context.Context, domainName string)
if err := r.db.WithContext(ctx).Where("domain = ?", domainName).First(&tenantDomain).Error; err != nil {
return nil, err
}
var tenant domain.Tenant
if err := r.db.WithContext(ctx).Preload("Domains").First(&tenant, "id = ?", tenantDomain.TenantID).Error; err != nil {
return nil, err

View File

@@ -50,4 +50,3 @@ func (r *userGroupRepository) ListByTenantID(ctx context.Context, tenantID strin
}
return groups, nil
}

View File

@@ -60,7 +60,6 @@ func (r *userRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.
return users, nil
}
func (r *userRepository) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
var users []domain.User
if err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Find(&users).Error; err != nil {

View File

@@ -8,14 +8,15 @@ import (
"fmt"
"time"
"golang.org/x/oauth2"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type FederationService struct {
repo repository.FederationRepository
hydraSvc *HydraAdminService
redisSvc *RedisService
repo repository.FederationRepository
hydraSvc *HydraAdminService
redisSvc *RedisService
}
func NewFederationService(repo repository.FederationRepository, hydraSvc *HydraAdminService, redisSvc *RedisService) *FederationService {
@@ -80,7 +81,6 @@ func (s *FederationService) HandleOIDCCallback(ctx context.Context, code, state
return "http://localhost:3000/login?login_successful=true", nil // Placeholder
}
func generateState() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)

View File

@@ -191,4 +191,4 @@ func (s *ketoService) DeleteRelation(ctx context.Context, namespace, object, rel
slog.Info("Keto relation deleted", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
return nil
}
}

View File

@@ -75,8 +75,8 @@ func (o *OryProvider) CreateUser(user *domain.BrokerUser, password string) (stri
}
traits := map[string]interface{}{
"email": user.Email,
"name": user.Name,
"email": user.Email,
"name": user.Name,
}
if user.PhoneNumber != "" {
traits["phone_number"] = user.PhoneNumber
@@ -521,10 +521,10 @@ type kratosRecoveryAddress struct {
}
type kratosIdentityFull struct {
SchemaID string `json:"schema_id"`
Traits map[string]interface{} `json:"traits"`
SchemaID string `json:"schema_id"`
Traits map[string]interface{} `json:"traits"`
VerifiableAddresses []kratosVerifiableAddress `json:"verifiable_addresses"`
RecoveryAddresses []kratosRecoveryAddress `json:"recovery_addresses"`
RecoveryAddresses []kratosRecoveryAddress `json:"recovery_addresses"`
}
func (o *OryProvider) patchIdentity(identityID string, ops []map[string]interface{}) error {

View File

@@ -38,8 +38,8 @@ func (s *relyingPartyService) Create(ctx context.Context, tenantID string, clien
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.
// 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)
@@ -72,7 +72,7 @@ func (s *relyingPartyService) Get(ctx context.Context, clientID string) (*domain
func (s *relyingPartyService) List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error) {
// 1. Fetch ClientIDs from Keto
// Subject: Tenant:<tenantID>, Relation: parent_tenant, Namespace: RelyingParty
// Note: ListRelations checks "who has relation to subject".
// 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)
@@ -105,12 +105,12 @@ func (s *relyingPartyService) ListAll(ctx context.Context) ([]domain.RelyingPart
// 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,
// 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")
return nil, fmt.Errorf("ListAll not implemented in SSOT mode yet")
}
func (s *relyingPartyService) ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error) {
@@ -176,4 +176,3 @@ func (s *relyingPartyService) mapHydraToDomain(client *domain.HydraClient) *doma
}
return rp
}

View File

@@ -109,7 +109,6 @@ func TestRelyingPartyService_Create_Success(t *testing.T) {
svc := NewRelyingPartyService(hydraSvc, mockKeto)
rp, err := svc.Create(context.Background(), tenantID, inputClient)
if err != nil {
t.Fatalf("Create failed: %v", err)
}
@@ -200,7 +199,6 @@ func TestRelyingPartyService_Get_Success(t *testing.T) {
svc := NewRelyingPartyService(hydraSvc, mockKeto)
rp, hc, err := svc.Get(context.Background(), clientID)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
@@ -233,7 +231,6 @@ func TestRelyingPartyService_Update_Success(t *testing.T) {
updateReq := domain.HydraClient{ClientName: "New Name"}
rp, err := svc.Update(context.Background(), clientID, updateReq)
if err != nil {
t.Fatalf("Update failed: %v", err)
}
@@ -272,7 +269,6 @@ func TestRelyingPartyService_Delete_Success(t *testing.T) {
svc := NewRelyingPartyService(hydraSvc, mockKeto)
err := svc.Delete(context.Background(), clientID)
if err != nil {
t.Fatalf("Delete failed: %v", err)
}

View File

@@ -141,12 +141,12 @@ func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain strin
if err != nil {
return nil, err
}
// Only return ACTIVE tenants for auto-assignment
if tenant.Status != domain.TenantStatusActive {
return nil, errors.New("tenant is not active")
}
return tenant, nil
}

View File

@@ -13,7 +13,7 @@ type UserGroupService interface {
Delete(ctx context.Context, id string) error
Get(ctx context.Context, id string) (*domain.UserGroup, error)
List(ctx context.Context, tenantID string) ([]domain.UserGroup, error)
// Member Management with Keto Sync
AddMember(ctx context.Context, groupID, userID string) error
RemoveMember(ctx context.Context, groupID, userID string) error

View File

@@ -6,7 +6,7 @@ import (
)
var (
slugRegex = regexp.MustCompile(`^[a-z0-9-]+$`)
slugRegex = regexp.MustCompile(`^[a-z0-9-]+$`)
reservedSlugs = map[string]bool{
"admin": true,
"api": true,
@@ -31,7 +31,7 @@ var (
// ValidateSlug checks if a slug meets requirements and is not reserved.
func ValidateSlug(slug string) (bool, string) {
s := strings.ToLower(strings.TrimSpace(slug))
if len(s) < 3 || len(s) > 32 {
return false, "slug must be between 3 and 32 characters"
}