Skip to content

Commit

Permalink
Merge branch 'master' into fakeclient-for-restclient-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MatousJobanek authored Sep 30, 2024
2 parents 489731a + a657f36 commit 0fa2ae7
Show file tree
Hide file tree
Showing 7 changed files with 823 additions and 79 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.20

require (
github.com/aws/aws-sdk-go v1.44.100
github.com/codeready-toolchain/api v0.0.0-20240815232340-d0c164a83d27
github.com/codeready-toolchain/api v0.0.0-20240927104325-b5bfcb3cb1b0
github.com/codeready-toolchain/toolchain-common v0.0.0-20240905135929-d55d86fdd41e
github.com/go-logr/logr v1.4.1
github.com/gofrs/uuid v4.2.0+incompatible
Expand Down Expand Up @@ -149,7 +149,7 @@ require (
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/atomic v1.10.0
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/codeready-toolchain/api v0.0.0-20240815232340-d0c164a83d27 h1:uEH8HAM81QZBccuqQpGKJUoJQe28+DFSYi/mRKZDYrA=
github.com/codeready-toolchain/api v0.0.0-20240815232340-d0c164a83d27/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E=
github.com/codeready-toolchain/api v0.0.0-20240927104325-b5bfcb3cb1b0 h1:7cXHlRpoi1Owo8fYawl80PUsVWz+9AtMge6OJ4DjvWU=
github.com/codeready-toolchain/api v0.0.0-20240927104325-b5bfcb3cb1b0/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E=
github.com/codeready-toolchain/toolchain-common v0.0.0-20240905135929-d55d86fdd41e h1:xTqyuImyon/P2QfV5NIJDsVkEWqb9b6Ax9INsmzpI1Q=
github.com/codeready-toolchain/toolchain-common v0.0.0-20240905135929-d55d86fdd41e/go.mod h1:aIbki5CFsykeqZn2/ZwvUb3Krx2f2Tbq58R6MGnk6H8=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
Expand Down
3 changes: 3 additions & 0 deletions make/go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ tidy:
vet:
go vet ./...

.PHONY: pre-verify
pre-verify:
echo "No Pre-requisite needed"
244 changes: 216 additions & 28 deletions pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/codeready-toolchain/registration-service/pkg/proxy/access"
"github.com/codeready-toolchain/registration-service/pkg/proxy/handlers"
"github.com/codeready-toolchain/registration-service/pkg/proxy/metrics"
"github.com/codeready-toolchain/registration-service/pkg/signup"
commoncluster "github.com/codeready-toolchain/toolchain-common/pkg/cluster"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
Expand Down Expand Up @@ -119,6 +120,7 @@ func (p *Proxy) StartProxy(port string) *http.Server {
}
},
p.ensureUserIsNotBanned(),
p.addPublicViewerContext(),
)

// middleware after routing
Expand All @@ -142,6 +144,7 @@ func (p *Proxy) StartProxy(port string) *http.Server {
// Space lister routes
wg.GET("/:workspace", handlers.HandleSpaceGetRequest(p.spaceLister, p.getMembersFunc))
wg.GET("", handlers.HandleSpaceListRequest(p.spaceLister))

router.GET(proxyHealthEndpoint, p.health)
// SSO routes. Used by web login (oc login -w).
// Here is the expected flow for the "oc login -w" command:
Expand Down Expand Up @@ -269,47 +272,211 @@ func (p *Proxy) health(ctx echo.Context) error {
}

func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, error) {
// retrieve required information from the HTTP request
userID, _ := ctx.Get(context.SubKey).(string)
username, _ := ctx.Get(context.UsernameKey).(string)
proxyPluginName, workspaceName, err := getWorkspaceContext(ctx.Request())
if err != nil {
return "", nil, crterrors.NewBadRequest("unable to get workspace context", err.Error())
}

ctx.Set(context.WorkspaceKey, workspaceName) // set workspace context for logging
cluster, err := p.app.MemberClusterService().GetClusterAccess(userID, username, workspaceName, proxyPluginName, false)
if err != nil {
return "", nil, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error())
}
// set workspace context for logging
ctx.Set(context.WorkspaceKey, workspaceName)

