mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 12:59:06 +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.
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -28,6 +28,7 @@ func (la *LocalAuthenticator) Init(
|
||||
|
||||
func (la *LocalAuthenticator) CanLogin(
|
||||
user *User,
|
||||
username string,
|
||||
rw http.ResponseWriter,
|
||||
r *http.Request) bool {
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user