diff --git a/go.mod b/go.mod index a41c3287..a6717390 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.19 require ( github.com/aws/aws-sdk-go v1.44.100 - github.com/codeready-toolchain/api v0.0.0-20230918195153-739e8fb09a33 - github.com/codeready-toolchain/toolchain-common v0.0.0-20231002120847-bf3a59c8351b + github.com/codeready-toolchain/api v0.0.0-20231010090546-098b27b43b3a + github.com/codeready-toolchain/toolchain-common v0.0.0-20231012065805-a23f3cfa676d github.com/go-logr/logr v1.2.3 github.com/gofrs/uuid v4.2.0+incompatible github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index c781dc69..39c4178d 100644 --- a/go.sum +++ b/go.sum @@ -61,7 +61,11 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= @@ -107,10 +111,10 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/codeready-toolchain/api v0.0.0-20230918195153-739e8fb09a33 h1:hxXfcFq2JgFISVxrkISg8m9DZMzpcPWRjPspx3M3Sxo= -github.com/codeready-toolchain/api v0.0.0-20230918195153-739e8fb09a33/go.mod h1:nn3W6eKb9PFIVwSwZW7wDeLACMBOwAV+4kddGuN+ARM= -github.com/codeready-toolchain/toolchain-common v0.0.0-20231002120847-bf3a59c8351b h1:3nk69kWcILOiu6Ul9DZZTRmx4pItVit+9uOAmjrYCfM= -github.com/codeready-toolchain/toolchain-common v0.0.0-20231002120847-bf3a59c8351b/go.mod h1:o/JGPWZ/9rVh/np0tcaPRXnreZ+X743o0Gxp1eP62/w= +github.com/codeready-toolchain/api v0.0.0-20231010090546-098b27b43b3a h1:UucbKqQ0bz9xe/Hr6kbrJkPK0YzCn2bdFwGme5rCfuU= +github.com/codeready-toolchain/api v0.0.0-20231010090546-098b27b43b3a/go.mod h1:nn3W6eKb9PFIVwSwZW7wDeLACMBOwAV+4kddGuN+ARM= +github.com/codeready-toolchain/toolchain-common v0.0.0-20231012065805-a23f3cfa676d h1:gQy0fpfCjl4XQhoXEQ3NUrvpRp4qKzov5TBKyePuwOM= +github.com/codeready-toolchain/toolchain-common v0.0.0-20231012065805-a23f3cfa676d/go.mod h1:SnZewh0DLwAELKLsW+R6NKaKmiRBuMI1iMYSkfyZG6A= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -293,6 +297,7 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslC github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -370,6 +375,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/migueleliasweb/go-github-mock v0.0.18 h1:0lWt9MYmZQGnQE2rFtjlft/YtD6hzxuN6JJRFpujzEI= github.com/migueleliasweb/go-github-mock v0.0.18/go.mod h1:CcgXcbMoRnf3rRVHqGssuBquZDIcaplxL2W6G+xs7kM= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -408,14 +415,17 @@ github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2Jn github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/redhat-cop/operator-utils v1.3.3-0.20220121120056-862ef22b8cdf h1:fsZiv9XuFo8G7IyzFWjG02vqzJG7kSqFvD1Wiq3V/o8= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -979,6 +989,7 @@ k8s.io/apiserver v0.25.0 h1:8kl2ifbNffD440MyvHtPaIz1mw4mGKVgWqM0nL+oyu4= k8s.io/apiserver v0.25.0/go.mod h1:BKwsE+PTC+aZK+6OJQDPr0v6uS91/HWxX7evElAH6xo= k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= +k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= @@ -986,6 +997,7 @@ k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/kubectl v0.24.0 h1:nA+WtMLVdXUs4wLogGd1mPTAesnLdBpCVgCmz3I7dXo= k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/proxy/handlers/spacelister.go b/pkg/proxy/handlers/spacelister.go index 85d06013..71b5492f 100644 --- a/pkg/proxy/handlers/spacelister.go +++ b/pkg/proxy/handlers/spacelister.go @@ -6,6 +6,8 @@ import ( "net/http" "sort" + "time" + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/registration-service/pkg/application" "github.com/codeready-toolchain/registration-service/pkg/application/service" @@ -14,8 +16,8 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/metrics" "github.com/codeready-toolchain/registration-service/pkg/signup" commonproxy "github.com/codeready-toolchain/toolchain-common/pkg/proxy" + "github.com/codeready-toolchain/toolchain-common/pkg/spacebinding" "github.com/gin-gonic/gin" - "time" "github.com/labstack/echo/v4" errs "github.com/pkg/errors" @@ -26,6 +28,15 @@ import ( "k8s.io/apimachinery/pkg/selection" ) +const ( + // UpdateBindingAction specifies that the current binding can be updated by providing a different Space Role. + UpdateBindingAction = "update" + // DeleteBindingAction specifies that the current binding can be deleted in order to revoke user access to the Space. + DeleteBindingAction = "delete" + // OverrideBindingAction specifies that the current binding can be overridden by creating a SpaceBindingRequest containing the same MUR but different Space Role. + OverrideBindingAction = "override" +) + type SpaceLister struct { GetSignupFunc func(ctx *gin.Context, userID, username string, checkUserSignupCompleted bool) (*signup.Signup, error) GetInformerServiceFunc func() service.InformerService @@ -71,6 +82,7 @@ func (s *SpaceLister) HandleSpaceGetRequest(ctx echo.Context) error { func (s *SpaceLister) GetUserWorkspace(ctx echo.Context) (*toolchainv1alpha1.Workspace, error) { userID, _ := ctx.Get(context.SubKey).(string) username, _ := ctx.Get(context.UsernameKey).(string) + workspaceName := ctx.Param("workspace") userSignup, err := s.GetSignupFunc(nil, userID, username, false) if err != nil { @@ -84,28 +96,41 @@ func (s *SpaceLister) GetUserWorkspace(ctx echo.Context) (*toolchainv1alpha1.Wor return nil, nil } - murName := userSignup.CompliantUsername - spaceBinding, err := s.listSpaceBindingForUserAndSpace(ctx, murName) + space, err := s.GetInformerServiceFunc().GetSpace(workspaceName) if err != nil { - ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) - return nil, err - } - if spaceBinding == nil { - // spacebinding not found, let's return a nil workspace which causes the handler to respond with a 404 status code + ctx.Logger().Error(errs.Wrap(err, "unable to get space")) return nil, nil } - space, err := s.getSpace(spaceBinding) + // recursively get all the spacebindings for the current workspace + listSpaceBindingsFunc := func(spaceName string) ([]toolchainv1alpha1.SpaceBinding, error) { + spaceSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingSpaceLabelKey, selection.Equals, []string{spaceName}) + if err != nil { + return nil, err + } + return s.GetInformerServiceFunc().ListSpaceBindings(*spaceSelector) + } + spaceBindingLister := spacebinding.NewLister(listSpaceBindingsFunc, s.GetInformerServiceFunc().GetSpace) + allSpaceBindings, err := spaceBindingLister.ListForSpace(space, []toolchainv1alpha1.SpaceBinding{}) if err != nil { - ctx.Logger().Error(errs.Wrap(err, "unable to get space")) + ctx.Logger().Error(err, "failed to list space bindings") return nil, err } - // ------------- - // TODO recursively get all the spacebindings for the current workspace - // and build the Bindings list with the available actions + // check if user has access to this workspace + userBinding := filterUserSpaceBinding(userSignup.CompliantUsername, allSpaceBindings) + if userBinding == nil { + // let's only log the issue and consider this as not found + ctx.Logger().Error(fmt.Sprintf("unauthorized access - there is no SpaceBinding present for the user %s and the workspace %s", userSignup.CompliantUsername, workspaceName)) + return nil, nil + } + // build the Bindings list with the available actions // this field is populated only for the GET workspace request - // ------------- + bindings, err := generateWorkspaceBindings(space, allSpaceBindings) + if err != nil { + ctx.Logger().Error(errs.Wrap(err, "unable to generate bindings field")) + return nil, err + } // add available roles, this field is populated only for the GET workspace request nsTemplateTier, err := s.GetInformerServiceFunc().GetNSTemplateTier(space.Spec.TierName) @@ -113,9 +138,11 @@ func (s *SpaceLister) GetUserWorkspace(ctx echo.Context) (*toolchainv1alpha1.Wor ctx.Logger().Error(errs.Wrap(err, "unable to get nstemplatetier")) return nil, err } - getOnlyWSOptions := commonproxy.WithAvailableRoles(getRolesFromNSTemplateTier(nsTemplateTier)) - return createWorkspaceObject(userSignup.Name, space, spaceBinding, getOnlyWSOptions), nil + return createWorkspaceObject(userSignup.Name, space, userBinding, + commonproxy.WithAvailableRoles(getRolesFromNSTemplateTier(nsTemplateTier)), + commonproxy.WithBindings(bindings), + ), nil } func (s *SpaceLister) ListUserWorkspaces(ctx echo.Context) ([]toolchainv1alpha1.Workspace, error) { @@ -151,38 +178,6 @@ func (s *SpaceLister) listSpaceBindingsForUser(murName string) ([]toolchainv1alp return s.GetInformerServiceFunc().ListSpaceBindings(requirements...) } -func (s *SpaceLister) listSpaceBindingForUserAndSpace(ctx echo.Context, murName string) (*toolchainv1alpha1.SpaceBinding, error) { - workspaceName := ctx.Param("workspace") - murSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey, selection.Equals, []string{murName}) - if err != nil { - return nil, err - } - // specific workspace requested so add label requirement to match the space - spaceSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingSpaceLabelKey, selection.Equals, []string{workspaceName}) - if err != nil { - return nil, err - } - requirements := []labels.Requirement{*murSelector, *spaceSelector} - - spaceBindings, err := s.GetInformerServiceFunc().ListSpaceBindings(requirements...) - if err != nil { - return nil, err - } - - if len(spaceBindings) == 0 { - // let's only log the issue and consider this as not found - ctx.Logger().Error(fmt.Sprintf("expected only 1 spacebinding, got 0 for user %s and workspace %s", murName, workspaceName)) - return nil, nil - } else if len(spaceBindings) > 1 { - // internal server error - cause := fmt.Errorf("expected only 1 spacebinding, got %d for user %s and workspace %s", len(spaceBindings), murName, workspaceName) - ctx.Logger().Error(cause.Error()) - return nil, cause - } - - return &spaceBindings[0], nil -} - func (s *SpaceLister) workspacesFromSpaceBindings(signupName string, spaceBindings []toolchainv1alpha1.SpaceBinding) []toolchainv1alpha1.Workspace { workspaces := []toolchainv1alpha1.Workspace{} for i := range spaceBindings { @@ -265,3 +260,62 @@ func errorResponse(ctx echo.Context, err *apierrors.StatusError) error { ctx.Response().Writer.WriteHeader(int(err.ErrStatus.Code)) return json.NewEncoder(ctx.Response().Writer).Encode(err.ErrStatus) } + +// filterUserSpaceBinding returns the spacebinding for a given username, or nil if not found +func filterUserSpaceBinding(username string, allSpaceBindings []toolchainv1alpha1.SpaceBinding) *toolchainv1alpha1.SpaceBinding { + for _, binding := range allSpaceBindings { + if binding.Spec.MasterUserRecord == username { + return &binding + } + } + return nil +} + +// generateWorkspaceBindings generates the bindings list starting from the spacebindings found on a given space resource and an all parent spaces. +// The Bindings list has the available actions for each entry in the list. +func generateWorkspaceBindings(space *toolchainv1alpha1.Space, spaceBindings []toolchainv1alpha1.SpaceBinding) ([]toolchainv1alpha1.Binding, error) { + var bindings []toolchainv1alpha1.Binding + for _, spaceBinding := range spaceBindings { + binding := toolchainv1alpha1.Binding{ + MasterUserRecord: spaceBinding.Spec.MasterUserRecord, + Role: spaceBinding.Spec.SpaceRole, + AvailableActions: []string{}, + } + spaceBindingSpaceName := spaceBinding.Labels[toolchainv1alpha1.SpaceBindingSpaceLabelKey] + sbrName, sbrNameFound := spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] + sbrNamespace, sbrNamespaceFound := spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] + // check if spacebinding was generated from SBR on the current space and not on a parentSpace. + if (sbrNameFound || sbrNamespaceFound) && spaceBindingSpaceName == space.GetName() { + if sbrName == "" { + // small corner case where the SBR name for some reason is not present as labels on the sb. + return nil, fmt.Errorf("SpaceBindingRequest name not found on binding: %s", spaceBinding.GetName()) + } + if sbrNamespace == "" { + // small corner case where the SBR namespace for some reason is not present as labels on the sb. + return nil, fmt.Errorf("SpaceBindingRequest namespace not found on binding: %s", spaceBinding.GetName()) + } + // this is a binding that was created via SpaceBindingRequest, it can be updated or deleted + binding.AvailableActions = []string{UpdateBindingAction, DeleteBindingAction} + binding.BindingRequest = toolchainv1alpha1.BindingRequest{ + Name: sbrName, + Namespace: sbrNamespace, + } + } else if spaceBindingSpaceName != space.GetName() { + // this is a binding that was inherited from a parent space, since the name on the spacebinding label doesn't match with the current space name. + // It can only be overridden by another SpaceBindingRequest containing the same MUR but different role. + binding.AvailableActions = []string{OverrideBindingAction} + } else { + // this is a system generated SpaceBinding, so it cannot be managed by workspace users. + binding.AvailableActions = []string{} + } + bindings = append(bindings, binding) + } + + // let's sort the list based on username, + // in order to make it deterministic + sort.Slice(bindings, func(i, j int) bool { + return bindings[i].MasterUserRecord < bindings[j].MasterUserRecord + }) + + return bindings, nil +} diff --git a/pkg/proxy/handlers/spacelister_test.go b/pkg/proxy/handlers/spacelister_test.go index 40ef195e..23cf4d69 100644 --- a/pkg/proxy/handlers/spacelister_test.go +++ b/pkg/proxy/handlers/spacelister_test.go @@ -15,6 +15,7 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/proxy/handlers" "github.com/codeready-toolchain/registration-service/pkg/signup" "github.com/codeready-toolchain/registration-service/test/fake" + spacetest "github.com/codeready-toolchain/toolchain-common/pkg/test/space" "github.com/gin-gonic/gin" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,6 +39,9 @@ func TestSpaceLister(t *testing.T) { newSignup("movielover", "movie.lover", true), newSignup("pandalover", "panda.lover", true), newSignup("usernospace", "user.nospace", true), + newSignup("foodlover", "food.lover", true), + newSignup("animelover", "anime.lover", true), + newSignup("carlover", "car.lover", true), newSignup("racinglover", "racing.lover", false), ) @@ -45,11 +49,35 @@ func TestSpaceLister(t *testing.T) { spaceNotProvisionedYet := fake.NewSpace("pandalover", "member-2", "pandalover") spaceNotProvisionedYet.Labels[toolchainv1alpha1.SpaceCreatorLabelKey] = "" + // spacebinding associated with SpaceBindingRequest + spaceBindingWithSBRonMovieLover := fake.NewSpaceBinding("foodlover-sb-from-sbr-on-movielover", "foodlover", "movielover", "maintainer") + spaceBindingWithSBRonMovieLover.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = "foodlover-sbr" + spaceBindingWithSBRonMovieLover.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = "movielover-tenant" + + // spacebinding associated with SpaceBindingRequest on a dancelover, + // which is also the parentSpace of foodlover + spaceBindingWithSBRonDanceLover := fake.NewSpaceBinding("animelover-sb-from-sbr-on-dancelover", "animelover", "dancelover", "viewer") + spaceBindingWithSBRonDanceLover.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = "animelover-sbr" + spaceBindingWithSBRonDanceLover.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = "dancelover-tenant" + + // spacebinding with SpaceBindingRequest but name is missing + spaceBindingWithInvalidSBRName := fake.NewSpaceBinding("carlover-sb-from-sbr", "carlover", "animelover", "viewer") + spaceBindingWithInvalidSBRName.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = "" // let's set the name to blank in order to trigger an error + spaceBindingWithInvalidSBRName.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = "anime-tenant" + + // spacebinding with SpaceBindingRequest but namespace is missing + spaceBindingWithInvalidSBRNamespace := fake.NewSpaceBinding("animelover-sb-from-sbr", "animelover", "carlover", "viewer") + spaceBindingWithInvalidSBRNamespace.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = "anime-sbr" + spaceBindingWithInvalidSBRNamespace.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = "" // let's set the name to blank in order to trigger an error + fakeClient := initFakeClient(t, // spaces fake.NewSpace("dancelover", "member-1", "dancelover"), fake.NewSpace("movielover", "member-1", "movielover"), fake.NewSpace("racinglover", "member-2", "racinglover"), + fake.NewSpace("foodlover", "member-2", "foodlover", spacetest.WithSpecParentSpace("dancelover")), + fake.NewSpace("animelover", "member-1", "animelover"), + fake.NewSpace("carlover", "member-1", "carlover"), spaceNotProvisionedYet, //spacebindings @@ -57,6 +85,12 @@ func TestSpaceLister(t *testing.T) { fake.NewSpaceBinding("dancer-sb2", "dancelover", "movielover", "other"), fake.NewSpaceBinding("moviegoer-sb", "movielover", "movielover", "admin"), fake.NewSpaceBinding("racer-sb", "racinglover", "racinglover", "admin"), + fake.NewSpaceBinding("anime-sb", "animelover", "animelover", "admin"), + fake.NewSpaceBinding("car-sb", "carlover", "carlover", "admin"), + spaceBindingWithSBRonMovieLover, + spaceBindingWithSBRonDanceLover, + spaceBindingWithInvalidSBRName, + spaceBindingWithInvalidSBRNamespace, //nstemplatetier fake.NewBase1NSTemplateTier(), @@ -197,6 +231,22 @@ func TestSpaceLister(t *testing.T) { "admin", "viewer", }, ), + commonproxy.WithBindings([]toolchainv1alpha1.Binding{ + { + MasterUserRecord: "animelover", + Role: "viewer", + AvailableActions: []string{"update", "delete"}, + BindingRequest: toolchainv1alpha1.BindingRequest{ // animelover was granted access to dancelover workspace using SpaceBindingRequest + Name: "animelover-sbr", + Namespace: "dancelover-tenant", + }, + }, + { + MasterUserRecord: "dancelover", + Role: "admin", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + }), ), }, expectedErr: "", @@ -208,18 +258,87 @@ func TestSpaceLister(t *testing.T) { workspaceFor(t, fakeClient, "movielover", "other", false, commonproxy.WithAvailableRoles([]string{ "admin", "viewer", - })), + }), + commonproxy.WithBindings([]toolchainv1alpha1.Binding{ + { + MasterUserRecord: "dancelover", + Role: "other", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + { + MasterUserRecord: "foodlover", + Role: "maintainer", + AvailableActions: []string{"update", "delete"}, + BindingRequest: toolchainv1alpha1.BindingRequest{ // foodlover was granted access to movielover workspace using SpaceBindingRequest + Name: "foodlover-sbr", + Namespace: "movielover-tenant", + }, + }, + { + MasterUserRecord: "movielover", + Role: "admin", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + }), + ), }, expectedErr: "", expectedWorkspace: "movielover", }, + "dancelover gets foodlover space": { + username: "dance.lover", + expectedWs: []toolchainv1alpha1.Workspace{ + workspaceFor(t, fakeClient, "foodlover", "admin", false, + commonproxy.WithAvailableRoles([]string{ + "admin", "viewer", + }), + commonproxy.WithBindings([]toolchainv1alpha1.Binding{ + { + MasterUserRecord: "animelover", + Role: "viewer", // animelover was granted access via SBR , but on the parentSpace, + AvailableActions: []string{"override"}, // since the binding is inherited from parent space, then it can only be overridden + }, + { + MasterUserRecord: "dancelover", + Role: "admin", // dancelover is admin since it's admin on the parent space, + AvailableActions: []string{"override"}, // since the binding is inherited from parent space, then it can only be overridden + }, + }), + ), + }, + expectedErr: "", + expectedWorkspace: "foodlover", + }, "movielover gets movielover space": { username: "movie.lover", expectedWs: []toolchainv1alpha1.Workspace{ workspaceFor(t, fakeClient, "movielover", "admin", true, commonproxy.WithAvailableRoles([]string{ "admin", "viewer", - })), + }), + // bindings are in alphabetical order using the MUR name + commonproxy.WithBindings([]toolchainv1alpha1.Binding{ + { + MasterUserRecord: "dancelover", + Role: "other", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + { + MasterUserRecord: "foodlover", + Role: "maintainer", + AvailableActions: []string{"update", "delete"}, + BindingRequest: toolchainv1alpha1.BindingRequest{ + Name: "foodlover-sbr", + Namespace: "movielover-tenant", + }, + }, + { + MasterUserRecord: "movielover", + Role: "admin", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + }), + ), }, expectedErr: "", expectedWorkspace: "movielover", @@ -237,7 +356,29 @@ func TestSpaceLister(t *testing.T) { workspaceFor(t, fakeClient, "movielover", "admin", true, commonproxy.WithAvailableRoles([]string{ "admin", "viewer", - })), + }), + commonproxy.WithBindings([]toolchainv1alpha1.Binding{ + { + MasterUserRecord: "dancelover", + Role: "other", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + { + MasterUserRecord: "foodlover", // foodlover was granted access to movielover workspace using SpaceBindingRequest + Role: "maintainer", + AvailableActions: []string{"update", "delete"}, + BindingRequest: toolchainv1alpha1.BindingRequest{ + Name: "foodlover-sbr", + Namespace: "movielover-tenant", + }, + }, + { + MasterUserRecord: "movielover", + Role: "admin", + AvailableActions: []string(nil), // this is system generated so no actions for the user + }, + }), + ), }, expectedErr: "", expectedWorkspace: "movielover", @@ -255,24 +396,6 @@ func TestSpaceLister(t *testing.T) { }, expectedWorkspace: "dancelover", }, - "too many spacebindings for user": { - username: "dance.lover", - expectedWs: []toolchainv1alpha1.Workspace{}, - expectedErr: "Internal error occurred: expected only 1 spacebinding, got 2 for user dancelover and workspace dancelover", - expectedErrCode: 500, - overrideInformerFunc: func() service.InformerService { - inf := fake.NewFakeInformer() - inf.ListSpaceBindingFunc = func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { - // let's return more than 1 spacebinding to trigger the error - return []toolchainv1alpha1.SpaceBinding{ - *fake.NewSpaceBinding("dancer-sb1", "dancelover", "dancelover", "admin"), - *fake.NewSpaceBinding("dancer-sb2", "dancelover", "dancelover", "other"), - }, nil - } - return inf - }, - expectedWorkspace: "dancelover", - }, "get signup error": { username: "dance.lover", expectedWs: []toolchainv1alpha1.Workspace{}, @@ -290,6 +413,76 @@ func TestSpaceLister(t *testing.T) { expectedErrCode: 404, expectedWorkspace: "racinglover", }, + "list spacebindings error": { + username: "dance.lover", + expectedWs: []toolchainv1alpha1.Workspace{}, + expectedErr: "list spacebindings error", + expectedErrCode: 500, + overrideInformerFunc: func() service.InformerService { + listSpaceBindingFunc := func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { + return nil, fmt.Errorf("list spacebindings error") + } + return getFakeInformerService(fakeClient, WithListSpaceBindingFunc(listSpaceBindingFunc))() + }, + expectedWorkspace: "dancelover", + }, + "unable to get space": { + username: "dance.lover", + expectedWs: []toolchainv1alpha1.Workspace{}, + expectedErr: "\"workspaces.toolchain.dev.openshift.com \\\"dancelover\\\" not found\"", + expectedErrCode: 404, + overrideInformerFunc: func() service.InformerService { + getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { + return nil, fmt.Errorf("no space") + } + return getFakeInformerService(fakeClient, WithGetSpaceFunc(getSpaceFunc))() + }, + expectedWorkspace: "dancelover", + }, + "unable to get parent-space": { + username: "food.lover", + expectedWs: []toolchainv1alpha1.Workspace{}, + expectedErr: "Internal error occurred: unable to get parent-space: parent-space error", + expectedErrCode: 500, + overrideInformerFunc: func() service.InformerService { + getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { + if name == "dancelover" { + // return the error only when trying to get the parent space + return nil, fmt.Errorf("parent-space error") + } + // return the foodlover space + return fake.NewSpace("foodlover", "member-2", "foodlover", spacetest.WithSpecParentSpace("dancelover")), nil + } + return getFakeInformerService(fakeClient, WithGetSpaceFunc(getSpaceFunc))() + }, + expectedWorkspace: "foodlover", + }, + "error spaceBinding request has no name": { + username: "anime.lover", + expectedWs: []toolchainv1alpha1.Workspace{ + workspaceFor(t, fakeClient, "animelover", "admin", true, + commonproxy.WithAvailableRoles([]string{ + "admin", "viewer", + }), + ), + }, + expectedErr: "Internal error occurred: SpaceBindingRequest name not found on binding: carlover-sb-from-sbr", + expectedErrCode: 500, + expectedWorkspace: "animelover", + }, + "error spaceBinding request has no namespace set": { + username: "car.lover", + expectedWs: []toolchainv1alpha1.Workspace{ + workspaceFor(t, fakeClient, "carlover", "admin", true, + commonproxy.WithAvailableRoles([]string{ + "admin", "viewer", + }), + ), + }, + expectedErr: "Internal error occurred: SpaceBindingRequest namespace not found on binding: animelover-sb-from-sbr", + expectedErrCode: 500, + expectedWorkspace: "carlover", + }, } for k, tc := range tests { @@ -369,6 +562,18 @@ func WithGetNSTemplateTierFunc(getNsTemplateTierFunc func(tier string) (*toolcha } } +func WithListSpaceBindingFunc(listSpaceBindingFunc func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error)) InformerServiceOptions { + return func(informer *fake.Informer) { + informer.ListSpaceBindingFunc = listSpaceBindingFunc + } +} + +func WithGetSpaceFunc(getSpaceFunc func(name string) (*toolchainv1alpha1.Space, error)) InformerServiceOptions { + return func(informer *fake.Informer) { + informer.GetSpaceFunc = getSpaceFunc + } +} + func getFakeInformerService(fakeClient client.Client, options ...InformerServiceOptions) func() service.InformerService { return func() service.InformerService { diff --git a/test/fake/informer.go b/test/fake/informer.go index 9f8bd55c..f97efa14 100644 --- a/test/fake/informer.go +++ b/test/fake/informer.go @@ -3,6 +3,7 @@ package fake import ( toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/registration-service/pkg/configuration" + spacetest "github.com/codeready-toolchain/toolchain-common/pkg/test/space" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -71,22 +72,15 @@ func (f Informer) GetNSTemplateTier(tier string) (*toolchainv1alpha1.NSTemplateT panic("not supposed to call GetNSTemplateTierFunc") } -func NewSpace(name, targetCluster, compliantUserName string) *toolchainv1alpha1.Space { - space := &toolchainv1alpha1.Space{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: configuration.Namespace(), - Labels: map[string]string{ - toolchainv1alpha1.SpaceCreatorLabelKey: compliantUserName, - }, - }, - Spec: toolchainv1alpha1.SpaceSpec{ - TargetCluster: targetCluster, - TierName: "base1ns", - }, - Status: toolchainv1alpha1.SpaceStatus{ - TargetCluster: targetCluster, - ProvisionedNamespaces: []toolchainv1alpha1.SpaceNamespace{ +func NewSpace(name, targetCluster, compliantUserName string, spaceTestOptions ...spacetest.Option) *toolchainv1alpha1.Space { + + spaceTestOptions = append(spaceTestOptions, + spacetest.WithLabel(toolchainv1alpha1.SpaceCreatorLabelKey, compliantUserName), + spacetest.WithSpecTargetCluster(targetCluster), + spacetest.WithStatusTargetCluster(targetCluster), + spacetest.WithTierName("base1ns"), + spacetest.WithStatusProvisionedNamespaces( + []toolchainv1alpha1.SpaceNamespace{ { Name: "john-dev", Type: "default", @@ -95,9 +89,11 @@ func NewSpace(name, targetCluster, compliantUserName string) *toolchainv1alpha1. Name: "john-stage", }, }, - }, - } - return space + ), + ) + return spacetest.NewSpace(configuration.Namespace(), name, + spaceTestOptions..., + ) } func NewSpaceBinding(name, murLabelValue, spaceLabelValue, role string) *toolchainv1alpha1.SpaceBinding { @@ -110,7 +106,9 @@ func NewSpaceBinding(name, murLabelValue, spaceLabelValue, role string) *toolcha }, }, Spec: toolchainv1alpha1.SpaceBindingSpec{ - SpaceRole: role, + SpaceRole: role, + MasterUserRecord: murLabelValue, + Space: spaceLabelValue, }, } }