// before proxying the request, verify that the user has a spacebinding for the workspace and that the namespace (if any) belongs to the workspace
var workspaces []toolchainv1alpha1.Workspace
if workspaceName != "" {
// when a workspace name was provided
// validate that the user has access to the workspace by getting all spacebindings recursively, starting from this workspace and going up to the parent workspaces till the "root" of the workspace tree.
workspace, err := handlers.GetUserWorkspace(ctx, p.spaceLister, workspaceName)
// if the target workspace is NOT explicitly declared in the HTTP request,
// process the request against the user's home workspace
if workspaceName == "" {
cluster, err := p.processHomeWorkspaceRequest(ctx, userID, username, proxyPluginName)
if err != nil {
return "", nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error())
}
if workspace == nil {
// not found
return "", nil, crterrors.NewForbiddenError("invalid workspace request", fmt.Sprintf("access to workspace '%s' is forbidden", workspaceName))
}
// workspace was found means we can forward the request
workspaces = []toolchainv1alpha1.Workspace{*workspace}
} else {
// list all workspaces
workspaces, err = handlers.ListUserWorkspaces(ctx, p.spaceLister)
if err != nil {
return "", nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error())
return "", nil, err
}
return proxyPluginName, cluster, nil
}

// if the target workspace is explicitly declared in the HTTP request,
// process the request against the declared workspace
cluster, err := p.processWorkspaceRequest(ctx, userID, username, workspaceName, proxyPluginName)
if err != nil {
return "", nil, err
}
return proxyPluginName, cluster, nil
}

// processHomeWorkspaceRequest process an HTTP Request targeting the user's home workspace.
func (p *Proxy) processHomeWorkspaceRequest(ctx echo.Context, userID, username, proxyPluginName string) (*access.ClusterAccess, error) {
// retrieves the ClusterAccess for the user and their home workspace
cluster, err := p.app.MemberClusterService().GetClusterAccess(userID, username, "", proxyPluginName, false)
if err != nil {
return nil, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error())
}

// list all workspaces the user has access to
workspaces, err := handlers.ListUserWorkspaces(ctx, p.spaceLister)
if err != nil {
return nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error())
}

// check whether the user has access to the home workspace
// and whether the requestedNamespace -if any- exists in the workspace.
requestedNamespace := namespaceFromCtx(ctx)
if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil {
return "", nil, crterrors.NewForbiddenError("invalid workspace request", err.Error())
if err := validateWorkspaceRequest("", requestedNamespace, workspaces...); err != nil {
return nil, crterrors.NewForbiddenError("invalid workspace request", err.Error())
}

return proxyPluginName, cluster, nil
// return the cluster access
return cluster, nil
}

// processWorkspaceRequest process an HTTP Request targeting a specific workspace.
func (p *Proxy) processWorkspaceRequest(ctx echo.Context, userID, username, workspaceName, proxyPluginName string) (*access.ClusterAccess, error) {
// check that the user is provisioned and the space exists.
// if the PublicViewer support is enabled, user check is skipped.
if err := p.checkUserIsProvisionedAndSpaceExists(ctx, userID, username, workspaceName); err != nil {
return nil, err
}

// retrieve the requested Workspace with SpaceBindings
workspace, err := p.getUserWorkspaceWithBindings(ctx, workspaceName)
if err != nil {
return nil, err
}

// check whether the user has access to the workspace
// and whether the requestedNamespace -if any- exists in the workspace.
requestedNamespace := namespaceFromCtx(ctx)
if err := validateWorkspaceRequest(workspaceName, requestedNamespace, *workspace); err != nil {
return nil, crterrors.NewForbiddenError("invalid workspace request", err.Error())
}

// retrieve the ClusterAccess for the user and the target workspace
return p.getClusterAccess(ctx, userID, username, proxyPluginName, workspace)
}

// checkUserIsProvisionedAndSpaceExists checks that the user is provisioned and the Space exists.
// If the PublicViewer support is enabled, User check is skipped.
func (p *Proxy) checkUserIsProvisionedAndSpaceExists(ctx echo.Context, userID, username, workspaceName string) error {
if err := p.checkUserIsProvisioned(ctx, userID, username); err != nil {
return crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error())
}
if err := p.checkSpaceExists(workspaceName); err != nil {
return crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error())
}
return nil
}

