mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-23 12:51:40 +02:00
refactor auth module
Restructure module Separate JWT auth variants Cleanup code Fixes #189
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
||||
// 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.
|
||||
@@ -7,12 +7,11 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
@@ -28,172 +27,25 @@ const (
|
||||
AuthViaToken
|
||||
)
|
||||
|
||||
type AuthType int
|
||||
|
||||
const (
|
||||
AuthToken AuthType = iota
|
||||
AuthSession
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"roles"`
|
||||
AuthSource AuthSource `json:"via"`
|
||||
AuthType AuthType `json:"authType"`
|
||||
AuthSource AuthSource `json:"authSource"`
|
||||
Email string `json:"email"`
|
||||
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 {
|
||||
for _, p := range u.Projects {
|
||||
if p == project {
|
||||
@@ -216,7 +68,6 @@ type Authenticator interface {
|
||||
Init(auth *Authentication, config interface{}) error
|
||||
CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool
|
||||
Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
|
||||
Auth(rw http.ResponseWriter, r *http.Request) (*User, error)
|
||||
}
|
||||
|
||||
type ContextKey string
|
||||
@@ -234,6 +85,47 @@ type Authentication struct {
|
||||
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")
|
||||
}
|
||||
|
||||
return &User{
|
||||
Username: username,
|
||||
Projects: projects,
|
||||
Roles: roles,
|
||||
AuthType: AuthSession,
|
||||
AuthSource: -1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Init(db *sqlx.DB,
|
||||
configs map[string]interface{}) (*Authentication, error) {
|
||||
auth := &Authentication{}
|
||||
@@ -257,19 +149,11 @@ func Init(db *sqlx.DB,
|
||||
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{}
|
||||
if err := auth.JwtAuth.Init(auth, configs["jwt"]); err != nil {
|
||||
log.Error("Error while initializing authentication -> jwtAuth init failed")
|
||||
return nil, err
|
||||
}
|
||||
auth.authenticators = append(auth.authenticators, auth.JwtAuth)
|
||||
|
||||
if config, ok := configs["ldap"]; ok {
|
||||
auth.LdapAuth = &LdapAuthenticator{}
|
||||
@@ -280,36 +164,30 @@ func Init(db *sqlx.DB,
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
onsuccess http.Handler,
|
||||
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
|
||||
@@ -317,18 +195,21 @@ func (auth *Authentication) Login(
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
err := errors.New("no authenticator applied")
|
||||
username := r.FormValue("username")
|
||||
user := (*User)(nil)
|
||||
dbUser := (*User)(nil)
|
||||
|
||||
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 {
|
||||
if !authenticator.CanLogin(user, rw, r) {
|
||||
if !authenticator.CanLogin(dbUser, rw, r) {
|
||||
continue
|
||||
}
|
||||
|
||||
user, err = authenticator.Login(user, rw, r)
|
||||
user, err := authenticator.Login(dbUser, rw, r)
|
||||
if err != nil {
|
||||
log.Warnf("user login failed: %s", err.Error())
|
||||
onfailure(rw, r, err)
|
||||
@@ -354,6 +235,14 @@ func (auth *Authentication) Login(
|
||||
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)
|
||||
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
@@ -365,39 +254,34 @@ 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(
|
||||
onsuccess http.Handler,
|
||||
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error)) http.Handler {
|
||||
|
||||
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 user == nil {
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("authentication failed: %s", "no authenticator applied")
|
||||
// http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
onfailure(rw, r, errors.New("unauthorized (login first or use a token)"))
|
||||
log.Debug("authentication failed: no authenticator applied")
|
||||
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
||||
})
|
||||
}
|
||||
|
||||
// Clears the session cookie
|
||||
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
|
||||
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
session, err := auth.sessionStore.Get(r, "session")
|
||||
if err != nil {
|
||||
|
Reference in New Issue
Block a user