package authserver import ( "fmt" "net/http" "path" "strings" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" authproxy "edge-infra.dev/pkg/edge/auth-proxy/types" ) // httpError wraps an error with an additional statusCode method. Any httpError // returned from an authURL method can be used to override the returned status // code when an error occurrs during the processing of a check function. The // default returned status code is 500 InternalServerError. type httpError struct { statusCode int err error } func (h *httpError) StatusCode() int { return h.statusCode } func (h *httpError) Error() string { return h.err.Error() } func (h *httpError) Unwrap() error { return h.err } type checkFunc func(*AuthServer, *gin.Context, sessions.Session) error // The check type can be used to run a check function against a subset of URL // paths. If the check returns an error the request is denied, optionally // setting the response status code to that set by the httpError. type check struct { // For the checkFunc to run, the pathFilter must must be in the incoming // request path. An empty pathFilter will apply to all routes. pathFilter string checkFunc checkFunc } // allChecks is a slice defining all checks that will be run var allChecks = []check{ { pathFilter: "", checkFunc: (*AuthServer).rejectDuplicateSlashes, }, { pathFilter: "", checkFunc: (*AuthServer).sessionUsernameCheck, }, { pathFilter: "/novnc/", checkFunc: (*AuthServer).validateVNCRoles, }, { // BWC with old stores, can be removed in n+2 pathFilter: "/novnc/authorize", checkFunc: (*AuthServer).injectVNCAuthHeaders, }, { pathFilter: "/novnc/write/authorize", checkFunc: (*AuthServer).injectVNCAuthHeaders, }, { pathFilter: "/novnc/read/authorize", checkFunc: (*AuthServer).injectVNCAuthHeaders, }, } // authFilter runs all applicable check functions for the incoming request, // returning the first error encountered or nil. func (as *AuthServer) authFilter(ctx *gin.Context, session sessions.Session) error { for _, check := range as.checks { if !strings.Contains(ctx.Request.URL.Path, check.pathFilter) { continue } err := check.checkFunc(as, ctx, session) if err != nil { return err } } return nil } // setBoolHeader sets a response header from the supplied value. The header // value is set to "1" if the supplied value is true, otherwise deletes the // header if the value is false. func setBoolHeader(ctx *gin.Context, header string, value bool) { if value { ctx.Header(header, "1") } else { ctx.Header(header, "") } } // getClusterEdgeIDFromPath returns the second path element of the incoming // request if the first path element matches "remoteaccess", otherwise returns // an empty string. func getClusterEdgeIDFromPath(path string) string { parts := strings.Split(path, "/") // strings.Split sets the first element to an empty string if the path // begins with a / if len(parts) < 4 || parts[1] != "remoteaccess" { return "" } return parts[2] } // ======================= Check function definitions ======================= // // rejectDuplicateSlashes is a checkFunc which is used to reject any connection // attempts that include unexpected patterns in the path segment of the request. // This is done to ensure that authserver's authFilter behaviour is consistent // regardless of any downstream servers behaviour when handling these patterns. // Example unexpected patterns are //, /./, and /../ func (as *AuthServer) rejectDuplicateSlashes(ctx *gin.Context, _ sessions.Session) error { if ctx.Request.URL.Path == "" || ctx.Request.URL.Path == "/" { // Allow access to the root path and when no path is specified return nil } originalPath := ctx.Request.URL.EscapedPath() // Paths are allowed to contain a single trailing slash. More than one // trailing slash should be rejected. originalPath, _ = strings.CutSuffix(originalPath, "/") // Temporarily allow ws connection to have a single and double slash, this // exception should be removed once the store side fix has filtered through // all environments // https://github.com/ncrvoyix-swt-retail/edge-roadmap/issues/13628 originalPath = strings.Replace(originalPath, "/novnc//ws", "/novnc/ws", 1) cleanedPath := ctx.Request.URL.EscapedPath() cleanedPath = path.Clean(cleanedPath) if cleanedPath != originalPath { return &httpError{ statusCode: http.StatusBadRequest, err: fmt.Errorf("invalid path"), } } return nil } func (as *AuthServer) sessionUsernameCheck(ctx *gin.Context, session sessions.Session) error { usr, ok := session.Get(authproxy.SessionUsernameField).(string) if !ok { return &httpError{ statusCode: http.StatusUnauthorized, err: fmt.Errorf("Could not find session username"), } } ctx.Header(headerKeyWebauthUser, usr) return nil }