Merge branch '189-refactor-authentication-module' into 105_modify_user_via_api

This commit is contained in:
Jan Eitzinger 2023-08-14 14:00:27 +02:00
commit 42e05fc999
13 changed files with 745 additions and 507 deletions

View File

@ -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

View File

@ -1,11 +1,13 @@
## Introduction ## Introduction
ClusterCockpit uses JSON Web Tokens (JWT) for authorization of its APIs. ClusterCockpit uses JSON Web Tokens (JWT) for authorization of its APIs. JSON
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. Web Token (JWT) is an open standard (RFC 7519) that defines a compact and
This information can be verified and trusted because it is digitally signed. self-contained way for securely transmitting information between parties as a
In ClusterCockpit JWTs are signed using a public/private key pair using ECDSA. JSON object. This information can be verified and trusted because it is
Because tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it. digitally signed. In ClusterCockpit JWTs are signed using a public/private key
Currently JWT tokens in ClusterCockpit not yet expire. pair using ECDSA. Because tokens are signed using public/private key pairs, the
signature also certifies that only the party holding the private key is the one
that signed it. Token expiration is set to the configuration option MaxAge.
## JWT Payload ## JWT Payload
@ -25,8 +27,14 @@ $ ./gen-keypair
2. Add keypair in your `.env` file. A template can be found in `./configs`. 2. Add keypair in your `.env` file. A template can be found in `./configs`.
There are two usage scenarios: There are two usage scenarios:
* The APIs are used during a browser session. In this case on login a JWT token is issued on login, that is used by the web frontend to authorize against the GraphQL and REST APIs. * The APIs are used during a browser session. API accesses are authorized with
* The REST API is used outside a browser session, e.g. by scripts. In this case you have to issue a token manually. This possible from within the configuration view or on the command line. It is recommended to issue a JWT token in this case for a special user that only has the `api` role. By using different users for different purposes a fine grained access control and access revocation management is possible. the active session.
* The REST API is used outside a browser session, e.g. by scripts. In this case
you have to issue a token manually. This possible from within the
configuration view or on the command line. It is recommended to issue a JWT
token in this case for a special user that only has the `api` role. By using
different users for different purposes a fine grained access control and
access revocation management is possible.
The token is commonly specified in the Authorization HTTP header using the Bearer schema. The token is commonly specified in the Authorization HTTP header using the Bearer schema.
@ -46,16 +54,24 @@ $ curl -X GET "<API ENDPOINT>" -H "accept: application/json" -H "Content-Type:
``` ```
## Accept externally generated JWTs provided via cookie ## Accept externally generated JWTs provided via cookie
If there is an external service like an AuthAPI that can generate JWTs and hand them over to ClusterCockpit via cookies, CC can be configured to accept them: If there is an external service like an AuthAPI that can generate JWTs and hand
them over to ClusterCockpit via cookies, CC can be configured to accept them:
1. `.env`: CC needs a public ed25519 key to verify foreign JWT signatures. Public keys in PEM format can be converted with the instructions in [/tools/convert-pem-pubkey-for-cc](../tools/convert-pem-pubkey-for-cc/Readme.md) . 1. `.env`: CC needs a public ed25519 key to verify foreign JWT signatures.
Public keys in PEM format can be converted with the instructions in
[/tools/convert-pem-pubkey-for-cc](../tools/convert-pem-pubkey-for-cc/Readme.md)
.
``` ```
CROSS_LOGIN_JWT_PUBLIC_KEY="+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc=" CROSS_LOGIN_JWT_PUBLIC_KEY="+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc="
``` ```
2. `config.json`: Insert a name for the cookie (set by the external service) containing the JWT so that CC knows where to look at. Define a trusted issuer (JWT claim 'iss'), otherwise it will be rejected. 2. `config.json`: Insert a name for the cookie (set by the external service)
If you want usernames and user roles from JWTs ('sub' and 'roles' claim) to be validated against CC's internal database, you need to enable it here. Unknown users will then be rejected and roles set via JWT will be ignored. containing the JWT so that CC knows where to look at. Define a trusted issuer
(JWT claim 'iss'), otherwise it will be rejected. If you want usernames and
user roles from JWTs ('sub' and 'roles' claim) to be validated against CC's
internal database, you need to enable it here. Unknown users will then be
rejected and roles set via JWT will be ignored.
```json ```json
"jwts": { "jwts": {
@ -65,7 +81,8 @@ If you want usernames and user roles from JWTs ('sub' and 'roles' claim) to be v
} }
``` ```
3. Make sure your external service includes the same issuer (`iss`) in its JWTs. Example JWT payload: 3. Make sure your external service includes the same issuer (`iss`) in its JWTs.
Example JWT payload:
```json ```json
{ {

View File

@ -1,13 +1,15 @@
# Overview # Overview
The implementation of authentication is not easy to understand by just looking The authentication is implemented in `internal/auth/`. In `auth.go`
at the code. The authentication is implemented in `internal/auth/`. In `auth.go`
an interface is defined that any authentication provider must fulfill. It also an interface is defined that any authentication provider must fulfill. It also
acts as a dispatcher to delegate the calls to the available authentication acts as a dispatcher to delegate the calls to the available authentication
providers. providers.
The most important routine are: Two authentication types are available:
* `CanLogin()` Check if the authentication method is supported for login attempt * JWT authentication for the REST API that does not create a session cookie
* Session based authentication using a session cookie
The most important routines in auth are:
* `Login()` Handle POST request to login user and start a new session * `Login()` Handle POST request to login user and start a new session
* `Auth()` Authenticate user and put User Object in context of the request * `Auth()` Authenticate user and put User Object in context of the request
@ -30,10 +32,9 @@ secured.Use(func(next http.Handler) http.Handler {
}) })
``` ```
For non API routes a JWT token can be used to initiate an authenticated user A JWT token can be used to initiate an authenticated user
session. This can either happen by calling the login route with a token session. This can either happen by calling the login route with a token
provided in a header or query URL or via the `Auth()` method on first access provided in a header or via a special cookie containing the JWT token.
to a secured URL via a special cookie containing the JWT token.
For API routes the access is authenticated on every request using the JWT token For API routes the access is authenticated on every request using the JWT token
and no session is initiated. and no session is initiated.
@ -43,12 +44,13 @@ The Login function (located in `auth.go`):
* Extracts the user name and gets the user from the user database table. In case the * Extracts the user name and gets the user from the user database table. In case the
user is not found the user object is set to nil. user is not found the user object is set to nil.
* Iterates over all authenticators and: * Iterates over all authenticators and:
- Calls the `CanLogin` function which checks if the authentication method is - Calls its `CanLogin` function which checks if the authentication method is
supported for this user and the user object is valid. supported for this user.
- Calls the `Login` function to authenticate the user. On success a valid user - Calls its `Login` function to authenticate the user. On success a valid user
object is returned. object is returned.
- Creates a new session object, stores the user attributes in the session and - Creates a new session object, stores the user attributes in the session and
saves the session. saves the session.
- If the user does not yet exist in the database try to add the user
- Starts the `onSuccess` http handler - Starts the `onSuccess` http handler
## Local authenticator ## Local authenticator
@ -82,17 +84,13 @@ if err := l.Bind(userDn, r.FormValue("password")); err != nil {
} }
``` ```
## JWT authenticator ## JWT Session authenticator
Login via JWT token will create a session without password. Login via JWT token will create a session without password.
For login the `X-Auth-Token` header is not supported. For login the `X-Auth-Token` header is not supported. This authenticator is
This authenticator is applied if either user is not nil and auth source is applied if the Authorization header is present:
`AuthViaToken` or the Authorization header is present or the URL query key
login-token is present:
``` ```
return (user != nil && user.AuthSource == AuthViaToken) || return r.Header.Get("Authorization") != ""
r.Header.Get("Authorization") != "" ||
r.URL.Query().Get("login-token") != ""
``` ```
The Login function: The Login function:
@ -108,41 +106,26 @@ The Login function:
- In case user is not yet present add user to user database table with `AuthViaToken` AuthSource. - In case user is not yet present add user to user database table with `AuthViaToken` AuthSource.
* Return valid user object * Return valid user object
# Auth ## JWT Cookie Session authenticator
The Auth function (located in `auth.go`): Login via JWT cookie token will create a session without password.
* Returns a new http handler function that is defined right away It is first checked if the required configuration keys are set:
* This handler iterates over all authenticators * `publicKeyCrossLogin`
* Calls `Auth()` on every authenticator * `TrustedExternalIssuer`
* If err is not nil and the user object is valid it puts the user object in the * `CookieName`
request context and starts the onSuccess http handler
* Otherwise it calls the onFailure handler
## Local This authenticator is applied if the configured cookie is present:
```
jwtCookie, err := r.Cookie(cookieName)
Calls the `AuthViaSession()` function in `auth.go`. This will extract username, if err == nil && jwtCookie.Value != "" {
projects and roles from the session and initialize a user object with those return true
values. }
```
## LDAP The Login function:
* Extracts and parses the token
Calls the `AuthViaSession()` function in `auth.go`. This will extract username, * Checks if signing method is Ed25519/EdDSA
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
@ -150,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

