diff --git a/backend/cmd/keto_test/main.go b/backend/cmd/keto_test/main.go index 82cc33fc..cf50db9b 100644 --- a/backend/cmd/keto_test/main.go +++ b/backend/cmd/keto_test/main.go @@ -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" diff --git a/backend/cmd/keygen/main.go b/backend/cmd/keygen/main.go index 8fc8b558..6047c95f 100644 --- a/backend/cmd/keygen/main.go +++ b/backend/cmd/keygen/main.go @@ -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) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 855f0545..301cab51 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -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) diff --git a/backend/internal/domain/auth_models.go b/backend/internal/domain/auth_models.go index 6814090e..3cfd6d98 100644 --- a/backend/internal/domain/auth_models.go +++ b/backend/internal/domain/auth_models.go @@ -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"` diff --git a/backend/internal/domain/federation_models.go b/backend/internal/domain/federation_models.go index 4f29b9db..58ea110a 100644 --- a/backend/internal/domain/federation_models.go +++ b/backend/internal/domain/federation_models.go @@ -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"` diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go index e21e91fe..d5b749b8 100644 --- a/backend/internal/domain/models.go +++ b/backend/internal/domain/models.go @@ -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 diff --git a/backend/internal/domain/user_group.go b/backend/internal/domain/user_group.go index 2d88ff3b..ecd48c78 100644 --- a/backend/internal/domain/user_group.go +++ b/backend/internal/domain/user_group.go @@ -31,4 +31,3 @@ func (ug *UserGroup) BeforeCreate(tx *gorm.DB) (err error) { } return } - diff --git a/backend/internal/handler/api_key_handler_test.go b/backend/internal/handler/api_key_handler_test.go index 1e5bc5d5..43b5ab93 100644 --- a/backend/internal/handler/api_key_handler_test.go +++ b/backend/internal/handler/api_key_handler_test.go @@ -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 diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 43ea6901..24f5af02 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -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 != "" { diff --git a/backend/internal/handler/auth_handler_login_test.go b/backend/internal/handler/auth_handler_login_test.go index ca2bff02..dd261e17 100644 --- a/backend/internal/handler/auth_handler_login_test.go +++ b/backend/internal/handler/auth_handler_login_test.go @@ -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"}}) }) diff --git a/backend/internal/handler/auth_handler_oidc_test.go b/backend/internal/handler/auth_handler_oidc_test.go index 0280c876..828e8e39 100644 --- a/backend/internal/handler/auth_handler_oidc_test.go +++ b/backend/internal/handler/auth_handler_oidc_test.go @@ -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 { diff --git a/backend/internal/handler/federation_handler.go b/backend/internal/handler/federation_handler.go index e4258a16..6807391e 100644 --- a/backend/internal/handler/federation_handler.go +++ b/backend/internal/handler/federation_handler.go @@ -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 diff --git a/backend/internal/handler/relying_party_handler.go b/backend/internal/handler/relying_party_handler.go index e4a12991..29611b23 100644 --- a/backend/internal/handler/relying_party_handler.go +++ b/backend/internal/handler/relying_party_handler.go @@ -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 { diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 15558be5..53d858bb 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -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"` diff --git a/backend/internal/handler/tenant_handler_test.go b/backend/internal/handler/tenant_handler_test.go index 1330d302..28bb903c 100644 --- a/backend/internal/handler/tenant_handler_test.go +++ b/backend/internal/handler/tenant_handler_test.go @@ -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) diff --git a/backend/internal/handler/user_group_handler.go b/backend/internal/handler/user_group_handler.go index 89df6a47..a3bde03b 100644 --- a/backend/internal/handler/user_group_handler.go +++ b/backend/internal/handler/user_group_handler.go @@ -3,6 +3,7 @@ package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" + "github.com/gofiber/fiber/v2" ) diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index c316ad5e..966e5b82 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -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) diff --git a/backend/internal/middleware/audit_middleware.go b/backend/internal/middleware/audit_middleware.go index 76275234..de18c3b4 100644 --- a/backend/internal/middleware/audit_middleware.go +++ b/backend/internal/middleware/audit_middleware.go @@ -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) diff --git a/backend/internal/middleware/audit_middleware_test.go b/backend/internal/middleware/audit_middleware_test.go index cf859370..05706b45 100644 --- a/backend/internal/middleware/audit_middleware_test.go +++ b/backend/internal/middleware/audit_middleware_test.go @@ -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) }) -} \ No newline at end of file +} diff --git a/backend/internal/middleware/rbac.go b/backend/internal/middleware/rbac.go index c1346034..920a87d4 100644 --- a/backend/internal/middleware/rbac.go +++ b/backend/internal/middleware/rbac.go @@ -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(), ) diff --git a/backend/internal/repository/federation_repository.go b/backend/internal/repository/federation_repository.go index 27b4ce3f..be65d4bc 100644 --- a/backend/internal/repository/federation_repository.go +++ b/backend/internal/repository/federation_repository.go @@ -7,4 +7,4 @@ import ( type FederationRepository interface { FindProviderByID(ctx context.Context, providerID string) (*domain.IdentityProviderConfig, error) -} \ No newline at end of file +} diff --git a/backend/internal/repository/gorm_federation_repository.go b/backend/internal/repository/gorm_federation_repository.go index df8a4e92..419963c0 100644 --- a/backend/internal/repository/gorm_federation_repository.go +++ b/backend/internal/repository/gorm_federation_repository.go @@ -3,6 +3,7 @@ package repository import ( "baron-sso-backend/internal/domain" "context" + "gorm.io/gorm" ) diff --git a/backend/internal/repository/tenant_repository.go b/backend/internal/repository/tenant_repository.go index 98fca342..5990e48b 100644 --- a/backend/internal/repository/tenant_repository.go +++ b/backend/internal/repository/tenant_repository.go @@ -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 diff --git a/backend/internal/repository/user_group_repository.go b/backend/internal/repository/user_group_repository.go index b44bd849..67f93fc1 100644 --- a/backend/internal/repository/user_group_repository.go +++ b/backend/internal/repository/user_group_repository.go @@ -50,4 +50,3 @@ func (r *userGroupRepository) ListByTenantID(ctx context.Context, tenantID strin } return groups, nil } - diff --git a/backend/internal/repository/user_repository.go b/backend/internal/repository/user_repository.go index e487c3b0..28b89202 100644 --- a/backend/internal/repository/user_repository.go +++ b/backend/internal/repository/user_repository.go @@ -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 { diff --git a/backend/internal/service/federation_service.go b/backend/internal/service/federation_service.go index fed14881..a32b5c09 100644 --- a/backend/internal/service/federation_service.go +++ b/backend/internal/service/federation_service.go @@ -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) diff --git a/backend/internal/service/keto_service.go b/backend/internal/service/keto_service.go index a75bb285..9303485b 100644 --- a/backend/internal/service/keto_service.go +++ b/backend/internal/service/keto_service.go @@ -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 -} \ No newline at end of file +} diff --git a/backend/internal/service/ory_service.go b/backend/internal/service/ory_service.go index b57833ff..a205ca68 100644 --- a/backend/internal/service/ory_service.go +++ b/backend/internal/service/ory_service.go @@ -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 { diff --git a/backend/internal/service/relying_party_service.go b/backend/internal/service/relying_party_service.go index 5ac1da45..24b693ef 100644 --- a/backend/internal/service/relying_party_service.go +++ b/backend/internal/service/relying_party_service.go @@ -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:, 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 } - diff --git a/backend/internal/service/relying_party_service_test.go b/backend/internal/service/relying_party_service_test.go index f1dd705a..f3464229 100644 --- a/backend/internal/service/relying_party_service_test.go +++ b/backend/internal/service/relying_party_service_test.go @@ -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) } diff --git a/backend/internal/service/tenant_service.go b/backend/internal/service/tenant_service.go index c7cee875..417d0b97 100644 --- a/backend/internal/service/tenant_service.go +++ b/backend/internal/service/tenant_service.go @@ -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 } diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index 4e735726..881c2d0f 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -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 diff --git a/backend/internal/utils/slug.go b/backend/internal/utils/slug.go index f86048b1..b618dcea 100644 --- a/backend/internal/utils/slug.go +++ b/backend/internal/utils/slug.go @@ -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" }