// checkSpaceExists checks whether the Space exists.
func (p *Proxy) checkSpaceExists(workspaceName string) error {
if _, err := p.app.InformerService().GetSpace(workspaceName); err != nil {
// log the actual error but do not return it so that it doesn't reveal information about a space that may not belong to the requestor
log.Errorf(nil, err, "requested space '%s' does not exist", workspaceName)
return fmt.Errorf("access to workspace '%s' is forbidden", workspaceName)
}
return nil
}

// checkUserIsProvisioned checks whether the user is Approved, if they are not an error is returned.
// If public-viewer is enabled, user validation is skipped.
func (p *Proxy) checkUserIsProvisioned(ctx echo.Context, userID, username string) error {
// skip if public-viewer is enabled: read-only operations on community workspaces are always permitted.
if context.IsPublicViewerEnabled(ctx) {
return nil
}

// retrieve the UserSignup for the requesting user.
//
// UserSignup complete status is not checked, since it might cause the proxy blocking the request
// and returning an error when quick transitions from ready to provisioning are happening.
userSignup, err := p.app.SignupService().GetSignupFromInformer(nil, userID, username, false)
if err != nil {
return err
}

// if the UserSignup is nil or has NOT the CompliantUsername set,
// it means that MUR was NOT created and useraccount is NOT provisioned yet
if userSignup == nil || userSignup.CompliantUsername == "" {
cause := errs.New("user is not provisioned (yet)")
log.Error(nil, cause, fmt.Sprintf("signup object: %+v", userSignup))
return cause
}
return nil
}

// getClusterAccess retrieves the access to the cluster hosting the requested workspace,
// if the user has access to it.
// Access can be either direct (a SpaceBinding linking the user to the workspace exists)
// or community (a SpaceBinding linking the PublicViewer user to the workspace exists).
func (p *Proxy) getClusterAccess(ctx echo.Context, userID, username, proxyPluginName string, workspace *toolchainv1alpha1.Workspace) (*access.ClusterAccess, error) {
// retrieve cluster access as requesting user or PublicViewer
cluster, err := p.getClusterAccessAsUserOrPublicViewer(ctx, userID, username, proxyPluginName, workspace)
if err != nil {
return nil, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error())
}
return cluster, nil
}

// getClusterAccessAsUserOrPublicViewer if the requesting user exists and has direct access to the workspace,
// this function returns the ClusterAccess impersonating the requesting user.
// If PublicViewer support is enabled and PublicViewer user has access to the workspace,
// this function returns the ClusterAccess impersonating the PublicViewer user.
// If requesting user does not exists and PublicViewer is disabled or does not have access to the workspace,
// this function returns an error.
func (p *Proxy) getClusterAccessAsUserOrPublicViewer(ctx echo.Context, userID, username, proxyPluginName string, workspace *toolchainv1alpha1.Workspace) (*access.ClusterAccess, error) {
// retrieve the requesting user's UserSignup
userSignup, err := p.app.SignupService().GetSignupFromInformer(nil, userID, username, false)
if err != nil {
log.Error(nil, err, fmt.Sprintf("error retrieving user signup for userID '%s' and username '%s'", userID, username))
return nil, crterrors.NewInternalError(errs.New("unable to get user info"), "error retrieving user")
}

// proceed as PublicViewer if the feature is enabled and userSignup is nil
publicViewerEnabled := context.IsPublicViewerEnabled(ctx)
if publicViewerEnabled && !userHasDirectAccess(userSignup, workspace) {
return p.app.MemberClusterService().GetClusterAccess(
toolchainv1alpha1.KubesawAuthenticatedUsername,
toolchainv1alpha1.KubesawAuthenticatedUsername,
workspace.Name,
proxyPluginName,
publicViewerEnabled)
}

// otherwise retrieve the ClusterAccess for the cluster hosting the workspace and the given user.
return p.app.MemberClusterService().GetClusterAccess(userID, username, workspace.Name, proxyPluginName, publicViewerEnabled)
}