View File

@ -1,4 +1,4 @@
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. // Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. // All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,12 +7,11 @@ package auth
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"database/sql"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -28,170 +27,22 @@ const (
AuthViaToken AuthViaToken
) )
type AuthType int
const (
AuthToken AuthType = iota
AuthSession
)
type User struct { type User struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"-"` Password string `json:"-"`
Name string `json:"name"` Name string `json:"name"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
AuthSource AuthSource `json:"via"` AuthType AuthType `json:"authType"`
AuthSource AuthSource `json:"authSource"`
Email string `json:"email"` Email string `json:"email"`
Projects []string `json:"projects"` Projects []string `json:"projects"`
Expiration time.Time
}
type Role int
const (
RoleAnonymous Role = iota
RoleApi
RoleUser
RoleManager
RoleSupport
RoleAdmin
RoleError
)
func GetRoleString(roleInt Role) string {
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
}
func getRoleEnum(roleStr string) Role {
switch strings.ToLower(roleStr) {
case "admin":
return RoleAdmin
case "support":
return RoleSupport
case "manager":
return RoleManager
case "user":
return RoleUser
case "api":
return RoleApi
case "anonymous":
return RoleAnonymous
default:
return RoleError
}
}
func isValidRole(role string) bool {
return getRoleEnum(role) != RoleError
}
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
if isValidRole(role) {
for _, r := range u.Roles {
if r == role {
return true, true
}
}
return false, true
}
return false, false
}
func (u *User) HasRole(role Role) bool {
for _, r := range u.Roles {
if r == GetRoleString(role) {
return true
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAnyRole(queryroles []Role) bool {
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
return true
}
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAllRoles(queryroles []Role) bool {
target := len(queryroles)
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
matches += 1
break
}
}
}
if matches == target {
return true
} else {
return false
}
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasNotRoles(queryroles []Role) bool {
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
matches += 1
break
}
}
}
if matches == 0 {
return true
} else {
return false
}
}
// Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role
func GetValidRoles(user *User) ([]string, error) {
var vals []string
if user.HasRole(RoleAdmin) {
for i := RoleApi; i < RoleError; i++ {
vals = append(vals, GetRoleString(i))
}
return vals, nil
}
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
}
// Called by routerConfig web.page setup in backend: Only requires known user
func GetValidRolesMap(user *User) (map[string]Role, error) {
named := make(map[string]Role)
if user.HasNotRoles([]Role{RoleAnonymous}) {
for i := RoleApi; i < RoleError; i++ {
named[GetRoleString(i)] = i
}
return named, nil
}
return named, fmt.Errorf("only known users are allowed to fetch a list of roles")
}
// Find highest role
func (u *User) GetAuthLevel() Role {
if u.HasRole(RoleAdmin) {
return RoleAdmin
} else if u.HasRole(RoleSupport) {
return RoleSupport
} else if u.HasRole(RoleManager) {
return RoleManager
} else if u.HasRole(RoleUser) {
return RoleUser
} else if u.HasRole(RoleApi) {
return RoleApi
} else if u.HasRole(RoleAnonymous) {
return RoleAnonymous
} else {
return RoleError
}
} }
func (u *User) HasProject(project string) bool { func (u *User) HasProject(project string) bool {
@ -214,9 +65,8 @@ 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)
Auth(rw http.ResponseWriter, r *http.Request) (*User, error)
} }
type ContextKey string type ContextKey string
@ -234,6 +84,50 @@ type Authentication struct {
LocalAuth *LocalAuthenticator LocalAuth *LocalAuthenticator
} }
func (auth *Authentication) AuthViaSession(
rw http.ResponseWriter,
r *http.Request) (*User, error) {
session, err := auth.sessionStore.Get(r, "session")
if err != nil {
log.Error("Error while getting session store")
return nil, err
}
if session.IsNew {
return nil, nil
}
//
// var username string
// var projects, roles []string
//
// if val, ok := session.Values["username"]; ok {
// username, _ = val.(string)
// } else {
// return nil, errors.New("no key username in session")
// }
// if val, ok := session.Values["projects"]; ok {
// projects, _ = val.([]string)
// } else {
// return nil, errors.New("no key projects in session")
// }
// if val, ok := session.Values["projects"]; ok {
// roles, _ = val.([]string)
// } else {
// return nil, errors.New("no key roles in session")
// }
//
username, _ := session.Values["username"].(string)
projects, _ := session.Values["projects"].([]string)
roles, _ := session.Values["roles"].([]string)
return &User{
Username: username,
Projects: projects,
Roles: roles,
AuthType: AuthSession,
AuthSource: -1,
}, nil
}
func Init(db *sqlx.DB, func Init(db *sqlx.DB,
configs map[string]interface{}) (*Authentication, error) { configs map[string]interface{}) (*Authentication, error) {
auth := &Authentication{} auth := &Authentication{}
@ -257,19 +151,11 @@ func Init(db *sqlx.DB,
auth.sessionStore = sessions.NewCookieStore(bytes) auth.sessionStore = sessions.NewCookieStore(bytes)
} }
auth.LocalAuth = &LocalAuthenticator{}
if err := auth.LocalAuth.Init(auth, nil); err != nil {
log.Error("Error while initializing authentication -> localAuth init failed")
return nil, err
}
auth.authenticators = append(auth.authenticators, auth.LocalAuth)
auth.JwtAuth = &JWTAuthenticator{} auth.JwtAuth = &JWTAuthenticator{}
if err := auth.JwtAuth.Init(auth, configs["jwt"]); err != nil { if err := auth.JwtAuth.Init(auth, configs["jwt"]); 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
} }
auth.authenticators = append(auth.authenticators, auth.JwtAuth)
if config, ok := configs["ldap"]; ok { if config, ok := configs["ldap"]; ok {
auth.LdapAuth = &LdapAuthenticator{} auth.LdapAuth = &LdapAuthenticator{}
@ -280,36 +166,30 @@ func Init(db *sqlx.DB,
auth.authenticators = append(auth.authenticators, auth.LdapAuth) auth.authenticators = append(auth.authenticators, auth.LdapAuth)
} }
jwtSessionAuth := &JWTSessionAuthenticator{}
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
log.Warn("Error while initializing authentication -> jwtSessionAuth init failed")
} else {
auth.authenticators = append(auth.authenticators, jwtSessionAuth)
}
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
log.Warn("Error while initializing authentication -> jwtCookieSessionAuth init failed")
} else {
auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth)
}
auth.LocalAuth = &LocalAuthenticator{}
if err := auth.LocalAuth.Init(auth, nil); err != nil {
log.Error("Error while initializing authentication -> localAuth init failed")
return nil, err
}
auth.authenticators = append(auth.authenticators, auth.LocalAuth)
return auth, nil return auth, nil
} }
func (auth *Authentication) AuthViaSession(
rw http.ResponseWriter,
r *http.Request) (*User, error) {
session, err := auth.sessionStore.Get(r, "session")
if err != nil {
log.Error("Error while getting session store")
return nil, err
}
if session.IsNew {
return nil, nil
}
// TODO Check if keys are present in session?
username, _ := session.Values["username"].(string)
projects, _ := session.Values["projects"].([]string)
roles, _ := session.Values["roles"].([]string)
return &User{
Username: username,
Projects: projects,
Roles: roles,
AuthSource: -1,
}, nil
}
// Handle a POST request that should log the user in, starting a new session.
func (auth *Authentication) Login( func (auth *Authentication) Login(
onsuccess http.Handler, onsuccess http.Handler,
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler { onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
@ -317,18 +197,21 @@ func (auth *Authentication) Login(
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
err := errors.New("no authenticator applied") err := errors.New("no authenticator applied")
username := r.FormValue("username") username := r.FormValue("username")
user := (*User)(nil) dbUser := (*User)(nil)
if username != "" { if username != "" {
user, _ = auth.GetUser(username) dbUser, err = auth.GetUser(username)
if err != nil && err != sql.ErrNoRows {
log.Errorf("Error while loading user '%v'", username)
}
} }
for _, authenticator := range auth.authenticators { for _, authenticator := range auth.authenticators {
if !authenticator.CanLogin(user, rw, r) { if !authenticator.CanLogin(dbUser, username, rw, r) {
continue continue
} }
user, err = authenticator.Login(user, rw, r) user, err := authenticator.Login(dbUser, rw, r)
if err != nil { if err != nil {
log.Warnf("user login failed: %s", err.Error()) log.Warnf("user login failed: %s", err.Error())
onfailure(rw, r, err) onfailure(rw, r, err)
@ -354,6 +237,14 @@ func (auth *Authentication) Login(
return return
} }
if dbUser == nil {
if err := auth.AddUser(user); err != nil {
// TODO Add AuthSource
log.Errorf("Error while adding user '%v' to auth from XX",
user.Username)
}
}
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects) log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
ctx := context.WithValue(r.Context(), ContextUserKey, user) ctx := context.WithValue(r.Context(), ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
@ -365,39 +256,40 @@ func (auth *Authentication) Login(
}) })
} }
// Authenticate the user and put a User object in the
// context of the request. If authentication fails,
// do not continue but send client to the login screen.
func (auth *Authentication) Auth( func (auth *Authentication) Auth(
onsuccess http.Handler, onsuccess http.Handler,
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error)) http.Handler { onfailure func(rw http.ResponseWriter, r *http.Request, authErr error)) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
for _, authenticator := range auth.authenticators {
user, err := authenticator.Auth(rw, r) user, err := auth.JwtAuth.AuthViaJWT(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) http.Error(rw, err.Error(), http.StatusUnauthorized)
return return
} }
if user == nil { if user == nil {
continue user, err = auth.AuthViaSession(rw, r)
if err != nil {
log.Infof("authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized)
return
}
} }
if user != nil {
ctx := context.WithValue(r.Context(), ContextUserKey, user) ctx := context.WithValue(r.Context(), ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return
} }
log.Debugf("authentication failed: %s", "no authenticator applied") log.Debug("authentication failed")
// http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) onfailure(rw, r, errors.New("unauthorized (please login first)"))
onfailure(rw, r, errors.New("unauthorized (login first or use a token)"))
}) })
} }
// Clears the session cookie
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler { func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
session, err := auth.sessionStore.Get(r, "session") session, err := auth.sessionStore.Get(r, "session")
if err != nil { if err != nil {

View File

@ -6,10 +6,8 @@ package auth
import ( import (
"crypto/ed25519" "crypto/ed25519"
"database/sql"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@ -25,15 +23,9 @@ type JWTAuthenticator struct {
publicKey ed25519.PublicKey publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
loginTokenKey []byte // HS256 key
config *schema.JWTAuthConfig config *schema.JWTAuthConfig
} }
var _ Authenticator = (*JWTAuthenticator)(nil)
func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error { func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
ja.auth = auth ja.auth = auth
@ -57,128 +49,10 @@ func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
ja.privateKey = ed25519.PrivateKey(bytes) ja.privateKey = ed25519.PrivateKey(bytes)
} }
if pubKey = os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil {
log.Warn("Could not decode cross login JWT HS512 key")
return err
}
ja.loginTokenKey = bytes
}
// Look for external public keys
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
if keyFound && pubKeyCrossLogin != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
if err != nil {
log.Warn("Could not decode cross login JWT public key")
return err
}
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
// Warn if other necessary settings are not configured
if ja.config != nil {
if ja.config.CookieName == "" {
log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
}
if !ja.config.ForceJWTValidationViaDatabase {
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
}
if ja.config.TrustedExternalIssuer == "" {
log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
}
} else {
log.Warn("cookieName and trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
}
} else {
ja.publicKeyCrossLogin = nil
log.Debug("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
}
return nil return nil
} }
func (ja *JWTAuthenticator) CanLogin( func (ja *JWTAuthenticator) AuthViaJWT(
user *User,
rw http.ResponseWriter,
r *http.Request) bool {
return (user != nil && user.AuthSource == AuthViaToken) ||
r.Header.Get("Authorization") != "" ||
r.URL.Query().Get("login-token") != ""
}
func (ja *JWTAuthenticator) Login(
user *User,
rw http.ResponseWriter,
r *http.Request) (*User, error) {
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if rawtoken == "" {
rawtoken = r.URL.Query().Get("login-token")
}
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
if t.Method == jwt.SigningMethodEdDSA {
return ja.publicKey, nil
}
if t.Method == jwt.SigningMethodHS256 || t.Method == jwt.SigningMethodHS512 {
return ja.loginTokenKey, nil
}
return nil, fmt.Errorf("AUTH/JWT > unkown signing method for login token: %s (known: HS256, HS512, EdDSA)", t.Method.Alg())
})
if err != nil {
log.Warn("Error while parsing jwt token")
return nil, err
}
if err = token.Claims.Valid(); err != nil {
log.Warn("jwt token claims are not valid")
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
exp, _ := claims["exp"].(float64)
var roles []string
if rawroles, ok := claims["roles"].([]interface{}); ok {
for _, rr := range rawroles {
if r, ok := rr.(string); ok {
if isValidRole(r) {
roles = append(roles, r)
}
}
}
}
if rawrole, ok := claims["roles"].(string); ok {
if isValidRole(rawrole) {
roles = append(roles, rawrole)
}
}
if user == nil {
user, err = ja.auth.GetUser(sub)
if err != nil && err != sql.ErrNoRows {
log.Errorf("Error while loading user '%v'", sub)
return nil, err
} else if user == nil {
user = &User{
Username: sub,
Roles: roles,
AuthSource: AuthViaToken,
}
if err := ja.auth.AddUser(user); err != nil {
log.Errorf("Error while adding user '%v' to auth from token", user.Username)
return nil, err
}
}
}
user.Expiration = time.Unix(int64(exp), 0)
return user, nil
}
func (ja *JWTAuthenticator) Auth(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*User, error) {
@ -188,59 +62,22 @@ func (ja *JWTAuthenticator) Auth(
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ") rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
} }
// If no auth header was found, check for a certain cookie containing a JWT // there is no token
cookieName := ""
cookieFound := false
if ja.config != nil && ja.config.CookieName != "" {
cookieName = ja.config.CookieName
}
// Try to read the JWT cookie
if rawtoken == "" && cookieName != "" {
jwtCookie, err := r.Cookie(cookieName)
if err == nil && jwtCookie.Value != "" {
rawtoken = jwtCookie.Value
cookieFound = true
}
}
// Because a user can also log in via a token, the
// session cookie must be checked here as well:
if rawtoken == "" { if rawtoken == "" {
return ja.auth.AuthViaSession(rw, r) return nil, nil
} }
// Try to parse JWT
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) { token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodEdDSA { if t.Method != jwt.SigningMethodEdDSA {
return nil, errors.New("only Ed25519/EdDSA supported") return nil, errors.New("only Ed25519/EdDSA supported")
} }
// Is there more than one public key?
if ja.publicKeyCrossLogin != nil &&
ja.config != nil &&
ja.config.TrustedExternalIssuer != "" {
// Determine whether to use the external public key
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer {
// The (unvalidated) issuer seems to be the expected one,
// use public cross login key from config
return ja.publicKeyCrossLogin, nil
}
}
// No cross login key configured or issuer not expected
// Try own key
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 JWT token")
return nil, err return nil, err
} }
// Check token validity
if err := token.Claims.Valid(); err != nil { if err := token.Claims.Valid(); err != nil {
log.Warn("jwt token claims are not valid") log.Warn("jwt token claims are not valid")
return nil, err return nil, err
@ -261,7 +98,6 @@ func (ja *JWTAuthenticator) Auth(
log.Warn("Could not find user from JWT in internal database.") log.Warn("Could not find user from JWT in internal database.")
return nil, errors.New("unknown user") return nil, errors.New("unknown user")
} }
// Take user roles from database instead of trusting the JWT // Take user roles from database instead of trusting the JWT
roles = user.Roles roles = user.Roles
} else { } else {
@ -275,42 +111,11 @@ func (ja *JWTAuthenticator) Auth(
} }
} }
if cookieFound {
// Create a session so that we no longer need the JTW Cookie
session, err := ja.auth.sessionStore.New(r, "session")
if err != nil {
log.Errorf("session creation failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
return nil, err
}
if ja.auth.SessionMaxAge != 0 {
session.Options.MaxAge = int(ja.auth.SessionMaxAge.Seconds())
}
session.Values["username"] = sub
session.Values["roles"] = roles
if err := ja.auth.sessionStore.Save(r, rw, session); err != nil {
log.Warnf("session save failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
return nil, err
}
// (Ask browser to) Delete JWT cookie
deletedCookie := &http.Cookie{
Name: cookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(rw, deletedCookie)
}
return &User{ return &User{
Username: sub, Username: sub,
Roles: roles, Roles: roles,
AuthSource: AuthViaToken, AuthType: AuthToken,
AuthSource: -1,
}, nil }, nil
} }

View File

@ -0,0 +1,204 @@
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"crypto/ed25519"
"encoding/base64"
"errors"
"net/http"
"os"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/golang-jwt/jwt/v4"
)
type JWTCookieSessionAuthenticator struct {
auth *Authentication
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
config *schema.JWTAuthConfig
}
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
ja.auth = auth
ja.config = conf.(*schema.JWTAuthConfig)
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
if pubKey == "" || privKey == "" {
log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
return errors.New("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
} else {
bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil {
log.Warn("Could not decode JWT public key")
return err
}
ja.publicKey = ed25519.PublicKey(bytes)
bytes, err = base64.StdEncoding.DecodeString(privKey)
if err != nil {
log.Warn("Could not decode JWT private key")
return err
}
ja.privateKey = ed25519.PrivateKey(bytes)
}
// Look for external public keys
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
if keyFound && pubKeyCrossLogin != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
if err != nil {
log.Warn("Could not decode cross login JWT public key")
return err
}
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
} else {
ja.publicKeyCrossLogin = nil
log.Debug("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)")
}
// Warn if other necessary settings are not configured
if ja.config != nil {
if ja.config.CookieName == "" {
log.Warn("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.ForceJWTValidationViaDatabase {
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
}
if ja.config.TrustedExternalIssuer == "" {
log.Warn("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 {
log.Warn("config for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("config for JWTs not configured (cross login via JWT cookie will fail)")
}
return nil
}
func (ja *JWTCookieSessionAuthenticator) CanLogin(
user *User,
username string,
rw http.ResponseWriter,
r *http.Request) bool {
cookieName := ""
if ja.config != nil && ja.config.CookieName != "" {
cookieName = ja.config.CookieName
}
// Try to read the JWT cookie
if cookieName != "" {
jwtCookie, err := r.Cookie(cookieName)
if err == nil && jwtCookie.Value != "" {
return true
}
}
return false
}
func (ja *JWTCookieSessionAuthenticator) Login(
user *User,
rw http.ResponseWriter,
r *http.Request) (*User, error) {
jwtCookie, err := r.Cookie(ja.config.CookieName)
var rawtoken string
if err == nil && jwtCookie.Value != "" {
rawtoken = jwtCookie.Value
}
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodEdDSA {
return nil, errors.New("only Ed25519/EdDSA supported")
}
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer {
// The (unvalidated) issuer seems to be the expected one,
// use public cross login key from config
return ja.publicKeyCrossLogin, nil
}
// No cross login key configured or issuer not expected
// Try own key
return ja.publicKey, nil
})
if err != nil {
log.Warn("error while parsing token")
return nil, err
}
// Check token validity and extract paypload
if err := token.Claims.Valid(); err != nil {
log.Warn("jwt token claims are not valid")
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
var name string
if val, ok := claims["name"]; ok {
name, _ = val.(string)
}
var roles []string
if ja.config.ForceJWTValidationViaDatabase {
// Deny any logins for unknown usernames
if user == nil {
log.Warn("Could not find user from JWT in internal database.")
return nil, errors.New("unknown user")
}
// Take user roles from database instead of trusting the JWT
roles = user.Roles
} else {
// Extract roles from JWT (if present)
if rawroles, ok := claims["roles"].([]interface{}); ok {
for _, rr := range rawroles {
if r, ok := rr.(string); ok {
roles = append(roles, r)
}
}
}
}
// (Ask browser to) Delete JWT cookie
deletedCookie := &http.Cookie{
Name: ja.config.CookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(rw, deletedCookie)
if user == nil {
user = &User{
Username: sub,
Name: name,
Roles: roles,
AuthType: AuthSession,
AuthSource: AuthViaToken,
}
}
return user, nil
}

105
internal/auth/jwtSession.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"strings"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/golang-jwt/jwt/v4"
)
type JWTSessionAuthenticator struct {
auth *Authentication
loginTokenKey []byte // HS256 key
}
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
ja.auth = auth
if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil {
log.Warn("Could not decode cross login JWT HS512 key")
return err
}
ja.loginTokenKey = bytes
}
return nil
}
func (ja *JWTSessionAuthenticator) CanLogin(
user *User,
username string,
rw http.ResponseWriter,
r *http.Request) bool {
return r.Header.Get("Authorization") != "" || r.URL.Query().Get("login-token") != ""
}
func (ja *JWTSessionAuthenticator) Login(
user *User,
rw http.ResponseWriter,
r *http.Request) (*User, error) {
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if rawtoken == "" {
rawtoken = r.URL.Query().Get("login-token")
}
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
if t.Method == jwt.SigningMethodHS256 || t.Method == jwt.SigningMethodHS512 {
return ja.loginTokenKey, nil
}
return nil, fmt.Errorf("AUTH/JWT > unkown signing method for login token: %s (known: HS256, HS512, EdDSA)", t.Method.Alg())
})
if err != nil {
log.Warn("Error while parsing jwt token")
return nil, err
}
if err = token.Claims.Valid(); err != nil {
log.Warn("jwt token claims are not valid")
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
var name string
if val, ok := claims["name"]; ok {
name, _ = val.(string)
}
var roles []string
if rawroles, ok := claims["roles"]; ok {
for _, r := range rawroles.([]string) {
if isValidRole(r) {
roles = append(roles, r)
}
}
}
if user == nil {
user = &User{
Username: sub,
Name: name,
Roles: roles,
AuthType: AuthSession,
AuthSource: AuthViaToken,
}
}
return user, nil
}

