diff --git a/CODEOWNERS b/CODEOWNERS index 629180bdf..b6e883210 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @MatousJobanek @xcoulon @alexeykazakov @rajivnathan @ranakan19 @sbryzak @mfrancisc +* @MatousJobanek @xcoulon @alexeykazakov @rajivnathan @ranakan19 @mfrancisc diff --git a/go.mod b/go.mod index f4591dc17..ee0b4dd59 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/codeready-toolchain/toolchain-e2e require ( - github.com/codeready-toolchain/api v0.0.0-20240815232340-d0c164a83d27 + github.com/codeready-toolchain/api v0.0.0-20240927104325-b5bfcb3cb1b0 github.com/codeready-toolchain/toolchain-common v0.0.0-20240905135929-d55d86fdd41e github.com/davecgh/go-spew v1.1.1 github.com/fatih/color v1.12.0 diff --git a/go.sum b/go.sum index 38e4bc0fe..de2411509 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/codeready-toolchain/api v0.0.0-20240815232340-d0c164a83d27 h1:uEH8HAM81QZBccuqQpGKJUoJQe28+DFSYi/mRKZDYrA= -github.com/codeready-toolchain/api v0.0.0-20240815232340-d0c164a83d27/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E= +github.com/codeready-toolchain/api v0.0.0-20240927104325-b5bfcb3cb1b0 h1:7cXHlRpoi1Owo8fYawl80PUsVWz+9AtMge6OJ4DjvWU= +github.com/codeready-toolchain/api v0.0.0-20240927104325-b5bfcb3cb1b0/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E= github.com/codeready-toolchain/toolchain-common v0.0.0-20240905135929-d55d86fdd41e h1:xTqyuImyon/P2QfV5NIJDsVkEWqb9b6Ax9INsmzpI1Q= github.com/codeready-toolchain/toolchain-common v0.0.0-20240905135929-d55d86fdd41e/go.mod h1:aIbki5CFsykeqZn2/ZwvUb3Krx2f2Tbq58R6MGnk6H8= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/make/clean.mk b/make/clean.mk index 3fa15b0ea..f3af9d0b9 100644 --- a/make/clean.mk +++ b/make/clean.mk @@ -85,3 +85,24 @@ clean-toolchain-crds: CRD_NAME=`oc get $${CRD} --template '{{.metadata.name}}'`; \ oc delete crd $${CRD_NAME}; \ done + +.PHONY: force-remove-finalizers-from-e2e-resources +## Sometimes after a failed run, the cluster doesn't have our operators running but still contain our resources +## with finalizers. This target removes those finalizers so that the subsequent call to some "clean-*" target +## doesn't get stuck. +## This goal is not called by default so that an attempt to clean up "cleanly" is always attempted first. If that +## fails, you can call this goal explicitly before attempting the cleanup again. +force-remove-finalizers-from-e2e-resources: + $(Q)for CRD in `oc get crd -o name | grep toolchain`; do \ + CRD_NAME=`oc get $${CRD} --template='{{.metadata.name}}'`; \ + for RES in `oc get $${CRD_NAME} --all-namespaces -ogo-template='{{range .items}}{{.metadata.name}},{{if ne .metadata.namespace nil}}{{.metadata.namespace}}{{else}}{{end}}{{"\n"}}{{end}}'`; do \ + NAME=`echo $${RES} | cut -d',' -f1`; \ + NS=`echo $${RES} | cut -d',' -f2`; \ + if [ -z "$$NS" ]; then \ + oc patch $${CRD_NAME} $${NAME} -p '{"metadata":{"finalizers": null}}' --type=merge; \ + else \ + oc patch $${CRD_NAME} $${NAME} -n $${NS} -p '{"metadata":{"finalizers": null}}' --type=merge; \ + fi \ + done \ + done + diff --git a/make/go.mk b/make/go.mk index 5e566f7a1..00a7e0c29 100644 --- a/make/go.mk +++ b/make/go.mk @@ -29,4 +29,8 @@ vet: .PHONY: go-test-skip-all go-test-skip-all: - go test ./... -skip '.*' \ No newline at end of file + go test ./... -skip '.*' + +.PHONY: pre-verify +pre-verify: + echo "No Pre-requisite needed" \ No newline at end of file diff --git a/make/test.mk b/make/test.mk index d3b5f7956..d16fde1ce 100644 --- a/make/test.mk +++ b/make/test.mk @@ -391,7 +391,6 @@ create-host-resources: create-spaceprovisionerconfigs-for-members tiers-via-ksct -oc apply -f deploy/host-operator/${ENVIRONMENT}/ -n ${HOST_NS} # patch toolchainconfig to prevent webhook deploy for 2nd member, a 2nd webhook deploy causes the webhook verification in e2e tests to fail # since e2e environment has 2 member operators running in the same cluster - # for details on how the TOOLCHAINCLUSTER_NAME is composed see https://github.com/codeready-toolchain/toolchain-cicd/blob/master/scripts/add-cluster.sh if [[ ${SECOND_MEMBER_MODE} == true ]]; then \ TOOLCHAIN_CLUSTER_NAME=`oc get toolchaincluster -n ${HOST_NS} --no-headers -o custom-columns=":metadata.name" | grep "2$$"`; \ if [[ -z $${TOOLCHAIN_CLUSTER_NAME} ]]; then \ diff --git a/multicluster_setup.adoc b/multicluster_setup.adoc index c05053607..23b682e27 100644 --- a/multicluster_setup.adoc +++ b/multicluster_setup.adoc @@ -99,9 +99,8 @@ toolchain-member-status False ... [source,bash] ---- # create/configure the ToolchainCluster resources on host and member clusters -$ curl -sSL https://raw.githubusercontent.com/codeready-toolchain/toolchain-common/master/scripts/add-cluster.sh | bash -s -- -t member -mn ${MEMBER_NS} -hn ${HOST_NS} -$ curl -sSL https://raw.githubusercontent.com/codeready-toolchain/toolchain-common/master/scripts/add-cluster.sh | bash -s -- -t host -mn ${MEMBER_NS} -hn ${HOST_NS} - +$ ksctl adm register-member --host-ns="${HOST_NS}" --member-ns="${MEMBER_NS}" --host-kubeconfig="${HOME}/.kube/host-config" --member-kubeconfig="${HOME}/.kube/member-config" + # verify $ oc config use-context host-admin && oc get toolchainstatus -n ${HOST_NS} diff --git a/test/e2e/parallel/proxy_test.go b/test/e2e/parallel/proxy_test.go index 750be8cbc..513c62a5a 100644 --- a/test/e2e/parallel/proxy_test.go +++ b/test/e2e/parallel/proxy_test.go @@ -441,7 +441,7 @@ func TestProxyFlow(t *testing.T) { proxyWorkspaceURL := hostAwait.ProxyURLWithWorkspaceContext("notexist") hostAwaitWithShorterTimeout := hostAwait.WithRetryOptions(wait.TimeoutOption(time.Second * 3)) // we expect an error so we can use a shorter timeout _, err := hostAwaitWithShorterTimeout.CreateAPIProxyClient(t, user.token, proxyWorkspaceURL) - require.EqualError(t, err, `an error on the server ("unable to get target cluster: the requested space is not available") has prevented the request from succeeding`) + require.EqualError(t, err, `an error on the server ("unable to get target cluster: access to workspace 'notexist' is forbidden") has prevented the request from succeeding`) }) t.Run("invalid request headers", func(t *testing.T) { diff --git a/test/e2e/proxy_publicviewer_test.go b/test/e2e/proxy_publicviewer_test.go new file mode 100644 index 000000000..9f728b9e6 --- /dev/null +++ b/test/e2e/proxy_publicviewer_test.go @@ -0,0 +1,313 @@ +package e2e_test + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + commonauth "github.com/codeready-toolchain/toolchain-common/pkg/test/auth" + testconfig "github.com/codeready-toolchain/toolchain-common/pkg/test/config" + testspace "github.com/codeready-toolchain/toolchain-common/pkg/test/space" + . "github.com/codeready-toolchain/toolchain-e2e/testsupport" + authsupport "github.com/codeready-toolchain/toolchain-e2e/testsupport/auth" + testsupportspace "github.com/codeready-toolchain/toolchain-e2e/testsupport/space" + "github.com/codeready-toolchain/toolchain-e2e/testsupport/spacebinding" + "github.com/codeready-toolchain/toolchain-e2e/testsupport/wait" +) + +type proxyUser struct { + signupResult *SignupResult + email string + token string +} + +func (u *proxyUser) Token() string { + switch { + case u.token != "": + return u.token + case u.signupResult != nil: + return u.signupResult.Token + default: + return "" + } +} + +func (u *proxyUser) Email() string { + switch { + case u.email != "": + return u.email + case u.signupResult != nil: + return u.signupResult.UserSignup.Spec.IdentityClaims.Email + default: + return "" + } +} + +// tests access to community-shared spaces +func TestProxyPublicViewer(t *testing.T) { + // make sure everything is ready before running the actual tests + awaitilities := WaitForDeployments(t) + hostAwait := awaitilities.Host() + memberAwait := awaitilities.Member1() + + // public viewer is enabled in ToolchainConfig + hostAwait.UpdateToolchainConfig(t, testconfig.PublicViewerConfig(true)) + + // we create a space to share + space, _, _ := testsupportspace.CreateSpace(t, awaitilities, + testspace.WithTierName("appstudio"), + testspace.WithSpecTargetCluster(memberAwait.ClusterName), + ) + + // users test cases + tt := map[string]struct { + proxyClientUser func() proxyUser + }{ + "approved user with space": { + proxyClientUser: func() proxyUser { + user := createAppStudioRandomUser(t, awaitilities, func(sr *SignupRequest) *SignupRequest { + return sr. + ManuallyApprove(). + EnsureMUR(). + RequireConditions( + wait.ConditionSet(wait.Default(), wait.ApprovedByAdmin())...) + }) + return proxyUser{signupResult: user} + }, + }, + "approved user without space": { + proxyClientUser: func() proxyUser { + user := createAppStudioRandomUser(t, awaitilities, func(sr *SignupRequest) *SignupRequest { + return sr. + ManuallyApprove(). + EnsureMUR(). + NoSpace(). + RequireConditions( + wait.ConditionSet(wait.Default(), wait.ApprovedByAdmin())...) + }) + return proxyUser{signupResult: user} + }, + }, + "not approved user": { + proxyClientUser: func() proxyUser { + user := createAppStudioRandomUser(t, awaitilities, func(sr *SignupRequest) *SignupRequest { + return sr. + NoSpace(). + RequireConditions( + wait.ConditionSet(wait.Default(), wait.VerificationRequired())...) + }) + return proxyUser{signupResult: user} + }, + }, + "sso user": { + proxyClientUser: func() proxyUser { + userIdentity := &commonauth.Identity{ + ID: uuid.New(), + Username: "joe", + } + email := "joe@joe.joe" + claims := []commonauth.ExtraClaim{commonauth.WithEmailClaim(email)} + token, err := authsupport.NewTokenFromIdentity(userIdentity, claims...) + require.NoError(t, err) + return proxyUser{token: token, email: email} + }, + }, + } + + t.Run("space is flagged as community", func(t *testing.T) { + sb := CreateCommunitySpaceBinding(t, hostAwait, space.Name, space.Namespace) + require.NotNil(t, sb) + + // Wait until space is flagged as community + _, err := hostAwait.WaitForSpaceBinding(t, toolchainv1alpha1.KubesawAuthenticatedUsername, space.Name, wait.UntilSpaceBindingHasSpaceRole("viewer")) + require.NoError(t, err) + + // wait until the space has ProvisionedNamespaces + sp, err := hostAwait.WaitForSpace(t, space.Name, wait.UntilSpaceHasAnyProvisionedNamespaces()) + require.NoError(t, err) + + t.Run("user is not banned", func(t *testing.T) { + for s, c := range tt { + t.Run(s, func(t *testing.T) { + // build proxy client + user := c.proxyClientUser() + proxyWorkspaceURL := hostAwait.ProxyURLWithWorkspaceContext(sp.Name) + communityUserProxyClient, err := hostAwait.CreateAPIProxyClient(t, user.Token(), proxyWorkspaceURL) + require.NoError(t, err) + + t.Run("can list config maps", func(t *testing.T) { + cms := corev1.ConfigMapList{} + err = communityUserProxyClient.List(context.TODO(), &cms, client.InNamespace(sp.Status.ProvisionedNamespaces[0].Name)) + require.NoError(t, err) + }) + + t.Run("cannot create config maps", func(t *testing.T) { + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: sp.Status.ProvisionedNamespaces[0].Name, + }, + } + err = communityUserProxyClient.Create(context.TODO(), &cm) + require.True(t, errors.IsForbidden(err), "expected Create ConfigMap as community user to return a Forbidden error, actual: %v", err) + }) + }) + } + }) + + t.Run("user is banned", func(t *testing.T) { + for s, c := range tt { + t.Run(s, func(t *testing.T) { + user := c.proxyClientUser() + // the client needs to be created before the ban, + // otherwise it won't initialize properly + url := hostAwait.ProxyURLWithWorkspaceContext(sp.Name) + proxyClient, err := hostAwait.CreateAPIProxyClient(t, user.Token(), url) + require.NoError(t, err) + + banUser(t, hostAwait, user) + + t.Run(s, func(t *testing.T) { + t.Run("user cannot initialize a new client", func(t *testing.T) { + url := hostAwait.ProxyURLWithWorkspaceContext(sp.Name) + proxyClient, err := hostAwait.CreateAPIProxyClient(t, user.Token(), url) + require.NoError(t, err) + + // as the client is not initialized correctly, + // any request should return a NoMatch error + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: sp.Status.ProvisionedNamespaces[0].Name, + }, + } + err = proxyClient.Create(context.TODO(), &cm) + require.True(t, meta.IsNoMatchError(err), "expected Create ConfigMap as SSO user to return a NoMatch error, actual: %v", err) + }) + + t.Run("cannot list config maps", func(t *testing.T) { + cms := corev1.ConfigMapList{} + err := proxyClient.List(context.TODO(), &cms, client.InNamespace(sp.Status.ProvisionedNamespaces[0].Name)) + require.Zero(t, cms) + require.Error(t, err) + }) + + t.Run("cannot create config maps", func(t *testing.T) { + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: sp.Status.ProvisionedNamespaces[0].Name, + }, + } + err := proxyClient.Create(context.TODO(), &cm) + require.True(t, errors.IsForbidden(err), "expected Create ConfigMap as SSO user to return a Forbidden error, actual: %v", err) + }) + }) + }) + } + }) + }) + + t.Run("space is not flagged as community", func(t *testing.T) { + // retrieve the space + sp := toolchainv1alpha1.Space{} + err := hostAwait.Client.Get(context.TODO(), client.ObjectKeyFromObject(space), &sp) + require.NoError(t, err) + + // ensure no SpaceBinding exists for Public-Viewer + sbs, err := hostAwait.GetSpaceBindingByListing(toolchainv1alpha1.KubesawAuthenticatedUsername, space.Name) + require.NoError(t, err) + require.Empty(t, sbs) + + testCases := map[string]bool{ + "user is not banned": false, + "user is banned": true, + } + for s, banRequired := range testCases { + t.Run(s, func(t *testing.T) { + for s, c := range tt { + t.Run(s, func(t *testing.T) { + user := c.proxyClientUser() + if banRequired { + banUser(t, hostAwait, user) + } + + t.Run("user cannot access to non-community space", func(t *testing.T) { + require.NotEmpty(t, sp.Status.ProvisionedNamespaces) + + proxyWorkspaceURL := hostAwait.ProxyURLWithWorkspaceContext(sp.Name) + communityUserProxyClient, err := hostAwait.CreateAPIProxyClient(t, user.Token(), proxyWorkspaceURL) + require.NoError(t, err) + + t.Run("user cannot list config maps from non-community space", func(t *testing.T) { + cms := corev1.ConfigMapList{} + + err = communityUserProxyClient.List(context.TODO(), &cms, client.InNamespace(sp.Status.ProvisionedNamespaces[0].Name)) + require.True(t, meta.IsNoMatchError(err), "expected List ConfigMap as community user to return a NoMatch error, actual: %v", err) + }) + + t.Run("user cannot create config maps into non-community space", func(t *testing.T) { + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cm", + Namespace: sp.Status.ProvisionedNamespaces[0].Name, + }, + } + err := communityUserProxyClient.Create(context.TODO(), &cm) + require.True(t, meta.IsNoMatchError(err), "expected Create ConfigMap as community user to return a NoMatch error, actual: %v", err) + }) + }) + }) + } + }) + } + }) +} + +func banUser(t *testing.T, hostAwait *wait.HostAwaitility, user proxyUser) { + bannedUser := NewBannedUser(hostAwait, user.Email()) + err := hostAwait.CreateWithCleanup(t, bannedUser) + require.NoError(t, err) + + if user.signupResult != nil { + _, err = hostAwait. + WithRetryOptions(wait.TimeoutOption(time.Second*10), wait.RetryInterval(time.Second*2)). + WaitForUserSignup(t, user.signupResult.UserSignup.Name, wait.UntilUserSignupHasConditions( + wait.ConditionSet(user.signupResult.UserSignup.Status.Conditions, wait.Banned())...)) + require.NoError(t, err) + } +} + +func createAppStudioRandomUser(t *testing.T, awaitilities wait.Awaitilities, withOptions ...func(*SignupRequest) *SignupRequest) *SignupResult { + suffix := rand.Int31n(999999) // nolint:gosec + sr := NewSignupRequest(awaitilities). + Username(fmt.Sprintf("user-%d", suffix)). + IdentityID(uuid.New()). + Email(fmt.Sprintf("user-%d@teste2e.com", suffix)). + SpaceTier("appstudio"). + RequireConditions(wait.Default()...) + for _, opts := range withOptions { + sr = opts(sr) + } + return sr.Execute(t) +} + +func CreateCommunitySpaceBinding( + t *testing.T, + hostAwait *wait.HostAwaitility, + spaceName, spaceNamespace string, +) *toolchainv1alpha1.SpaceBinding { + return spacebinding.CreateSpaceBindingStr(t, hostAwait, toolchainv1alpha1.KubesawAuthenticatedUsername, spaceName, spaceNamespace, "viewer") +} diff --git a/testsupport/spacebinding/spacebinding.go b/testsupport/spacebinding/spacebinding.go index 856c36d88..3444a98f5 100644 --- a/testsupport/spacebinding/spacebinding.go +++ b/testsupport/spacebinding/spacebinding.go @@ -30,6 +30,13 @@ func CreateSpaceBinding(t *testing.T, hostAwait *wait.HostAwaitility, mur *toolc return spaceBinding } +func CreateSpaceBindingStr(t *testing.T, hostAwait *wait.HostAwaitility, murName, spaceName, spaceNamespace, spaceRole string) *toolchainv1alpha1.SpaceBinding { + spaceBinding := NewSpaceBindingStr(murName, spaceName, spaceNamespace, spaceRole) + err := hostAwait.CreateWithCleanup(t, spaceBinding) + require.NoError(t, err) + return spaceBinding +} + // CreateSpaceBindingWithoutCleanup creates SpaceBinding resource for the given MUR & Space with the given space role; and doesn't mark the resource to be ready for cleanup func CreateSpaceBindingWithoutCleanup(t *testing.T, hostAwait *wait.HostAwaitility, mur *toolchainv1alpha1.MasterUserRecord, space *toolchainv1alpha1.Space, spaceRole string) *toolchainv1alpha1.SpaceBinding { spaceBinding := NewSpaceBinding(mur, space, spaceRole) @@ -41,22 +48,26 @@ func CreateSpaceBindingWithoutCleanup(t *testing.T, hostAwait *wait.HostAwaitili // NewSpaceBinding create an object SpaceBinding with the given values func NewSpaceBinding(mur *toolchainv1alpha1.MasterUserRecord, space *toolchainv1alpha1.Space, spaceRole string) *toolchainv1alpha1.SpaceBinding { - namePrefix := fmt.Sprintf("%s-%s", mur.Name, space.Name) + return NewSpaceBindingStr(mur.Name, space.Name, space.Namespace, spaceRole) +} + +func NewSpaceBindingStr(murName, spaceName, spaceNamespace, spaceRole string) *toolchainv1alpha1.SpaceBinding { + namePrefix := fmt.Sprintf("%s-%s", murName, spaceName) if len(namePrefix) > 50 { namePrefix = namePrefix[0:50] } return &toolchainv1alpha1.SpaceBinding{ ObjectMeta: metav1.ObjectMeta{ GenerateName: namePrefix + "-", - Namespace: space.Namespace, + Namespace: spaceNamespace, Labels: map[string]string{ - toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey: mur.Name, - toolchainv1alpha1.SpaceBindingSpaceLabelKey: space.Name, + toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey: murName, + toolchainv1alpha1.SpaceBindingSpaceLabelKey: spaceName, }, }, Spec: toolchainv1alpha1.SpaceBindingSpec{ - MasterUserRecord: mur.Name, - Space: space.Name, + MasterUserRecord: murName, + Space: spaceName, SpaceRole: spaceRole, }, }