// userHasDirectAccess checks if an UserSignup has access to a workspace.
// Workspace's bindings are obtained from its `status.bindings` property.
func userHasDirectAccess(signup *signup.Signup, workspace *toolchainv1alpha1.Workspace) bool {
if signup == nil {
return false
}

return userHasBinding(signup.CompliantUsername, workspace)
}

func userHasBinding(username string, workspace *toolchainv1alpha1.Workspace) bool {
for _, b := range workspace.Status.Bindings {
if b.MasterUserRecord == username {
return true
}
}
return false

}

// getUserWorkspaceWithBindings retrieves the workspace with the SpaceBindings if the requesting user has access to it.
// User access to the Workspace is checked by getting all spacebindings recursively,
// starting from this workspace and going up to the parent workspaces till the "root" of the workspace tree.
func (p *Proxy) getUserWorkspaceWithBindings(ctx echo.Context, workspaceName string) (*toolchainv1alpha1.Workspace, error) {
workspace, err := handlers.GetUserWorkspaceWithBindings(ctx, p.spaceLister, workspaceName, p.getMembersFunc)
if err != nil {
return nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error())
}
if workspace == nil {
// not found
return nil, crterrors.NewForbiddenError("invalid workspace request", fmt.Sprintf("access to workspace '%s' is forbidden", workspaceName))
}
// workspace was found means we can forward the request
return workspace, nil
}

func (p *Proxy) handleRequestAndRedirect(ctx echo.Context) error {
Expand Down Expand Up @@ -409,6 +576,18 @@ func (p *Proxy) addUserContext() echo.MiddlewareFunc {
}
}

// addPublicViewerContext updates echo.Context with the configuration's PublicViewerEnabled value.
func (p *Proxy) addPublicViewerContext() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
publicViewerEnabled := configuration.GetRegistrationServiceConfig().PublicViewerEnabled()
ctx.Set(context.PublicViewerEnabled, publicViewerEnabled)

return next(ctx)
}
}
}

// ensureUserIsNotBanned rejects the request if the user is banned.
// This Middleware requires the context to contain the email of the user,
// so it needs to be executed after the `addUserContext` Middleware.
Expand Down Expand Up @@ -503,11 +682,17 @@ func extractUserToken(req *http.Request) (string, error) {
func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, isPlugin bool) *httputil.ReverseProxy {
req := ctx.Request()
targetQuery := target.APIURL().RawQuery
username, _ := ctx.Get(context.UsernameKey).(string)
// set username in context for logging purposes
ctx.Set(context.ImpersonateUser, target.Username())

director := func(req *http.Request) {
origin := req.URL.String()
req.URL.Scheme = target.APIURL().Scheme
req.URL.Host = target.APIURL().Host
req.URL.Path = singleJoiningSlash(target.APIURL().Path, req.URL.Path)
req.Header.Set("X-SSO-User", username)

if isPlugin {
// for non k8s clients testing, like vanilla http clients accessing plugin proxy flows, testing has proven that the request
// host needs to be updated in addition to the URL in order to have the reverse proxy contact the openshift
Expand Down Expand Up @@ -671,7 +856,10 @@ func replaceTokenInWebsocketRequest(req *http.Request, newToken string) {
req.Header.Set(ph, strings.Join(protocols, ","))
}

func validateWorkspaceRequest(requestedWorkspace, requestedNamespace string, workspaces []toolchainv1alpha1.Workspace) error {
// validateWorkspaceRequest checks whether the requested workspace is in the list of workspaces the user has visibility on (retrieved via the spaceLister).
// If `requestedWorkspace` is zero, this function looks for the home workspace (the one with `status.Type` set to `home`).
// If `requestedNamespace` is NOT zero, this function checks if the namespace exists in the workspace.
func validateWorkspaceRequest(requestedWorkspace, requestedNamespace string, workspaces ...toolchainv1alpha1.Workspace) error {
// check workspace access
isHomeWSRequested := requestedWorkspace == ""

Expand Down
Loading

0 comments on commit 0fa2ae7

Please sign in to comment.