cc-backend/internal/auth/auth.go

367 lines
9.1 KiB
Go
Raw Normal View History

// 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 (
"context"
2022-07-25 09:33:36 +02:00
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/http"
2022-07-25 09:33:36 +02:00
"os"
"time"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/gorilla/sessions"
"github.com/jmoiron/sqlx"
)
const (
2022-09-13 07:38:18 +02:00
RoleAdmin string = "admin"
RoleSupport string = "support"
RoleManager string = "manager"
2022-09-13 07:38:18 +02:00
RoleUser string = "user"
RoleApi string = "api"
)
var validRoles = [5]string{RoleUser, RoleManager, RoleSupport, RoleAdmin, RoleApi}
2022-07-07 14:08:37 +02:00
const (
AuthViaLocalPassword int8 = 0
AuthViaLDAP int8 = 1
AuthViaToken int8 = 2
)
type User struct {
Username string `json:"username"`
Password string `json:"-"`
Name string `json:"name"`
Roles []string `json:"roles"`
AuthSource int8 `json:"via"`
Email string `json:"email"`
Projects []string `json:"projects"`
2022-07-07 14:08:37 +02:00
Expiration time.Time
}
func (u *User) HasRole(role string) bool {
for _, r := range u.Roles {
if r == role {
return true
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAnyRole(queryroles []string) bool {
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == qr {
return true
}
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAllRoles(queryroles []string) bool {
target := len(queryroles)
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == 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 []string) bool {
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == qr {
matches += 1
break
}
}
}
if matches == 0 {
return true
} else {
return false
}
}
// Find highest role, returns integer
func (u *User) GetAuthLevel() int {
if u.HasRole(RoleAdmin) {
return 5
} else if u.HasRole(RoleSupport) {
return 4
} else if u.HasRole(RoleManager) {
return 3
} else if u.HasRole(RoleUser) {
return 2
} else if u.HasRole(RoleApi) {
return 1
} else {
return 0
}
}
func (u *User) HasProject(project string) bool {
for _, p := range u.Projects {
if p == project {
return true
}
}
return false
}
func IsValidRole(role string) bool {
for _, r := range validRoles {
if r == role {
return true
}
}
return false
}
func GetValidRoles(user *User) ([5]string, error) {
var vals [5]string
if !user.HasRole(RoleAdmin) {
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
} else {
return validRoles, nil
}
}
2022-07-07 14:08:37 +02:00
func GetUser(ctx context.Context) *User {
x := ctx.Value(ContextUserKey)
if x == nil {
return nil
}
return x.(*User)
}
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
const ContextUserKey ContextKey = "user"
2022-02-14 14:22:44 +01:00
type Authentication struct {
db *sqlx.DB
sessionStore *sessions.CookieStore
SessionMaxAge time.Duration
2022-07-07 14:08:37 +02:00
authenticators []Authenticator
LdapAuth *LdapAuthenticator
2022-07-07 14:08:37 +02:00
JwtAuth *JWTAuthenticator
LocalAuth *LocalAuthenticator
2022-02-14 14:22:44 +01:00
}
2022-09-06 15:43:57 +02:00
func Init(db *sqlx.DB,
configs map[string]interface{}) (*Authentication, error) {
2022-07-07 14:08:37 +02:00
auth := &Authentication{}
2022-02-14 14:22:44 +01:00
auth.db = db
2022-03-03 14:54:37 +01:00
2022-07-25 09:33:36 +02:00
sessKey := os.Getenv("SESSION_KEY")
if sessKey == "" {
log.Warn("environment variable 'SESSION_KEY' not set (will use non-persistent random key)")
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
log.Error("Error while initializing authentication -> failed to generate random bytes for session key")
2022-07-25 09:33:36 +02:00
return nil, err
}
auth.sessionStore = sessions.NewCookieStore(bytes)
} else {
bytes, err := base64.StdEncoding.DecodeString(sessKey)
if err != nil {
log.Error("Error while initializing authentication -> decoding session key failed")
2022-07-25 09:33:36 +02:00
return nil, err
}
auth.sessionStore = sessions.NewCookieStore(bytes)
}
2022-07-07 14:08:37 +02:00
auth.LocalAuth = &LocalAuthenticator{}
if err := auth.LocalAuth.Init(auth, nil); err != nil {
log.Error("Error while initializing authentication -> localAuth init failed")
2022-07-07 14:08:37 +02:00
return nil, err
2022-03-03 14:54:37 +01:00
}
2022-07-07 14:08:37 +02:00
auth.authenticators = append(auth.authenticators, auth.LocalAuth)
2022-03-03 14:54:37 +01:00
2022-07-07 14:08:37 +02:00
auth.JwtAuth = &JWTAuthenticator{}
if err := auth.JwtAuth.Init(auth, configs["jwt"]); err != nil {
log.Error("Error while initializing authentication -> jwtAuth init failed")
2022-07-07 14:08:37 +02:00
return nil, err
}
2022-07-07 14:08:37 +02:00
auth.authenticators = append(auth.authenticators, auth.JwtAuth)
2022-07-07 14:08:37 +02:00
if config, ok := configs["ldap"]; ok {
auth.LdapAuth = &LdapAuthenticator{}
2022-07-07 14:08:37 +02:00
if err := auth.LdapAuth.Init(auth, config); err != nil {
log.Error("Error while initializing authentication -> ldapAuth init failed")
2022-02-14 14:22:44 +01:00
return nil, err
}
2022-07-07 14:08:37 +02:00
auth.authenticators = append(auth.authenticators, auth.LdapAuth)
}
2022-07-07 14:08:37 +02:00
return auth, nil
}
func (auth *Authentication) AuthViaSession(
rw http.ResponseWriter,
r *http.Request) (*User, error) {
2022-07-07 14:08:37 +02:00
session, err := auth.sessionStore.Get(r, "session")
if err != nil {
log.Error("Error while getting session store")
2022-07-07 14:08:37 +02:00
return nil, err
2022-03-15 11:04:54 +01:00
}
2022-07-07 14:08:37 +02:00
if session.IsNew {
return nil, nil
2022-03-15 11:04:54 +01:00
}
2022-03-17 10:54:17 +01:00
2022-07-07 14:08:37 +02:00
username, _ := session.Values["username"].(string)
projects, _ := session.Values["projects"].([]string)
2022-07-07 14:08:37 +02:00
roles, _ := session.Values["roles"].([]string)
return &User{
Username: username,
Projects: projects,
2022-07-07 14:08:37 +02:00
Roles: roles,
AuthSource: -1,
}, nil
2022-03-15 11:04:54 +01:00
}
2022-02-14 14:22:44 +01:00
// 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 {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
2022-07-25 10:36:20 +02:00
var err error = errors.New("no authenticator applied")
2022-07-07 14:08:37 +02:00
username := r.FormValue("username")
user := (*User)(nil)
if username != "" {
if user, _ = auth.GetUser(username); err != nil {
// log.Warnf("login of unkown user %v", username)
2022-07-26 11:00:41 +02:00
_ = err
}
}
2022-07-07 14:08:37 +02:00
for _, authenticator := range auth.authenticators {
if !authenticator.CanLogin(user, rw, r) {
continue
}
2022-07-07 14:08:37 +02:00
user, err = authenticator.Login(user, rw, r)
if err != nil {
log.Warnf("user '%s' login failed: %s", user.Username, err.Error())
2022-07-07 14:08:37 +02:00
onfailure(rw, r, err)
return
}
2022-07-07 14:08:37 +02:00
session, err := auth.sessionStore.New(r, "session")
if err != nil {
log.Errorf("session creation failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2022-07-07 14:08:37 +02:00
if auth.SessionMaxAge != 0 {
session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
}
session.Values["username"] = user.Username
session.Values["projects"] = user.Projects
2022-07-07 14:08:37 +02:00
session.Values["roles"] = user.Roles
if err := auth.sessionStore.Save(r, rw, session); err != nil {
log.Warnf("session save failed: %s", err.Error())
2022-07-07 14:08:37 +02:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
2022-07-07 14:08:37 +02:00
ctx := context.WithValue(r.Context(), ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
2022-07-25 09:33:36 +02:00
return
}
2022-07-07 14:08:37 +02:00
log.Warn("login failed: no authenticator applied")
onfailure(rw, r, err)
})
}
// 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) {
2022-07-07 14:08:37 +02:00
for _, authenticator := range auth.authenticators {
user, err := authenticator.Auth(rw, r)
if err != nil {
log.Warnf("authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized)
return
}
if user == nil {
continue
}
ctx := context.WithValue(r.Context(), ContextUserKey, user)
2022-02-14 14:22:44 +01:00
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
2022-07-25 09:33:36 +02:00
return
}
2022-07-07 14:08:37 +02:00
log.Warnf("authentication failed: %s", "no authenticator applied")
2022-07-25 09:33:36 +02:00
// http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
onfailure(rw, r, errors.New("unauthorized (login first or use a token)"))
})
}
// Clears the session cookie
2022-02-14 14:22:44 +01:00
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
2022-02-14 14:22:44 +01:00
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
session, err := auth.sessionStore.Get(r, "session")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2022-02-14 14:22:44 +01:00
if !session.IsNew {
session.Options.MaxAge = -1
if err := auth.sessionStore.Save(r, rw, session); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
onsuccess.ServeHTTP(rw, r)
})
}