Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86900b0bd4 |
0
kms/.gitignore → .gitignore
vendored
@ -88,6 +88,9 @@ npm test
|
||||
# Start all services (PostgreSQL, API, Nginx, Frontend)
|
||||
podman-compose up -d
|
||||
|
||||
# Start with SSO testing enabled (Keycloak + SAML IdP)
|
||||
podman-compose -f docker-compose.yml -f docker-compose.sso.yml up -d
|
||||
|
||||
# Check service health
|
||||
curl http://localhost:8081/health
|
||||
|
||||
@ -97,10 +100,15 @@ podman-compose logs -f
|
||||
# View specific service logs
|
||||
podman-compose logs -f api-service
|
||||
podman-compose logs -f postgres
|
||||
podman-compose logs -f keycloak
|
||||
podman-compose logs -f saml-idp
|
||||
|
||||
# Stop services
|
||||
podman-compose down
|
||||
|
||||
# Stop SSO services
|
||||
podman-compose -f docker-compose.yml -f docker-compose.sso.yml down
|
||||
|
||||
# Rebuild services after code changes
|
||||
podman-compose up -d --build
|
||||
```
|
||||
@ -182,12 +190,34 @@ podman-compose down
|
||||
- **Port 3000**: React frontend (direct access)
|
||||
- **Port 5432**: PostgreSQL database
|
||||
- **Port 9090**: Metrics endpoint (if enabled)
|
||||
- **Port 8090**: Keycloak SSO server (admin console)
|
||||
- **Port 8091**: SimpleSAMLphp IdP (SAML console: /simplesaml)
|
||||
- **Port 8443**: SimpleSAMLphp IdP (HTTPS)
|
||||
|
||||
The service provides different test user contexts:
|
||||
- Regular user: `test@example.com`
|
||||
- Admin user: `admin@example.com`
|
||||
- Limited user: `limited@example.com`
|
||||
|
||||
### SSO Testing Users
|
||||
|
||||
For SSO testing with Keycloak or SAML IdP, use these credentials:
|
||||
|
||||
| Email | Password | Permissions | Provider |
|
||||
|-------|----------|-------------|----------|
|
||||
| admin@example.com | admin123 | internal.* | Keycloak |
|
||||
| test@example.com | test123 | app.read, token.read | Keycloak |
|
||||
| limited@example.com | limited123 | repo.read | Keycloak |
|
||||
| user1@example.com | user1pass | Basic access | SAML IdP |
|
||||
| user2@example.com | user2pass | Basic access | SAML IdP |
|
||||
|
||||
### SSO Access Points
|
||||
|
||||
- **Keycloak Admin Console**: http://localhost:8090 (admin / admin)
|
||||
- **SAML IdP Admin Console**: http://localhost:8091/simplesaml (admin / secret)
|
||||
- **Keycloak Realm**: http://localhost:8090/realms/kms
|
||||
- **SAML IdP Metadata**: http://localhost:8091/simplesaml/saml2/idp/metadata.php
|
||||
|
||||
## Key Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
@ -211,14 +241,28 @@ SERVER_HOST=0.0.0.0
|
||||
SERVER_PORT=8080
|
||||
|
||||
# Authentication
|
||||
AUTH_PROVIDER=header # or 'sso'
|
||||
AUTH_PROVIDER=header # 'header', 'sso', or 'saml'
|
||||
AUTH_HEADER_USER_EMAIL=X-User-Email
|
||||
|
||||
# SSO / OAuth2 Configuration (for Keycloak)
|
||||
OAUTH2_ENABLED=false # Set to true for OAuth2/OIDC auth
|
||||
OAUTH2_PROVIDER_URL=http://keycloak:8080/realms/kms
|
||||
OAUTH2_CLIENT_ID=kms-api
|
||||
OAUTH2_CLIENT_SECRET=kms-client-secret
|
||||
OAUTH2_REDIRECT_URL=http://localhost:8081/api/oauth2/callback
|
||||
|
||||
# SAML Configuration (for SimpleSAMLphp)
|
||||
SAML_ENABLED=false # Set to true for SAML auth
|
||||
SAML_IDP_SSO_URL=http://saml-idp:8080/simplesaml/saml2/idp/SSOService.php
|
||||
SAML_IDP_METADATA_URL=http://saml-idp:8080/simplesaml/saml2/idp/metadata.php
|
||||
SAML_SP_ENTITY_ID=http://localhost:8081
|
||||
SAML_SP_ACS_URL=http://localhost:8081/api/saml/acs
|
||||
SAML_SP_SLS_URL=http://localhost:8081/api/saml/sls
|
||||
|
||||
# Features
|
||||
RATE_LIMIT_ENABLED=true
|
||||
CACHE_ENABLED=false # Set to true to enable Redis
|
||||
METRICS_ENABLED=true
|
||||
SAML_ENABLED=false # Set to true for SAML auth
|
||||
```
|
||||
|
||||
### Optional Configuration
|
||||
@ -323,15 +367,67 @@ Example: `repo` permission includes `repo.read` and `repo.write`.
|
||||
- **Filtering**: Support for date ranges, event types, statuses, users, resource types
|
||||
- **Statistics**: Aggregated metrics by type, severity, status, and time
|
||||
|
||||
## SSO Testing Workflow
|
||||
|
||||
### Quick Start - OAuth2/OIDC Testing (Keycloak)
|
||||
|
||||
```bash
|
||||
# 1. Start services with SSO enabled
|
||||
podman-compose -f docker-compose.yml -f docker-compose.sso.yml up -d
|
||||
|
||||
# 2. Wait for Keycloak to start (check logs)
|
||||
podman-compose logs -f keycloak
|
||||
|
||||
# 3. Test OAuth2 login flow
|
||||
curl -v "http://localhost:8090/realms/kms/protocol/openid-connect/auth?client_id=kms-api&response_type=code&redirect_uri=http://localhost:8081/api/oauth2/callback"
|
||||
|
||||
# 4. Access Keycloak admin console
|
||||
open http://localhost:8090
|
||||
# Login with: admin / admin
|
||||
|
||||
# 5. Test API with OAuth2 token
|
||||
# (Use Keycloak to get access token, then use in Authorization: Bearer header)
|
||||
```
|
||||
|
||||
### Quick Start - SAML Testing (SimpleSAMLphp)
|
||||
|
||||
```bash
|
||||
# 1. Services should already be running from previous step
|
||||
|
||||
# 2. Access SAML IdP admin console
|
||||
open http://localhost:8091/simplesaml
|
||||
# Login with: admin / secret
|
||||
|
||||
# 3. View IdP metadata
|
||||
curl http://localhost:8091/simplesaml/saml2/idp/metadata.php
|
||||
|
||||
# 4. Test SAML authentication flow
|
||||
# Navigate to your app and it should redirect to SAML IdP for auth
|
||||
```
|
||||
|
||||
### Environment Switching
|
||||
|
||||
```bash
|
||||
# Switch to OAuth2 mode
|
||||
podman exec kms-api-service sh -c "export AUTH_PROVIDER=sso OAUTH2_ENABLED=true && supervisorctl restart all"
|
||||
|
||||
# Switch to SAML mode
|
||||
podman exec kms-api-service sh -c "export AUTH_PROVIDER=sso SAML_ENABLED=true && supervisorctl restart all"
|
||||
|
||||
# Switch back to header mode
|
||||
podman exec kms-api-service sh -c "export AUTH_PROVIDER=header && supervisorctl restart all"
|
||||
```
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Critical Information
|
||||
- **Go Version**: Requires Go 1.23+ (currently using 1.24.4)
|
||||
- **Node Version**: Requires Node 24+ and npm 11+
|
||||
- **Database**: Auto-migrations run on startup
|
||||
- **Container Names**: Use `kms-postgres`, `kms-api-service`, `kms-frontend`, `kms-nginx`
|
||||
- **Default Ports**: API:8080, Nginx:8081, Frontend:3000, DB:5432, Metrics:9090
|
||||
- **Container Names**: Use `kms-postgres`, `kms-api-service`, `kms-frontend`, `kms-nginx`, `kms-keycloak`, `kms-saml-idp`
|
||||
- **Default Ports**: API:8080, Nginx:8081, Frontend:3000, DB:5432, Metrics:9090, Keycloak:8090, SAML:8091
|
||||
- **Test Database**: `kms_test` (separate from `kms`)
|
||||
- **SSO Config**: Located in `sso-config/` directory
|
||||
|
||||
### Important Files
|
||||
- `internal/config/config.go` - Complete configuration management
|
||||
343
cmd/server/main.go
Normal file
@ -0,0 +1,343 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/audit"
|
||||
"github.com/kms/api-key-service/internal/config"
|
||||
"github.com/kms/api-key-service/internal/database"
|
||||
"github.com/kms/api-key-service/internal/domain"
|
||||
"github.com/kms/api-key-service/internal/handlers"
|
||||
"github.com/kms/api-key-service/internal/metrics"
|
||||
"github.com/kms/api-key-service/internal/middleware"
|
||||
"github.com/kms/api-key-service/internal/repository/postgres"
|
||||
"github.com/kms/api-key-service/internal/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize configuration
|
||||
cfg := config.NewConfig()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
log.Fatal("Configuration validation failed:", err)
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger := initLogger(cfg)
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info("Starting API Key Management Service",
|
||||
zap.String("version", cfg.GetString("APP_VERSION")),
|
||||
zap.String("environment", cfg.GetString("APP_ENV")),
|
||||
)
|
||||
|
||||
// Initialize database
|
||||
logger.Info("Connecting to database",
|
||||
zap.String("dsn", cfg.GetDatabaseDSNForLogging()))
|
||||
|
||||
db, err := database.NewPostgresProvider(
|
||||
cfg.GetDatabaseDSN(),
|
||||
cfg.GetInt("DB_MAX_OPEN_CONNS"),
|
||||
cfg.GetInt("DB_MAX_IDLE_CONNS"),
|
||||
cfg.GetString("DB_CONN_MAX_LIFETIME"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to initialize database",
|
||||
zap.String("dsn", cfg.GetDatabaseDSNForLogging()),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Info("Database connection established successfully")
|
||||
|
||||
// Database migrations are handled by PostgreSQL docker-entrypoint-initdb.d
|
||||
logger.Info("Database migrations are handled by PostgreSQL on container startup")
|
||||
|
||||
// Initialize repositories
|
||||
appRepo := postgres.NewApplicationRepository(db)
|
||||
tokenRepo := postgres.NewStaticTokenRepository(db)
|
||||
permRepo := postgres.NewPermissionRepository(db)
|
||||
grantRepo := postgres.NewGrantedPermissionRepository(db)
|
||||
auditRepo := postgres.NewAuditRepository(db)
|
||||
|
||||
// Initialize audit logger
|
||||
auditLogger := audit.NewAuditLogger(cfg, logger, auditRepo)
|
||||
|
||||
// Initialize services
|
||||
appService := services.NewApplicationService(appRepo, auditRepo, logger)
|
||||
tokenService := services.NewTokenService(tokenRepo, appRepo, permRepo, grantRepo, cfg.GetString("INTERNAL_HMAC_KEY"), cfg, logger)
|
||||
authService := services.NewAuthenticationService(cfg, logger, permRepo)
|
||||
|
||||
// Initialize handlers
|
||||
healthHandler := handlers.NewHealthHandler(db, logger)
|
||||
appHandler := handlers.NewApplicationHandler(appService, authService, logger)
|
||||
tokenHandler := handlers.NewTokenHandler(tokenService, authService, logger)
|
||||
authHandler := handlers.NewAuthHandler(authService, tokenService, cfg, logger)
|
||||
auditHandler := handlers.NewAuditHandler(auditLogger, authService, logger)
|
||||
testHandler := handlers.NewTestHandler(logger)
|
||||
|
||||
// Set up router
|
||||
router := setupRouter(cfg, logger, healthHandler, appHandler, tokenHandler, authHandler, auditHandler, testHandler)
|
||||
|
||||
// Create HTTP server
|
||||
srv := &http.Server{
|
||||
Addr: cfg.GetServerAddress(),
|
||||
Handler: router,
|
||||
ReadTimeout: cfg.GetDuration("SERVER_READ_TIMEOUT"),
|
||||
WriteTimeout: cfg.GetDuration("SERVER_WRITE_TIMEOUT"),
|
||||
IdleTimeout: cfg.GetDuration("SERVER_IDLE_TIMEOUT"),
|
||||
}
|
||||
|
||||
// Initialize bootstrap data
|
||||
logger.Info("Initializing bootstrap data")
|
||||
if err := initializeBootstrapData(context.Background(), appService, tokenService, cfg, logger); err != nil {
|
||||
logger.Fatal("Failed to initialize bootstrap data", zap.Error(err))
|
||||
}
|
||||
|
||||
// Start server in goroutine
|
||||
go func() {
|
||||
logger.Info("Starting HTTP server", zap.String("address", srv.Addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal("Failed to start server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// Start metrics server if enabled
|
||||
var metricsSrv *http.Server
|
||||
if cfg.GetBool("METRICS_ENABLED") {
|
||||
metricsSrv = startMetricsServer(cfg, logger)
|
||||
}
|
||||
|
||||
// Wait for interrupt signal to gracefully shutdown the server
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
logger.Info("Shutting down server...")
|
||||
|
||||
// Give outstanding requests time to complete
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Shutdown main server
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
logger.Error("Server forced to shutdown", zap.Error(err))
|
||||
}
|
||||
|
||||
// Shutdown metrics server
|
||||
if metricsSrv != nil {
|
||||
if err := metricsSrv.Shutdown(ctx); err != nil {
|
||||
logger.Error("Metrics server forced to shutdown", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Server exited")
|
||||
}
|
||||
|
||||
func initLogger(cfg config.ConfigProvider) *zap.Logger {
|
||||
var logger *zap.Logger
|
||||
var err error
|
||||
|
||||
if cfg.IsProduction() {
|
||||
logger, err = zap.NewProduction()
|
||||
} else {
|
||||
logger, err = zap.NewDevelopment()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize logger:", err)
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *handlers.HealthHandler, appHandler *handlers.ApplicationHandler, tokenHandler *handlers.TokenHandler, authHandler *handlers.AuthHandler, auditHandler *handlers.AuditHandler, testHandler *handlers.TestHandler) *gin.Engine {
|
||||
// Set Gin mode based on environment
|
||||
if cfg.IsProduction() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
|
||||
// Add middleware
|
||||
router.Use(middleware.Logger(logger))
|
||||
router.Use(middleware.Recovery(logger))
|
||||
router.Use(metrics.Middleware(logger))
|
||||
router.Use(middleware.CORS())
|
||||
router.Use(middleware.Security())
|
||||
router.Use(middleware.ValidateContentType())
|
||||
|
||||
if cfg.GetBool("RATE_LIMIT_ENABLED") {
|
||||
router.Use(middleware.RateLimit(cfg.GetInt("RATE_LIMIT_RPS"), cfg.GetInt("RATE_LIMIT_BURST")))
|
||||
}
|
||||
|
||||
// Health check endpoint (no authentication required)
|
||||
router.GET("/health", healthHandler.Health)
|
||||
router.GET("/ready", healthHandler.Ready)
|
||||
|
||||
// Development/Testing endpoints (no authentication required)
|
||||
if !cfg.IsProduction() {
|
||||
router.GET("/test/sso", testHandler.SSOTestPage)
|
||||
}
|
||||
|
||||
// API routes
|
||||
api := router.Group("/api")
|
||||
{
|
||||
// Authentication endpoints (no prior auth required)
|
||||
api.GET("/login", authHandler.Login) // HTML page for browser access
|
||||
api.POST("/login", authHandler.Login) // JSON API for programmatic access
|
||||
api.POST("/verify", authHandler.Verify)
|
||||
api.POST("/renew", authHandler.Renew)
|
||||
|
||||
// Protected routes (require authentication)
|
||||
protected := api.Group("/")
|
||||
protected.Use(middleware.Authentication(cfg, logger))
|
||||
{
|
||||
// Application management
|
||||
protected.GET("/applications", appHandler.List)
|
||||
protected.POST("/applications", appHandler.Create)
|
||||
protected.GET("/applications/:id", appHandler.GetByID)
|
||||
protected.PUT("/applications/:id", appHandler.Update)
|
||||
protected.DELETE("/applications/:id", appHandler.Delete)
|
||||
|
||||
// Token management
|
||||
protected.GET("/applications/:id/tokens", tokenHandler.ListByApp)
|
||||
protected.POST("/applications/:id/tokens", tokenHandler.Create)
|
||||
protected.DELETE("/tokens/:id", tokenHandler.Delete)
|
||||
|
||||
// Audit management
|
||||
protected.GET("/audit/events", auditHandler.ListEvents)
|
||||
protected.GET("/audit/events/:id", auditHandler.GetEvent)
|
||||
protected.GET("/audit/stats", auditHandler.GetStats)
|
||||
|
||||
// Documentation endpoint
|
||||
protected.GET("/docs", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"service": "API Key Management Service",
|
||||
"version": cfg.GetString("APP_VERSION"),
|
||||
"documentation": "See README.md and docs/ directory",
|
||||
"endpoints": map[string]interface{}{
|
||||
"authentication": []string{
|
||||
"POST /api/login",
|
||||
"POST /api/verify",
|
||||
"POST /api/renew",
|
||||
},
|
||||
"applications": []string{
|
||||
"GET /api/applications",
|
||||
"POST /api/applications",
|
||||
"GET /api/applications/:id",
|
||||
"PUT /api/applications/:id",
|
||||
"DELETE /api/applications/:id",
|
||||
},
|
||||
"tokens": []string{
|
||||
"GET /api/applications/:id/tokens",
|
||||
"POST /api/applications/:id/tokens",
|
||||
"DELETE /api/tokens/:id",
|
||||
},
|
||||
"audit": []string{
|
||||
"GET /api/audit/events",
|
||||
"GET /api/audit/events/:id",
|
||||
"GET /api/audit/stats",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func startMetricsServer(cfg config.ConfigProvider, logger *zap.Logger) *http.Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Prometheus metrics endpoint
|
||||
mux.HandleFunc("/metrics", metrics.PrometheusHandler())
|
||||
|
||||
// Health endpoint for metrics server
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.GetMetricsAddress(),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info("Starting metrics server", zap.String("address", srv.Addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("Failed to start metrics server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func initializeBootstrapData(ctx context.Context, appService services.ApplicationService, tokenService services.TokenService, cfg config.ConfigProvider, logger *zap.Logger) error {
|
||||
// Check if internal application already exists
|
||||
internalAppID := cfg.GetString("INTERNAL_APP_ID")
|
||||
_, err := appService.GetByID(ctx, internalAppID)
|
||||
if err == nil {
|
||||
logger.Info("Internal application already exists, skipping bootstrap")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("Creating internal application for bootstrap", zap.String("app_id", internalAppID))
|
||||
|
||||
// Create internal application for system operations
|
||||
internalAppReq := &domain.CreateApplicationRequest{
|
||||
AppID: internalAppID,
|
||||
AppLink: "https://kms.internal/system",
|
||||
Type: []domain.ApplicationType{domain.ApplicationTypeStatic, domain.ApplicationTypeUser},
|
||||
CallbackURL: "https://kms.internal/callback",
|
||||
TokenPrefix: "KMS",
|
||||
TokenRenewalDuration: domain.Duration{Duration: 365 * 24 * time.Hour}, // 1 year
|
||||
MaxTokenDuration: domain.Duration{Duration: 365 * 24 * time.Hour}, // 1 year
|
||||
Owner: domain.Owner{
|
||||
Type: domain.OwnerTypeTeam,
|
||||
Name: "KMS System",
|
||||
Owner: "system@kms.internal",
|
||||
},
|
||||
}
|
||||
|
||||
app, err := appService.Create(ctx, internalAppReq, "system")
|
||||
if err != nil {
|
||||
logger.Error("Failed to create internal application", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Internal application created successfully",
|
||||
zap.String("app_id", app.AppID),
|
||||
zap.String("hmac_key", app.HMACKey))
|
||||
|
||||
// Create a static token for internal system operations if needed
|
||||
internalTokenReq := &domain.CreateStaticTokenRequest{
|
||||
AppID: internalAppID,
|
||||
Owner: domain.Owner{
|
||||
Type: domain.OwnerTypeTeam,
|
||||
Name: "KMS System Token",
|
||||
Owner: "system@kms.internal",
|
||||
},
|
||||
Permissions: []string{"internal.*", "app.*", "token.*", "audit.*"},
|
||||
}
|
||||
|
||||
token, err := tokenService.CreateStaticToken(ctx, internalTokenReq, "system")
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create internal system token, continuing...", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("Internal system token created successfully",
|
||||
zap.String("token_id", token.ID.String()))
|
||||
}
|
||||
|
||||
logger.Info("Bootstrap data initialization completed successfully")
|
||||
return nil
|
||||
}
|
||||
22
docker-compose.sso.yml
Normal file
@ -0,0 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
# Override file for enabling SSO testing
|
||||
# Usage: podman-compose -f docker-compose.yml -f docker-compose.sso.yml up -d
|
||||
|
||||
services:
|
||||
api-service:
|
||||
environment:
|
||||
# Enable OAuth2 for Keycloak testing
|
||||
OAUTH2_ENABLED: true
|
||||
# Enable SAML for SimpleSAMLphp testing
|
||||
SAML_ENABLED: true
|
||||
# Switch to SSO auth provider instead of header
|
||||
AUTH_PROVIDER: sso
|
||||
# Set the required SSO configuration
|
||||
SSO_PROVIDER_URL: http://keycloak:8080/realms/kms
|
||||
SSO_CLIENT_ID: kms-api
|
||||
SSO_CLIENT_SECRET: kms-client-secret
|
||||
SSO_REDIRECT_URL: http://localhost:8081/api/sso/callback
|
||||
depends_on:
|
||||
- keycloak
|
||||
- saml-idp
|
||||
@ -63,6 +63,19 @@ services:
|
||||
RATE_LIMIT_ENABLED: true
|
||||
CACHE_ENABLED: false
|
||||
METRICS_ENABLED: true
|
||||
# OAuth2 / OIDC Configuration (for Keycloak)
|
||||
OAUTH2_ENABLED: false
|
||||
OAUTH2_PROVIDER_URL: http://keycloak:8080/realms/kms
|
||||
OAUTH2_CLIENT_ID: kms-api
|
||||
OAUTH2_CLIENT_SECRET: kms-client-secret
|
||||
OAUTH2_REDIRECT_URL: http://localhost:8081/api/oauth2/callback
|
||||
# SAML Configuration (for SimpleSAMLphp)
|
||||
SAML_ENABLED: false
|
||||
SAML_IDP_SSO_URL: http://saml-idp:8080/simplesaml/saml2/idp/SSOService.php
|
||||
SAML_IDP_METADATA_URL: http://saml-idp:8080/simplesaml/saml2/idp/metadata.php
|
||||
SAML_SP_ENTITY_ID: http://localhost:8081
|
||||
SAML_SP_ACS_URL: http://localhost:8081/api/saml/acs
|
||||
SAML_SP_SLS_URL: http://localhost:8081/api/saml/sls
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "9090:9090" # Metrics port
|
||||
@ -86,6 +99,39 @@ services:
|
||||
- kms-network
|
||||
restart: unless-stopped
|
||||
|
||||
# Keycloak OAuth2/OIDC Identity Provider for testing
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:25.0.2
|
||||
container_name: kms-keycloak
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||
KC_DB: dev-file
|
||||
ports:
|
||||
- "8090:8080"
|
||||
networks:
|
||||
- kms-network
|
||||
command: ["start-dev", "--import-realm"]
|
||||
volumes:
|
||||
- ./sso-config/keycloak:/opt/keycloak/data/import:Z
|
||||
restart: unless-stopped
|
||||
|
||||
# SimpleSAMLphp SAML Identity Provider for testing
|
||||
saml-idp:
|
||||
image: kristophjunge/test-saml-idp:1.15
|
||||
container_name: kms-saml-idp
|
||||
environment:
|
||||
SIMPLESAMLPHP_SP_ENTITY_ID: http://localhost:8081
|
||||
SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: http://localhost:8081/api/saml/acs
|
||||
SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: http://localhost:8081/api/saml/sls
|
||||
SIMPLESAMLPHP_TRUSTED_DOMAINS: '["localhost", "kms-api-service", "kms-nginx"]'
|
||||
ports:
|
||||
- "8091:8080"
|
||||
- "8443:8443"
|
||||
networks:
|
||||
- kms-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
491
internal/handlers/test.go
Normal file
@ -0,0 +1,491 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestHandler handles test endpoints for development
|
||||
type TestHandler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTestHandler creates a new test handler
|
||||
func NewTestHandler(logger *zap.Logger) *TestHandler {
|
||||
return &TestHandler{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SSOTestPage serves the SSO manual test page
|
||||
func (h *TestHandler) SSOTestPage(c *gin.Context) {
|
||||
h.logger.Debug("Serving SSO test page")
|
||||
|
||||
html := `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KMS SSO Manual Testing</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; border-bottom: 3px solid #007acc; padding-bottom: 10px; }
|
||||
h2 { color: #007acc; margin-top: 30px; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
|
||||
.btn { display: inline-block; padding: 12px 20px; margin: 10px 5px; background: #007acc; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; }
|
||||
.btn:hover { background: #005a9e; }
|
||||
.btn-secondary { background: #28a745; }
|
||||
.btn-warning { background: #ffc107; color: #333; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 3px; }
|
||||
.status.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.status.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||||
.status.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
.code { background: #f8f9fa; padding: 15px; border-radius: 5px; font-family: monospace; margin: 10px 0; overflow-x: auto; }
|
||||
.endpoint { margin: 10px 0; }
|
||||
.endpoint strong { color: #007acc; }
|
||||
pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0; }
|
||||
.card { padding: 20px; border: 1px solid #ddd; border-radius: 5px; background: white; }
|
||||
.user-info { background: #e7f3ff; padding: 10px; border-radius: 5px; margin: 10px 0; }
|
||||
button { background: #007acc; color: white; border: none; padding: 12px 20px; border-radius: 5px; cursor: pointer; font-weight: bold; margin: 5px; }
|
||||
button:hover { background: #005a9e; }
|
||||
.test-result { margin: 10px 0; padding: 10px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 KMS SSO Manual Testing Suite</h1>
|
||||
<p><strong>Served from KMS API</strong> - No CORS issues!</p>
|
||||
|
||||
<div class="status info">
|
||||
<strong>Environment Status:</strong> Local Development
|
||||
<div id="services-status">Loading service status...</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>🎯 OAuth2/OIDC Testing (Keycloak)</h2>
|
||||
|
||||
<div class="user-info">
|
||||
<strong>Test Users:</strong><br>
|
||||
• admin@example.com / admin123 (Full access)<br>
|
||||
• test@example.com / test123 (Limited access)<br>
|
||||
• limited@example.com / limited123 (Read-only)
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Admin Console:</strong><br>
|
||||
<a href="http://localhost:8090" target="_blank" class="btn">Open Keycloak Admin</a>
|
||||
<small>Login: admin / admin</small>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>OAuth2 Authorization Flow:</strong><br>
|
||||
<a href="http://localhost:8090/realms/kms/protocol/openid-connect/auth?client_id=kms-api&response_type=code&redirect_uri=http://localhost:3000/callback&scope=openid+email+profile&state=test123" target="_blank" class="btn">Test OAuth2 Login</a>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Discovery Document:</strong><br>
|
||||
<button onclick="testOIDCDiscovery()">Test OIDC Discovery</button>
|
||||
<div id="oidc-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📝 SAML Testing (SimpleSAMLphp)</h2>
|
||||
|
||||
<div class="user-info">
|
||||
<strong>Test Users:</strong><br>
|
||||
• user1 / user1pass<br>
|
||||
• user2 / user2pass
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Admin Console:</strong><br>
|
||||
<a href="http://localhost:8091/simplesaml" target="_blank" class="btn">Open SAML Admin</a>
|
||||
<small>Login: admin / secret</small>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>SAML Metadata:</strong><br>
|
||||
<button onclick="testSAMLMetadata()">Test SAML Metadata</button>
|
||||
<div id="saml-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Test Authentication:</strong><br>
|
||||
<a href="http://localhost:8091/simplesaml/module.php/core/authenticate.php?as=example-userpass" target="_blank" class="btn">Test SAML Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🚀 KMS API Testing</h2>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Frontend Application:</strong><br>
|
||||
<a href="http://localhost:3000" target="_blank" class="btn">Open KMS Frontend</a>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>API Health Check:</strong><br>
|
||||
<button onclick="testHealth()">Check API Health</button>
|
||||
<div id="health-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Test API with Different Users:</strong><br>
|
||||
<button onclick="testAPI('admin@example.com')">Test as Admin</button>
|
||||
<button onclick="testAPI('test@example.com')">Test as User</button>
|
||||
<button onclick="testAPI('limited@example.com')">Test as Limited</button>
|
||||
<div id="api-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<strong>Create Test Application:</strong><br>
|
||||
<button onclick="createTestApp()">Create Test App</button>
|
||||
<div id="create-app-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🔍 Permission System Testing</h2>
|
||||
<button onclick="testPermissions()">Test Permission System</button>
|
||||
<div id="permission-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>📊 Current Implementation Status</h2>
|
||||
|
||||
<div class="status success">
|
||||
<strong>✅ Working:</strong><br>
|
||||
• Keycloak OAuth2/OIDC provider with test realm<br>
|
||||
• SimpleSAMLphp SAML IdP with test users<br>
|
||||
• KMS API with header authentication<br>
|
||||
• Hierarchical permission system (25+ permissions)<br>
|
||||
• Application and token management<br>
|
||||
• Database with proper permission structure
|
||||
</div>
|
||||
|
||||
<div class="status error">
|
||||
<strong>❌ Missing:</strong><br>
|
||||
• OAuth2 callback handler in KMS API<br>
|
||||
• SAML assertion processing in KMS API<br>
|
||||
• Frontend SSO login integration<br>
|
||||
• Automatic permission mapping from SSO claims
|
||||
</div>
|
||||
|
||||
<div class="status info">
|
||||
<strong>ℹ️ Next Steps:</strong><br>
|
||||
• Complete OAuth2 callback implementation<br>
|
||||
• Add SAML response handling<br>
|
||||
• Map SSO user attributes to KMS permissions<br>
|
||||
• Add SSO login buttons to frontend
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🛠️ Development Commands</h2>
|
||||
<div class="code">
|
||||
<pre># Start SSO services
|
||||
podman-compose -f docker-compose.yml -f docker-compose.sso.yml up -d
|
||||
|
||||
# Run automated tests
|
||||
./test/quick_sso_test.sh
|
||||
|
||||
# Check service logs
|
||||
podman-compose logs keycloak
|
||||
podman-compose logs saml-idp
|
||||
podman-compose logs api-service
|
||||
|
||||
# Reset to header auth mode
|
||||
podman-compose up -d</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Test OIDC Discovery
|
||||
async function testOIDCDiscovery() {
|
||||
const resultDiv = document.getElementById('oidc-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Testing OIDC discovery...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8090/realms/kms/.well-known/openid-configuration');
|
||||
const data = await response.json();
|
||||
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ OIDC Discovery Successful!</strong><br>
|
||||
<strong>Authorization Endpoint:</strong> ${data.authorization_endpoint}<br>
|
||||
<strong>Token Endpoint:</strong> ${data.token_endpoint}<br>
|
||||
<strong>UserInfo Endpoint:</strong> ${data.userinfo_endpoint}<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Full Response</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ OIDC Discovery Failed</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test SAML Metadata
|
||||
async function testSAMLMetadata() {
|
||||
const resultDiv = document.getElementById('saml-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Testing SAML metadata...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8091/simplesaml/saml2/idp/metadata.php');
|
||||
const text = await response.text();
|
||||
|
||||
if (text.includes('EntityDescriptor')) {
|
||||
const entityId = text.match(/entityID="([^"]*)"/)?.[1] || 'Not found';
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ SAML Metadata Accessible!</strong><br>
|
||||
<strong>Entity ID:</strong> ${entityId}<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Metadata</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${text}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ Invalid SAML Metadata</strong><br>
|
||||
Response does not contain EntityDescriptor
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ SAML Metadata Test Failed</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test API Health
|
||||
async function testHealth() {
|
||||
const resultDiv = document.getElementById('health-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Checking API health...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/health');
|
||||
const text = await response.text();
|
||||
|
||||
if (text === 'healthy') {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ API is Healthy!</strong>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Health Check Failed</strong><br>
|
||||
Response: ${text}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Health Check Error</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test API with different users (simulating SSO result)
|
||||
async function testAPI(userEmail) {
|
||||
const resultDiv = document.getElementById('api-result');
|
||||
resultDiv.innerHTML = ` + "`" + `<div style="color: blue;">Testing API as ${userEmail}...</div>` + "`" + `;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/applications', {
|
||||
headers: {
|
||||
'X-User-Email': userEmail,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ API Test Successful (${userEmail})!</strong><br>
|
||||
Found ${data.count} applications<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Response</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Test Failed (${userEmail})</strong><br>
|
||||
Status: ${response.status} ${response.statusText}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ API Test Error (${userEmail})</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Create test application
|
||||
async function createTestApp() {
|
||||
const resultDiv = document.getElementById('create-app-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Creating test application...</div>';
|
||||
|
||||
const testAppData = {
|
||||
app_id: ` + "`" + `sso-test-${Date.now()}` + "`" + `,
|
||||
app_link: "https://test.example.com",
|
||||
type: ["static"],
|
||||
callback_url: "https://test.example.com/callback",
|
||||
token_prefix: "TEST",
|
||||
token_renewal_duration: 604800000000000,
|
||||
max_token_duration: 2592000000000000,
|
||||
owner: {
|
||||
type: "individual",
|
||||
name: "SSO Test App",
|
||||
owner: "admin@example.com"
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/applications', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-User-Email': 'admin@example.com',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(testAppData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
|
||||
<strong>✅ Application Created Successfully!</strong><br>
|
||||
<strong>App ID:</strong> ${data.app_id}<br>
|
||||
<strong>HMAC Key:</strong> ${data.hmac_key.substring(0, 20)}...<br>
|
||||
<details style="margin-top: 10px;">
|
||||
<summary>View Full Response</summary>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` + "`" + `;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ Application Creation Failed</strong><br>
|
||||
Status: ${response.status} ${response.statusText}<br>
|
||||
${errorText}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = ` + "`" + `
|
||||
<div class="test-result" style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;">
|
||||
<strong>❌ Application Creation Error</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
// Test permission system
|
||||
async function testPermissions() {
|
||||
const resultDiv = document.getElementById('permission-result');
|
||||
resultDiv.innerHTML = '<div style="color: blue;">Testing permission system...</div>';
|
||||
|
||||
// Test different permission levels by trying to access different endpoints
|
||||
const tests = [
|
||||
{ user: 'admin@example.com', description: 'Admin (full access)' },
|
||||
{ user: 'test@example.com', description: 'Test user (limited access)' },
|
||||
{ user: 'limited@example.com', description: 'Limited user (read-only)' }
|
||||
];
|
||||
|
||||
let results = '<div class="test-result" style="background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460;"><strong>Permission Test Results:</strong><br><br>';
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const response = await fetch('/api/applications', {
|
||||
headers: {
|
||||
'X-User-Email': test.user,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
results += ` + "`" + `<span style="color: #28a745;">✅ ${test.description}: Can access applications (${data.count} found)</span><br>` + "`" + `;
|
||||
} else {
|
||||
results += ` + "`" + `<span style="color: #dc3545;">❌ ${test.description}: Access denied (${response.status})</span><br>` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
results += ` + "`" + `<span style="color: #dc3545;">❌ ${test.description}: Error - ${error.message}</span><br>` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
results += '</div>';
|
||||
resultDiv.innerHTML = results;
|
||||
}
|
||||
|
||||
// Check service status on load
|
||||
window.addEventListener('load', async function() {
|
||||
const statusDiv = document.getElementById('services-status');
|
||||
statusDiv.innerHTML = '<br>Checking services...';
|
||||
|
||||
const services = [
|
||||
{ name: 'KMS API', test: () => fetch('/health') },
|
||||
{ name: 'Keycloak', test: () => fetch('http://localhost:8090') },
|
||||
{ name: 'SAML IdP', test: () => fetch('http://localhost:8091/simplesaml') }
|
||||
];
|
||||
|
||||
let statusHtml = '<br>';
|
||||
|
||||
for (const service of services) {
|
||||
try {
|
||||
const response = await service.test();
|
||||
if (response.ok) {
|
||||
statusHtml += ` + "`" + `<span style="color: green;">✅ ${service.name}: Online</span><br>` + "`" + `;
|
||||
} else {
|
||||
statusHtml += ` + "`" + `<span style="color: orange;">⚠️ ${service.name}: Response ${response.status}</span><br>` + "`" + `;
|
||||
}
|
||||
} catch (error) {
|
||||
statusHtml += ` + "`" + `<span style="color: red;">❌ ${service.name}: Not accessible</span><br>` + "`" + `;
|
||||
}
|
||||
}
|
||||
|
||||
statusDiv.innerHTML = statusHtml;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
|
||||
}
|
||||
@ -59,7 +59,10 @@ func Security() gin.HandlerFunc {
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
c.Header("X-XSS-Protection", "1; mode=block")
|
||||
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
c.Header("Content-Security-Policy", "default-src 'self'")
|
||||
|
||||
// Set Content Security Policy - more permissive for test pages in development
|
||||
csp := "default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
c.Header("Content-Security-Policy", csp)
|
||||
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
|
||||
c.Next()
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
@ -167,7 +168,17 @@ func (s *SecurityMiddleware) SecurityHeadersMiddleware(next http.Handler) http.H
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
||||
|
||||
// Set Content Security Policy - more permissive for test pages in development
|
||||
csp := "default-src 'self'"
|
||||
if !s.config.IsProduction() && strings.HasPrefix(r.URL.Path, "/test/") {
|
||||
// Allow inline styles and scripts for test pages in development
|
||||
csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
s.logger.Debug("Using permissive CSP for test page", zap.String("path", r.URL.Path), zap.String("csp", csp))
|
||||
} else {
|
||||
s.logger.Debug("Using default CSP", zap.String("path", r.URL.Path), zap.Bool("is_production", s.config.IsProduction()))
|
||||
}
|
||||
w.Header().Set("Content-Security-Policy", csp)
|
||||
|
||||
// Add HSTS header for HTTPS
|
||||
if r.TLS != nil {
|
||||
@ -178,6 +189,35 @@ func (s *SecurityMiddleware) SecurityHeadersMiddleware(next http.Handler) http.H
|
||||
})
|
||||
}
|
||||
|
||||
// GinSecurityHeaders returns a Gin-compatible middleware function
|
||||
func (s *SecurityMiddleware) GinSecurityHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Add security headers
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
c.Header("X-XSS-Protection", "1; mode=block")
|
||||
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Set Content Security Policy - more permissive for test pages in development
|
||||
csp := "default-src 'self'"
|
||||
if !s.config.IsProduction() && strings.HasPrefix(c.Request.URL.Path, "/test/") {
|
||||
// Allow inline styles and scripts for test pages in development
|
||||
csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
s.logger.Debug("Using permissive CSP for test page", zap.String("path", c.Request.URL.Path), zap.String("csp", csp))
|
||||
} else {
|
||||
s.logger.Debug("Using default CSP", zap.String("path", c.Request.URL.Path), zap.Bool("is_production", s.config.IsProduction()))
|
||||
}
|
||||
c.Header("Content-Security-Policy", csp)
|
||||
|
||||
// Add HSTS header for HTTPS
|
||||
if c.Request.TLS != nil {
|
||||
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticationFailureTracker tracks authentication failures for brute force protection
|
||||
func (s *SecurityMiddleware) TrackAuthenticationFailure(clientIP, userID string) {
|
||||
ctx := context.Background()
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |