Refactor and cleanup Auth configuration

This commit is contained in:
Jan Eitzinger 2023-08-18 10:43:06 +02:00
parent 14c487c9e4
commit 32b0c8bdd7
8 changed files with 62 additions and 71 deletions

View File

@ -211,10 +211,7 @@ func main() {
var authentication *auth.Authentication var authentication *auth.Authentication
if !config.Keys.DisableAuthentication { if !config.Keys.DisableAuthentication {
var err error var err error
if authentication, err = auth.Init(map[string]interface{}{ if authentication, err = auth.Init(); err != nil {
"ldap": config.Keys.LdapConfig,
"jwt": config.Keys.JwtConfig,
}); err != nil {
log.Fatalf("auth initialization failed: %v", err) log.Fatalf("auth initialization failed: %v", err)
} }

View File

@ -14,6 +14,7 @@ import (
"os" "os"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -21,7 +22,6 @@ import (
) )
type Authenticator interface { type Authenticator interface {
Init(config interface{}) error
CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool) CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)
Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error) Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
} }
@ -80,7 +80,7 @@ func (auth *Authentication) AuthViaSession(
}, nil }, nil
} }
func Init(configs map[string]interface{}) (*Authentication, error) { func Init() (*Authentication, error) {
auth := &Authentication{} auth := &Authentication{}
sessKey := os.Getenv("SESSION_KEY") sessKey := os.Getenv("SESSION_KEY")
@ -101,9 +101,9 @@ func Init(configs map[string]interface{}) (*Authentication, error) {
auth.sessionStore = sessions.NewCookieStore(bytes) auth.sessionStore = sessions.NewCookieStore(bytes)
} }
if config, ok := configs["ldap"]; ok { if config.Keys.LdapConfig != nil {
ldapAuth := &LdapAuthenticator{} ldapAuth := &LdapAuthenticator{}
if err := ldapAuth.Init(config); err != nil { if err := ldapAuth.Init(); err != nil {
log.Warn("Error while initializing authentication -> ldapAuth init failed") log.Warn("Error while initializing authentication -> ldapAuth init failed")
} else { } else {
auth.LdapAuth = ldapAuth auth.LdapAuth = ldapAuth
@ -113,32 +113,32 @@ func Init(configs map[string]interface{}) (*Authentication, error) {
log.Info("Missing LDAP configuration: No LDAP support!") log.Info("Missing LDAP configuration: No LDAP support!")
} }
if config, ok := configs["jwt"]; ok { if config.Keys.JwtConfig != nil {
auth.JwtAuth = &JWTAuthenticator{} auth.JwtAuth = &JWTAuthenticator{}
if err := auth.JwtAuth.Init(config); err != nil { if err := auth.JwtAuth.Init(); err != nil {
log.Error("Error while initializing authentication -> jwtAuth init failed") log.Error("Error while initializing authentication -> jwtAuth init failed")
return nil, err return nil, err
} }
jwtSessionAuth := &JWTSessionAuthenticator{} jwtSessionAuth := &JWTSessionAuthenticator{}
if err := jwtSessionAuth.Init(config); err != nil { if err := jwtSessionAuth.Init(); err != nil {
log.Warn("Error while initializing authentication -> jwtSessionAuth init failed") log.Info("jwtSessionAuth init failed: No JWT login support!")
} else { } else {
auth.authenticators = append(auth.authenticators, jwtSessionAuth) auth.authenticators = append(auth.authenticators, jwtSessionAuth)
} }
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{} jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
if err := jwtCookieSessionAuth.Init(configs["jwt"]); err != nil { if err := jwtCookieSessionAuth.Init(); err != nil {
log.Warn("Error while initializing authentication -> jwtCookieSessionAuth init failed") log.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
} else { } else {
auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth) auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth)
} }
} else { } else {
log.Info("Missing JWT configuration: No JWT token login support!") log.Info("Missing JWT configuration: No JWT token support!")
} }
auth.LocalAuth = &LocalAuthenticator{} auth.LocalAuth = &LocalAuthenticator{}
if err := auth.LocalAuth.Init(nil); err != nil { if err := auth.LocalAuth.Init(); err != nil {
log.Error("Error while initializing authentication -> localAuth init failed") log.Error("Error while initializing authentication -> localAuth init failed")
return nil, err return nil, err
} }

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -22,12 +23,9 @@ import (
type JWTAuthenticator struct { type JWTAuthenticator struct {
publicKey ed25519.PublicKey publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
config *schema.JWTAuthConfig
} }
func (ja *JWTAuthenticator) Init(conf interface{}) error { func (ja *JWTAuthenticator) Init() error {
ja.config = conf.(*schema.JWTAuthConfig)
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY") pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
if pubKey == "" || privKey == "" { if pubKey == "" || privKey == "" {
log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)") log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
@ -87,7 +85,7 @@ func (ja *JWTAuthenticator) AuthViaJWT(
var roles []string var roles []string
// Validate user + roles from JWT against database? // Validate user + roles from JWT against database?
if ja.config != nil && ja.config.ValidateUser { if config.Keys.JwtConfig.ValidateUser {
ur := repository.GetUserRepository() ur := repository.GetUserRepository()
user, err := ur.GetUser(sub) user, err := ur.GetUser(sub)
@ -130,8 +128,8 @@ func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
"roles": user.Roles, "roles": user.Roles,
"iat": now.Unix(), "iat": now.Unix(),
} }
if ja.config != nil && ja.config.MaxAge != "" { if config.Keys.JwtConfig.MaxAge != "" {
d, err := time.ParseDuration(ja.config.MaxAge) d, err := time.ParseDuration(config.Keys.JwtConfig.MaxAge)
if err != nil { if err != nil {
return "", errors.New("cannot parse max-age config key") return "", errors.New("cannot parse max-age config key")
} }

View File

@ -12,6 +12,7 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -22,15 +23,11 @@ type JWTCookieSessionAuthenticator struct {
publicKey ed25519.PublicKey publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
config *schema.JWTAuthConfig
} }
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil) var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
func (ja *JWTCookieSessionAuthenticator) Init(conf interface{}) error { func (ja *JWTCookieSessionAuthenticator) Init() error {
ja.config = conf.(*schema.JWTAuthConfig)
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY") pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
if pubKey == "" || privKey == "" { if pubKey == "" || privKey == "" {
log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)") log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
@ -65,17 +62,18 @@ func (ja *JWTCookieSessionAuthenticator) Init(conf interface{}) error {
return errors.New("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)") return errors.New("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
} }
jc := config.Keys.JwtConfig
// Warn if other necessary settings are not configured // Warn if other necessary settings are not configured
if ja.config != nil { if jc != nil {
if ja.config.CookieName == "" { if jc.CookieName == "" {
log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)") log.Info("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)") return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
} }
if !ja.config.ValidateUser { if !jc.ValidateUser {
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!") log.Info("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
} }
if ja.config.TrustedIssuer == "" { if jc.TrustedIssuer == "" {
log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)") log.Info("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)") return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
} }
} else { } else {
@ -93,9 +91,10 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*schema.User, bool) { r *http.Request) (*schema.User, bool) {
jc := config.Keys.JwtConfig
cookieName := "" cookieName := ""
if ja.config != nil && ja.config.CookieName != "" { if jc.CookieName != "" {
cookieName = ja.config.CookieName cookieName = jc.CookieName
} }
// Try to read the JWT cookie // Try to read the JWT cookie
@ -115,7 +114,8 @@ func (ja *JWTCookieSessionAuthenticator) Login(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*schema.User, error) { r *http.Request) (*schema.User, error) {
jwtCookie, err := r.Cookie(ja.config.CookieName) jc := config.Keys.JwtConfig
jwtCookie, err := r.Cookie(jc.CookieName)
var rawtoken string var rawtoken string
if err == nil && jwtCookie.Value != "" { if err == nil && jwtCookie.Value != "" {
@ -128,7 +128,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
} }
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string) unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
if success && unvalidatedIssuer == ja.config.TrustedIssuer { if success && unvalidatedIssuer == jc.TrustedIssuer {
// The (unvalidated) issuer seems to be the expected one, // The (unvalidated) issuer seems to be the expected one,
// use public cross login key from config // use public cross login key from config
return ja.publicKeyCrossLogin, nil return ja.publicKeyCrossLogin, nil
@ -167,7 +167,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
var roles []string var roles []string
if ja.config.ValidateUser { if jc.ValidateUser {
// Deny any logins for unknown usernames // Deny any logins for unknown usernames
if user == nil { if user == nil {
log.Warn("Could not find user from JWT in internal database.") log.Warn("Could not find user from JWT in internal database.")
@ -189,7 +189,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
// (Ask browser to) Delete JWT cookie // (Ask browser to) Delete JWT cookie
deletedCookie := &http.Cookie{ deletedCookie := &http.Cookie{
Name: ja.config.CookieName, Name: jc.CookieName,
Value: "", Value: "",
Path: "/", Path: "/",
MaxAge: -1, MaxAge: -1,
@ -208,7 +208,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
AuthSource: schema.AuthViaToken, AuthSource: schema.AuthViaToken,
} }
if ja.config.SyncUserOnLogin { if jc.SyncUserOnLogin {
if err := repository.GetUserRepository().AddUser(user); err != nil { if err := repository.GetUserRepository().AddUser(user); err != nil {
log.Errorf("Error while adding user '%s' to DB", user.Username) log.Errorf("Error while adding user '%s' to DB", user.Username)
} }

View File

@ -12,6 +12,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -20,15 +21,11 @@ import (
type JWTSessionAuthenticator struct { type JWTSessionAuthenticator struct {
loginTokenKey []byte // HS256 key loginTokenKey []byte // HS256 key
config *schema.JWTAuthConfig
} }
var _ Authenticator = (*JWTSessionAuthenticator)(nil) var _ Authenticator = (*JWTSessionAuthenticator)(nil)
func (ja *JWTSessionAuthenticator) Init(conf interface{}) error { func (ja *JWTSessionAuthenticator) Init() error {
ja.config = conf.(*schema.JWTAuthConfig)
if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" { if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKey) bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil { if err != nil {
@ -96,7 +93,7 @@ func (ja *JWTSessionAuthenticator) Login(
var roles []string var roles []string
if ja.config.ValidateUser { if config.Keys.JwtConfig.ValidateUser {
// Deny any logins for unknown usernames // Deny any logins for unknown usernames
if user == nil { if user == nil {
log.Warn("Could not find user from JWT in internal database.") log.Warn("Could not find user from JWT in internal database.")
@ -142,7 +139,7 @@ func (ja *JWTSessionAuthenticator) Login(
AuthSource: schema.AuthViaToken, AuthSource: schema.AuthViaToken,
} }
if ja.config.SyncUserOnLogin { if config.Keys.JwtConfig.SyncUserOnLogin {
if err := repository.GetUserRepository().AddUser(user); err != nil { if err := repository.GetUserRepository().AddUser(user); err != nil {
log.Errorf("Error while adding user '%s' to DB", user.Username) log.Errorf("Error while adding user '%s' to DB", user.Username)
} }

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -19,25 +20,22 @@ import (
) )
type LdapAuthenticator struct { type LdapAuthenticator struct {
config *schema.LdapConfig
syncPassword string syncPassword string
} }
var _ Authenticator = (*LdapAuthenticator)(nil) var _ Authenticator = (*LdapAuthenticator)(nil)
func (la *LdapAuthenticator) Init(conf interface{}) error { func (la *LdapAuthenticator) Init() error {
la.config = conf.(*schema.LdapConfig)
la.syncPassword = os.Getenv("LDAP_ADMIN_PASSWORD") la.syncPassword = os.Getenv("LDAP_ADMIN_PASSWORD")
if la.syncPassword == "" { if la.syncPassword == "" {
log.Warn("environment variable 'LDAP_ADMIN_PASSWORD' not set (ldap sync will not work)") log.Warn("environment variable 'LDAP_ADMIN_PASSWORD' not set (ldap sync will not work)")
} }
if la.config != nil && la.config.SyncInterval != "" { if config.Keys.LdapConfig.SyncInterval != "" {
interval, err := time.ParseDuration(la.config.SyncInterval) interval, err := time.ParseDuration(config.Keys.LdapConfig.SyncInterval)
if err != nil { if err != nil {
log.Warnf("Could not parse duration for sync interval: %v", la.config.SyncInterval) log.Warnf("Could not parse duration for sync interval: %v",
config.Keys.LdapConfig.SyncInterval)
return err return err
} }
@ -57,7 +55,7 @@ func (la *LdapAuthenticator) Init(conf interface{}) error {
} }
}() }()
} else { } else {
log.Info("Missing LDAP configuration key sync_interval") log.Info("LDAP configuration key sync_interval invalid")
} }
return nil return nil
@ -69,12 +67,14 @@ func (la *LdapAuthenticator) CanLogin(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*schema.User, bool) { r *http.Request) (*schema.User, bool) {
lc := config.Keys.LdapConfig
if user != nil { if user != nil {
if user.AuthSource == schema.AuthViaLDAP { if user.AuthSource == schema.AuthViaLDAP {
return user, true return user, true
} }
} else { } else {
if la.config.SyncUserOnLogin { if lc.SyncUserOnLogin {
l, err := la.getLdapConnection(true) l, err := la.getLdapConnection(true)
if err != nil { if err != nil {
log.Error("LDAP connection error") log.Error("LDAP connection error")
@ -83,9 +83,9 @@ func (la *LdapAuthenticator) CanLogin(
// Search for the given username // Search for the given username
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
la.config.UserBase, lc.UserBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&%s(uid=%s))", la.config.UserFilter, username), fmt.Sprintf("(&%s(uid=%s))", lc.UserFilter, username),
[]string{"dn", "uid", "gecos"}, nil) []string{"dn", "uid", "gecos"}, nil)
sr, err := l.Search(searchRequest) sr, err := l.Search(searchRequest)
@ -138,7 +138,7 @@ func (la *LdapAuthenticator) Login(
} }
defer l.Close() defer l.Close()
userDn := strings.Replace(la.config.UserBind, "{username}", user.Username, -1) userDn := strings.Replace(config.Keys.LdapConfig.UserBind, "{username}", user.Username, -1)
if err := l.Bind(userDn, r.FormValue("password")); err != nil { if err := l.Bind(userDn, r.FormValue("password")); err != nil {
log.Errorf("AUTH/LOCAL > Authentication for user %s failed: %v", log.Errorf("AUTH/LOCAL > Authentication for user %s failed: %v",
user.Username, err) user.Username, err)
@ -153,6 +153,7 @@ func (la *LdapAuthenticator) Sync() error {
const IN_LDAP int = 2 const IN_LDAP int = 2
const IN_BOTH int = 3 const IN_BOTH int = 3
ur := repository.GetUserRepository() ur := repository.GetUserRepository()
lc := config.Keys.LdapConfig
users := map[string]int{} users := map[string]int{}
usernames, err := ur.GetLdapUsernames() usernames, err := ur.GetLdapUsernames()
@ -172,9 +173,9 @@ func (la *LdapAuthenticator) Sync() error {
defer l.Close() defer l.Close()
ldapResults, err := l.Search(ldap.NewSearchRequest( ldapResults, err := l.Search(ldap.NewSearchRequest(
la.config.UserBase, lc.UserBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
la.config.UserFilter, lc.UserFilter,
[]string{"dn", "uid", "gecos"}, nil)) []string{"dn", "uid", "gecos"}, nil))
if err != nil { if err != nil {
log.Warn("LDAP search error") log.Warn("LDAP search error")
@ -198,7 +199,7 @@ func (la *LdapAuthenticator) Sync() error {
} }
for username, where := range users { for username, where := range users {
if where == IN_DB && la.config.SyncDelOldUsers { if where == IN_DB && lc.SyncDelOldUsers {
ur.DelUser(username) ur.DelUser(username)
log.Debugf("sync: remove %v (does not show up in LDAP anymore)", username) log.Debugf("sync: remove %v (does not show up in LDAP anymore)", username)
} else if where == IN_LDAP { } else if where == IN_LDAP {
@ -231,14 +232,15 @@ func (la *LdapAuthenticator) Sync() error {
// that so that connections can be reused/cached. // that so that connections can be reused/cached.
func (la *LdapAuthenticator) getLdapConnection(admin bool) (*ldap.Conn, error) { func (la *LdapAuthenticator) getLdapConnection(admin bool) (*ldap.Conn, error) {
conn, err := ldap.DialURL(la.config.Url) lc := config.Keys.LdapConfig
conn, err := ldap.DialURL(lc.Url)
if err != nil { if err != nil {
log.Warn("LDAP URL dial failed") log.Warn("LDAP URL dial failed")
return nil, err return nil, err
} }
if admin { if admin {
if err := conn.Bind(la.config.SearchDN, la.syncPassword); err != nil { if err := conn.Bind(lc.SearchDN, la.syncPassword); err != nil {
conn.Close() conn.Close()
log.Warn("LDAP connection bind failed") log.Warn("LDAP connection bind failed")
return nil, err return nil, err

View File

@ -19,9 +19,7 @@ type LocalAuthenticator struct {
var _ Authenticator = (*LocalAuthenticator)(nil) var _ Authenticator = (*LocalAuthenticator)(nil)
func (la *LocalAuthenticator) Init( func (la *LocalAuthenticator) Init() error {
_ interface{}) error {
return nil return nil
} }

View File

@ -22,7 +22,6 @@ var Keys schema.ProgramConfig = schema.ProgramConfig{
Archive: json.RawMessage(`{\"kind\":\"file\",\"path\":\"./var/job-archive\"}`), Archive: json.RawMessage(`{\"kind\":\"file\",\"path\":\"./var/job-archive\"}`),
DisableArchive: false, DisableArchive: false,
Validate: false, Validate: false,
LdapConfig: nil,
SessionMaxAge: "168h", SessionMaxAge: "168h",
StopJobsExceedingWalltime: 0, StopJobsExceedingWalltime: 0,
ShortRunningJobsDuration: 5 * 60, ShortRunningJobsDuration: 5 * 60,