diff --git a/cmd/main.go b/cmd/main.go index baf30732..0cf1061c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -92,7 +92,8 @@ func main() { panic(err.Error()) } - app, err := server.NewInClusterApplication(*informer) + pvcfg := crtConfig.PublicViewer() + app, err := server.NewInClusterApplication(*informer, pvcfg) if err != nil { panic(err.Error()) } @@ -114,7 +115,7 @@ func main() { metricsSrv := proxyMetrics.StartMetricsServer() // Start the proxy server - p, err := proxy.NewProxy(app, proxyMetrics, cluster.GetMemberClusters) + p, err := proxy.NewProxy(app, proxyMetrics, cluster.GetMemberClusters, &pvcfg) if err != nil { panic(errs.Wrap(err, "failed to create proxy")) } diff --git a/go.mod b/go.mod index 346479b5..2ffb4e2e 100644 --- a/go.mod +++ b/go.mod @@ -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-20240227210924-371ddb054d87 + github.com/codeready-toolchain/api v0.0.0-20240322110702-5ab3840476e9 github.com/codeready-toolchain/toolchain-common v0.0.0-20240313081501-5cafefaa6598 github.com/go-logr/logr v1.2.3 github.com/gofrs/uuid v4.2.0+incompatible @@ -146,3 +146,7 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace github.com/codeready-toolchain/toolchain-common => github.com/filariow/toolchain-common v0.0.0-20240322165115-87027670d9b3 + +replace github.com/codeready-toolchain/api => github.com/filariow/toolchain-api v0.0.0-20240322163859-f974f2dbbc8e diff --git a/go.sum b/go.sum index 74ca0e5d..9a282a83 100644 --- a/go.sum +++ b/go.sum @@ -115,10 +115,6 @@ 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-20240227210924-371ddb054d87 h1:eQLsrMqfjAzGfuO9t6pVxO4K6cUDKOMxEvl0ujQq/2I= -github.com/codeready-toolchain/api v0.0.0-20240227210924-371ddb054d87/go.mod h1:FO7kgXH1x1LqkF327D5a36u0WIrwjVCbeijPkzgwaZc= -github.com/codeready-toolchain/toolchain-common v0.0.0-20240313081501-5cafefaa6598 h1:06nit/nCQFVKp51ZtIOyY49ncmxEK5shJGTaM+Ogicw= -github.com/codeready-toolchain/toolchain-common v0.0.0-20240313081501-5cafefaa6598/go.mod h1:c2JxboVI7keMD5fx5bB7LwzowFYYTwbepJhzPWSYXVs= 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= @@ -143,6 +139,10 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/filariow/toolchain-api v0.0.0-20240322163859-f974f2dbbc8e h1:h+uO6jYovPD3j9VIo9JajpmTZIyl7x6wSap/4RjS6aw= +github.com/filariow/toolchain-api v0.0.0-20240322163859-f974f2dbbc8e/go.mod h1:cfNN6YPX4TORvhhZXMSjSPesqAHlB3nD/WAfGe4WLKQ= +github.com/filariow/toolchain-common v0.0.0-20240322165115-87027670d9b3 h1:k1kjVkS1CUazxpA8Hu0X6M4ChubNhGmHSQUEUhfGo4I= +github.com/filariow/toolchain-common v0.0.0-20240322165115-87027670d9b3/go.mod h1:2vTPf4wRr6Q9Pq3zkJ5I3wBNagXEMvyjmdk8w0/UJRE= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= diff --git a/pkg/application/service/factory/service_factory.go b/pkg/application/service/factory/service_factory.go index dd9f5660..2411449d 100644 --- a/pkg/application/service/factory/service_factory.go +++ b/pkg/application/service/factory/service_factory.go @@ -13,6 +13,7 @@ import ( clusterservice "github.com/codeready-toolchain/registration-service/pkg/proxy/service" signupservice "github.com/codeready-toolchain/registration-service/pkg/signup/service" verificationservice "github.com/codeready-toolchain/registration-service/pkg/verification/service" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" ) type serviceContextImpl struct { @@ -54,6 +55,7 @@ type ServiceFactory struct { verificationServiceOptions []verificationservice.VerificationServiceOption signupServiceFunc func(opts ...signupservice.SignupServiceOption) service.SignupService signupServiceOptions []signupservice.SignupServiceOption + publicViewerConfig commonconfig.PublicViewerConfig } func (s *ServiceFactory) defaultServiceContextProducer() servicecontext.ServiceContextProducer { @@ -69,7 +71,7 @@ func (s *ServiceFactory) InformerService() service.InformerService { } func (s *ServiceFactory) MemberClusterService() service.MemberClusterService { - return clusterservice.NewMemberClusterService(s.getContext()) + return clusterservice.NewMemberClusterService(s.getContext(), clusterservice.WithPublicViewerConfig(s.publicViewerConfig)) } func (s *ServiceFactory) SignupService() service.SignupService { @@ -97,6 +99,12 @@ func WithServiceContextOptions(opts ...ServiceContextOption) func(f *ServiceFact } } +func WithPublicViewerConfig(config commonconfig.PublicViewerConfig) func(f *ServiceFactory) { + return func(f *ServiceFactory) { + f.publicViewerConfig = config + } +} + func NewServiceFactory(options ...Option) *ServiceFactory { f := &ServiceFactory{ serviceContextOptions: []ServiceContextOption{}, diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index a6611d6f..96863248 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -121,6 +121,10 @@ func (r RegistrationServiceConfig) Print() { logger.Info("Registration Service Configuration", "config", r.cfg.Host.RegistrationService) } +func (r RegistrationServiceConfig) PublicViewer() commonconfig.PublicViewerConfig { + return commonconfig.PublicViewerConfig{Config: r.cfg.Global.PublicViewer} +} + func (r RegistrationServiceConfig) Environment() string { return commonconfig.GetString(r.cfg.Host.RegistrationService.Environment, prodEnvironment) } diff --git a/pkg/log/log.go b/pkg/log/log.go index f2107f8e..39174bf3 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -149,6 +149,14 @@ func (l *Logger) InfoEchof(ctx echo.Context, msg string, args ...string) { ctxFields = append(ctxFields, "url") ctxFields = append(ctxFields, ctx.Request().URL) + if ssoUser, ok := ctx.Get("SSO-User").(string); ok { + ctxFields = append(ctxFields, "SSO-User", ssoUser) + } + + if impersonateUser, ok := ctx.Get("Impersonate-User").(string); ok { + ctxFields = append(ctxFields, "Impersonate-User", impersonateUser) + } + l.infof(ctxFields, msg, args...) } diff --git a/pkg/proxy/handlers/spacelister.go b/pkg/proxy/handlers/spacelister.go index bbf152a6..5803b4f4 100644 --- a/pkg/proxy/handlers/spacelister.go +++ b/pkg/proxy/handlers/spacelister.go @@ -56,7 +56,7 @@ func (s *SpaceLister) GetProvisionedUserSignup(ctx echo.Context) (*signup.Signup return userSignup, nil } -func createWorkspaceObject(signupName string, space *toolchainv1alpha1.Space, spaceBinding *toolchainv1alpha1.SpaceBinding, wsAdditionalOptions ...commonproxy.WorkspaceOption) *toolchainv1alpha1.Workspace { +func createWorkspaceObject(signupName *string, space *toolchainv1alpha1.Space, spaceBinding *toolchainv1alpha1.SpaceBinding, wsAdditionalOptions ...commonproxy.WorkspaceOption) *toolchainv1alpha1.Workspace { // TODO right now we get SpaceCreatorLabelKey but should get owner from Space once it's implemented ownerName := space.Labels[toolchainv1alpha1.SpaceCreatorLabelKey] @@ -68,7 +68,7 @@ func createWorkspaceObject(signupName string, space *toolchainv1alpha1.Space, sp } // set the workspace type to "home" to indicate it is the user's home space // TODO set home type based on UserSignup.Status.HomeSpace once it's implemented - if ownerName == signupName { + if signupName != nil && ownerName == *signupName { wsOptions = append(wsOptions, commonproxy.WithType("home")) } wsOptions = append(wsOptions, wsAdditionalOptions...) diff --git a/pkg/proxy/handlers/spacelister_get.go b/pkg/proxy/handlers/spacelister_get.go index 1d54cb33..a2d5216b 100644 --- a/pkg/proxy/handlers/spacelister_get.go +++ b/pkg/proxy/handlers/spacelister_get.go @@ -12,6 +12,7 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/metrics" "github.com/codeready-toolchain/registration-service/pkg/signup" "github.com/codeready-toolchain/toolchain-common/pkg/cluster" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" commonproxy "github.com/codeready-toolchain/toolchain-common/pkg/proxy" "github.com/codeready-toolchain/toolchain-common/pkg/spacebinding" "github.com/labstack/echo/v4" @@ -23,11 +24,11 @@ import ( runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) -func HandleSpaceGetRequest(spaceLister *SpaceLister, GetMembersFunc cluster.GetMemberClustersFunc) echo.HandlerFunc { +func HandleSpaceGetRequest(spaceLister *SpaceLister, GetMembersFunc cluster.GetMemberClustersFunc, publicViewerConfig *commonconfig.PublicViewerConfig) echo.HandlerFunc { // get specific workspace return func(ctx echo.Context) error { requestReceivedTime := ctx.Get(regsercontext.RequestReceivedTime).(time.Time) - workspace, err := GetUserWorkspaceWithBindings(ctx, spaceLister, ctx.Param("workspace"), GetMembersFunc) + workspace, err := GetUserWorkspaceWithBindings(ctx, spaceLister, ctx.Param("workspace"), GetMembersFunc, publicViewerConfig) if err != nil { spaceLister.ProxyMetrics.RegServWorkspaceHistogramVec.WithLabelValues(fmt.Sprintf("%d", http.StatusInternalServerError), metrics.MetricsLabelVerbGet).Observe(time.Since(requestReceivedTime).Seconds()) // using list as the default value for verb to minimize label combinations for prometheus to process return errorResponse(ctx, apierrors.NewInternalError(err)) @@ -45,15 +46,27 @@ func HandleSpaceGetRequest(spaceLister *SpaceLister, GetMembersFunc cluster.GetM // GetUserWorkspace returns a workspace object with the required fields used by the proxy func GetUserWorkspace(ctx echo.Context, spaceLister *SpaceLister, workspaceName string) (*toolchainv1alpha1.Workspace, error) { - userSignup, space, err := getUserSignupAndSpace(ctx, spaceLister, workspaceName) + userSignup, err := spaceLister.GetProvisionedUserSignup(ctx) if err != nil { + ctx.Logger().Error(errs.Wrap(err, "provisioned user signup error")) return nil, err } - // signup is not ready - if userSignup == nil || space == nil { + + if userSignup == nil { return nil, nil } + return GetUserWorkspaceForSignup(ctx, spaceLister, userSignup, workspaceName) +} + +// GetUserWorkspace returns a workspace object with the required fields used by the proxy +func GetUserWorkspaceForSignup(ctx echo.Context, spaceLister *SpaceLister, userSignup *signup.Signup, workspaceName string) (*toolchainv1alpha1.Workspace, error) { + space, err := spaceLister.GetInformerServiceFunc().GetSpace(workspaceName) + if err != nil { + ctx.Logger().Error(errs.Wrap(err, "unable to get space")) + return nil, err + } + // 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}) @@ -84,20 +97,27 @@ func GetUserWorkspace(ctx echo.Context, spaceLister *SpaceLister, workspaceName return nil, userBindingsErr } - return createWorkspaceObject(userSignup.Name, space, &userSpaceBindings[0]), nil + return createWorkspaceObject(&userSignup.Name, space, &userSpaceBindings[0]), nil } // GetUserWorkspaceWithBindings returns a workspace object with the required fields+bindings (the list with all the users access details) -func GetUserWorkspaceWithBindings(ctx echo.Context, spaceLister *SpaceLister, workspaceName string, GetMembersFunc cluster.GetMemberClustersFunc) (*toolchainv1alpha1.Workspace, error) { +func GetUserWorkspaceWithBindings(ctx echo.Context, spaceLister *SpaceLister, workspaceName string, GetMembersFunc cluster.GetMemberClustersFunc, publicViewerConfig *commonconfig.PublicViewerConfig) (*toolchainv1alpha1.Workspace, error) { userSignup, space, err := getUserSignupAndSpace(ctx, spaceLister, workspaceName) if err != nil { return nil, err } // signup is not ready - if userSignup == nil || space == nil { + if space == nil || (userSignup == nil && !publicViewerConfig.Enabled()) { return nil, nil } + if userSignup == nil { + userSignup = &signup.Signup{ + CompliantUsername: publicViewerConfig.Username(), + Name: publicViewerConfig.Username(), + } + } + // 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}) @@ -116,9 +136,12 @@ func GetUserWorkspaceWithBindings(ctx echo.Context, spaceLister *SpaceLister, wo // 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 + userBinding = filterUserSpaceBinding(publicViewerConfig.Username(), 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 + } } // list all SpaceBindingRequests , just in case there might be some failing to create a SpaceBinding resource. @@ -142,7 +165,7 @@ func GetUserWorkspaceWithBindings(ctx echo.Context, spaceLister *SpaceLister, wo return nil, err } - return createWorkspaceObject(userSignup.Name, space, userBinding, + return createWorkspaceObject(&userSignup.Name, space, userBinding, commonproxy.WithAvailableRoles(getRolesFromNSTemplateTier(nsTemplateTier)), commonproxy.WithBindings(bindings), ), nil diff --git a/pkg/proxy/handlers/spacelister_get_test.go b/pkg/proxy/handlers/spacelister_get_test.go index 5b0f9382..de825a0d 100644 --- a/pkg/proxy/handlers/spacelister_get_test.go +++ b/pkg/proxy/handlers/spacelister_get_test.go @@ -18,6 +18,7 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/signup" "github.com/codeready-toolchain/registration-service/test/fake" commoncluster "github.com/codeready-toolchain/toolchain-common/pkg/cluster" + "github.com/codeready-toolchain/toolchain-common/pkg/configuration" commonproxy "github.com/codeready-toolchain/toolchain-common/pkg/proxy" "github.com/codeready-toolchain/toolchain-common/pkg/test" spacetest "github.com/codeready-toolchain/toolchain-common/pkg/test/space" @@ -32,7 +33,8 @@ import ( ) func TestSpaceListerGet(t *testing.T) { - fakeSignupService, fakeClient := buildSpaceListerFakes(t) + cfg := &configuration.PublicViewerConfig{Config: toolchainv1alpha1.PublicViewerConfig{Enabled: false}} + fakeSignupService, fakeClient := buildSpaceListerFakes(t, cfg) memberFakeClient := fake.InitClient(t, // spacebinding requests @@ -564,7 +566,7 @@ func TestSpaceListerGet(t *testing.T) { if tc.overrideGetMembersFunc != nil { getMembersFunc = tc.overrideGetMembersFunc } - err := handlers.HandleSpaceGetRequest(s, getMembersFunc)(ctx) + err := handlers.HandleSpaceGetRequest(s, getMembersFunc, cfg)(ctx) // then if tc.expectedErr != "" { diff --git a/pkg/proxy/handlers/spacelister_list.go b/pkg/proxy/handlers/spacelister_list.go index f0776d9c..f9b056d2 100644 --- a/pkg/proxy/handlers/spacelister_list.go +++ b/pkg/proxy/handlers/spacelister_list.go @@ -6,22 +6,24 @@ import ( "net/http" "time" - toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" - "github.com/codeready-toolchain/registration-service/pkg/context" - "github.com/codeready-toolchain/registration-service/pkg/metrics" "github.com/labstack/echo/v4" errs "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/registration-service/pkg/context" + "github.com/codeready-toolchain/registration-service/pkg/metrics" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" ) -func HandleSpaceListRequest(spaceLister *SpaceLister) echo.HandlerFunc { +func HandleSpaceListRequest(spaceLister *SpaceLister, publicViewerConfig *commonconfig.PublicViewerConfig) echo.HandlerFunc { return func(ctx echo.Context) error { // list all user workspaces requestReceivedTime := ctx.Get(context.RequestReceivedTime).(time.Time) - workspaces, err := ListUserWorkspaces(ctx, spaceLister) + workspaces, err := ListUserWorkspaces(ctx, spaceLister, publicViewerConfig) if err != nil { spaceLister.ProxyMetrics.RegServWorkspaceHistogramVec.WithLabelValues(fmt.Sprintf("%d", http.StatusInternalServerError), metrics.MetricsLabelVerbList).Observe(time.Since(requestReceivedTime).Seconds()) // using list as the default value for verb to minimize label combinations for prometheus to process return errorResponse(ctx, apierrors.NewInternalError(err)) @@ -33,24 +35,40 @@ func HandleSpaceListRequest(spaceLister *SpaceLister) echo.HandlerFunc { // ListUserWorkspaces returns a list of Workspaces for the current user. // The function lists all SpaceBindings for the user and return all the workspaces found from this list. -func ListUserWorkspaces(ctx echo.Context, spaceLister *SpaceLister) ([]toolchainv1alpha1.Workspace, error) { +func ListUserWorkspaces(ctx echo.Context, spaceLister *SpaceLister, publicViewerConfig *commonconfig.PublicViewerConfig) ([]toolchainv1alpha1.Workspace, error) { signup, err := spaceLister.GetProvisionedUserSignup(ctx) if err != nil { return nil, err } + // signup is not ready - if signup == nil { - return []toolchainv1alpha1.Workspace{}, nil + var murName *string + if signup != nil { + murName = &signup.CompliantUsername } - murName := signup.CompliantUsername + murNames := func() []string { + names := []string{} + if publicViewerConfig.Enabled() { + names = append(names, publicViewerConfig.Username()) + } + + if murName != nil { + names = append(names, *murName) + } + return names + }() + if len(murNames) == 0 { + return nil, nil + } // get all spacebindings with given mur since no workspace was provided - spaceBindings, err := listSpaceBindingsForUser(spaceLister, murName) + spaceBindings, err := listSpaceBindingsForUsers(spaceLister, murNames) if err != nil { ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) return nil, err } - return workspacesFromSpaceBindings(ctx, spaceLister, signup.Name, spaceBindings), nil + + return workspacesFromSpaceBindings(ctx, spaceLister, murName, spaceBindings), nil } func listWorkspaceResponse(ctx echo.Context, workspaces []toolchainv1alpha1.Workspace) error { @@ -67,16 +85,15 @@ func listWorkspaceResponse(ctx echo.Context, workspaces []toolchainv1alpha1.Work return json.NewEncoder(ctx.Response().Writer).Encode(workspaceList) } -func listSpaceBindingsForUser(spaceLister *SpaceLister, murName string) ([]toolchainv1alpha1.SpaceBinding, error) { - murSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey, selection.Equals, []string{murName}) +func listSpaceBindingsForUsers(spaceLister *SpaceLister, murNames []string) ([]toolchainv1alpha1.SpaceBinding, error) { + murSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey, selection.In, murNames) if err != nil { return nil, err } - requirements := []labels.Requirement{*murSelector} - return spaceLister.GetInformerServiceFunc().ListSpaceBindings(requirements...) + return spaceLister.GetInformerServiceFunc().ListSpaceBindings(*murSelector) } -func workspacesFromSpaceBindings(ctx echo.Context, spaceLister *SpaceLister, signupName string, spaceBindings []toolchainv1alpha1.SpaceBinding) []toolchainv1alpha1.Workspace { +func workspacesFromSpaceBindings(ctx echo.Context, spaceLister *SpaceLister, signupName *string, spaceBindings []toolchainv1alpha1.SpaceBinding) []toolchainv1alpha1.Workspace { workspaces := []toolchainv1alpha1.Workspace{} for i := range spaceBindings { spacebinding := &spaceBindings[i] diff --git a/pkg/proxy/handlers/spacelister_list_test.go b/pkg/proxy/handlers/spacelister_list_test.go index 22e4b4a7..1a230c03 100644 --- a/pkg/proxy/handlers/spacelister_list_test.go +++ b/pkg/proxy/handlers/spacelister_list_test.go @@ -22,10 +22,104 @@ 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" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" ) +func TestSpaceListerListCommunity(t *testing.T) { + cfg := &commonconfig.PublicViewerConfig{ + Config: toolchainv1alpha1.PublicViewerConfig{ + Enabled: true, + Username: "public-viewer", + }, + } + fakeSignupService, fakeClient := buildSpaceListerFakes(t, cfg) + tests := map[string]struct { + username string + expectedWs []toolchainv1alpha1.Workspace + expectedErr string + expectedErrCode int + expectedWorkspace string + overrideSignupFunc func(ctx *gin.Context, userID, username string, checkUserSignupComplete bool) (*signup.Signup, error) + overrideInformerFunc func() service.InformerService + }{ + "dancelover lists spaces": { + username: "dance.lover", + expectedWs: []toolchainv1alpha1.Workspace{ + workspaceFor(t, fakeClient, "communityspace", "viewer", false), + workspaceFor(t, fakeClient, "dancelover", "admin", true), + workspaceFor(t, fakeClient, "movielover", "other", false), + }, + expectedErr: "", + }, + "nospacer lists spaces": { + username: "no.spacer", + expectedWs: []toolchainv1alpha1.Workspace{ + workspaceFor(t, fakeClient, "communityspace", "viewer", false), + }, + expectedErr: "", + }, + } + + t.Run("HandleSpaceListRequest", func(t *testing.T) { + for k, tc := range tests { + t.Run(k, func(t *testing.T) { + // given + signupProvider := fakeSignupService.GetSignupFromInformer + if tc.overrideSignupFunc != nil { + signupProvider = tc.overrideSignupFunc + } + + informerFunc := fake.GetInformerService(fakeClient) + if tc.overrideInformerFunc != nil { + informerFunc = tc.overrideInformerFunc + } + + proxyMetrics := metrics.NewProxyMetrics(prometheus.NewRegistry()) + + s := &handlers.SpaceLister{ + GetSignupFunc: signupProvider, + GetInformerServiceFunc: informerFunc, + ProxyMetrics: proxyMetrics, + } + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader("")) + rec := httptest.NewRecorder() + ctx := e.NewContext(req, rec) + ctx.Set(rcontext.UsernameKey, tc.username) + ctx.Set(rcontext.RequestReceivedTime, time.Now()) + + // when + err := handlers.HandleSpaceListRequest(s, cfg)(ctx) + + // then + if tc.expectedErr != "" { + // error case + require.Equal(t, tc.expectedErrCode, rec.Code) + require.Contains(t, rec.Body.String(), tc.expectedErr) + } else { + require.NoError(t, err) + // list workspace case + workspaceList, decodeErr := decodeResponseToWorkspaceList(rec.Body.Bytes()) + require.NoError(t, decodeErr) + require.Equal(t, len(tc.expectedWs), len(workspaceList.Items)) + for i := range tc.expectedWs { + assert.Equal(t, tc.expectedWs[i].Name, workspaceList.Items[i].Name) + assert.Equal(t, tc.expectedWs[i].Status, workspaceList.Items[i].Status) + } + } + }) + } + }) +} + func TestSpaceListerList(t *testing.T) { - fakeSignupService, fakeClient := buildSpaceListerFakes(t) + cfg := &commonconfig.PublicViewerConfig{ + Config: toolchainv1alpha1.PublicViewerConfig{ + Enabled: false, + }, + } + fakeSignupService, fakeClient := buildSpaceListerFakes(t, cfg) t.Run("HandleSpaceListRequest", func(t *testing.T) { // given @@ -124,7 +218,7 @@ func TestSpaceListerList(t *testing.T) { ctx.Set(rcontext.RequestReceivedTime, time.Now()) // when - err := handlers.HandleSpaceListRequest(s)(ctx) + err := handlers.HandleSpaceListRequest(s, cfg)(ctx) // then if tc.expectedErr != "" { diff --git a/pkg/proxy/handlers/spacelister_test.go b/pkg/proxy/handlers/spacelister_test.go index f9804e74..e28569a4 100644 --- a/pkg/proxy/handlers/spacelister_test.go +++ b/pkg/proxy/handlers/spacelister_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" @@ -20,8 +21,8 @@ import ( spacetest "github.com/codeready-toolchain/toolchain-common/pkg/test/space" ) -func buildSpaceListerFakes(t *testing.T) (*fake.SignupService, *test.FakeClient) { - fakeSignupService := fake.NewSignupService( +func buildSpaceListerFakes(t *testing.T, publicViewerConfig *commonconfig.PublicViewerConfig) (*fake.SignupService, *test.FakeClient) { + signups := []fake.SignupDef{ newSignup("dancelover", "dance.lover", true), newSignup("movielover", "movie.lover", true), newSignup("pandalover", "panda.lover", true), @@ -33,7 +34,15 @@ func buildSpaceListerFakes(t *testing.T) (*fake.SignupService, *test.FakeClient) newSignup("parentspace", "parent.space", true), newSignup("childspace", "child.space", true), newSignup("grandchildspace", "grandchild.space", true), - ) + } + if publicViewerConfig.Enabled() { + signups = append(signups, + newSignup("nospacer", "no.spacer", false), + newSignup("communityspace", "community.space", true), + newSignup("communitylover", "community.lover", true), + ) + } + fakeSignupService := fake.NewSignupService(signups...) // space that is not provisioned yet spaceNotProvisionedYet := fake.NewSpace("pandalover", "member-2", "pandalover") @@ -60,7 +69,7 @@ func buildSpaceListerFakes(t *testing.T) (*fake.SignupService, *test.FakeClient) spaceBindingWithInvalidSBRNamespace.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = "anime-sbr" spaceBindingWithInvalidSBRNamespace.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = "" // let's set the name to blank in order to trigger an error - fakeClient := fake.InitClient(t, + objs := []runtime.Object{ // spaces fake.NewSpace("dancelover", "member-1", "dancelover"), fake.NewSpace("movielover", "member-1", "movielover"), @@ -74,6 +83,8 @@ func buildSpaceListerFakes(t *testing.T) (*fake.SignupService, *test.FakeClient) fake.NewSpace("grandchildspace", "member-1", "grandchildspace", spacetest.WithSpecParentSpace("childspace")), // noise space, user will have a different role here , just to make sure this is not returned anywhere in the tests fake.NewSpace("otherspace", "member-1", "otherspace", spacetest.WithSpecParentSpace("otherspace")), + // space flagged as community + fake.NewSpace("communityspace", "member-2", "communityspace"), //spacebindings fake.NewSpaceBinding("dancer-sb1", "dancelover", "dancelover", "admin"), @@ -94,7 +105,14 @@ func buildSpaceListerFakes(t *testing.T) (*fake.SignupService, *test.FakeClient) //nstemplatetier fake.NewBase1NSTemplateTier(), - ) + } + if publicViewerConfig.Enabled() { + objs = append(objs, + fake.NewSpaceBinding("communityspace-sb", "communityspace", "communityspace", "admin"), + fake.NewSpaceBinding("community-sb", publicViewerConfig.Username(), "communityspace", "viewer"), + ) + } + fakeClient := fake.InitClient(t, objs...) return fakeSignupService, fakeClient } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index fc574ebe..599c4c9b 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -26,7 +26,9 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/metrics" "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/signup" commoncluster "github.com/codeready-toolchain/toolchain-common/pkg/cluster" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" glog "github.com/labstack/gommon/log" @@ -63,23 +65,24 @@ func authorizationEndpointTarget() string { } type Proxy struct { - app application.Application - cl client.Client - tokenParser *auth.TokenParser - spaceLister *handlers.SpaceLister - metrics *metrics.ProxyMetrics - getMembersFunc commoncluster.GetMemberClustersFunc + app application.Application + cl client.Client + tokenParser *auth.TokenParser + spaceLister *handlers.SpaceLister + metrics *metrics.ProxyMetrics + getMembersFunc commoncluster.GetMemberClustersFunc + publicViewerConfig *commonconfig.PublicViewerConfig } -func NewProxy(app application.Application, proxyMetrics *metrics.ProxyMetrics, getMembersFunc commoncluster.GetMemberClustersFunc) (*Proxy, error) { +func NewProxy(app application.Application, proxyMetrics *metrics.ProxyMetrics, getMembersFunc commoncluster.GetMemberClustersFunc, publicViewerConfig *commonconfig.PublicViewerConfig) (*Proxy, error) { cl, err := newClusterClient() if err != nil { return nil, err } - return newProxyWithClusterClient(app, cl, proxyMetrics, getMembersFunc) + return newProxyWithClusterClient(app, cl, proxyMetrics, getMembersFunc, publicViewerConfig) } -func newProxyWithClusterClient(app application.Application, cln client.Client, proxyMetrics *metrics.ProxyMetrics, getMembersFunc commoncluster.GetMemberClustersFunc) (*Proxy, error) { +func newProxyWithClusterClient(app application.Application, cln client.Client, proxyMetrics *metrics.ProxyMetrics, getMembersFunc commoncluster.GetMemberClustersFunc, publicViewerConfig *commonconfig.PublicViewerConfig) (*Proxy, error) { tokenParser, err := auth.DefaultTokenParser() if err != nil { return nil, err @@ -88,12 +91,13 @@ func newProxyWithClusterClient(app application.Application, cln client.Client, p // init handlers spaceLister := handlers.NewSpaceLister(app, proxyMetrics) return &Proxy{ - app: app, - cl: cln, - tokenParser: tokenParser, - spaceLister: spaceLister, - metrics: proxyMetrics, - getMembersFunc: getMembersFunc, + app: app, + cl: cln, + tokenParser: tokenParser, + spaceLister: spaceLister, + metrics: proxyMetrics, + getMembersFunc: getMembersFunc, + publicViewerConfig: publicViewerConfig, }, nil } @@ -139,8 +143,8 @@ func (p *Proxy) StartProxy(port string) *http.Server { // routes wg := router.Group("/apis/toolchain.dev.openshift.com/v1alpha1/workspaces") // Space lister routes - wg.GET("/:workspace", handlers.HandleSpaceGetRequest(p.spaceLister, p.getMembersFunc)) - wg.GET("", handlers.HandleSpaceListRequest(p.spaceLister)) + wg.GET("/:workspace", handlers.HandleSpaceGetRequest(p.spaceLister, p.getMembersFunc, p.publicViewerConfig)) + wg.GET("", handlers.HandleSpaceListRequest(p.spaceLister, p.publicViewerConfig)) 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: @@ -263,58 +267,124 @@ func (p *Proxy) health(ctx echo.Context) error { return err } -func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, error) { +func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, bool, error) { 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()) + return "", nil, false, 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) - if err != nil { - return "", nil, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error()) - } - // 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) + // access to user's home workspace + if workspaceName == "" { + // list all workspaces + workspaces, err := handlers.ListUserWorkspaces(ctx, p.spaceLister, p.publicViewerConfig) if err != nil { - return "", nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error()) + return "", nil, false, 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) + + cluster, err := p.app.MemberClusterService().GetClusterAccess(userID, username, workspaceName, proxyPluginName) if err != nil { - return "", nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error()) + return "", nil, false, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error()) + } + + requestedNamespace := namespaceFromCtx(ctx) + if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { + return "", nil, false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) } + + return proxyPluginName, cluster, false, nil } + + // 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. requestedNamespace := namespaceFromCtx(ctx) + workspaces, isPublicViewer, err := p.processDirectWorkspaceAccessRequest(ctx, workspaceName) if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { - return "", nil, crterrors.NewForbiddenError("invalid workspace request", err.Error()) + return "", nil, false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) + } + + cluster, err := func() (*access.ClusterAccess, error) { + if isPublicViewer { + return p.app.MemberClusterService().GetClusterAccess( + p.publicViewerConfig.Username(), p.publicViewerConfig.Username(), workspaceName, proxyPluginName) + } + + return p.app.MemberClusterService().GetClusterAccess(userID, username, workspaceName, proxyPluginName) + }() + if err != nil { + return "", nil, false, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error()) + } + + return proxyPluginName, cluster, isPublicViewer, nil +} + +func (p *Proxy) processDirectWorkspaceAccessRequest(ctx echo.Context, workspaceName string) ([]toolchainv1alpha1.Workspace, bool, *crterrors.Error) { + // collect information for logging + userID, _ := ctx.Get(context.SubKey).(string) + username, _ := ctx.Get(context.UsernameKey).(string) + + // check user has access to workspace + workspace, crterr := p.processUserRequest(ctx, workspaceName) + // user has direct access to workspace: use user identity + if crterr != nil { + ctx.Logger().Debugf("user %s/%s doesn't have direct access to workspace %s", userID, username, workspaceName) + // if community is enabled, try using public-viewer user + if p.publicViewerConfig.Enabled() { + workspaces, err := p.processPublicViewerRequest(ctx, workspaceName) + if err == nil { + ctx.Logger().Debugf("user %s/%s has no direct access to workspace %s, but it's visible to community: using community user", userID, username, workspaceName) + return workspaces, true, nil + } + } + return nil, false, crterr + } + + ctx.Logger().Debugf("user %s/%s has direct access to workspace %s", userID, username, workspaceName) + return []toolchainv1alpha1.Workspace{*workspace}, false, nil +} + +func (p *Proxy) processPublicViewerRequest(ctx echo.Context, workspaceName string) ([]toolchainv1alpha1.Workspace, *crterrors.Error) { + puSignup := &signup.Signup{ + Name: p.publicViewerConfig.Username(), + CompliantUsername: p.publicViewerConfig.Username(), + } + workspace, err := handlers.GetUserWorkspaceForSignup(ctx, p.spaceLister, puSignup, workspaceName) + 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)) } - return proxyPluginName, cluster, nil + return []toolchainv1alpha1.Workspace{*workspace}, nil +} + +func (p *Proxy) processUserRequest(ctx echo.Context, workspaceName string) (*toolchainv1alpha1.Workspace, *crterrors.Error) { + workspace, err := handlers.GetUserWorkspace(ctx, p.spaceLister, workspaceName) + 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)) + } + + return workspace, nil } func (p *Proxy) handleRequestAndRedirect(ctx echo.Context) error { requestReceivedTime := ctx.Get(context.RequestReceivedTime).(time.Time) - proxyPluginName, cluster, err := p.processRequest(ctx) + proxyPluginName, cluster, isPublicViewer, err := p.processRequest(ctx) if err != nil { p.metrics.RegServProxyAPIHistogramVec.WithLabelValues(fmt.Sprintf("%d", http.StatusNotAcceptable), metrics.MetricLabelRejected).Observe(time.Since(requestReceivedTime).Seconds()) return err } - reverseProxy := p.newReverseProxy(ctx, cluster, len(proxyPluginName) > 0) + + reverseProxy := p.newReverseProxy(ctx, cluster, len(proxyPluginName) > 0, isPublicViewer) routeTime := time.Since(requestReceivedTime) p.metrics.RegServProxyAPIHistogramVec.WithLabelValues(fmt.Sprintf("%d", http.StatusAccepted), cluster.APIURL().Host).Observe(routeTime.Seconds()) // Note that ServeHttp is non-blocking and uses a go routine under the hood @@ -461,10 +531,29 @@ func extractUserToken(req *http.Request) (string, error) { return token[1], nil } -func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, isPlugin bool) *httputil.ReverseProxy { +func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, isPlugin bool, isPublicViewer bool) *httputil.ReverseProxy { + director := p.buildImpersonatingDirector(ctx, target, isPlugin, isPublicViewer) + req := ctx.Request() + transport := getTransport(req.Header) + m := &responseModifier{req.Header.Get("Origin")} + + return &httputil.ReverseProxy{ + Director: director, + Transport: transport, + FlushInterval: -1, + ModifyResponse: m.addCorsToResponse, + } +} + +func (p *Proxy) buildImpersonatingDirector(ctx echo.Context, target *access.ClusterAccess, isPlugin bool, isPublicViewer bool) func(*http.Request) { targetQuery := target.APIURL().RawQuery - director := func(req *http.Request) { + + impersonateUser := target.Username() + ctx.Set("Impersonate-User", impersonateUser) // set impersonate-user context for logging + ctx.Logger().Infof("building reverse proxy impersonating %s", impersonateUser) + + return func(req *http.Request) { origin := req.URL.String() req.URL.Scheme = target.APIURL().Scheme req.URL.Host = target.APIURL().Host @@ -475,6 +564,7 @@ func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, // route on the member cluster req.Host = target.APIURL().Host } + log.InfoEchof(ctx, "forwarding %s to %s", origin, req.URL.String()) if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery @@ -485,6 +575,13 @@ func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, // explicitly disable User-Agent so it's not set to default value req.Header.Set("User-Agent", "") } + + if isPublicViewer { + // set SSO-User context for logging + username, _ := ctx.Get(context.UsernameKey).(string) + req.Header.Set("SSO-User", username) + } + // Replace token if wsstream.IsWebSocketRequest(req) { replaceTokenInWebsocketRequest(req, target.ImpersonatorToken()) @@ -493,15 +590,7 @@ func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, } // Set impersonation header - req.Header.Set("Impersonate-User", target.Username()) - } - transport := getTransport(req.Header) - m := &responseModifier{req.Header.Get("Origin")} - return &httputil.ReverseProxy{ - Director: director, - Transport: transport, - FlushInterval: -1, - ModifyResponse: m.addCorsToResponse, + req.Header.Set("Impersonate-User", impersonateUser) } } diff --git a/pkg/proxy/proxy_community_test.go b/pkg/proxy/proxy_community_test.go new file mode 100644 index 00000000..dd3213ba --- /dev/null +++ b/pkg/proxy/proxy_community_test.go @@ -0,0 +1,232 @@ +package proxy + +import ( + "context" + "fmt" + "log" + "net/http" + "net/http/httptest" + "time" + + appservice "github.com/codeready-toolchain/registration-service/pkg/application/service" + "github.com/codeready-toolchain/registration-service/pkg/auth" + "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" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" + testconfig "github.com/codeready-toolchain/toolchain-common/pkg/test/config" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (s *TestProxySuite) TestProxyCommunity() { + // given + + port := "30456" + + env := s.DefaultConfig().Environment() + defer s.SetConfig(testconfig.RegistrationService(). + Environment(env)) + s.SetConfig(testconfig.RegistrationService(). + Environment(string(testconfig.E2E))) // We use e2e-test environment just to be able to re-use token generation + _, err := auth.InitializeDefaultTokenParser() + require.NoError(s.T(), err) + + cfg := commonconfig.PublicViewerConfig{ + Config: toolchainv1alpha1.PublicViewerConfig{ + Enabled: true, + Username: "public-viewer", + }, + } + for _, environment := range []testconfig.EnvName{testconfig.E2E, testconfig.Dev, testconfig.Prod} { + s.Run("for environment "+string(environment), func() { + // spin up proxy + s.SetConfig(testconfig.RegistrationService(). + Environment(string(environment))) + fakeApp := &fake.ProxyFakeApp{} + p, server := s.spinUpProxy(fakeApp, cfg, port) + defer func() { + _ = server.Close() + }() + + // wait for proxy to be alive + s.Run("is alive", func() { + s.waitForProxyToBeAlive(port) + }) + s.Run("health check ok", func() { + s.checkProxyIsHealthy(port) + }) + + // run community tests + s.checkProxyCommunityOK(fakeApp, p, port, &cfg) + }) + } +} + +func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Proxy, port string, publicViewerConfig *commonconfig.PublicViewerConfig) { + s.Run("successfully proxy", func() { + owner, err := uuid.NewV4() + require.NoError(s.T(), err) + communityUser, err := uuid.NewV4() + require.NoError(s.T(), err) + + // Start the member-2 API Server + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + // Set the Access-Control-Allow-Origin header to make sure it's overridden by the proxy response modifier + w.Header().Set("Access-Control-Allow-Origin", "dummy") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("my response")) + require.NoError(s.T(), err) + })) + defer testServer.Close() + + type testCase struct { + ProxyRequestMethod string + ProxyRequestHeaders http.Header + ExpectedAPIServerRequestHeaders http.Header + ExpectedProxyResponseStatus int + RequestPath string + } + + tests := map[string]testCase{ + "plain http actual request as owner": { + ProxyRequestMethod: "GET", + ProxyRequestHeaders: map[string][]string{"Authorization": {"Bearer " + s.token(owner)}}, + ExpectedAPIServerRequestHeaders: map[string][]string{ + "Authorization": {"Bearer clusterSAToken"}, + "Impersonate-User": {"smith2"}, + }, + ExpectedProxyResponseStatus: http.StatusOK, + RequestPath: fmt.Sprintf("http://localhost:%s/workspaces/communityspace/api/communityspace/pods", port), + }, + "plain http actual request as community user": { + ProxyRequestMethod: "GET", + ProxyRequestHeaders: map[string][]string{"Authorization": {"Bearer " + s.token(communityUser)}}, + ExpectedAPIServerRequestHeaders: map[string][]string{ + "Authorization": {"Bearer clusterSAToken"}, + "Impersonate-User": {publicViewerConfig.Username()}, + "SSO-User": {"username-" + communityUser.String()}, + }, + ExpectedProxyResponseStatus: http.StatusOK, + RequestPath: fmt.Sprintf("http://localhost:%s/workspaces/communityspace/api/communityspace/pods", port), + }, + } + + for k, tc := range tests { + s.Run(k, func() { + + // given + fakeApp.Err = nil + + testServer.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + // Set the Access-Control-Allow-Origin header to make sure it's overridden by the proxy response modifier + w.Header().Set("Access-Control-Allow-Origin", "dummy") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("my response")) + require.NoError(s.T(), err) + for hk, hv := range tc.ExpectedAPIServerRequestHeaders { + require.Len(s.T(), r.Header.Values(hk), len(hv)) + for i := range hv { + assert.Equal(s.T(), hv[i], r.Header.Values(hk)[i], "header %s", hk) + } + } + }) + fakeApp.SignupServiceMock = fake.NewSignupService( + fake.Signup(owner.String(), &signup.Signup{ + Name: "smith2", + APIEndpoint: testServer.URL, + ClusterName: "member-2", + CompliantUsername: "smith2", + Username: "smith2@", + Status: signup.Status{ + Ready: true, + }, + }), + fake.Signup(communityUser.String(), &signup.Signup{ + Name: "communityUser", + APIEndpoint: testServer.URL, + ClusterName: "member-2", + CompliantUsername: "communityuser", + Username: "communityUser@", + Status: signup.Status{ + Ready: true, + }, + }), + ) + s.Application.MockSignupService(fakeApp.SignupServiceMock) + inf := fake.NewFakeInformer() + inf.GetSpaceFunc = func(name string) (*toolchainv1alpha1.Space, error) { + switch name { + case "communityspace": + return fake.NewSpace("communityspace", "member-2", "smith2"), nil + } + return nil, fmt.Errorf("space not found error") + } + + sbmycoolSmith2 := fake.NewSpaceBinding("communityspace-smith2", "smith2", "communityspace", "admin") + commSpacePublicViewer := fake.NewSpaceBinding("communityspace-publicviewer", p.publicViewerConfig.Username(), "communityspace", "viewer") + + cli := fake.InitClient(s.T(), sbmycoolSmith2, commSpacePublicViewer) + inf.ListSpaceBindingFunc = func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { + sbs := toolchainv1alpha1.SpaceBindingList{} + opts := &client.ListOptions{ + LabelSelector: labels.NewSelector().Add(reqs...), + } + log.Printf("received reqs: %v", reqs) + if err := cli.Client.List(context.TODO(), &sbs, opts); err != nil { + return nil, err + } + log.Printf("returning sbs: %v", sbs.Items) + return sbs.Items, nil + } + inf.GetProxyPluginConfigFunc = func(_ string) (*toolchainv1alpha1.ProxyPlugin, error) { + return nil, fmt.Errorf("proxy plugin not found") + } + inf.GetNSTemplateTierFunc = func(_ string) (*toolchainv1alpha1.NSTemplateTier, error) { + return fake.NewBase1NSTemplateTier(), nil + } + s.Application.MockInformerService(inf) + fakeApp.MemberClusterServiceMock = s.newMemberClusterServiceWithMembers(testServer.URL, publicViewerConfig) + fakeApp.InformerServiceMock = inf + + p.spaceLister = &handlers.SpaceLister{ + GetSignupFunc: fakeApp.SignupServiceMock.GetSignupFromInformer, + GetInformerServiceFunc: func() appservice.InformerService { + return inf + }, + ProxyMetrics: p.metrics, + } + + // prepare request + req, err := http.NewRequest(tc.ProxyRequestMethod, tc.RequestPath, nil) + require.NoError(s.T(), err) + require.NotNil(s.T(), req) + + for hk, hv := range tc.ProxyRequestHeaders { + for _, v := range hv { + req.Header.Add(hk, v) + } + } + + // when + client := http.Client{Timeout: 3 * time.Second} + resp, err := client.Do(req) + + // then + require.NoError(s.T(), err) + require.NotNil(s.T(), resp) + defer resp.Body.Close() + assert.Equal(s.T(), tc.ExpectedProxyResponseStatus, resp.StatusCode) + s.assertResponseBody(resp, "my response") + }) + } + }) +} diff --git a/pkg/proxy/proxy_test.go b/pkg/proxy/proxy_test.go index 05f0ad78..0fb6a4be 100644 --- a/pkg/proxy/proxy_test.go +++ b/pkg/proxy/proxy_test.go @@ -31,6 +31,7 @@ import ( toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" commoncluster "github.com/codeready-toolchain/toolchain-common/pkg/cluster" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" commontest "github.com/codeready-toolchain/toolchain-common/pkg/test" authsupport "github.com/codeready-toolchain/toolchain-common/pkg/test/auth" testconfig "github.com/codeready-toolchain/toolchain-common/pkg/test/config" @@ -66,62 +67,27 @@ func (s *TestProxySuite) TestProxy() { _, err := auth.InitializeDefaultTokenParser() require.NoError(s.T(), err) + cfg := commonconfig.PublicViewerConfig{ + Config: toolchainv1alpha1.PublicViewerConfig{ + Enabled: false, + }, + } for _, environment := range []testconfig.EnvName{testconfig.E2E, testconfig.Dev, testconfig.Prod} { s.Run("for environment "+string(environment), func() { s.SetConfig(testconfig.RegistrationService(). Environment(string(environment))) fakeApp := &fake.ProxyFakeApp{} - proxyMetrics := metrics.NewProxyMetrics(prometheus.NewRegistry()) - p, err := newProxyWithClusterClient(fakeApp, nil, proxyMetrics, proxytest.NewGetMembersFunc(fake.InitClient(s.T()))) - require.NoError(s.T(), err) - - server := p.StartProxy(DefaultPort) - require.NotNil(s.T(), server) + p, server := s.spinUpProxy(fakeApp, cfg, DefaultPort) defer func() { _ = server.Close() }() - // Wait up to N seconds for the Proxy server to start - ready := false - sec := 10 - for i := 0; i < sec; i++ { - log.Println("Checking if Proxy is started...") - req, err := http.NewRequest("GET", "http://localhost:8081/api/mycoolworkspace/pods", nil) - require.NoError(s.T(), err) - require.NotNil(s.T(), req) - resp, err := http.DefaultClient.Do(req) - if err != nil { - time.Sleep(time.Second) - continue - } - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - if resp.StatusCode != http.StatusUnauthorized { - // The server may be running but still not fully ready to accept requests - time.Sleep(time.Second) - continue - } - // Server is up and running! - ready = true - break - } - require.True(s.T(), ready, "Proxy is not ready after %d seconds", sec) - + s.Run("is alive", func() { + s.waitForProxyToBeAlive(DefaultPort) + }) s.Run("health check ok", func() { - req, err := http.NewRequest("GET", "http://localhost:8081/proxyhealth", nil) - require.NoError(s.T(), err) - require.NotNil(s.T(), req) - - // when - resp, err := http.DefaultClient.Do(req) - - // then - require.NoError(s.T(), err) - require.NotNil(s.T(), resp) - defer resp.Body.Close() - assert.Equal(s.T(), http.StatusOK, resp.StatusCode) - s.assertResponseBody(resp, `{"alive": true}`) + s.checkProxyIsHealthy(DefaultPort) }) s.checkPlainHTTPErrors(fakeApp) @@ -132,6 +98,62 @@ func (s *TestProxySuite) TestProxy() { } } +func (s *TestProxySuite) spinUpProxy(fakeApp *fake.ProxyFakeApp, cfg commonconfig.PublicViewerConfig, port string) (*Proxy, *http.Server) { + proxyMetrics := metrics.NewProxyMetrics(prometheus.NewRegistry()) + p, err := newProxyWithClusterClient( + fakeApp, nil, proxyMetrics, proxytest.NewGetMembersFunc(fake.InitClient(s.T())), &cfg) + require.NoError(s.T(), err) + + server := p.StartProxy(port) + require.NotNil(s.T(), server) + + return p, server +} + +func (s *TestProxySuite) waitForProxyToBeAlive(port string) { + // Wait up to N seconds for the Proxy server to start + ready := false + sec := 10 + for i := 0; i < sec; i++ { + log.Println("Checking if Proxy is started...") + req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%s/api/mycoolworkspace/pods", port), nil) + require.NoError(s.T(), err) + require.NotNil(s.T(), req) + resp, err := http.DefaultClient.Do(req) + if err != nil { + time.Sleep(time.Second) + continue + } + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + if resp.StatusCode != http.StatusUnauthorized { + // The server may be running but still not fully ready to accept requests + time.Sleep(time.Second) + continue + } + // Server is up and running! + ready = true + break + } + require.True(s.T(), ready, "Proxy is not ready after %d seconds", sec) +} + +func (s *TestProxySuite) checkProxyIsHealthy(port string) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%s/proxyhealth", port), nil) + require.NoError(s.T(), err) + require.NotNil(s.T(), req) + + // when + resp, err := http.DefaultClient.Do(req) + + // then + require.NoError(s.T(), err) + require.NotNil(s.T(), resp) + defer resp.Body.Close() + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + s.assertResponseBody(resp, `{"alive": true}`) +} + func (s *TestProxySuite) checkPlainHTTPErrors(fakeApp *fake.ProxyFakeApp) { s.Run("plain http error", func() { s.Run("unauthorized if no token present", func() { @@ -667,6 +689,9 @@ func (s *TestProxySuite) checkProxyOK(fakeApp *fake.ProxyFakeApp, p *Proxy) { if req.Values().List()[0] == "smith2" || req.Values().List()[0] == "mycoolworkspace" { spaceBindings = []toolchainv1alpha1.SpaceBinding{*fake.NewSpaceBinding("mycoolworkspace-smith2", "smith2", "mycoolworkspace", "admin")} } + if p.publicViewerConfig.Enabled() && req.Values().List()[0] == p.publicViewerConfig.Username() { + spaceBindings = []toolchainv1alpha1.SpaceBinding{*fake.NewSpaceBinding("communityspace-publicviewer", "publicviewer", "communityspace", "viewer")} + } } return spaceBindings, nil } @@ -693,7 +718,7 @@ func (s *TestProxySuite) checkProxyOK(fakeApp *fake.ProxyFakeApp, p *Proxy) { return fake.NewBase1NSTemplateTier(), nil } s.Application.MockInformerService(inf) - fakeApp.MemberClusterServiceMock = s.newMemberClusterServiceWithMembers(testServer.URL) + fakeApp.MemberClusterServiceMock = s.newMemberClusterServiceWithMembers(testServer.URL, &commonconfig.PublicViewerConfig{}) p.spaceLister = &handlers.SpaceLister{ GetSignupFunc: fakeApp.SignupServiceMock.GetSignupFromInformer, @@ -735,7 +760,7 @@ type headerToAdd struct { key, value string } -func (s *TestProxySuite) newMemberClusterServiceWithMembers(serverURL string) appservice.MemberClusterService { +func (s *TestProxySuite) newMemberClusterServiceWithMembers(serverURL string, publicViewerConfig *commonconfig.PublicViewerConfig) appservice.MemberClusterService { fakeClient := commontest.NewFakeClient(s.T()) serverHost := serverURL switch { @@ -765,6 +790,7 @@ func (s *TestProxySuite) newMemberClusterServiceWithMembers(serverURL string) ap Svcs: s.Application, }, func(si *service.ServiceImpl) { + si.PublicViewerConfig = *publicViewerConfig si.GetMembersFunc = func(_ ...commoncluster.Condition) []*commoncluster.CachedToolchainCluster { return []*commoncluster.CachedToolchainCluster{ { diff --git a/pkg/proxy/service/cluster_service.go b/pkg/proxy/service/cluster_service.go index 95551105..90799da0 100644 --- a/pkg/proxy/service/cluster_service.go +++ b/pkg/proxy/service/cluster_service.go @@ -12,6 +12,7 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/log" "github.com/codeready-toolchain/registration-service/pkg/proxy/access" "github.com/codeready-toolchain/toolchain-common/pkg/cluster" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" routev1 "github.com/openshift/api/route/v1" @@ -25,7 +26,8 @@ type Option func(f *ServiceImpl) // ServiceImpl represents the implementation of the member cluster service. type ServiceImpl struct { // nolint:revive base.BaseService - GetMembersFunc cluster.GetMemberClustersFunc + GetMembersFunc cluster.GetMemberClustersFunc + PublicViewerConfig commonconfig.PublicViewerConfig } // NewMemberClusterService creates a service object for performing toolchain cluster related activities. @@ -40,23 +42,49 @@ func NewMemberClusterService(context servicecontext.ServiceContext, options ...O return si } +func WithPublicViewerConfig(config commonconfig.PublicViewerConfig) Option { + return func(f *ServiceImpl) { + f.PublicViewerConfig = config + } +} + func (s *ServiceImpl) GetClusterAccess(userID, username, workspace, proxyPluginName string) (*access.ClusterAccess, error) { + // if workspace is not provided then return the default space access + if workspace == "" { + return s.getClusterAccessForDefaultWorkspace(userID, username, proxyPluginName) + } + + return s.getSpaceAccess(userID, username, workspace, proxyPluginName) +} + +func (s *ServiceImpl) getSpaceAccess(userID, username, workspace, proxyPluginName string) (*access.ClusterAccess, error) { signup, err := s.Services().SignupService().GetSignupFromInformer(nil, userID, username, false) // don't check for usersignup complete status, since it might cause the proxy blocking the request and returning an error when quick transitions from ready to provisioning are happening. if err != nil { return nil, err } // if signup has the CompliantUsername set it means that MUR was created and useraccount is provisioned if signup == nil || signup.CompliantUsername == "" { + if s.PublicViewerConfig.Enabled() { + return s.getSpaceAccessAsPublicViewer(workspace, proxyPluginName) + } + cause := errs.New("user is not provisioned (yet)") log.Error(nil, cause, fmt.Sprintf("signup object: %+v", signup)) return nil, cause } - // if workspace is not provided then return the default space access - if workspace == "" { - return s.accessForCluster(signup.APIEndpoint, signup.ClusterName, signup.CompliantUsername, proxyPluginName) + // look up space + space, err := s.Services().InformerService().GetSpace(workspace) + if 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.Error(nil, err, "unable to get target cluster for workspace "+workspace) + return nil, fmt.Errorf("the requested space is not available") } + return s.accessForSpace(space, signup.CompliantUsername, proxyPluginName) +} + +func (s *ServiceImpl) getSpaceAccessAsPublicViewer(workspace, proxyPluginName string) (*access.ClusterAccess, error) { // look up space space, err := s.Services().InformerService().GetSpace(workspace) if err != nil { @@ -65,7 +93,23 @@ func (s *ServiceImpl) GetClusterAccess(userID, username, workspace, proxyPluginN return nil, fmt.Errorf("the requested space is not available") } - return s.accessForSpace(space, signup.CompliantUsername, proxyPluginName) + // return access as public-viewer + return s.accessForSpace(space, s.PublicViewerConfig.Username(), proxyPluginName) +} + +func (s *ServiceImpl) getClusterAccessForDefaultWorkspace(userID, username, proxyPluginName string) (*access.ClusterAccess, error) { + signup, err := s.Services().SignupService().GetSignupFromInformer(nil, userID, username, false) // don't check for usersignup complete status, since it might cause the proxy blocking the request and returning an error when quick transitions from ready to provisioning are happening. + if err != nil { + return nil, err + } + // if signup has the CompliantUsername set it means that MUR was created and useraccount is provisioned + if signup == nil || signup.CompliantUsername == "" { + cause := errs.New("user is not provisioned (yet)") + log.Error(nil, cause, fmt.Sprintf("signup object: %+v", signup)) + return nil, cause + } + + return s.accessForCluster(signup.APIEndpoint, signup.ClusterName, signup.CompliantUsername, proxyPluginName) } func (s *ServiceImpl) accessForSpace(space *toolchainv1alpha1.Space, username, proxyPluginName string) (*access.ClusterAccess, error) { diff --git a/pkg/server/in_cluster_application.go b/pkg/server/in_cluster_application.go index 979cba2d..a8f9cef1 100644 --- a/pkg/server/in_cluster_application.go +++ b/pkg/server/in_cluster_application.go @@ -7,6 +7,7 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/configuration" "github.com/codeready-toolchain/registration-service/pkg/informers" "github.com/codeready-toolchain/registration-service/pkg/kubeclient" + commonconfig "github.com/codeready-toolchain/toolchain-common/pkg/configuration" "k8s.io/client-go/rest" ) @@ -14,7 +15,7 @@ import ( // application type is intended to run inside a Kubernetes cluster, where it makes use of the rest.InClusterConfig() // function to determine which Kubernetes configuration to use to create the REST client that interacts with the // Kubernetes service endpoints. -func NewInClusterApplication(informer informers.Informer) (application.Application, error) { +func NewInClusterApplication(informer informers.Informer, config commonconfig.PublicViewerConfig) (application.Application, error) { k8sConfig, err := rest.InClusterConfig() if err != nil { return nil, err @@ -29,7 +30,9 @@ func NewInClusterApplication(informer informers.Informer) (application.Applicati serviceFactory: factory.NewServiceFactory( factory.WithServiceContextOptions(factory.CRTClientOption(kubeClient), factory.InformerOption(informer), - )), + ), + factory.WithPublicViewerConfig(config), + ), }, nil } diff --git a/test/fake/proxy.go b/test/fake/proxy.go index bad15e29..238343ec 100644 --- a/test/fake/proxy.go +++ b/test/fake/proxy.go @@ -17,10 +17,15 @@ type ProxyFakeApp struct { Err error SignupServiceMock service.SignupService MemberClusterServiceMock service.MemberClusterService + InformerServiceMock service.InformerService } func (a *ProxyFakeApp) InformerService() service.InformerService { - panic("InformerService shouldn't be called") + if a.InformerServiceMock == nil { + panic("InformerService shouldn't be called") + } + + return a.InformerServiceMock } func (a *ProxyFakeApp) SignupService() service.SignupService {