mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-10-08 23:24:32 +02:00
.github
api
cmd
configs
init
internal
api
archiver
auth
auth.go
jwt.go
jwtCookieSession.go
jwtSession.go
ldap.go
local.go
oidc.go
config
graph
importer
metricDataDispatcher
metricdata
repository
routerConfig
tagger
taskManager
util
pkg
tools
web
.gitignore
.goreleaser.yaml
LICENSE
Makefile
README.md
ReleaseNotes.md
go.mod
go.sum
gqlgen.yml
startDemo.sh
tools.go
495 lines
15 KiB
Go
495 lines
15 KiB
Go
// Copyright (C) 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"
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
|
"github.com/ClusterCockpit/cc-backend/internal/util"
|
|
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
|
"github.com/ClusterCockpit/cc-backend/pkg/schema"
|
|
"github.com/gorilla/sessions"
|
|
)
|
|
|
|
type Authenticator interface {
|
|
CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)
|
|
Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
|
|
}
|
|
|
|
var (
|
|
initOnce sync.Once
|
|
authInstance *Authentication
|
|
)
|
|
|
|
var ipUserLimiters sync.Map
|
|
|
|
func getIPUserLimiter(ip, username string) *rate.Limiter {
|
|
key := ip + ":" + username
|
|
limiter, ok := ipUserLimiters.Load(key)
|
|
if !ok {
|
|
newLimiter := rate.NewLimiter(rate.Every(time.Hour/10), 10)
|
|
ipUserLimiters.Store(key, newLimiter)
|
|
return newLimiter
|
|
}
|
|
return limiter.(*rate.Limiter)
|
|
}
|
|
|
|
type Authentication struct {
|
|
sessionStore *sessions.CookieStore
|
|
LdapAuth *LdapAuthenticator
|
|
JwtAuth *JWTAuthenticator
|
|
LocalAuth *LocalAuthenticator
|
|
authenticators []Authenticator
|
|
SessionMaxAge time.Duration
|
|
}
|
|
|
|
func (auth *Authentication) AuthViaSession(
|
|
rw http.ResponseWriter,
|
|
r *http.Request,
|
|
) (*schema.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 session keys exist
|
|
username, _ := session.Values["username"].(string)
|
|
projects, _ := session.Values["projects"].([]string)
|
|
roles, _ := session.Values["roles"].([]string)
|
|
return &schema.User{
|
|
Username: username,
|
|
Projects: projects,
|
|
Roles: roles,
|
|
AuthType: schema.AuthSession,
|
|
AuthSource: -1,
|
|
}, nil
|
|
}
|
|
|
|
func Init() {
|
|
initOnce.Do(func() {
|
|
authInstance = &Authentication{}
|
|
|
|
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.Fatal("Error while initializing authentication -> failed to generate random bytes for session key")
|
|
}
|
|
authInstance.sessionStore = sessions.NewCookieStore(bytes)
|
|
} else {
|
|
bytes, err := base64.StdEncoding.DecodeString(sessKey)
|
|
if err != nil {
|
|
log.Fatal("Error while initializing authentication -> decoding session key failed")
|
|
}
|
|
authInstance.sessionStore = sessions.NewCookieStore(bytes)
|
|
}
|
|
|
|
if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err == nil {
|
|
authInstance.SessionMaxAge = d
|
|
}
|
|
|
|
if config.Keys.LdapConfig != nil {
|
|
ldapAuth := &LdapAuthenticator{}
|
|
if err := ldapAuth.Init(); err != nil {
|
|
log.Warn("Error while initializing authentication -> ldapAuth init failed")
|
|
} else {
|
|
authInstance.LdapAuth = ldapAuth
|
|
authInstance.authenticators = append(authInstance.authenticators, authInstance.LdapAuth)
|
|
}
|
|
} else {
|
|
log.Info("Missing LDAP configuration: No LDAP support!")
|
|
}
|
|
|
|
if config.Keys.JwtConfig != nil {
|
|
authInstance.JwtAuth = &JWTAuthenticator{}
|
|
if err := authInstance.JwtAuth.Init(); err != nil {
|
|
log.Fatal("Error while initializing authentication -> jwtAuth init failed")
|
|
}
|
|
|
|
jwtSessionAuth := &JWTSessionAuthenticator{}
|
|
if err := jwtSessionAuth.Init(); err != nil {
|
|
log.Info("jwtSessionAuth init failed: No JWT login support!")
|
|
} else {
|
|
authInstance.authenticators = append(authInstance.authenticators, jwtSessionAuth)
|
|
}
|
|
|
|
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
|
|
if err := jwtCookieSessionAuth.Init(); err != nil {
|
|
log.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
|
|
} else {
|
|
authInstance.authenticators = append(authInstance.authenticators, jwtCookieSessionAuth)
|
|
}
|
|
} else {
|
|
log.Info("Missing JWT configuration: No JWT token support!")
|
|
}
|
|
|
|
authInstance.LocalAuth = &LocalAuthenticator{}
|
|
if err := authInstance.LocalAuth.Init(); err != nil {
|
|
log.Fatal("Error while initializing authentication -> localAuth init failed")
|
|
}
|
|
authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth)
|
|
})
|
|
}
|
|
|
|
func GetAuthInstance() *Authentication {
|
|
if authInstance == nil {
|
|
log.Fatal("Authentication module not initialized!")
|
|
}
|
|
|
|
return authInstance
|
|
}
|
|
|
|
func handleTokenUser(tokenUser *schema.User) {
|
|
r := repository.GetUserRepository()
|
|
dbUser, err := r.GetUser(tokenUser.Username)
|
|
|
|
if err != nil && err != sql.ErrNoRows {
|
|
log.Errorf("Error while loading user '%s': %v", tokenUser.Username, err)
|
|
} else if err == sql.ErrNoRows && config.Keys.JwtConfig.SyncUserOnLogin { // Adds New User
|
|
if err := r.AddUser(tokenUser); err != nil {
|
|
log.Errorf("Error while adding user '%s' to DB: %v", tokenUser.Username, err)
|
|
}
|
|
} else if err == nil && config.Keys.JwtConfig.UpdateUserOnLogin { // Update Existing User
|
|
if err := r.UpdateUser(dbUser, tokenUser); err != nil {
|
|
log.Errorf("Error while updating user '%s' to DB: %v", dbUser.Username, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleOIDCUser(OIDCUser *schema.User) {
|
|
r := repository.GetUserRepository()
|
|
dbUser, err := r.GetUser(OIDCUser.Username)
|
|
|
|
if err != nil && err != sql.ErrNoRows {
|
|
log.Errorf("Error while loading user '%s': %v", OIDCUser.Username, err)
|
|
} else if err == sql.ErrNoRows && config.Keys.OpenIDConfig.SyncUserOnLogin { // Adds New User
|
|
if err := r.AddUser(OIDCUser); err != nil {
|
|
log.Errorf("Error while adding user '%s' to DB: %v", OIDCUser.Username, err)
|
|
}
|
|
} else if err == nil && config.Keys.OpenIDConfig.UpdateUserOnLogin { // Update Existing User
|
|
if err := r.UpdateUser(dbUser, OIDCUser); err != nil {
|
|
log.Errorf("Error while updating user '%s' to DB: %v", dbUser.Username, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error {
|
|
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 err
|
|
}
|
|
|
|
if auth.SessionMaxAge != 0 {
|
|
session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
|
|
}
|
|
if config.Keys.HttpsCertFile == "" && config.Keys.HttpsKeyFile == "" {
|
|
session.Options.Secure = false
|
|
}
|
|
session.Options.SameSite = http.SameSiteStrictMode
|
|
session.Values["username"] = user.Username
|
|
session.Values["projects"] = user.Projects
|
|
session.Values["roles"] = user.Roles
|
|
if err := auth.sessionStore.Save(r, rw, session); err != nil {
|
|
log.Warnf("session save failed: %s", err.Error())
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (auth *Authentication) Login(
|
|
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
ip = r.RemoteAddr
|
|
}
|
|
|
|
username := r.FormValue("username")
|
|
|
|
limiter := getIPUserLimiter(ip, username)
|
|
if !limiter.Allow() {
|
|
log.Warnf("AUTH/RATE > Too many login attempts for combination IP: %s, Username: %s", ip, username)
|
|
onfailure(rw, r, errors.New("too many login attempts, try again in a few minutes"))
|
|
return
|
|
}
|
|
|
|
var dbUser *schema.User
|
|
if username != "" {
|
|
var err error
|
|
dbUser, err = repository.GetUserRepository().GetUser(username)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
log.Errorf("Error while loading user '%v'", username)
|
|
}
|
|
}
|
|
|
|
for _, authenticator := range auth.authenticators {
|
|
var ok bool
|
|
var user *schema.User
|
|
if user, ok = authenticator.CanLogin(dbUser, username, rw, r); !ok {
|
|
continue
|
|
} else {
|
|
log.Debugf("Can login with user %v", user)
|
|
}
|
|
|
|
user, err := authenticator.Login(user, rw, r)
|
|
if err != nil {
|
|
log.Warnf("user login failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
|
|
if err := auth.SaveSession(rw, r, user); err != nil {
|
|
return
|
|
}
|
|
|
|
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
|
|
if r.FormValue("redirect") != "" {
|
|
http.RedirectHandler(r.FormValue("redirect"), http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
log.Debugf("login failed: no authenticator applied")
|
|
onfailure(rw, r, errors.New("no authenticator applied"))
|
|
})
|
|
}
|
|
|
|
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) {
|
|
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
|
if err != nil {
|
|
log.Infof("auth -> authentication failed: %s", err.Error())
|
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if user == nil {
|
|
user, err = auth.AuthViaSession(rw, r)
|
|
if err != nil {
|
|
log.Infof("auth -> authentication failed: %s", err.Error())
|
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
if user != nil {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
log.Info("auth -> authentication failed")
|
|
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
|
})
|
|
}
|
|
|
|
func (auth *Authentication) AuthApi(
|
|
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) {
|
|
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
|
if err != nil {
|
|
log.Infof("auth api -> authentication failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
|
|
ipErr := securedCheck(user, r)
|
|
if ipErr != nil {
|
|
log.Infof("auth api -> secured check failed: %s", ipErr.Error())
|
|
onfailure(rw, r, ipErr)
|
|
return
|
|
}
|
|
|
|
if user != nil {
|
|
switch {
|
|
case len(user.Roles) == 1:
|
|
if user.HasRole(schema.RoleApi) {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
case len(user.Roles) >= 2:
|
|
if user.HasAllRoles([]schema.Role{schema.RoleAdmin, schema.RoleApi}) {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
default:
|
|
log.Info("auth api -> authentication failed: missing role")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
}
|
|
}
|
|
log.Info("auth api -> authentication failed: no auth")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
})
|
|
}
|
|
|
|
func (auth *Authentication) AuthUserApi(
|
|
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) {
|
|
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
|
if err != nil {
|
|
log.Infof("auth user api -> authentication failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
|
|
if user != nil {
|
|
switch {
|
|
case len(user.Roles) == 1:
|
|
if user.HasRole(schema.RoleApi) {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
case len(user.Roles) >= 2:
|
|
if user.HasRole(schema.RoleApi) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleAdmin}) {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
default:
|
|
log.Info("auth user api -> authentication failed: missing role")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
}
|
|
}
|
|
log.Info("auth user api -> authentication failed: no auth")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
})
|
|
}
|
|
|
|
func (auth *Authentication) AuthConfigApi(
|
|
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) {
|
|
user, err := auth.AuthViaSession(rw, r)
|
|
if err != nil {
|
|
log.Infof("auth config api -> authentication failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
if user != nil && user.HasRole(schema.RoleAdmin) {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
log.Info("auth config api -> authentication failed: no auth")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
})
|
|
}
|
|
|
|
func (auth *Authentication) AuthFrontendApi(
|
|
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) {
|
|
user, err := auth.AuthViaSession(rw, r)
|
|
if err != nil {
|
|
log.Infof("auth frontend api -> authentication failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
if user != nil {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
log.Info("auth frontend api -> authentication failed: no auth")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
})
|
|
}
|
|
|
|
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 {
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
// Helper Moved To MiddleWare Auth Handlers
|
|
func securedCheck(user *schema.User, r *http.Request) error {
|
|
if user == nil {
|
|
return fmt.Errorf("no user for secured check")
|
|
}
|
|
|
|
// extract IP address for checking
|
|
IPAddress := r.Header.Get("X-Real-Ip")
|
|
if IPAddress == "" {
|
|
IPAddress = r.Header.Get("X-Forwarded-For")
|
|
}
|
|
if IPAddress == "" {
|
|
IPAddress = r.RemoteAddr
|
|
}
|
|
|
|
if strings.Contains(IPAddress, ":") {
|
|
IPAddress = strings.Split(IPAddress, ":")[0]
|
|
}
|
|
|
|
// If nothing declared in config: deny all request to this api endpoint
|
|
if len(config.Keys.ApiAllowedIPs) == 0 {
|
|
return fmt.Errorf("missing configuration key ApiAllowedIPs")
|
|
}
|
|
// If wildcard declared in config: Continue
|
|
if config.Keys.ApiAllowedIPs[0] == "*" {
|
|
return nil
|
|
}
|
|
// check if IP is allowed
|
|
if !util.Contains(config.Keys.ApiAllowedIPs, IPAddress) {
|
|
return fmt.Errorf("unknown ip: %v", IPAddress)
|
|
}
|
|
|
|
return nil
|
|
}
|