Skip to content

Commit

Permalink
Merge pull request #198 from ClusterCockpit/189-refactor-authenticati…
Browse files Browse the repository at this point in the history
…on-module

refactor auth module
  • Loading branch information
moebiusband73 committed Aug 18, 2023
2 parents cf91563 + 9fec8a4 commit 3b9b37a
Show file tree
Hide file tree
Showing 34 changed files with 1,730 additions and 1,147 deletions.
9 changes: 7 additions & 2 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
67 changes: 41 additions & 26 deletions cmd/cc-backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,7 @@ func main() {
var authentication *auth.Authentication
if !config.Keys.DisableAuthentication {
var err error
if authentication, err = auth.Init(db.DB, map[string]interface{}{
"ldap": config.Keys.LdapConfig,
"jwt": config.Keys.JwtConfig,
}); err != nil {
if authentication, err = auth.Init(); err != nil {
log.Fatalf("auth initialization failed: %v", err)
}

Expand All @@ -228,14 +225,16 @@ func main() {
log.Fatal("invalid argument format for user creation")
}

if err := authentication.AddUser(&auth.User{
ur := repository.GetUserRepository()
if err := ur.AddUser(&schema.User{
Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","),
}); err != nil {
log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
}
}
if flagDelUser != "" {
if err := authentication.DelUser(flagDelUser); err != nil {
ur := repository.GetUserRepository()
if err := ur.DelUser(flagDelUser); err != nil {
log.Fatalf("deleting user failed: %v", err)
}
}
Expand All @@ -252,12 +251,13 @@ func main() {
}

if flagGenJWT != "" {
user, err := authentication.GetUser(flagGenJWT)
ur := repository.GetUserRepository()
user, err := ur.GetUser(flagGenJWT)
if err != nil {
log.Fatalf("could not get user from JWT: %v", err)
}

if !user.HasRole(auth.RoleApi) {
if !user.HasRole(schema.RoleApi) {
log.Warnf("user '%s' does not have the API role", user.Username)
}

Expand Down Expand Up @@ -327,21 +327,19 @@ func main() {

r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo})
web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo})
}).Methods(http.MethodGet)
r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, r, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
web.RenderTemplate(rw, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
})
r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, r, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
})

// Some routes, such as /login or /query, should only be accessible to a user that is logged in.
// Those should be mounted to this subrouter. If authentication is enabled, a middleware will prevent
// any unauthenticated accesses.
secured := r.PathPrefix("/").Subrouter()

if !config.Keys.DisableAuthentication {
r.Handle("/login", authentication.Login(
// On success:
Expand All @@ -351,24 +349,41 @@ func main() {
func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Login failed - ClusterCockpit",
MsgType: "alert-warning",
Message: err.Error(),
Build: buildInfo,
})
})).Methods(http.MethodPost)

r.Handle("/logout", authentication.Logout(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Bye - ClusterCockpit",
MsgType: "alert-info",
Message: "Logout successful",
Build: buildInfo,
})
}))).Methods(http.MethodPost)
r.Handle("/jwt-login", authentication.Login(
// On success:
http.RedirectHandler("/", http.StatusTemporaryRedirect),

// On failure:
func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Login failed - ClusterCockpit",
MsgType: "alert-warning",
Message: err.Error(),
Build: buildInfo,
})
}))

r.Handle("/logout", authentication.Logout(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Bye - ClusterCockpit",
MsgType: "alert-info",
Message: "Logout successful",
Build: buildInfo,
})
}))).Methods(http.MethodPost)

secured.Use(func(next http.Handler) http.Handler {
return authentication.Auth(
Expand All @@ -378,7 +393,7 @@ func main() {
// On failure:
func(rw http.ResponseWriter, r *http.Request, err error) {
rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Authentication failed - ClusterCockpit",
MsgType: "alert-danger",
Message: err.Error(),
Expand Down
3 changes: 3 additions & 0 deletions configs/config-demo.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"kind": "file",
"path": "./var/job-archive"
},
"jwts": {
"max-age": "2m"
},
"clusters": [
{
"name": "fritz",
Expand Down
6 changes: 3 additions & 3 deletions configs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
],
"jwts": {
"cookieName": "",
"forceJWTValidationViaDatabase": false,
"max-age": 0,
"trustedExternalIssuer": ""
"validateUser": false,
"max-age": "2m",
"trustedIssuer": ""
},
"short-running-jobs-duration": 300
}
43 changes: 30 additions & 13 deletions docs/JWT-Handling.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
## Introduction

ClusterCockpit uses JSON Web Tokens (JWT) for authorization of its APIs.
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.
This information can be verified and trusted because it is digitally signed.
In ClusterCockpit JWTs are signed using a public/private key 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.
Currently JWT tokens in ClusterCockpit not yet expire.
ClusterCockpit uses JSON Web Tokens (JWT) for authorization of its APIs. 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. This information can be verified and trusted because it is
digitally signed. In ClusterCockpit JWTs are signed using a public/private key
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

Expand All @@ -25,8 +27,14 @@ $ ./gen-keypair
2. Add keypair in your `.env` file. A template can be found in `./configs`.

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 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 APIs are used during a browser session. API accesses are authorized with
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.

Expand All @@ -46,16 +54,24 @@ $ curl -X GET "<API ENDPOINT>" -H "accept: application/json" -H "Content-Type:
```

## 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="
```

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.
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.
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. 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
"jwts": {
Expand All @@ -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
{
Expand Down
Loading

0 comments on commit 3b9b37a

Please sign in to comment.