mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-06-17 17:07:29 +02:00
Secrets (JWT keys, LDAP sync password, OIDC client id/secret, cross-login keys) are now configured directly in config.json under the auth section where they are used. Each secret can still be supplied via its existing environment variable, which takes precedence over the config value. The godotenv dependency, the .env file, configs/env-template.txt and the loadEnvironment() bootstrap step are removed. -init now writes the demo JWT keys into config.json instead of a .env file. Closes #283 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 3a7cb814c53f
689 lines
22 KiB
Go
689 lines
22 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package auth implements various authentication methods
|
|
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
|
"github.com/ClusterCockpit/cc-lib/v2/util"
|
|
"github.com/alexedwards/scs/sqlite3store"
|
|
"github.com/alexedwards/scs/v2"
|
|
)
|
|
|
|
// Authenticator is the interface for all authentication methods.
|
|
// Each authenticator determines if it can handle a login request (CanLogin)
|
|
// and performs the actual authentication (Login).
|
|
type Authenticator interface {
|
|
// CanLogin determines if this authenticator can handle the login request.
|
|
// It returns the user object if available and a boolean indicating if this
|
|
// authenticator should attempt the login. This method should not perform
|
|
// expensive operations or actual authentication.
|
|
CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)
|
|
|
|
// Login performs the actually authentication for the user.
|
|
// It returns the authenticated user or an error if authentication fails.
|
|
// The user parameter may be nil if the user doesn't exist in the database yet.
|
|
Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
|
|
}
|
|
|
|
var (
|
|
initOnce sync.Once
|
|
authInstance *Authentication
|
|
)
|
|
|
|
// rateLimiterEntry tracks a rate limiter and its last use time for cleanup
|
|
type rateLimiterEntry struct {
|
|
limiter *rate.Limiter
|
|
lastUsed time.Time
|
|
}
|
|
|
|
var ipUserLimiters sync.Map
|
|
|
|
// getIPUserLimiter returns a rate limiter for the given IP and username combination.
|
|
// Rate limiters are created on demand and track 5 attempts per 15 minutes.
|
|
func getIPUserLimiter(ip, username string) *rate.Limiter {
|
|
key := ip + ":" + username
|
|
now := time.Now()
|
|
|
|
if entry, ok := ipUserLimiters.Load(key); ok {
|
|
rle := entry.(*rateLimiterEntry)
|
|
rle.lastUsed = now
|
|
return rle.limiter
|
|
}
|
|
|
|
// More aggressive rate limiting: 5 attempts per 15 minutes
|
|
newLimiter := rate.NewLimiter(rate.Every(15*time.Minute/5), 5)
|
|
ipUserLimiters.Store(key, &rateLimiterEntry{
|
|
limiter: newLimiter,
|
|
lastUsed: now,
|
|
})
|
|
return newLimiter
|
|
}
|
|
|
|
// cleanupOldRateLimiters removes rate limiters that haven't been used recently
|
|
func cleanupOldRateLimiters(olderThan time.Time) {
|
|
ipUserLimiters.Range(func(key, value any) bool {
|
|
entry := value.(*rateLimiterEntry)
|
|
if entry.lastUsed.Before(olderThan) {
|
|
ipUserLimiters.Delete(key)
|
|
cclog.Debugf("Cleaned up rate limiter for %v", key)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// startRateLimiterCleanup starts a background goroutine to clean up old rate limiters
|
|
func startRateLimiterCleanup() {
|
|
go func() {
|
|
ticker := time.NewTicker(1 * time.Hour)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
// Clean up limiters not used in the last 24 hours
|
|
cleanupOldRateLimiters(time.Now().Add(-24 * time.Hour))
|
|
}
|
|
}()
|
|
}
|
|
|
|
// AuthConfig contains configuration for all authentication methods
|
|
type AuthConfig struct {
|
|
LdapConfig *LdapConfig `json:"ldap"`
|
|
JwtConfig *JWTAuthConfig `json:"jwts"`
|
|
OpenIDConfig *OpenIDConfig `json:"oidc"`
|
|
}
|
|
|
|
// Keys holds the global authentication configuration
|
|
var Keys AuthConfig
|
|
|
|
// secretFromEnv resolves a secret from the environment or config. The
|
|
// environment variable takes precedence when set and non-empty; otherwise the
|
|
// value configured in config.json is used. This lets deployments inject secrets
|
|
// via the environment (or a secret manager) while keeping config.json
|
|
// self-contained for simple setups.
|
|
func secretFromEnv(envVar, configValue string) string {
|
|
if v := os.Getenv(envVar); v != "" {
|
|
return v
|
|
}
|
|
return configValue
|
|
}
|
|
|
|
// Authentication manages all authentication methods and session handling
|
|
type Authentication struct {
|
|
sessionManager *scs.SessionManager
|
|
LdapAuth *LdapAuthenticator
|
|
JwtAuth *JWTAuthenticator
|
|
LocalAuth *LocalAuthenticator
|
|
authenticators []Authenticator
|
|
SessionMaxAge time.Duration
|
|
}
|
|
|
|
// SessionManager exposes the scs session manager so the HTTP router can install
|
|
// the session middleware (LoadAndSave on write paths, LoadSession on read paths).
|
|
func (auth *Authentication) SessionManager() *scs.SessionManager {
|
|
return auth.sessionManager
|
|
}
|
|
|
|
// LoadSession is a non-buffering, read-only session middleware. It loads any
|
|
// existing session into the request context so AuthViaSession can read it, but
|
|
// (unlike scs.LoadAndSave) it never wraps the ResponseWriter and never commits,
|
|
// so large responses (e.g. the GraphQL /query endpoint) stream without being
|
|
// buffered in memory. Use it on routes that only read the session to
|
|
// authenticate; use scs.LoadAndSave on the login/logout routes that mutate it.
|
|
func (auth *Authentication) LoadSession(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
var token string
|
|
if c, err := r.Cookie(auth.sessionManager.Cookie.Name); err == nil {
|
|
token = c.Value
|
|
}
|
|
ctx, err := auth.sessionManager.Load(r.Context(), token)
|
|
if err != nil {
|
|
cclog.Errorf("session load failed: %s", err.Error())
|
|
http.Error(rw, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
next.ServeHTTP(rw, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// expireSessionCookie clears a (corrupted) session cookie on the client. Used on
|
|
// read paths where there is no commit to drive the deletion.
|
|
func expireSessionCookie(rw http.ResponseWriter) {
|
|
http.SetCookie(rw, &http.Cookie{
|
|
Name: "session",
|
|
Path: "/",
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func (auth *Authentication) AuthViaSession(
|
|
rw http.ResponseWriter,
|
|
r *http.Request,
|
|
) (*schema.User, error) {
|
|
// The session data was loaded into the request context by the LoadSession
|
|
// middleware. No active session cookie => not logged in (mirrors session.IsNew).
|
|
ctx := r.Context()
|
|
if !auth.sessionManager.Exists(ctx, "username") {
|
|
return nil, nil
|
|
}
|
|
|
|
// Validate session data with proper type checking
|
|
username := auth.sessionManager.GetString(ctx, "username")
|
|
if username == "" {
|
|
cclog.Warn("Invalid session: missing or invalid username")
|
|
expireSessionCookie(rw)
|
|
return nil, errors.New("invalid session data")
|
|
}
|
|
|
|
projects, ok := auth.sessionManager.Get(ctx, "projects").([]string)
|
|
if !ok {
|
|
cclog.Warn("Invalid session: projects not found or invalid type, using empty list")
|
|
projects = []string{}
|
|
}
|
|
|
|
roles, ok := auth.sessionManager.Get(ctx, "roles").([]string)
|
|
if !ok || len(roles) == 0 {
|
|
cclog.Warn("Invalid session: missing or invalid roles")
|
|
expireSessionCookie(rw)
|
|
return nil, errors.New("invalid session data")
|
|
}
|
|
|
|
// GetInt returns 0 (== schema.AuthViaLocalPassword) when the key is absent.
|
|
authSourceInt := auth.sessionManager.GetInt(ctx, "authSource")
|
|
|
|
return &schema.User{
|
|
Username: username,
|
|
Projects: projects,
|
|
Roles: roles,
|
|
AuthType: schema.AuthSession,
|
|
AuthSource: schema.AuthSource(authSourceInt),
|
|
}, nil
|
|
}
|
|
|
|
func Init(authCfg *json.RawMessage) {
|
|
initOnce.Do(func() {
|
|
authInstance = &Authentication{}
|
|
|
|
// Start background cleanup of rate limiters
|
|
startRateLimiterCleanup()
|
|
|
|
// Server-side sessions via scs, persisted in the existing SQLite DB so
|
|
// sessions survive restarts. Only an opaque random token is stored in the
|
|
// cookie, so no secret signing key (the former SESSION_KEY) is required.
|
|
gob.Register([]string{}) // user.Projects / user.Roles are stored as []string
|
|
sm := scs.New()
|
|
sm.Store = sqlite3store.New(repository.GetConnection().DB.DB)
|
|
sm.Cookie.Name = "session"
|
|
sm.Cookie.Path = "/"
|
|
sm.Cookie.HttpOnly = true
|
|
sm.Cookie.SameSite = http.SameSiteLaxMode
|
|
// scs sets Secure globally (no per-request option). Enable it when this
|
|
// process terminates TLS itself. Deployments terminating TLS at a reverse
|
|
// proxy can set this via a future config flag if needed.
|
|
sm.Cookie.Secure = config.Keys.HTTPSCertFile != ""
|
|
|
|
if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err == nil && d != 0 {
|
|
sm.Lifetime = d
|
|
authInstance.SessionMaxAge = d
|
|
} else {
|
|
// SessionMaxAge of 0/empty means "do not expire": approximate with a
|
|
// long absolute lifetime (the cookie remains persistent).
|
|
sm.Lifetime = 10 * 365 * 24 * time.Hour
|
|
}
|
|
authInstance.sessionManager = sm
|
|
|
|
// When authentication is disabled no authenticators are required; the
|
|
// session store created above is enough for the server to run with a
|
|
// valid (non-nil) auth instance.
|
|
if config.Keys.DisableAuthentication {
|
|
return
|
|
}
|
|
|
|
if authCfg == nil {
|
|
return
|
|
}
|
|
|
|
config.Validate(configSchema, *authCfg)
|
|
dec := json.NewDecoder(bytes.NewReader(*authCfg))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&Keys); err != nil {
|
|
cclog.Errorf("error while decoding ldap config: %v", err)
|
|
}
|
|
|
|
if Keys.LdapConfig != nil {
|
|
ldapAuth := &LdapAuthenticator{}
|
|
if err := ldapAuth.Init(); err != nil {
|
|
cclog.Warn("Error while initializing authentication -> ldapAuth init failed")
|
|
} else {
|
|
authInstance.LdapAuth = ldapAuth
|
|
authInstance.authenticators = append(authInstance.authenticators, authInstance.LdapAuth)
|
|
}
|
|
} else {
|
|
cclog.Info("Missing LDAP configuration: No LDAP support!")
|
|
}
|
|
|
|
if Keys.JwtConfig != nil {
|
|
authInstance.JwtAuth = &JWTAuthenticator{}
|
|
if err := authInstance.JwtAuth.Init(); err != nil {
|
|
cclog.Fatal("Error while initializing authentication -> jwtAuth init failed")
|
|
}
|
|
|
|
jwtSessionAuth := &JWTSessionAuthenticator{}
|
|
if err := jwtSessionAuth.Init(); err != nil {
|
|
cclog.Info("jwtSessionAuth init failed: No JWT login support!")
|
|
} else {
|
|
authInstance.authenticators = append(authInstance.authenticators, jwtSessionAuth)
|
|
}
|
|
|
|
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
|
|
if err := jwtCookieSessionAuth.Init(); err != nil {
|
|
cclog.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
|
|
} else {
|
|
authInstance.authenticators = append(authInstance.authenticators, jwtCookieSessionAuth)
|
|
}
|
|
} else {
|
|
cclog.Info("Missing JWT configuration: No JWT token support!")
|
|
}
|
|
|
|
authInstance.LocalAuth = &LocalAuthenticator{}
|
|
if err := authInstance.LocalAuth.Init(); err != nil {
|
|
cclog.Fatal("Error while initializing authentication -> localAuth init failed")
|
|
}
|
|
authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth)
|
|
})
|
|
}
|
|
|
|
func GetAuthInstance() *Authentication {
|
|
if authInstance == nil {
|
|
cclog.Fatal("Authentication module not initialized!")
|
|
}
|
|
|
|
return authInstance
|
|
}
|
|
|
|
// handleUserSync syncs or updates a user in the database based on configuration.
|
|
// This is used for LDAP, JWT and OIDC authentications when syncUserOnLogin or updateUserOnLogin is enabled.
|
|
func handleUserSync(user *schema.User, syncUserOnLogin, updateUserOnLogin bool) {
|
|
r := repository.GetUserRepository()
|
|
dbUser, err := r.GetUser(user.Username)
|
|
|
|
if err != nil && err != sql.ErrNoRows {
|
|
cclog.Errorf("Error while loading user '%s': %v", user.Username, err)
|
|
return
|
|
}
|
|
|
|
if err == sql.ErrNoRows && syncUserOnLogin { // Add new user
|
|
if err := r.AddUser(user); err != nil {
|
|
cclog.Errorf("Error while adding user '%s' to DB: %v", user.Username, err)
|
|
}
|
|
} else if err == nil && updateUserOnLogin { // Update existing user
|
|
if err := r.UpdateUser(dbUser, user); err != nil {
|
|
cclog.Errorf("Error while updating user '%s' in DB: %v", dbUser.Username, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleTokenUser syncs JWT token user with database
|
|
func handleTokenUser(tokenUser *schema.User) {
|
|
handleUserSync(tokenUser, Keys.JwtConfig.SyncUserOnLogin, Keys.JwtConfig.UpdateUserOnLogin)
|
|
}
|
|
|
|
// handleOIDCUser syncs OIDC user with database
|
|
func handleOIDCUser(OIDCUser *schema.User) {
|
|
handleUserSync(OIDCUser, Keys.OpenIDConfig.SyncUserOnLogin, Keys.OpenIDConfig.UpdateUserOnLogin)
|
|
}
|
|
|
|
// handleLdapUser syncs LDAP user with database
|
|
func handleLdapUser(ldapUser *schema.User) {
|
|
handleUserSync(ldapUser, Keys.LdapConfig.SyncUserOnLogin, Keys.LdapConfig.UpdateUserOnLogin)
|
|
}
|
|
|
|
func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error {
|
|
// The login routes are wrapped by scs.LoadAndSave, which loaded the session
|
|
// into the request context and will commit it (persist to the store and write
|
|
// the Set-Cookie header) after the handler returns.
|
|
ctx := r.Context()
|
|
|
|
// Generate a new session token to prevent session fixation.
|
|
if err := auth.sessionManager.RenewToken(ctx); err != nil {
|
|
cclog.Errorf("session renew failed: %s", err.Error())
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
|
|
auth.sessionManager.Put(ctx, "username", user.Username)
|
|
auth.sessionManager.Put(ctx, "projects", user.Projects)
|
|
auth.sessionManager.Put(ctx, "roles", user.Roles)
|
|
auth.sessionManager.Put(ctx, "authSource", int(user.AuthSource))
|
|
|
|
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() {
|
|
cclog.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 {
|
|
cclog.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 {
|
|
cclog.Debugf("Can login with user %v", user)
|
|
}
|
|
|
|
user, err := authenticator.Login(user, rw, r)
|
|
if err != nil {
|
|
cclog.Warnf("user login failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
|
|
if err := auth.SaveSession(rw, r, user); err != nil {
|
|
return
|
|
}
|
|
|
|
cclog.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
|
|
if redirect := r.FormValue("redirect"); redirect != "" {
|
|
if u, perr := url.Parse(redirect); perr == nil && u.Scheme == "" && u.Host == "" {
|
|
http.RedirectHandler(redirect, http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
cclog.Warnf("login redirect rejected (not a relative path): %q", redirect)
|
|
}
|
|
|
|
http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
cclog.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 {
|
|
cclog.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 {
|
|
cclog.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
|
|
}
|
|
|
|
cclog.Infof("auth -> authentication failed: no valid session or JWT for %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
|
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 {
|
|
cclog.Infof("auth api -> authentication failed: %s", err.Error())
|
|
onfailure(rw, r, err)
|
|
return
|
|
}
|
|
|
|
ipErr := securedCheck(user, r)
|
|
if ipErr != nil {
|
|
cclog.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:
|
|
cclog.Info("auth api -> authentication failed: missing role")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
}
|
|
}
|
|
cclog.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 {
|
|
cclog.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.RoleSupport, schema.RoleAdmin}) {
|
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
|
return
|
|
}
|
|
default:
|
|
cclog.Info("auth user api -> authentication failed: missing role")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
}
|
|
}
|
|
cclog.Info("auth user api -> authentication failed: no auth")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
})
|
|
}
|
|
|
|
func (auth *Authentication) AuthMetricStoreAPI(
|
|
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 {
|
|
cclog.Infof("auth metricstore 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:
|
|
cclog.Info("auth metricstore api -> authentication failed: missing role")
|
|
onfailure(rw, r, errors.New("unauthorized"))
|
|
}
|
|
}
|
|
cclog.Info("auth metricstore 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 {
|
|
cclog.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
|
|
}
|
|
cclog.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 {
|
|
cclog.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
|
|
}
|
|
cclog.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) {
|
|
// The logout route is wrapped by scs.LoadAndSave: Destroy removes the
|
|
// session from the store and the middleware clears the cookie on the way out.
|
|
if err := auth.sessionManager.Destroy(r.Context()); 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
|
|
}
|
|
|
|
// Handle both IPv4 and IPv6 addresses properly
|
|
// For IPv6, this will strip the port and brackets
|
|
// For IPv4, this will strip the port
|
|
if host, _, err := net.SplitHostPort(IPAddress); err == nil {
|
|
IPAddress = host
|
|
}
|
|
// If SplitHostPort fails, IPAddress is already just a host (no port)
|
|
|
|
// If nothing declared in config: Continue // FIXME: Allow All If Not Declared?
|
|
if len(config.Keys.APIAllowedIPs) == 0 {
|
|
return nil
|
|
}
|
|
// 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
|
|
}
|