View File

@ -1,4 +1,4 @@
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. // Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. // All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -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(
@ -93,13 +134,6 @@ func (la *LdapAuthenticator) Login(
return user, nil return user, nil
} }
func (la *LdapAuthenticator) Auth(
rw http.ResponseWriter,
r *http.Request) (*User, error) {
return la.auth.AuthViaSession(rw, r)
}
func (la *LdapAuthenticator) Sync() error { func (la *LdapAuthenticator) Sync() error {
const IN_DB int = 1 const IN_DB int = 1
@ -131,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

View File

@ -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 {
@ -39,17 +40,11 @@ func (la *LocalAuthenticator) Login(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*User, error) {
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); e != nil { if e := bcrypt.CompareHashAndPassword([]byte(user.Password),
[]byte(r.FormValue("password"))); e != nil {
log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username) log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username)
return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed") return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed")
} }
return user, nil return user, nil
} }
func (la *LocalAuthenticator) Auth(
rw http.ResponseWriter,
r *http.Request) (*User, error) {
return la.auth.AuthViaSession(rw, r)
}

165
internal/auth/roles.go Normal file
View File

@ -0,0 +1,165 @@
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"fmt"
"strings"
)
type Role int
const (
RoleAnonymous Role = iota
RoleApi
RoleUser
RoleManager
RoleSupport
RoleAdmin
RoleError
)
func GetRoleString(roleInt Role) string {
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
}
func getRoleEnum(roleStr string) Role {
switch strings.ToLower(roleStr) {
case "admin":
return RoleAdmin
case "support":
return RoleSupport
case "manager":
return RoleManager
case "user":
return RoleUser
case "api":
return RoleApi
case "anonymous":
return RoleAnonymous
default:
return RoleError
}
}
func isValidRole(role string) bool {
return getRoleEnum(role) != RoleError
}
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
if isValidRole(role) {
for _, r := range u.Roles {
if r == role {
return true, true
}
}
return false, true
}
return false, false
}
func (u *User) HasRole(role Role) bool {
for _, r := range u.Roles {
if r == GetRoleString(role) {
return true
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAnyRole(queryroles []Role) bool {
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
return true
}
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAllRoles(queryroles []Role) bool {
target := len(queryroles)
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
matches += 1
break
}
}
}
if matches == target {
return true
} else {
return false
}
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasNotRoles(queryroles []Role) bool {
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
matches += 1
break
}
}
}
if matches == 0 {
return true
} else {
return false
}
}
// Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role
func GetValidRoles(user *User) ([]string, error) {
var vals []string
if user.HasRole(RoleAdmin) {
for i := RoleApi; i < RoleError; i++ {
vals = append(vals, GetRoleString(i))
}
return vals, nil
}
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
}
// Called by routerConfig web.page setup in backend: Only requires known user
func GetValidRolesMap(user *User) (map[string]Role, error) {
named := make(map[string]Role)
if user.HasNotRoles([]Role{RoleAnonymous}) {
for i := RoleApi; i < RoleError; i++ {
named[GetRoleString(i)] = i
}
return named, nil
}
return named, fmt.Errorf("only known users are allowed to fetch a list of roles")
}
// Find highest role
func (u *User) GetAuthLevel() Role {
if u.HasRole(RoleAdmin) {
return RoleAdmin
} else if u.HasRole(RoleSupport) {
return RoleSupport
} else if u.HasRole(RoleManager) {
return RoleManager
} else if u.HasRole(RoleUser) {
return RoleUser
} else if u.HasRole(RoleApi) {
return RoleApi
} else if u.HasRole(RoleAnonymous) {
return RoleAnonymous
} else {
return RoleError
}
}

