...

Source file src/edge-infra.dev/pkg/sds/remoteaccess/authserver/checks.go

Documentation: edge-infra.dev/pkg/sds/remoteaccess/authserver

     1  package authserver
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"path"
     7  	"strings"
     8  
     9  	"github.com/gin-contrib/sessions"
    10  	"github.com/gin-gonic/gin"
    11  
    12  	authproxy "edge-infra.dev/pkg/edge/auth-proxy/types"
    13  )
    14  
    15  // httpError wraps an error with an additional statusCode method. Any httpError
    16  // returned from an authURL method can be used to override the returned status
    17  // code when an error occurrs during the processing of a check function. The
    18  // default returned status code is 500 InternalServerError.
    19  type httpError struct {
    20  	statusCode int
    21  	err        error
    22  }
    23  
    24  func (h *httpError) StatusCode() int {
    25  	return h.statusCode
    26  }
    27  
    28  func (h *httpError) Error() string {
    29  	return h.err.Error()
    30  }
    31  
    32  func (h *httpError) Unwrap() error {
    33  	return h.err
    34  }
    35  
    36  type checkFunc func(*AuthServer, *gin.Context, sessions.Session) error
    37  
    38  // The check type can be used to run a check function against a subset of URL
    39  // paths. If the check returns an error the request is denied, optionally
    40  // setting the response status code to that set by the httpError.
    41  type check struct {
    42  	// For the checkFunc to run, the pathFilter must must be in the incoming
    43  	// request path. An empty pathFilter will apply to all routes.
    44  	pathFilter string
    45  	checkFunc  checkFunc
    46  }
    47  
    48  // allChecks is a slice defining all checks that will be run
    49  var allChecks = []check{
    50  	{
    51  		pathFilter: "",
    52  		checkFunc:  (*AuthServer).rejectDuplicateSlashes,
    53  	},
    54  	{
    55  		pathFilter: "",
    56  		checkFunc:  (*AuthServer).sessionUsernameCheck,
    57  	},
    58  	{
    59  		pathFilter: "/novnc/",
    60  		checkFunc:  (*AuthServer).validateVNCRoles,
    61  	},
    62  	{
    63  		// BWC with old stores, can be removed in n+2
    64  		pathFilter: "/novnc/authorize",
    65  		checkFunc:  (*AuthServer).injectVNCAuthHeaders,
    66  	},
    67  	{
    68  		pathFilter: "/novnc/write/authorize",
    69  		checkFunc:  (*AuthServer).injectVNCAuthHeaders,
    70  	},
    71  	{
    72  		pathFilter: "/novnc/read/authorize",
    73  		checkFunc:  (*AuthServer).injectVNCAuthHeaders,
    74  	},
    75  }
    76  
    77  // authFilter runs all applicable check functions for the incoming request,
    78  // returning the first error encountered or nil.
    79  func (as *AuthServer) authFilter(ctx *gin.Context, session sessions.Session) error {
    80  	for _, check := range as.checks {
    81  		if !strings.Contains(ctx.Request.URL.Path, check.pathFilter) {
    82  			continue
    83  		}
    84  
    85  		err := check.checkFunc(as, ctx, session)
    86  		if err != nil {
    87  			return err
    88  		}
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  // setBoolHeader sets a response header from the supplied value. The header
    95  // value is set to "1" if the supplied value is true, otherwise deletes the
    96  // header if the value is false.
    97  func setBoolHeader(ctx *gin.Context, header string, value bool) {
    98  	if value {
    99  		ctx.Header(header, "1")
   100  	} else {
   101  		ctx.Header(header, "")
   102  	}
   103  }
   104  
   105  // getClusterEdgeIDFromPath returns the second path element of the incoming
   106  // request if the first path element matches "remoteaccess", otherwise returns
   107  // an empty string.
   108  func getClusterEdgeIDFromPath(path string) string {
   109  	parts := strings.Split(path, "/")
   110  	// strings.Split sets the first element to an empty string if the path
   111  	// begins with a /
   112  	if len(parts) < 4 || parts[1] != "remoteaccess" {
   113  		return ""
   114  	}
   115  	return parts[2]
   116  }
   117  
   118  // ======================= Check function definitions ======================= //
   119  
   120  // rejectDuplicateSlashes is a checkFunc which is used to reject any connection
   121  // attempts that include unexpected patterns in the path segment of the request.
   122  // This is done to ensure that authserver's authFilter behaviour is consistent
   123  // regardless of any downstream servers behaviour when handling these patterns.
   124  // Example unexpected patterns are //, /./, and /../
   125  func (as *AuthServer) rejectDuplicateSlashes(ctx *gin.Context, _ sessions.Session) error {
   126  	if ctx.Request.URL.Path == "" || ctx.Request.URL.Path == "/" {
   127  		// Allow access to the root path and when no path is specified
   128  		return nil
   129  	}
   130  
   131  	originalPath := ctx.Request.URL.EscapedPath()
   132  
   133  	// Paths are allowed to contain a single trailing slash. More than one
   134  	// trailing slash should be rejected.
   135  	originalPath, _ = strings.CutSuffix(originalPath, "/")
   136  
   137  	// Temporarily allow ws connection to have a single and double slash, this
   138  	// exception should be removed once the store side fix has filtered through
   139  	// all environments
   140  	// https://github.com/ncrvoyix-swt-retail/edge-roadmap/issues/13628
   141  	originalPath = strings.Replace(originalPath, "/novnc//ws", "/novnc/ws", 1)
   142  
   143  	cleanedPath := ctx.Request.URL.EscapedPath()
   144  	cleanedPath = path.Clean(cleanedPath)
   145  
   146  	if cleanedPath != originalPath {
   147  		return &httpError{
   148  			statusCode: http.StatusBadRequest,
   149  			err:        fmt.Errorf("invalid path"),
   150  		}
   151  	}
   152  	return nil
   153  }
   154  
   155  func (as *AuthServer) sessionUsernameCheck(ctx *gin.Context, session sessions.Session) error {
   156  	usr, ok := session.Get(authproxy.SessionUsernameField).(string)
   157  	if !ok {
   158  		return &httpError{
   159  			statusCode: http.StatusUnauthorized,
   160  			err:        fmt.Errorf("Could not find session username"),
   161  		}
   162  	}
   163  	ctx.Header(headerKeyWebauthUser, usr)
   164  	return nil
   165  }
   166  

View as plain text