mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29:05 +01:00
Merge branch '189-refactor-authentication-module' of https://github.com/ClusterCockpit/cc-backend into 189-refactor-authentication-module
This commit is contained in:
commit
fe6de5bc68
@ -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.
|
Supports job archive version 1 and database version 6.
|
||||||
|
|
||||||
This is a minor release of `cc-backend`, the API backend and frontend
|
This is a minor release of `cc-backend`, the API backend and frontend
|
||||||
implementation of ClusterCockpit.
|
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
|
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
|
column `num_hwthreads`. In a future release this column will be renamed to
|
||||||
|
@ -124,42 +124,8 @@ It is first checked if the required configuration keys are set:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The Login function:
|
The Login function:
|
||||||
|
* Extracts and parses the token
|
||||||
# Auth
|
* Checks if signing method is Ed25519/EdDSA
|
||||||
|
|
||||||
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
|
|
||||||
* In case publicKeyCrossLogin is configured:
|
* In case publicKeyCrossLogin is configured:
|
||||||
- Check if `iss` issuer claim matched trusted issuer from configuration
|
- Check if `iss` issuer claim matched trusted issuer from configuration
|
||||||
- Return public cross login key
|
- 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
|
* Check if claims are valid
|
||||||
* Depending on the option `ForceJWTValidationViaDatabase ` the roles are
|
* Depending on the option `ForceJWTValidationViaDatabase ` the roles are
|
||||||
extracted from JWT token or taken from user object fetched from database
|
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
|
* Ask browser to delete the JWT cookie
|
||||||
browser to delete the JWT cookie
|
|
||||||
* Return valid user object
|
* 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
|
||||||
|
@ -43,7 +43,6 @@ type User struct {
|
|||||||
AuthSource AuthSource `json:"authSource"`
|
AuthSource AuthSource `json:"authSource"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Projects []string `json:"projects"`
|
Projects []string `json:"projects"`
|
||||||
Expiration time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) HasProject(project string) bool {
|
func (u *User) HasProject(project string) bool {
|
||||||
@ -66,7 +65,7 @@ func GetUser(ctx context.Context) *User {
|
|||||||
|
|
||||||
type Authenticator interface {
|
type Authenticator interface {
|
||||||
Init(auth *Authentication, config interface{}) error
|
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)
|
Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +207,7 @@ func (auth *Authentication) Login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, authenticator := range auth.authenticators {
|
for _, authenticator := range auth.authenticators {
|
||||||
if !authenticator.CanLogin(dbUser, rw, r) {
|
if !authenticator.CanLogin(dbUser, username, rw, r) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,11 +86,6 @@ func (ja *JWTAuthenticator) AuthViaJWT(
|
|||||||
// Token is valid, extract payload
|
// Token is valid, extract payload
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
sub, _ := claims["sub"].(string)
|
sub, _ := claims["sub"].(string)
|
||||||
exp, _ := claims["exp"].(float64)
|
|
||||||
|
|
||||||
if exp < float64(time.Now().Unix()) {
|
|
||||||
return nil, errors.New("token is expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
var roles []string
|
var roles []string
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
||||||
@ -91,6 +90,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
|
|||||||
|
|
||||||
func (ja *JWTCookieSessionAuthenticator) CanLogin(
|
func (ja *JWTCookieSessionAuthenticator) CanLogin(
|
||||||
user *User,
|
user *User,
|
||||||
|
username string,
|
||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) bool {
|
r *http.Request) bool {
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
|
|||||||
return ja.publicKey, nil
|
return ja.publicKey, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while parsing token")
|
log.Warn("error while parsing token")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +152,6 @@ func (ja *JWTCookieSessionAuthenticator) Login(
|
|||||||
|
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
sub, _ := claims["sub"].(string)
|
sub, _ := claims["sub"].(string)
|
||||||
exp, _ := claims["exp"].(float64)
|
|
||||||
|
|
||||||
var name string
|
var name string
|
||||||
if val, ok := claims["name"]; ok {
|
if val, ok := claims["name"]; ok {
|
||||||
@ -201,6 +200,5 @@ func (ja *JWTCookieSessionAuthenticator) Login(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Expiration = time.Unix(int64(exp), 0)
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
@ -42,6 +41,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{})
|
|||||||
|
|
||||||
func (ja *JWTSessionAuthenticator) CanLogin(
|
func (ja *JWTSessionAuthenticator) CanLogin(
|
||||||
user *User,
|
user *User,
|
||||||
|
username string,
|
||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) bool {
|
r *http.Request) bool {
|
||||||
|
|
||||||
@ -76,7 +76,6 @@ func (ja *JWTSessionAuthenticator) Login(
|
|||||||
|
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
sub, _ := claims["sub"].(string)
|
sub, _ := claims["sub"].(string)
|
||||||
exp, _ := claims["exp"].(float64)
|
|
||||||
|
|
||||||
var name string
|
var name string
|
||||||
// Java/Grails Issued Token
|
// Java/Grails Issued Token
|
||||||
@ -131,6 +130,5 @@ func (ja *JWTSessionAuthenticator) Login(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Expiration = time.Unix(int64(exp), 0)
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
@ -66,10 +66,51 @@ func (la *LdapAuthenticator) Init(
|
|||||||
|
|
||||||
func (la *LdapAuthenticator) CanLogin(
|
func (la *LdapAuthenticator) CanLogin(
|
||||||
user *User,
|
user *User,
|
||||||
|
username string,
|
||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) bool {
|
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(
|
func (la *LdapAuthenticator) Login(
|
||||||
@ -124,8 +165,10 @@ 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, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
la.config.UserBase,
|
||||||
la.config.UserFilter, []string{"dn", "uid", "gecos"}, nil))
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
fmt.Sprintf("(%s(uid=%s))", la.config.UserFilter, "*"),
|
||||||
|
[]string{"dn", "uid", "gecos"}, nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("LDAP search error")
|
log.Warn("LDAP search error")
|
||||||
return err
|
return err
|
||||||
|
@ -28,6 +28,7 @@ func (la *LocalAuthenticator) Init(
|
|||||||
|
|
||||||
func (la *LocalAuthenticator) CanLogin(
|
func (la *LocalAuthenticator) CanLogin(
|
||||||
user *User,
|
user *User,
|
||||||
|
username string,
|
||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request) bool {
|
r *http.Request) bool {
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ type LdapConfig struct {
|
|||||||
UserFilter string `json:"user_filter"`
|
UserFilter string `json:"user_filter"`
|
||||||
SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration.
|
SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration.
|
||||||
SyncDelOldUsers bool `json:"sync_del_old_users"`
|
SyncDelOldUsers bool `json:"sync_del_old_users"`
|
||||||
|
SyncUserOnLogin bool `json:"syncUserOnLogin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTAuthConfig struct {
|
type JWTAuthConfig struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user