Skip to content
Snippets Groups Projects
auth.go 5.27 KiB
Newer Older
  • Learn to ignore specific revisions
  • package auth
    
    import (
    	"context"
    	"encoding/json"
    	"errors"
    	"fmt"
    	"net"
    	"net/http"
    	"strings"
    
    
    	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/common"
    	"forge.grandlyon.com/web-et-numerique/factory/llle_project/backoffice-server/internal/tokens"
    
    )
    
    type key int
    
    const (
    	authTokenKey string = "auth_token"
    	// ContextData is the user
    	ContextData key = 0
    )
    
    var (
    
    	AnimatorRole = common.StringValueFromEnv("ANIMATOR_ROLE", "ANIMATORS")
    
    	// AdminRole represents the role reserved for admins
    
    	AdminRole   = common.StringValueFromEnv("ADMIN_ROLE", "ADMINS")
    	hostname    = common.StringValueFromEnv("HOSTNAME", "ecolyobackoffice.127.0.0.1.nip.io")
    	SGEApiToken = common.StringValueFromEnv("SGE_API_TOKEN", "sgetoken")
    
    )
    
    // User represents a logged in user
    type User struct {
    	ID           string   `json:"id,omitempty"`
    	Login        string   `json:"login"`
    	DisplayName  string   `json:"displayName,omitempty"`
    	Email        string   `json:"email,omitempty"`
    	Roles        []string `json:"memberOf"`
    	IsAdmin      bool     `json:"isAdmin,omitempty"`
    	Name         string   `json:"name,omitempty"`
    	Surname      string   `json:"surname,omitempty"`
    	PasswordHash string   `json:"passwordHash,omitempty"`
    	Password     string   `json:"password,omitempty"`
    }
    
    // TokenData represents the data held into a token
    type TokenData struct {
    	User
    	URL              string `json:"url,omitempty"`
    	ReadOnly         bool   `json:"readonly,omitempty"`
    	SharingUserLogin string `json:"sharinguserlogin,omitempty"`
    	XSRFToken        string `json:"xsrftoken,omitempty"`
    }
    
    
    func AnimatorAuthMiddleware(next http.Handler) http.Handler {
    	return ValidateAuthMiddleware(next, []string{AdminRole, AnimatorRole}, true)
    
    func AdminAuthMiddleware(next http.Handler) http.Handler {
    	return ValidateAuthMiddleware(next, []string{AdminRole}, true)
    
    // ValidateAuthMiddleware validates that the token is valid and that the user has the correct roles
    func ValidateAuthMiddleware(next http.Handler, allowedRoles []string, checkXSRF bool) http.Handler {
    	roleChecker := func(w http.ResponseWriter, r *http.Request) {
    		user := TokenData{}
    		checkXSRF, err := tokens.ExtractAndValidateToken(r, authTokenKey, &user, checkXSRF)
    		if err != nil {
    			// Handle CORS preflight requests
    			if r.Method == "OPTIONS" {
    				return
    			}
    			// Default to redirect to authentication
    			redirectTo := hostname
    			_, port, perr := net.SplitHostPort(r.Host)
    			if perr == nil {
    				redirectTo += ":" + port
    			}
    			w.Header().Set("Content-Type", "text/html")
    			w.WriteHeader(http.StatusUnauthorized)
    			responseContent := fmt.Sprintf("error extracting token: %v<meta http-equiv=\"Refresh\" content=\"0; url=https://%v#login\"/>", err.Error(), redirectTo)
    			fmt.Fprint(w, responseContent)
    			return
    
    		}
    		// Check XSRF Token
    		if checkXSRF && r.Header.Get("XSRF-TOKEN") != user.XSRFToken {
    			http.Error(w, "XSRF protection triggered", http.StatusUnauthorized)
    			return
    		}
    		err = checkUserHasRole(user, allowedRoles)
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusForbidden)
    			return
    		}
    		err = checkUserHasRole(user, []string{AdminRole})
    		if err == nil {
    			user.IsAdmin = true
    		}
    		// Check for url
    		if user.URL != "" {
    			requestURL := strings.Split(r.Host, ":")[0] + r.URL.EscapedPath()
    			if user.URL != requestURL {
    				http.Error(w, "token restricted to url: "+user.URL, http.StatusUnauthorized)
    				return
    			}
    		}
    		// Check for method
    		if user.ReadOnly && r.Method != http.MethodGet {
    			http.Error(w, "token is read only", http.StatusForbidden)
    			return
    		}
    		ctx := context.WithValue(r.Context(), ContextData, user)
    		next.ServeHTTP(w, r.WithContext(ctx))
    	}
    	return http.HandlerFunc(roleChecker)
    }
    
    
    func SGEAuthMiddleware(next http.Handler) http.Handler {
    	tokenChecker := func(w http.ResponseWriter, r *http.Request) {
    		// Check API Token
    		if r.Header.Get("Authorization") != "Bearer "+SGEApiToken {
    			http.Error(w, "invalid token", http.StatusUnauthorized)
    			return
    		}
    		next.ServeHTTP(w, r)
    	}
    	return http.HandlerFunc(tokenChecker)
    }
    
    
    // HandleLogout remove the user from the cookie store
    func (m Manager) HandleLogout(w http.ResponseWriter, r *http.Request) {
    	// Delete the auth cookie
    	c := http.Cookie{
    		Name:   authTokenKey,
    		Domain: m.Hostname,
    		MaxAge: -1,
    	}
    	http.SetCookie(w, &c)
    
    	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
    
    }
    
    // WhoAmI returns the user data
    func WhoAmI() http.Handler {
    	whoAmI := func(w http.ResponseWriter, r *http.Request) {
    		user, err := GetTokenData(r)
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusBadRequest)
    			return
    		}
    		json.NewEncoder(w).Encode(user)
    	}
    	return http.HandlerFunc(whoAmI)
    }
    
    // checkUserHasRole checks if the user has the required role
    func checkUserHasRole(user TokenData, allowedRoles []string) error {
    	for _, allowedRole := range allowedRoles {
    		if allowedRole == "*" {
    			return nil
    		}
    		for _, userRole := range user.Roles {
    			if userRole != "" && (userRole == allowedRole) {
    				return nil
    			}
    		}
    	}
    	return fmt.Errorf("no user role among %v is in allowed roles (%v)", user.Roles, allowedRoles)
    }
    
    // GetTokenData gets an user from a request
    func GetTokenData(r *http.Request) (TokenData, error) {
    	user, ok := r.Context().Value(ContextData).(TokenData)
    	if !ok {
    		return user, errors.New("user could not be got from context")
    	}
    	return user, nil
    }