Merge branch 'main' into feature/526-average-resample

This commit is contained in:
2026-06-18 07:17:41 +02:00
34 changed files with 423 additions and 226 deletions

View File

@@ -17,7 +17,7 @@ var (
)
func cliInit() {
flag.BoolVar(&flagInit, "init", false, "Setup var directory, initialize sqlite database file, config.json and .env")
flag.BoolVar(&flagInit, "init", false, "Setup var directory, initialize sqlite database file and config.json")
flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize the 'job', 'tag', and 'jobtag' tables (all running jobs will be lost!)")
flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the 'hpc_user' table with ldap")
flag.BoolVar(&flagServer, "server", false, "Start a server, continues listening on port after initialization and argument handling")

View File

@@ -18,16 +18,6 @@ import (
"github.com/ClusterCockpit/cc-lib/v2/util"
)
const envString = `
# Base64 encoded Ed25519 keys (DO NOT USE THESE TWO IN PRODUCTION!)
# You can generate your own keypair using the gen-keypair tool
JWT_PUBLIC_KEY="kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
JWT_PRIVATE_KEY="dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
# Some random bytes used as secret for cookie-based sessions (DO NOT USE THIS ONE IN PRODUCTION)
SESSION_KEY="67d829bf61dc5f87a73fd814e2c9f629"
`
const configString = `
{
"main": {
@@ -53,7 +43,9 @@ const configString = `
},
"auth": {
"jwts": {
"max-age": "2000h"
"max-age": "2000h",
"public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0=",
"private-key": "dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
}
}
}
@@ -68,10 +60,6 @@ func initEnv() {
cclog.Abortf("Could not write default ./config.json with permissions '0o666'. Application initialization failed, exited.\nError: %s\n", err.Error())
}
if err := os.WriteFile(".env", []byte(envString), 0o666); err != nil {
cclog.Abortf("Could not write default ./.env file with permissions '0o666'. Application initialization failed, exited.\nError: %s\n", err.Error())
}
if err := os.Mkdir("var", 0o777); err != nil {
cclog.Abortf("Could not create default ./var folder with permissions '0o777'. Application initialization failed, exited.\nError: %s\n", err.Error())
}

View File

@@ -39,7 +39,6 @@ import (
"github.com/ClusterCockpit/cc-lib/v2/schema"
"github.com/ClusterCockpit/cc-lib/v2/util"
"github.com/google/gops/agent"
"github.com/joho/godotenv"
_ "github.com/mattn/go-sqlite3"
)
@@ -89,13 +88,6 @@ func initGops() error {
return nil
}
func loadEnvironment() error {
if err := godotenv.Load(); err != nil {
return fmt.Errorf("loading .env file: %w", err)
}
return nil
}
func initConfiguration() error {
ccconf.Init(flagConfigFile)
@@ -224,7 +216,14 @@ func checkDefaultSecurityKeys() {
// Default JWT public key from init.go
defaultJWTPublic := "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
if os.Getenv("JWT_PUBLIC_KEY") == defaultJWTPublic {
// Resolve the public key the same way the authenticators do: environment
// variable takes precedence over the value configured in config.json.
pubKey := os.Getenv("JWT_PUBLIC_KEY")
if pubKey == "" && auth.Keys.JwtConfig != nil {
pubKey = auth.Keys.JwtConfig.PublicKey
}
if pubKey == defaultJWTPublic {
cclog.Warn("Using default JWT keys - not recommended for production environments")
}
}
@@ -495,7 +494,7 @@ func run() error {
if flagInit {
initEnv()
cclog.Exit("Successfully setup environment!\n" +
"Please review config.json and .env and adjust it to your needs.\n" +
"Please review config.json and adjust it to your needs.\n" +
"Add your job-archive at ./var/job-archive.")
}
@@ -505,17 +504,12 @@ func run() error {
}
// Initialize subsystems in dependency order:
// 1. Load environment variables from .env file (contains sensitive configuration)
// 2. Load configuration from config.json (may reference environment variables)
// 3. Handle database migration commands if requested
// 4. Initialize database connection (requires config for connection string)
// 5. Handle user commands if requested (requires database and authentication config)
// 6. Initialize subsystems like archive and metrics (require config and database)
// Load environment and configuration
if err := loadEnvironment(); err != nil {
return err
}
// 1. Load configuration from config.json (secrets live in config; individual
// secrets may be overridden via environment variables)
// 2. Handle database migration commands if requested
// 3. Initialize database connection (requires config for connection string)
// 4. Handle user commands if requested (requires database and authentication config)
// 5. Initialize subsystems like archive and metrics (require config and database)
if err := initConfiguration(); err != nil {
return err

View File

@@ -121,6 +121,7 @@ func (s *Server) init() error {
}
authHandle := auth.GetAuthInstance()
sessionManager := authHandle.SessionManager()
// Middleware must be defined before routes in chi
s.router.Use(func(next http.Handler) http.Handler {
@@ -220,10 +221,12 @@ func (s *Server) init() error {
})
}
s.router.Post("/login", authHandle.Login(loginFailureHandler).ServeHTTP)
s.router.HandleFunc("/jwt-login", authHandle.Login(loginFailureHandler).ServeHTTP)
// Login/logout mutate the session, so they are wrapped with
// scs.LoadAndSave, which commits the session and writes the cookie.
s.router.Post("/login", sessionManager.LoadAndSave(authHandle.Login(loginFailureHandler)).ServeHTTP)
s.router.Handle("/jwt-login", sessionManager.LoadAndSave(authHandle.Login(loginFailureHandler)))
s.router.Post("/logout", authHandle.Logout(
s.router.Post("/logout", sessionManager.LoadAndSave(authHandle.Logout(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
@@ -234,7 +237,7 @@ func (s *Server) init() error {
Build: buildInfo,
Infos: info,
})
})).ServeHTTP)
}))).ServeHTTP)
}
if flagDev {
@@ -246,6 +249,10 @@ func (s *Server) init() error {
// Secured routes (require authentication)
s.router.Group(func(secured chi.Router) {
if !config.Keys.DisableAuthentication {
// Non-buffering session load: makes the session available to
// AuthViaSession without wrapping/buffering the (potentially large,
// e.g. GraphQL /query) response.
secured.Use(authHandle.LoadSession)
secured.Use(func(next http.Handler) http.Handler {
return authHandle.Auth(
next,
@@ -309,6 +316,7 @@ func (s *Server) init() error {
// the /config page route that is registered in the secured group)
s.router.Group(func(configapi chi.Router) {
if !config.Keys.DisableAuthentication {
configapi.Use(authHandle.LoadSession)
configapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthConfigAPI(next, onFailureResponse)
})
@@ -319,6 +327,7 @@ func (s *Server) init() error {
// Frontend API routes
s.router.Route("/frontend", func(frontendapi chi.Router) {
if !config.Keys.DisableAuthentication {
frontendapi.Use(authHandle.LoadSession)
frontendapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthFrontendAPI(next, onFailureResponse)
})