344 lines
11 KiB
Go
344 lines
11 KiB
Go
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
|
|
}
|