From 93998910df65cd388d747ce882a05bb37d539012 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Mon, 18 Mar 2024 13:12:42 +0100 Subject: [PATCH 1/8] Adding public-viewer configuration Signed-off-by: Francesco Ilario --- go.mod | 4 ++++ go.sum | 8 ++++---- pkg/configuration/configuration.go | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 346479b5..cf9a550f 100644 --- a/go.mod +++ b/go.mod @@ -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-20240318115656-7ddb03191083 + +replace github.com/codeready-toolchain/api => github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285 diff --git a/go.sum b/go.sum index 74ca0e5d..d9acfcf0 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-20240318114050-43313b449285 h1:rwbjw9Sx2i/KTXAsS7+SmoeSCM+qYmwbdK/0I+RADMo= +github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285/go.mod h1:cfNN6YPX4TORvhhZXMSjSPesqAHlB3nD/WAfGe4WLKQ= +github.com/filariow/toolchain-common v0.0.0-20240318115656-7ddb03191083 h1:8d5w/P992nUVESD2e+8lLmj/T0Zy5rejEy9V0gjo7/U= +github.com/filariow/toolchain-common v0.0.0-20240318115656-7ddb03191083/go.mod h1:p6LHpV0Cq/Ope2WyDY2qeLqFmm19yfbArwUV9XpaRE4= 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/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) } From 38a5e5d25f4e2ebd8e81459cc86f50481de5ca98 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Mon, 18 Mar 2024 18:13:20 +0100 Subject: [PATCH 2/8] retrieving spaces Signed-off-by: Francesco Ilario --- cmd/main.go | 3 +- go.mod | 2 +- go.sum | 4 +- pkg/proxy/handlers/spacelister_get.go | 26 ++++-- pkg/proxy/handlers/spacelister_get_test.go | 28 +++--- pkg/proxy/handlers/spacelister_list.go | 51 +++++++---- pkg/proxy/handlers/spacelister_list_test.go | 98 ++++++++++++++++++++- pkg/proxy/handlers/spacelister_test.go | 28 ++++-- pkg/proxy/proxy.go | 39 ++++---- pkg/proxy/proxy_test.go | 6 +- 10 files changed, 220 insertions(+), 65 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index baf30732..70deb041 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -114,7 +114,8 @@ func main() { metricsSrv := proxyMetrics.StartMetricsServer() // Start the proxy server - p, err := proxy.NewProxy(app, proxyMetrics, cluster.GetMemberClusters) + pvcfg := crtConfig.PublicViewer() + 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 cf9a550f..ea59a701 100644 --- a/go.mod +++ b/go.mod @@ -147,6 +147,6 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace github.com/codeready-toolchain/toolchain-common => github.com/filariow/toolchain-common v0.0.0-20240318115656-7ddb03191083 +replace github.com/codeready-toolchain/toolchain-common => github.com/filariow/toolchain-common v0.0.0-20240318122124-5b365181bd76 replace github.com/codeready-toolchain/api => github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285 diff --git a/go.sum b/go.sum index d9acfcf0..d537bc0d 100644 --- a/go.sum +++ b/go.sum @@ -141,8 +141,8 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285 h1:rwbjw9Sx2i/KTXAsS7+SmoeSCM+qYmwbdK/0I+RADMo= github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285/go.mod h1:cfNN6YPX4TORvhhZXMSjSPesqAHlB3nD/WAfGe4WLKQ= -github.com/filariow/toolchain-common v0.0.0-20240318115656-7ddb03191083 h1:8d5w/P992nUVESD2e+8lLmj/T0Zy5rejEy9V0gjo7/U= -github.com/filariow/toolchain-common v0.0.0-20240318115656-7ddb03191083/go.mod h1:p6LHpV0Cq/Ope2WyDY2qeLqFmm19yfbArwUV9XpaRE4= +github.com/filariow/toolchain-common v0.0.0-20240318122124-5b365181bd76 h1:vEzqg+pqEUXBpo0IIYVScHJETZfNp483lsnM+acweIQ= +github.com/filariow/toolchain-common v0.0.0-20240318122124-5b365181bd76/go.mod h1:p6LHpV0Cq/Ope2WyDY2qeLqFmm19yfbArwUV9XpaRE4= 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/proxy/handlers/spacelister_get.go b/pkg/proxy/handlers/spacelister_get.go index 1d54cb33..f7748b12 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)) @@ -47,6 +48,7 @@ func HandleSpaceGetRequest(spaceLister *SpaceLister, GetMembersFunc cluster.GetM func GetUserWorkspace(ctx echo.Context, spaceLister *SpaceLister, workspaceName string) (*toolchainv1alpha1.Workspace, error) { userSignup, space, err := getUserSignupAndSpace(ctx, spaceLister, workspaceName) if err != nil { + ctx.Logger().Error(errs.Wrap(err, "provisioned user signup error")) return nil, err } // signup is not ready @@ -88,16 +90,23 @@ func GetUserWorkspace(ctx echo.Context, spaceLister *SpaceLister, workspaceName } // 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 +125,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. diff --git a/pkg/proxy/handlers/spacelister_get_test.go b/pkg/proxy/handlers/spacelister_get_test.go index 5b0f9382..a1c24931 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 @@ -244,7 +246,7 @@ func TestSpaceListerGet(t *testing.T) { expectedErr: "nstemplatetier error", expectedErrCode: 500, overrideInformerFunc: func() service.InformerService { - informerFunc := fake.GetInformerService(fakeClient, fake.WithGetNSTemplateTierFunc(func(_ string) (*toolchainv1alpha1.NSTemplateTier, error) { + informerFunc := fake.GetInformerService(fakeClient, fake.WithGetNSTemplateTierFunc(func(tierName string) (*toolchainv1alpha1.NSTemplateTier, error) { return nil, fmt.Errorf("nstemplatetier error") })) return informerFunc() @@ -256,7 +258,7 @@ func TestSpaceListerGet(t *testing.T) { expectedWs: []toolchainv1alpha1.Workspace{}, expectedErr: "signup error", expectedErrCode: 500, - overrideSignupFunc: func(_ *gin.Context, _, _ string, _ bool) (*signup.Signup, error) { + overrideSignupFunc: func(ctx *gin.Context, userID, username string, checkUserSignupComplete bool) (*signup.Signup, error) { return nil, fmt.Errorf("signup error") }, expectedWorkspace: "dancelover", @@ -274,7 +276,7 @@ func TestSpaceListerGet(t *testing.T) { expectedErr: "list spacebindings error", expectedErrCode: 500, overrideInformerFunc: func() service.InformerService { - listSpaceBindingFunc := func(_ ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { + listSpaceBindingFunc := func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { return nil, fmt.Errorf("list spacebindings error") } return fake.GetInformerService(fakeClient, fake.WithListSpaceBindingFunc(listSpaceBindingFunc))() @@ -287,7 +289,7 @@ func TestSpaceListerGet(t *testing.T) { expectedErr: "\"workspaces.toolchain.dev.openshift.com \\\"dancelover\\\" not found\"", expectedErrCode: 404, overrideInformerFunc: func() service.InformerService { - getSpaceFunc := func(_ string) (*toolchainv1alpha1.Space, error) { + getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { return nil, fmt.Errorf("no space") } return fake.GetInformerService(fakeClient, fake.WithGetSpaceFunc(getSpaceFunc))() @@ -512,7 +514,7 @@ func TestSpaceListerGet(t *testing.T) { expectedWorkspace: "movielover", expectedErr: "no member clusters found", expectedErrCode: 500, - overrideGetMembersFunc: func(_ ...commoncluster.Condition) []*commoncluster.CachedToolchainCluster { + overrideGetMembersFunc: func(conditions ...commoncluster.Condition) []*commoncluster.CachedToolchainCluster { return []*commoncluster.CachedToolchainCluster{} }, }, @@ -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 != "" { @@ -576,7 +578,7 @@ func TestSpaceListerGet(t *testing.T) { // get workspace case workspace, decodeErr := decodeResponseToWorkspace(rec.Body.Bytes()) require.NoError(t, decodeErr) - require.Len(t, tc.expectedWs, 1, "test case should have exactly one expected item since it's a get request") + require.Equal(t, 1, len(tc.expectedWs), "test case should have exactly one expected item since it's a get request") for i := range tc.expectedWs { assert.Equal(t, tc.expectedWs[i].Name, workspace.Name) assert.Equal(t, tc.expectedWs[i].Status, workspace.Status) @@ -638,7 +640,7 @@ func TestGetUserWorkspace(t *testing.T) { username: "invalid.user", workspaceRequest: "batman", overrideInformerFunc: func() service.InformerService { - getSpaceFunc := func(_ string) (*toolchainv1alpha1.Space, error) { + getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { return nil, fmt.Errorf("no space") } return fake.GetInformerService(fakeClient, fake.WithGetSpaceFunc(getSpaceFunc))() @@ -649,7 +651,7 @@ func TestGetUserWorkspace(t *testing.T) { username: "invalid.user", workspaceRequest: "batman", overrideInformerFunc: func() service.InformerService { - getSpaceFunc := func(_ string) (*toolchainv1alpha1.Space, error) { + getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { return nil, fmt.Errorf("no space") } return fake.GetInformerService(fakeClient, fake.WithGetSpaceFunc(getSpaceFunc))() @@ -660,7 +662,7 @@ func TestGetUserWorkspace(t *testing.T) { username: "batman.space", workspaceRequest: "batman", expectedErr: "signup error", - overrideSignupFunc: func(_ *gin.Context, _, _ string, _ bool) (*signup.Signup, error) { + overrideSignupFunc: func(ctx *gin.Context, userID, username string, checkUserSignupComplete bool) (*signup.Signup, error) { return nil, fmt.Errorf("signup error") }, expectedWorkspace: nil, @@ -670,7 +672,7 @@ func TestGetUserWorkspace(t *testing.T) { workspaceRequest: "robin", expectedErr: "list spacebindings error", overrideInformerFunc: func() service.InformerService { - listSpaceBindingFunc := func(_ ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { + listSpaceBindingFunc := func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { return nil, fmt.Errorf("list spacebindings error") } return fake.GetInformerService(fakeClient, fake.WithListSpaceBindingFunc(listSpaceBindingFunc))() @@ -719,7 +721,7 @@ func TestGetUserWorkspace(t *testing.T) { } if tc.expectedWorkspace != nil { - require.Equal(t, tc.expectedWorkspace, wrk) + require.Equal(t, wrk, tc.expectedWorkspace) } else { require.Nil(t, wrk) // user is not authorized to get this workspace } diff --git a/pkg/proxy/handlers/spacelister_list.go b/pkg/proxy/handlers/spacelister_list.go index f0776d9c..a00ec89b 100644 --- a/pkg/proxy/handlers/spacelister_list.go +++ b/pkg/proxy/handlers/spacelister_list.go @@ -4,24 +4,27 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "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,16 +36,18 @@ 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 - } - murName := signup.CompliantUsername + murName := func() string { + // signup is not ready + if signup == nil { + return publicViewerConfig.Username() + } + return signup.CompliantUsername + }() // get all spacebindings with given mur since no workspace was provided spaceBindings, err := listSpaceBindingsForUser(spaceLister, murName) @@ -50,7 +55,24 @@ func ListUserWorkspaces(ctx echo.Context, spaceLister *SpaceLister) ([]toolchain ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) return nil, err } - return workspacesFromSpaceBindings(ctx, spaceLister, signup.Name, spaceBindings), nil + + if publicViewerConfig.Enabled() { + sbs, err := listSpaceBindingsForUser(spaceLister, publicViewerConfig.Username()) + if err != nil { + ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) + return nil, err + } + + for _, sb := range sbs { + if !slices.ContainsFunc(spaceBindings, func(lsb toolchainv1alpha1.SpaceBinding) bool { + return sb.Name == lsb.Name + }) { + spaceBindings = append(spaceBindings, sb) + } + } + } + + return workspacesFromSpaceBindings(ctx, spaceLister, murName, spaceBindings), nil } func listWorkspaceResponse(ctx echo.Context, workspaces []toolchainv1alpha1.Workspace) error { @@ -68,12 +90,11 @@ func listWorkspaceResponse(ctx echo.Context, workspaces []toolchainv1alpha1.Work } func listSpaceBindingsForUser(spaceLister *SpaceLister, murName string) ([]toolchainv1alpha1.SpaceBinding, error) { - murSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey, selection.Equals, []string{murName}) + murSelector, err := labels.NewRequirement(toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey, selection.In, []string{murName}) 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 { diff --git a/pkg/proxy/handlers/spacelister_list_test.go b/pkg/proxy/handlers/spacelister_list_test.go index 22e4b4a7..ac6a72a4 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, "dancelover", "admin", true), + workspaceFor(t, fakeClient, "movielover", "other", false), + workspaceFor(t, fakeClient, "communityspace", "viewer", 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..19764c3e 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -27,6 +27,7 @@ import ( "github.com/codeready-toolchain/registration-service/pkg/proxy/access" "github.com/codeready-toolchain/registration-service/pkg/proxy/handlers" 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 +64,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 +90,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 +142,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: @@ -294,7 +297,7 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, workspaces = []toolchainv1alpha1.Workspace{*workspace} } else { // list all workspaces - workspaces, err = handlers.ListUserWorkspaces(ctx, p.spaceLister) + 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()) } diff --git a/pkg/proxy/proxy_test.go b/pkg/proxy/proxy_test.go index 05f0ad78..c21668b6 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,6 +67,8 @@ func (s *TestProxySuite) TestProxy() { _, err := auth.InitializeDefaultTokenParser() require.NoError(s.T(), err) + cfg := commonconfig.PublicViewerConfig{} + for _, environment := range []testconfig.EnvName{testconfig.E2E, testconfig.Dev, testconfig.Prod} { s.Run("for environment "+string(environment), func() { @@ -73,7 +76,8 @@ func (s *TestProxySuite) TestProxy() { Environment(string(environment))) fakeApp := &fake.ProxyFakeApp{} proxyMetrics := metrics.NewProxyMetrics(prometheus.NewRegistry()) - p, err := newProxyWithClusterClient(fakeApp, nil, proxyMetrics, proxytest.NewGetMembersFunc(fake.InitClient(s.T()))) + p, err := newProxyWithClusterClient( + fakeApp, nil, proxyMetrics, proxytest.NewGetMembersFunc(fake.InitClient(s.T())), &cfg) require.NoError(s.T(), err) server := p.StartProxy(DefaultPort) From 6c5b2ce444a4395c6a20a18de934862e1a65b394 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Tue, 19 Mar 2024 01:55:37 +0100 Subject: [PATCH 3/8] proxy as public-viewer Signed-off-by: Francesco Ilario --- pkg/proxy/handlers/spacelister_get.go | 17 +- pkg/proxy/proxy.go | 148 ++++++++++++---- pkg/proxy/proxy_community_test.go | 232 ++++++++++++++++++++++++++ pkg/proxy/proxy_test.go | 117 +++++++------ test/fake/proxy.go | 7 +- 5 files changed, 434 insertions(+), 87 deletions(-) create mode 100644 pkg/proxy/proxy_community_test.go diff --git a/pkg/proxy/handlers/spacelister_get.go b/pkg/proxy/handlers/spacelister_get.go index f7748b12..440ebc6b 100644 --- a/pkg/proxy/handlers/spacelister_get.go +++ b/pkg/proxy/handlers/spacelister_get.go @@ -46,16 +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}) diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 19764c3e..ad6625ec 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -26,6 +26,7 @@ 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" @@ -266,58 +267,113 @@ 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, string, 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()) + return "", nil, "", false, 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) - if err != nil { - return "", nil, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error()) - } - if workspace == nil { - // not found - return "", nil, crterrors.NewForbiddenError("invalid workspace request", fmt.Sprintf("access to workspace '%s' is forbidden", workspaceName)) - } - // workspace was found means we can forward the request - workspaces = []toolchainv1alpha1.Workspace{*workspace} - } else { + + // access to user's home workspace + if workspaceName == "" { // list all workspaces - workspaces, err = handlers.ListUserWorkspaces(ctx, p.spaceLister, p.publicViewerConfig) + 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()) + } + + requestedNamespace := namespaceFromCtx(ctx) + if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { + return "", nil, "", false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) } + + return proxyPluginName, cluster, workspaceName, 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. + + // workspace was found means we can forward the request 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()) + } + return proxyPluginName, cluster, workspaceName, isPublicViewer, nil +} + +func (p *Proxy) processDirectWorkspaceAccessRequest(ctx echo.Context, workspaceName string) ([]toolchainv1alpha1.Workspace, bool, *crterrors.Error) { + userID, _ := ctx.Get(context.SubKey).(string) + username, _ := ctx.Get(context.UsernameKey).(string) + workspaces, crterr := p.processUserRequest(ctx, workspaceName) + switch { + case crterr == nil: + // user has direct access to workspace: use user identity + ctx.Logger().Debugf("user %s/%s has direct access to workspace %s", userID, username, workspaceName) + return workspaces, false, nil + case crterr.Code == http.StatusForbidden && p.publicViewerConfig.Enabled(): + // user has no direct access, but workspace is community: use public-viewer + ctx.Logger().Debugf("user %s/%s doesn't have direct access to workspace %s", userID, username, workspaceName) + workspaces, err := p.processPublicViewerRequest(ctx, workspaceName) + if err != nil { + return nil, false, crterr + } + return workspaces, true, nil + default: + // user has no direct access, neither the workspace is community + ctx.Logger().Debugf("user %s/%s doesn't have direct access to workspace private %s", userID, username, workspaceName) + return nil, false, crterr + } +} + +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 []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 proxyPluginName, cluster, nil + return []toolchainv1alpha1.Workspace{*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, workspace, 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, workspace, 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 @@ -464,10 +520,34 @@ 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, workspace string, 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 := func() string { + if p.publicViewerConfig.Enabled() && isPublicViewer { + return p.publicViewerConfig.Username() + } + return target.Username() + }() + + 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 @@ -478,6 +558,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 @@ -488,6 +569,11 @@ 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 { + req.Header.Set("SSO-User", target.Username()) + } + // Replace token if wsstream.IsWebSocketRequest(req) { replaceTokenInWebsocketRequest(req, target.ImpersonatorToken()) @@ -496,15 +582,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..05e2500e --- /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.Username()) + }) + } +} + +func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Proxy, port, publicViewer string) { + 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": {publicViewer}, + "SSO-User": {"communityuser"}, + }, + 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]) + } + } + }) + 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(name 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) + 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 c21668b6..25c20c58 100644 --- a/pkg/proxy/proxy_test.go +++ b/pkg/proxy/proxy_test.go @@ -67,65 +67,27 @@ func (s *TestProxySuite) TestProxy() { _, err := auth.InitializeDefaultTokenParser() require.NoError(s.T(), err) - cfg := commonconfig.PublicViewerConfig{} - + 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())), &cfg) - 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) @@ -136,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() { @@ -671,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 } 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 { From 17f69f598fe1b307791a1831e10be1d198e972c8 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Wed, 20 Mar 2024 14:50:20 +0100 Subject: [PATCH 4/8] Remove slices dependency Signed-off-by: Francesco Ilario --- pkg/proxy/handlers/spacelister.go | 4 +- pkg/proxy/handlers/spacelister_get.go | 4 +- pkg/proxy/handlers/spacelister_list.go | 50 ++++++++++----------- pkg/proxy/handlers/spacelister_list_test.go | 2 +- 4 files changed, 28 insertions(+), 32 deletions(-) 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 440ebc6b..a2d5216b 100644 --- a/pkg/proxy/handlers/spacelister_get.go +++ b/pkg/proxy/handlers/spacelister_get.go @@ -97,7 +97,7 @@ func GetUserWorkspaceForSignup(ctx echo.Context, spaceLister *SpaceLister, userS 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) @@ -165,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_list.go b/pkg/proxy/handlers/spacelister_list.go index a00ec89b..f9b056d2 100644 --- a/pkg/proxy/handlers/spacelister_list.go +++ b/pkg/proxy/handlers/spacelister_list.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "slices" "time" "github.com/labstack/echo/v4" @@ -41,35 +40,32 @@ func ListUserWorkspaces(ctx echo.Context, spaceLister *SpaceLister, publicViewer if err != nil { return nil, err } - murName := func() string { - // signup is not ready - if signup == nil { - return publicViewerConfig.Username() - } - return signup.CompliantUsername - }() - // get all spacebindings with given mur since no workspace was provided - spaceBindings, err := listSpaceBindingsForUser(spaceLister, murName) - if err != nil { - ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) - return nil, err + // signup is not ready + var murName *string + if signup != nil { + murName = &signup.CompliantUsername } - if publicViewerConfig.Enabled() { - sbs, err := listSpaceBindingsForUser(spaceLister, publicViewerConfig.Username()) - if err != nil { - ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) - return nil, err + murNames := func() []string { + names := []string{} + if publicViewerConfig.Enabled() { + names = append(names, publicViewerConfig.Username()) } - for _, sb := range sbs { - if !slices.ContainsFunc(spaceBindings, func(lsb toolchainv1alpha1.SpaceBinding) bool { - return sb.Name == lsb.Name - }) { - spaceBindings = append(spaceBindings, sb) - } + 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 := listSpaceBindingsForUsers(spaceLister, murNames) + if err != nil { + ctx.Logger().Error(errs.Wrap(err, "error listing space bindings")) + return nil, err } return workspacesFromSpaceBindings(ctx, spaceLister, murName, spaceBindings), nil @@ -89,15 +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.In, []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 } 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 ac6a72a4..1a230c03 100644 --- a/pkg/proxy/handlers/spacelister_list_test.go +++ b/pkg/proxy/handlers/spacelister_list_test.go @@ -45,9 +45,9 @@ func TestSpaceListerListCommunity(t *testing.T) { "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), - workspaceFor(t, fakeClient, "communityspace", "viewer", false), }, expectedErr: "", }, From 5aaf2d4ec90bef42a2aeb6818aa00f40e110b836 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Wed, 20 Mar 2024 20:55:04 +0100 Subject: [PATCH 5/8] fixes for e2e tests Signed-off-by: Francesco Ilario --- cmd/main.go | 4 +- .../service/factory/service_factory.go | 10 ++- pkg/log/log.go | 8 +++ pkg/proxy/proxy.go | 64 +++++++++++-------- pkg/proxy/service/cluster_service.go | 54 ++++++++++++++-- pkg/server/in_cluster_application.go | 7 +- 6 files changed, 112 insertions(+), 35 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 70deb041..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,6 @@ func main() { metricsSrv := proxyMetrics.StartMetricsServer() // Start the proxy server - pvcfg := crtConfig.PublicViewer() p, err := proxy.NewProxy(app, proxyMetrics, cluster.GetMemberClusters, &pvcfg) if err != nil { panic(errs.Wrap(err, "failed to create proxy")) 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/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/proxy.go b/pkg/proxy/proxy.go index ad6625ec..dc2a00d1 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -276,12 +276,6 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, } 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, "", false, 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 // access to user's home workspace if workspaceName == "" { @@ -296,43 +290,60 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, return "", nil, "", false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) } + cluster, err := 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, workspaceName, 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. - - // workspace was found means we can forward the request requestedNamespace := namespaceFromCtx(ctx) workspaces, isPublicViewer, err := p.processDirectWorkspaceAccessRequest(ctx, workspaceName) if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { 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, workspaceName, 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) - workspaces, crterr := p.processUserRequest(ctx, workspaceName) - switch { - case crterr == nil: - // user has direct access to workspace: use user identity - ctx.Logger().Debugf("user %s/%s has direct access to workspace %s", userID, username, workspaceName) - return workspaces, false, nil - case crterr.Code == http.StatusForbidden && p.publicViewerConfig.Enabled(): - // user has no direct access, but workspace is community: use public-viewer + + // 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) - workspaces, err := p.processPublicViewerRequest(ctx, workspaceName) - if err != nil { - return nil, false, crterr + // 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 workspaces, true, nil - default: - // user has no direct access, neither the workspace is community - ctx.Logger().Debugf("user %s/%s doesn't have direct access to workspace private %s", userID, username, workspaceName) 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) { @@ -352,7 +363,7 @@ func (p *Proxy) processPublicViewerRequest(ctx echo.Context, workspaceName strin return []toolchainv1alpha1.Workspace{*workspace}, nil } -func (p *Proxy) processUserRequest(ctx echo.Context, workspaceName string) ([]toolchainv1alpha1.Workspace, *crterrors.Error) { +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()) @@ -362,7 +373,7 @@ func (p *Proxy) processUserRequest(ctx echo.Context, workspaceName string) ([]to return nil, crterrors.NewForbiddenError("invalid workspace request", fmt.Sprintf("access to workspace '%s' is forbidden", workspaceName)) } - return []toolchainv1alpha1.Workspace{*workspace}, nil + return workspace, nil } func (p *Proxy) handleRequestAndRedirect(ctx echo.Context) error { @@ -545,6 +556,7 @@ func (p *Proxy) buildImpersonatingDirector(ctx echo.Context, target *access.Clus return 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) { @@ -571,6 +583,8 @@ func (p *Proxy) buildImpersonatingDirector(ctx echo.Context, target *access.Clus } if isPublicViewer { + // set SSO-User context for logging + ctx.Set("SSO-User", target.Username()) req.Header.Set("SSO-User", target.Username()) } 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 } From 458137cc525e26d86daff73b20957d1a48fc9701 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Fri, 22 Mar 2024 17:01:09 +0100 Subject: [PATCH 6/8] update go mod, fix tests Signed-off-by: Francesco Ilario --- go.mod | 4 ++-- go.sum | 8 ++++---- pkg/proxy/proxy.go | 22 ++++++++-------------- pkg/proxy/proxy_community_test.go | 12 ++++++------ pkg/proxy/proxy_test.go | 5 +++-- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index ea59a701..429dc31f 100644 --- a/go.mod +++ b/go.mod @@ -147,6 +147,6 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace github.com/codeready-toolchain/toolchain-common => github.com/filariow/toolchain-common v0.0.0-20240318122124-5b365181bd76 +replace github.com/codeready-toolchain/toolchain-common => github.com/filariow/toolchain-common v0.0.0-20240322111241-6d0b188b7a5d -replace github.com/codeready-toolchain/api => github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285 +replace github.com/codeready-toolchain/api => github.com/filariow/toolchain-api v0.0.0-20240322104820-8a9034b5d047 diff --git a/go.sum b/go.sum index d537bc0d..1b436346 100644 --- a/go.sum +++ b/go.sum @@ -139,10 +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-20240318114050-43313b449285 h1:rwbjw9Sx2i/KTXAsS7+SmoeSCM+qYmwbdK/0I+RADMo= -github.com/filariow/toolchain-api v0.0.0-20240318114050-43313b449285/go.mod h1:cfNN6YPX4TORvhhZXMSjSPesqAHlB3nD/WAfGe4WLKQ= -github.com/filariow/toolchain-common v0.0.0-20240318122124-5b365181bd76 h1:vEzqg+pqEUXBpo0IIYVScHJETZfNp483lsnM+acweIQ= -github.com/filariow/toolchain-common v0.0.0-20240318122124-5b365181bd76/go.mod h1:p6LHpV0Cq/Ope2WyDY2qeLqFmm19yfbArwUV9XpaRE4= +github.com/filariow/toolchain-api v0.0.0-20240322104820-8a9034b5d047 h1:RYjaty4k8pSHB9YORl5r5Yd3De6sPv1gpH8wm2pcy5w= +github.com/filariow/toolchain-api v0.0.0-20240322104820-8a9034b5d047/go.mod h1:cfNN6YPX4TORvhhZXMSjSPesqAHlB3nD/WAfGe4WLKQ= +github.com/filariow/toolchain-common v0.0.0-20240322111241-6d0b188b7a5d h1:k+bpJltkdNWTCAwX9bix2SXS0wTjNJcpN2E+lWx8b4c= +github.com/filariow/toolchain-common v0.0.0-20240322111241-6d0b188b7a5d/go.mod h1:p6LHpV0Cq/Ope2WyDY2qeLqFmm19yfbArwUV9XpaRE4= 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/proxy/proxy.go b/pkg/proxy/proxy.go index dc2a00d1..e1926faf 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -285,16 +285,16 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, return "", nil, "", false, crterrors.NewInternalError(errs.New("unable to retrieve user workspaces"), err.Error()) } - requestedNamespace := namespaceFromCtx(ctx) - if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { - return "", nil, "", false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) - } - cluster, err := 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()) } + requestedNamespace := namespaceFromCtx(ctx) + if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { + return "", nil, "", false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) + } + return proxyPluginName, cluster, workspaceName, false, nil } @@ -549,13 +549,7 @@ func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, func (p *Proxy) buildImpersonatingDirector(ctx echo.Context, target *access.ClusterAccess, isPlugin bool, isPublicViewer bool) func(*http.Request) { targetQuery := target.APIURL().RawQuery - impersonateUser := func() string { - if p.publicViewerConfig.Enabled() && isPublicViewer { - return p.publicViewerConfig.Username() - } - return target.Username() - }() - + impersonateUser := target.Username() ctx.Set("Impersonate-User", impersonateUser) // set impersonate-user context for logging ctx.Logger().Infof("building reverse proxy impersonating %s", impersonateUser) @@ -584,8 +578,8 @@ func (p *Proxy) buildImpersonatingDirector(ctx echo.Context, target *access.Clus if isPublicViewer { // set SSO-User context for logging - ctx.Set("SSO-User", target.Username()) - req.Header.Set("SSO-User", target.Username()) + username, _ := ctx.Get(context.UsernameKey).(string) + req.Header.Set("SSO-User", username) } // Replace token diff --git a/pkg/proxy/proxy_community_test.go b/pkg/proxy/proxy_community_test.go index 05e2500e..93e895f6 100644 --- a/pkg/proxy/proxy_community_test.go +++ b/pkg/proxy/proxy_community_test.go @@ -64,12 +64,12 @@ func (s *TestProxySuite) TestProxyCommunity() { }) // run community tests - s.checkProxyCommunityOK(fakeApp, p, port, cfg.Username()) + s.checkProxyCommunityOK(fakeApp, p, port, &cfg) }) } } -func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Proxy, port, publicViewer string) { +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) @@ -111,8 +111,8 @@ func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Pr ProxyRequestHeaders: map[string][]string{"Authorization": {"Bearer " + s.token(communityUser)}}, ExpectedAPIServerRequestHeaders: map[string][]string{ "Authorization": {"Bearer clusterSAToken"}, - "Impersonate-User": {publicViewer}, - "SSO-User": {"communityuser"}, + "Impersonate-User": {publicViewerConfig.Username()}, + "SSO-User": {"username-" + communityUser.String()}, }, ExpectedProxyResponseStatus: http.StatusOK, RequestPath: fmt.Sprintf("http://localhost:%s/workspaces/communityspace/api/communityspace/pods", port), @@ -135,7 +135,7 @@ func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Pr 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]) + assert.Equal(s.T(), hv[i], r.Header.Values(hk)[i], "header %s", hk) } } }) @@ -194,7 +194,7 @@ func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Pr return fake.NewBase1NSTemplateTier(), nil } s.Application.MockInformerService(inf) - fakeApp.MemberClusterServiceMock = s.newMemberClusterServiceWithMembers(testServer.URL) + fakeApp.MemberClusterServiceMock = s.newMemberClusterServiceWithMembers(testServer.URL, publicViewerConfig) fakeApp.InformerServiceMock = inf p.spaceLister = &handlers.SpaceLister{ diff --git a/pkg/proxy/proxy_test.go b/pkg/proxy/proxy_test.go index 25c20c58..0fb6a4be 100644 --- a/pkg/proxy/proxy_test.go +++ b/pkg/proxy/proxy_test.go @@ -718,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, @@ -760,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 { @@ -790,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{ { From 3aa5079a38490f60cdbb9976bff543bb31215940 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Fri, 22 Mar 2024 17:34:12 +0100 Subject: [PATCH 7/8] update go mod Signed-off-by: Francesco Ilario --- go.mod | 6 +++--- go.sum | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 429dc31f..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 @@ -147,6 +147,6 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace github.com/codeready-toolchain/toolchain-common => github.com/filariow/toolchain-common v0.0.0-20240322111241-6d0b188b7a5d +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-20240322104820-8a9034b5d047 +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 1b436346..9a282a83 100644 --- a/go.sum +++ b/go.sum @@ -139,10 +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-20240322104820-8a9034b5d047 h1:RYjaty4k8pSHB9YORl5r5Yd3De6sPv1gpH8wm2pcy5w= -github.com/filariow/toolchain-api v0.0.0-20240322104820-8a9034b5d047/go.mod h1:cfNN6YPX4TORvhhZXMSjSPesqAHlB3nD/WAfGe4WLKQ= -github.com/filariow/toolchain-common v0.0.0-20240322111241-6d0b188b7a5d h1:k+bpJltkdNWTCAwX9bix2SXS0wTjNJcpN2E+lWx8b4c= -github.com/filariow/toolchain-common v0.0.0-20240322111241-6d0b188b7a5d/go.mod h1:p6LHpV0Cq/Ope2WyDY2qeLqFmm19yfbArwUV9XpaRE4= +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= From f4d7e2e5813180d60ac433787c717515526ca16e Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Mon, 25 Mar 2024 13:16:03 +0100 Subject: [PATCH 8/8] fix linter complaints Signed-off-by: Francesco Ilario --- pkg/proxy/handlers/spacelister_get_test.go | 22 ++++++++++---------- pkg/proxy/proxy.go | 24 +++++++++++----------- pkg/proxy/proxy_community_test.go | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pkg/proxy/handlers/spacelister_get_test.go b/pkg/proxy/handlers/spacelister_get_test.go index a1c24931..de825a0d 100644 --- a/pkg/proxy/handlers/spacelister_get_test.go +++ b/pkg/proxy/handlers/spacelister_get_test.go @@ -246,7 +246,7 @@ func TestSpaceListerGet(t *testing.T) { expectedErr: "nstemplatetier error", expectedErrCode: 500, overrideInformerFunc: func() service.InformerService { - informerFunc := fake.GetInformerService(fakeClient, fake.WithGetNSTemplateTierFunc(func(tierName string) (*toolchainv1alpha1.NSTemplateTier, error) { + informerFunc := fake.GetInformerService(fakeClient, fake.WithGetNSTemplateTierFunc(func(_ string) (*toolchainv1alpha1.NSTemplateTier, error) { return nil, fmt.Errorf("nstemplatetier error") })) return informerFunc() @@ -258,7 +258,7 @@ func TestSpaceListerGet(t *testing.T) { expectedWs: []toolchainv1alpha1.Workspace{}, expectedErr: "signup error", expectedErrCode: 500, - overrideSignupFunc: func(ctx *gin.Context, userID, username string, checkUserSignupComplete bool) (*signup.Signup, error) { + overrideSignupFunc: func(_ *gin.Context, _, _ string, _ bool) (*signup.Signup, error) { return nil, fmt.Errorf("signup error") }, expectedWorkspace: "dancelover", @@ -276,7 +276,7 @@ func TestSpaceListerGet(t *testing.T) { expectedErr: "list spacebindings error", expectedErrCode: 500, overrideInformerFunc: func() service.InformerService { - listSpaceBindingFunc := func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { + listSpaceBindingFunc := func(_ ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { return nil, fmt.Errorf("list spacebindings error") } return fake.GetInformerService(fakeClient, fake.WithListSpaceBindingFunc(listSpaceBindingFunc))() @@ -289,7 +289,7 @@ func TestSpaceListerGet(t *testing.T) { expectedErr: "\"workspaces.toolchain.dev.openshift.com \\\"dancelover\\\" not found\"", expectedErrCode: 404, overrideInformerFunc: func() service.InformerService { - getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { + getSpaceFunc := func(_ string) (*toolchainv1alpha1.Space, error) { return nil, fmt.Errorf("no space") } return fake.GetInformerService(fakeClient, fake.WithGetSpaceFunc(getSpaceFunc))() @@ -514,7 +514,7 @@ func TestSpaceListerGet(t *testing.T) { expectedWorkspace: "movielover", expectedErr: "no member clusters found", expectedErrCode: 500, - overrideGetMembersFunc: func(conditions ...commoncluster.Condition) []*commoncluster.CachedToolchainCluster { + overrideGetMembersFunc: func(_ ...commoncluster.Condition) []*commoncluster.CachedToolchainCluster { return []*commoncluster.CachedToolchainCluster{} }, }, @@ -578,7 +578,7 @@ func TestSpaceListerGet(t *testing.T) { // get workspace case workspace, decodeErr := decodeResponseToWorkspace(rec.Body.Bytes()) require.NoError(t, decodeErr) - require.Equal(t, 1, len(tc.expectedWs), "test case should have exactly one expected item since it's a get request") + require.Len(t, tc.expectedWs, 1, "test case should have exactly one expected item since it's a get request") for i := range tc.expectedWs { assert.Equal(t, tc.expectedWs[i].Name, workspace.Name) assert.Equal(t, tc.expectedWs[i].Status, workspace.Status) @@ -640,7 +640,7 @@ func TestGetUserWorkspace(t *testing.T) { username: "invalid.user", workspaceRequest: "batman", overrideInformerFunc: func() service.InformerService { - getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { + getSpaceFunc := func(_ string) (*toolchainv1alpha1.Space, error) { return nil, fmt.Errorf("no space") } return fake.GetInformerService(fakeClient, fake.WithGetSpaceFunc(getSpaceFunc))() @@ -651,7 +651,7 @@ func TestGetUserWorkspace(t *testing.T) { username: "invalid.user", workspaceRequest: "batman", overrideInformerFunc: func() service.InformerService { - getSpaceFunc := func(name string) (*toolchainv1alpha1.Space, error) { + getSpaceFunc := func(_ string) (*toolchainv1alpha1.Space, error) { return nil, fmt.Errorf("no space") } return fake.GetInformerService(fakeClient, fake.WithGetSpaceFunc(getSpaceFunc))() @@ -662,7 +662,7 @@ func TestGetUserWorkspace(t *testing.T) { username: "batman.space", workspaceRequest: "batman", expectedErr: "signup error", - overrideSignupFunc: func(ctx *gin.Context, userID, username string, checkUserSignupComplete bool) (*signup.Signup, error) { + overrideSignupFunc: func(_ *gin.Context, _, _ string, _ bool) (*signup.Signup, error) { return nil, fmt.Errorf("signup error") }, expectedWorkspace: nil, @@ -672,7 +672,7 @@ func TestGetUserWorkspace(t *testing.T) { workspaceRequest: "robin", expectedErr: "list spacebindings error", overrideInformerFunc: func() service.InformerService { - listSpaceBindingFunc := func(reqs ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { + listSpaceBindingFunc := func(_ ...labels.Requirement) ([]toolchainv1alpha1.SpaceBinding, error) { return nil, fmt.Errorf("list spacebindings error") } return fake.GetInformerService(fakeClient, fake.WithListSpaceBindingFunc(listSpaceBindingFunc))() @@ -721,7 +721,7 @@ func TestGetUserWorkspace(t *testing.T) { } if tc.expectedWorkspace != nil { - require.Equal(t, wrk, tc.expectedWorkspace) + require.Equal(t, tc.expectedWorkspace, wrk) } else { require.Nil(t, wrk) // user is not authorized to get this workspace } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index e1926faf..599c4c9b 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -267,12 +267,12 @@ func (p *Proxy) health(ctx echo.Context) error { return err } -func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, string, bool, 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, "", false, 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 @@ -282,20 +282,20 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, // list all workspaces workspaces, err := handlers.ListUserWorkspaces(ctx, p.spaceLister, p.publicViewerConfig) if err != nil { - return "", nil, "", false, 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()) } cluster, err := 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 "", 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 "", nil, false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) } - return proxyPluginName, cluster, workspaceName, false, nil + return proxyPluginName, cluster, false, nil } // when a workspace name was provided @@ -303,7 +303,7 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, requestedNamespace := namespaceFromCtx(ctx) workspaces, isPublicViewer, err := p.processDirectWorkspaceAccessRequest(ctx, workspaceName) if err := validateWorkspaceRequest(workspaceName, requestedNamespace, workspaces); err != nil { - return "", nil, "", false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) + return "", nil, false, crterrors.NewForbiddenError("invalid workspace request", err.Error()) } cluster, err := func() (*access.ClusterAccess, error) { @@ -315,10 +315,10 @@ func (p *Proxy) processRequest(ctx echo.Context) (string, *access.ClusterAccess, 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 "", nil, false, crterrors.NewInternalError(errs.New("unable to get target cluster"), err.Error()) } - return proxyPluginName, cluster, workspaceName, isPublicViewer, nil + return proxyPluginName, cluster, isPublicViewer, nil } func (p *Proxy) processDirectWorkspaceAccessRequest(ctx echo.Context, workspaceName string) ([]toolchainv1alpha1.Workspace, bool, *crterrors.Error) { @@ -378,13 +378,13 @@ func (p *Proxy) processUserRequest(ctx echo.Context, workspaceName string) (*too func (p *Proxy) handleRequestAndRedirect(ctx echo.Context) error { requestReceivedTime := ctx.Get(context.RequestReceivedTime).(time.Time) - proxyPluginName, cluster, workspace, isPublicViewer, 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, workspace, len(proxyPluginName) > 0, isPublicViewer) + 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 @@ -531,7 +531,7 @@ func extractUserToken(req *http.Request) (string, error) { return token[1], nil } -func (p *Proxy) newReverseProxy(ctx echo.Context, target *access.ClusterAccess, workspace string, isPlugin bool, isPublicViewer 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() diff --git a/pkg/proxy/proxy_community_test.go b/pkg/proxy/proxy_community_test.go index 93e895f6..dd3213ba 100644 --- a/pkg/proxy/proxy_community_test.go +++ b/pkg/proxy/proxy_community_test.go @@ -187,7 +187,7 @@ func (s *TestProxySuite) checkProxyCommunityOK(fakeApp *fake.ProxyFakeApp, p *Pr log.Printf("returning sbs: %v", sbs.Items) return sbs.Items, nil } - inf.GetProxyPluginConfigFunc = func(name string) (*toolchainv1alpha1.ProxyPlugin, error) { + inf.GetProxyPluginConfigFunc = func(_ string) (*toolchainv1alpha1.ProxyPlugin, error) { return nil, fmt.Errorf("proxy plugin not found") } inf.GetNSTemplateTierFunc = func(_ string) (*toolchainv1alpha1.NSTemplateTier, error) {