View File

@ -326,7 +326,6 @@ func (pdb *PrometheusDataRepository) LoadData(
Timestep: metricConfig.Timestep, Timestep: metricConfig.Timestep,
Series: make([]schema.Series, 0), Series: make([]schema.Series, 0),
} }
jobData[metric][scope] = jobMetric
} }
step := int64(metricConfig.Timestep) step := int64(metricConfig.Timestep)
steps := int64(to.Sub(from).Seconds()) / step steps := int64(to.Sub(from).Seconds()) / step
@ -335,6 +334,10 @@ func (pdb *PrometheusDataRepository) LoadData(
jobMetric.Series = append(jobMetric.Series, jobMetric.Series = append(jobMetric.Series,
pdb.RowToSeries(from, step, steps, row)) pdb.RowToSeries(from, step, steps, row))
} }
// only add metric if at least one host returned data
if !ok && len(jobMetric.Series) > 0{
jobData[metric][scope] = jobMetric
}
// sort by hostname to get uniform coloring // sort by hostname to get uniform coloring
sort.Slice(jobMetric.Series, func(i, j int) bool { sort.Slice(jobMetric.Series, func(i, j int) bool {
return (jobMetric.Series[i].Hostname < jobMetric.Series[j].Hostname) return (jobMetric.Series[i].Hostname < jobMetric.Series[j].Hostname)

Binary file not shown.

View File

@ -17,10 +17,11 @@ 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 {
// Specifies for how long a session or JWT shall be valid // Specifies for how long a JWT token shall be valid
// as a string parsable by time.ParseDuration(). // as a string parsable by time.ParseDuration().
MaxAge int64 `json:"max-age"` MaxAge int64 `json:"max-age"`
@ -105,7 +106,7 @@ type ProgramConfig struct {
LdapConfig *LdapConfig `json:"ldap"` LdapConfig *LdapConfig `json:"ldap"`
JwtConfig *JWTAuthConfig `json:"jwts"` JwtConfig *JWTAuthConfig `json:"jwts"`
// If 0 or empty, the session/token does not expire! // If 0 or empty, the session does not expire!
SessionMaxAge string `json:"session-max-age"` SessionMaxAge string `json:"session-max-age"`
// If both those options are not empty, use HTTPS using those certificates. // If both those options are not empty, use HTTPS using those certificates.
@ -116,7 +117,7 @@ type ProgramConfig struct {
// redirect every request incoming at port 80 to that url. // redirect every request incoming at port 80 to that url.
RedirectHttpTo string `json:"redirect-http-to"` RedirectHttpTo string `json:"redirect-http-to"`
// If overwriten, at least all the options in the defaults below must // If overwritten, at least all the options in the defaults below must
// be provided! Most options here can be overwritten by the user. // be provided! Most options here can be overwritten by the user.
UiDefaults map[string]interface{} `json:"ui-defaults"` UiDefaults map[string]interface{} `json:"ui-defaults"`