diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 127d4a8..d66a27f 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,11 +1,16 @@ -# `cc-backend` version 1.1.0 +# `cc-backend` version 1.2.0 Supports job archive version 1 and database version 6. This is a minor release of `cc-backend`, the API backend and frontend implementation of ClusterCockpit. -** Breaking changes v1 ** +** Breaking changes ** + +The LDAP configuration option user_filter was changed and now should not include +the wildcard. Example: +* Old: `"user_filter": "(&(objectclass=posixAccount)(uid=*))"` +* New: `"user_filter": "&(objectclass=posixAccount)"` The aggregate job statistic core hours is now computed using the job table column `num_hwthreads`. In a future release this column will be renamed to diff --git a/docs/dev-authentication.md b/docs/dev-authentication.md index d40bdb0..9b84c4b 100644 --- a/docs/dev-authentication.md +++ b/docs/dev-authentication.md @@ -124,42 +124,8 @@ It is first checked if the required configuration keys are set: ``` The Login function: - -# Auth - -The Auth function (located in `auth.go`): -* Returns a new http handler function that is defined right away -* This handler iterates over all authenticators -* Calls `Auth()` on every authenticator -* If err is not nil and the user object is valid it puts the user object in the - request context and starts the onSuccess http handler -* Otherwise it calls the onFailure handler - -## Local - -Calls the `AuthViaSession()` function in `auth.go`. This will extract username, -projects and roles from the session and initialize a user object with those -values. - -## LDAP - -Calls the `AuthViaSession()` function in `auth.go`. This will extract username, -projects and roles from the session and initialize a user object with those -values. - -# JWT - -Check for JWT token: -* Is token passed in the `X-Auth-Token` or `Authorization` header -* If no token is found in a header it tries to read the token from a configured -cookie. - -Finally it calls AuthViaSession in `auth.go` if a valid session exists. This is -true if a JWT token was previously used to initiate a session. In this case the -user object initialized with the session is returned right away. - -In case a token was found extract and parse the token: -* Check if signing method is Ed25519/EdDSA +* Extracts and parses the token +* Checks if signing method is Ed25519/EdDSA * In case publicKeyCrossLogin is configured: - Check if `iss` issuer claim matched trusted issuer from configuration - Return public cross login key @@ -167,7 +133,34 @@ In case a token was found extract and parse the token: * Check if claims are valid * Depending on the option `ForceJWTValidationViaDatabase ` the roles are extracted from JWT token or taken from user object fetched from database -* In case the token was extracted from cookie create a new session and ask the - browser to delete the JWT cookie +* Ask browser to delete the JWT cookie * Return valid user object +# Auth + +The Auth function (located in `auth.go`): +* Returns a new http handler function that is defined right away +* This handler tries two methods to authenticate a user: + - Via a JWT API token in `AuthViaJWT()` + - Via a valid session in `AuthViaSession()` +* If err is not nil and the user object is valid it puts the user object in the + request context and starts the onSuccess http handler +* Otherwise it calls the onFailure handler + +## AuthViaJWT + +Implemented in JWTAuthenticator: +* Extract token either from header `X-Auth-Token` or `Authorization` with Bearer + prefix +* Parse token and check if it is valid. The Parse routine will also check if the + token is expired. +* If the option `ForceJWTValidationViaDatabase` is set it will ensure the + user object exists in the database and takes the roles from the database user +* Otherwise the roles are extracted from the roles claim +* Returns a valid user object with AuthType set to AuthToken + +## AuthViaSession + +* Extracts session +* Get values username, projects, and roles from session +* Returns a valid user object with AuthType set to AuthSession diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 3d40500..8149bc1 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -43,7 +43,6 @@ type User struct { AuthSource AuthSource `json:"authSource"` Email string `json:"email"` Projects []string `json:"projects"` - Expiration time.Time } func (u *User) HasProject(project string) bool { @@ -66,7 +65,7 @@ func GetUser(ctx context.Context) *User { type Authenticator interface { Init(auth *Authentication, config interface{}) error - CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool + CanLogin(user *User, username string, rw http.ResponseWriter, r *http.Request) bool Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error) } @@ -208,7 +207,7 @@ func (auth *Authentication) Login( } for _, authenticator := range auth.authenticators { - if !authenticator.CanLogin(dbUser, rw, r) { + if !authenticator.CanLogin(dbUser, username, rw, r) { continue } diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 0ac446e..6a77fc4 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -86,11 +86,6 @@ func (ja *JWTAuthenticator) AuthViaJWT( // Token is valid, extract payload claims := token.Claims.(jwt.MapClaims) sub, _ := claims["sub"].(string) - exp, _ := claims["exp"].(float64) - - if exp < float64(time.Now().Unix()) { - return nil, errors.New("token is expired") - } var roles []string diff --git a/internal/auth/jwtCookieSession.go b/internal/auth/jwtCookieSession.go index af7fb64..8f31335 100644 --- a/internal/auth/jwtCookieSession.go +++ b/internal/auth/jwtCookieSession.go @@ -10,7 +10,6 @@ import ( "errors" "net/http" "os" - "time" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/schema" @@ -91,6 +90,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa func (ja *JWTCookieSessionAuthenticator) CanLogin( user *User, + username string, rw http.ResponseWriter, r *http.Request) bool { @@ -140,7 +140,7 @@ func (ja *JWTCookieSessionAuthenticator) Login( return ja.publicKey, nil }) if err != nil { - log.Warn("Error while parsing token") + log.Warn("error while parsing token") return nil, err } @@ -152,7 +152,6 @@ func (ja *JWTCookieSessionAuthenticator) Login( claims := token.Claims.(jwt.MapClaims) sub, _ := claims["sub"].(string) - exp, _ := claims["exp"].(float64) var name string if val, ok := claims["name"]; ok { @@ -201,6 +200,5 @@ func (ja *JWTCookieSessionAuthenticator) Login( } } - user.Expiration = time.Unix(int64(exp), 0) return user, nil } diff --git a/internal/auth/jwtSession.go b/internal/auth/jwtSession.go index 90725d4..13af7c1 100644 --- a/internal/auth/jwtSession.go +++ b/internal/auth/jwtSession.go @@ -10,7 +10,6 @@ import ( "net/http" "os" "strings" - "time" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/golang-jwt/jwt/v4" @@ -42,6 +41,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) func (ja *JWTSessionAuthenticator) CanLogin( user *User, + username string, rw http.ResponseWriter, r *http.Request) bool { @@ -76,7 +76,6 @@ func (ja *JWTSessionAuthenticator) Login( claims := token.Claims.(jwt.MapClaims) sub, _ := claims["sub"].(string) - exp, _ := claims["exp"].(float64) var name string // Java/Grails Issued Token @@ -131,6 +130,5 @@ func (ja *JWTSessionAuthenticator) Login( } } - user.Expiration = time.Unix(int64(exp), 0) return user, nil } diff --git a/internal/auth/ldap.go b/internal/auth/ldap.go index fc9753d..17b5c0c 100644 --- a/internal/auth/ldap.go +++ b/internal/auth/ldap.go @@ -66,10 +66,51 @@ func (la *LdapAuthenticator) Init( func (la *LdapAuthenticator) CanLogin( user *User, + username string, rw http.ResponseWriter, r *http.Request) bool { - return user != nil && user.AuthSource == AuthViaLDAP + if user != nil && user.AuthSource == AuthViaLDAP { + return true + } else { + if la.config.SyncUserOnLogin { + l, err := la.getLdapConnection(true) + if err != nil { + log.Error("LDAP connection error") + } + + // Search for the given username + searchRequest := ldap.NewSearchRequest( + la.config.UserBase, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(%s(uid=%s))", la.config.UserFilter, username), + []string{"dn", "uid", "gecos"}, nil) + + sr, err := l.Search(searchRequest) + if err != nil { + log.Warn(err) + return false + } + + if len(sr.Entries) != 1 { + log.Warn("User does not exist or too many entries returned") + return false + } + + entry := sr.Entries[0] + name := entry.GetAttributeValue("gecos") + + if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`, + username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil { + log.Errorf("User '%s' new in LDAP: Insert into DB failed", username) + return false + } + + return true + } + } + + return false } func (la *LdapAuthenticator) Login( @@ -124,8 +165,10 @@ func (la *LdapAuthenticator) Sync() error { defer l.Close() ldapResults, err := l.Search(ldap.NewSearchRequest( - la.config.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - la.config.UserFilter, []string{"dn", "uid", "gecos"}, nil)) + la.config.UserBase, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(%s(uid=%s))", la.config.UserFilter, "*"), + []string{"dn", "uid", "gecos"}, nil)) if err != nil { log.Warn("LDAP search error") return err diff --git a/internal/auth/local.go b/internal/auth/local.go index dd41a25..700db3a 100644 --- a/internal/auth/local.go +++ b/internal/auth/local.go @@ -28,6 +28,7 @@ func (la *LocalAuthenticator) Init( func (la *LocalAuthenticator) CanLogin( user *User, + username string, rw http.ResponseWriter, r *http.Request) bool { diff --git a/pkg/schema/config.go b/pkg/schema/config.go index 190ee03..2a4047c 100644 --- a/pkg/schema/config.go +++ b/pkg/schema/config.go @@ -17,6 +17,7 @@ type LdapConfig struct { UserFilter string `json:"user_filter"` SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration. SyncDelOldUsers bool `json:"sync_del_old_users"` + SyncUserOnLogin bool `json:"syncUserOnLogin"` } type JWTAuthConfig struct {