diff --git a/go.mod b/go.mod index 69296b89..6c3e3605 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/codeready-toolchain/toolchain-common go 1.20 require ( - github.com/codeready-toolchain/api v0.0.0-20240708122235-0af5a9a178bb + github.com/codeready-toolchain/api v0.0.0-20240717145630-bb67a632867a github.com/go-logr/logr v1.2.3 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/lestrrat-go/jwx v1.2.29 @@ -32,6 +32,7 @@ require ( github.com/migueleliasweb/go-github-mock v0.0.18 golang.org/x/oauth2 v0.7.0 gopkg.in/yaml.v2 v2.4.0 + k8s.io/kubectl v0.24.0 k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed ) @@ -107,7 +108,6 @@ require ( k8s.io/component-base v0.25.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect - k8s.io/kubectl v0.24.0 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 5bc1fb18..72984047 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,8 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/codeready-toolchain/api v0.0.0-20240708122235-0af5a9a178bb h1:Wc9CMsv0ODZv9dM5qF3OI0mFDO95YNIXV/8oRvoz8aE= -github.com/codeready-toolchain/api v0.0.0-20240708122235-0af5a9a178bb/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E= +github.com/codeready-toolchain/api v0.0.0-20240717145630-bb67a632867a h1:La7GOCysmkU+4vnN8lDzXFJwJiA1LWZ9YkX/yQXYnpw= +github.com/codeready-toolchain/api v0.0.0-20240717145630-bb67a632867a/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= diff --git a/pkg/banneduser/banneduser.go b/pkg/banneduser/banneduser.go new file mode 100644 index 00000000..c9407c01 --- /dev/null +++ b/pkg/banneduser/banneduser.go @@ -0,0 +1,53 @@ +package banneduser + +import ( + "context" + "fmt" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// NewBannedUser creates a bannedUser resource +func NewBannedUser(userSignup *toolchainv1alpha1.UserSignup, bannedBy string) (*toolchainv1alpha1.BannedUser, error) { + var emailHashLbl, phoneHashLbl string + var exists bool + + if emailHashLbl, exists = userSignup.Labels[toolchainv1alpha1.UserSignupUserEmailHashLabelKey]; !exists { + return nil, fmt.Errorf("the UserSignup %s doesn't have the label '%s' set", userSignup.Name, toolchainv1alpha1.UserSignupUserEmailHashLabelKey) // nolint:loggercheck + } + + bannedUser := &toolchainv1alpha1.BannedUser{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: userSignup.Namespace, + Name: fmt.Sprintf("banneduser-%s", emailHashLbl), + Labels: map[string]string{ + toolchainv1alpha1.BannedUserEmailHashLabelKey: emailHashLbl, + toolchainv1alpha1.BannedByLabelKey: bannedBy, + }, + }, + Spec: toolchainv1alpha1.BannedUserSpec{ + Email: userSignup.Spec.IdentityClaims.Email, + }, + } + + if phoneHashLbl, exists = userSignup.Labels[toolchainv1alpha1.UserSignupUserPhoneHashLabelKey]; exists { + bannedUser.Labels[toolchainv1alpha1.BannedUserPhoneNumberHashLabelKey] = phoneHashLbl + } + return bannedUser, nil +} + +// IsAlreadyBanned checks if the user was already banned +func IsAlreadyBanned(ctx context.Context, userEmailHash string, hostClient client.Client, hostNamespace string) (bool, error) { + emailHashLabelMatch := client.MatchingLabels(map[string]string{ + toolchainv1alpha1.BannedUserEmailHashLabelKey: userEmailHash, + }) + bannedUsers := &toolchainv1alpha1.BannedUserList{} + + if err := hostClient.List(ctx, bannedUsers, emailHashLabelMatch, client.InNamespace(hostNamespace)); err != nil { + return false, err + } + + return len(bannedUsers.Items) > 0, nil +} diff --git a/pkg/banneduser/banneduser_test.go b/pkg/banneduser/banneduser_test.go new file mode 100644 index 00000000..9d3892c1 --- /dev/null +++ b/pkg/banneduser/banneduser_test.go @@ -0,0 +1,171 @@ +package banneduser + +import ( + "context" + "fmt" + "reflect" + "testing" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/toolchain-common/pkg/test" + commonsignup "github.com/codeready-toolchain/toolchain-common/pkg/test/usersignup" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestNewBannedUser(t *testing.T) { + userSignup1 := commonsignup.NewUserSignup(commonsignup.WithName("johny"), commonsignup.WithEmail("jonhy@example.com")) + userSignup1UserEmailHash := userSignup1.Labels[toolchainv1alpha1.UserSignupUserEmailHashLabelKey] + + userSignup2 := commonsignup.NewUserSignup(commonsignup.WithName("bob"), commonsignup.WithEmail("bob@example.com")) + userSignup2.Labels = nil + + userSignup3 := commonsignup.NewUserSignup(commonsignup.WithName("oliver"), commonsignup.WithEmail("oliver@example.com")) + userSignup3PhoneHash := "fd276563a8232d16620da8ec85d0575f" + userSignup3.Labels[toolchainv1alpha1.UserSignupUserPhoneHashLabelKey] = userSignup3PhoneHash + userSignup3EmailHash := userSignup3.Labels[toolchainv1alpha1.UserSignupUserEmailHashLabelKey] + + tests := []struct { + name string + userSignup *toolchainv1alpha1.UserSignup + bannedBy string + wantError bool + wantErrorMsg string + expectedBannedUser *toolchainv1alpha1.BannedUser + }{ + { + name: "userSignup with email hash label", + userSignup: userSignup1, + bannedBy: "admin", + wantError: false, + wantErrorMsg: "", + expectedBannedUser: &toolchainv1alpha1.BannedUser{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: userSignup1.Namespace, + Name: fmt.Sprintf("banneduser-%s", userSignup1UserEmailHash), + Labels: map[string]string{ + toolchainv1alpha1.BannedUserEmailHashLabelKey: userSignup1UserEmailHash, + toolchainv1alpha1.BannedByLabelKey: "admin", + }, + }, + Spec: toolchainv1alpha1.BannedUserSpec{ + Email: userSignup1.Spec.IdentityClaims.Email, + }, + }, + }, + { + name: "userSignup without email hash label and phone hash label", + userSignup: userSignup2, + bannedBy: "admin", + wantError: true, + wantErrorMsg: fmt.Sprintf("the UserSignup %s doesn't have the label '%s' set", userSignup2.Name, toolchainv1alpha1.UserSignupUserEmailHashLabelKey), + expectedBannedUser: nil, + }, + { + name: "userSignup with email hash label and phone hash label", + userSignup: userSignup3, + bannedBy: "admin", + wantError: false, + wantErrorMsg: "", + expectedBannedUser: &toolchainv1alpha1.BannedUser{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: userSignup3.Namespace, + Name: fmt.Sprintf("banneduser-%s", userSignup3EmailHash), + Labels: map[string]string{ + toolchainv1alpha1.BannedUserEmailHashLabelKey: userSignup3EmailHash, + toolchainv1alpha1.BannedByLabelKey: "admin", + toolchainv1alpha1.UserSignupUserPhoneHashLabelKey: userSignup3PhoneHash, + }, + }, + Spec: toolchainv1alpha1.BannedUserSpec{ + Email: userSignup3.Spec.IdentityClaims.Email, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewBannedUser(tt.userSignup, tt.bannedBy) + + if tt.wantError { + require.Error(t, err) + assert.Equal(t, tt.wantErrorMsg, err.Error()) + require.Nil(t, got) + } else { + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, tt.expectedBannedUser.ObjectMeta.Namespace, got.ObjectMeta.Namespace) + assert.Equal(t, tt.expectedBannedUser.ObjectMeta.Name, got.ObjectMeta.Name) + assert.Equal(t, tt.expectedBannedUser.Spec.Email, got.Spec.Email) + + if tt.expectedBannedUser != nil { + assert.True(t, reflect.DeepEqual(tt.expectedBannedUser.Labels, got.Labels)) + } + } + }) + } +} + +func TestIsAlreadyBanned(t *testing.T) { + userSignup1 := commonsignup.NewUserSignup(commonsignup.WithName("johny"), commonsignup.WithEmail("johny@example.com")) + userSignup2 := commonsignup.NewUserSignup(commonsignup.WithName("bob"), commonsignup.WithEmail("bob@example.com")) + userSignup3 := commonsignup.NewUserSignup(commonsignup.WithName("oliver"), commonsignup.WithEmail("oliver@example.com")) + bannedUser1, err := NewBannedUser(userSignup1, "admin") + require.NoError(t, err) + bannedUser2, err := NewBannedUser(userSignup2, "admin") + require.NoError(t, err) + bannedUser3, err := NewBannedUser(userSignup3, "admin") + require.NoError(t, err) + + mockT := test.NewMockT() + fakeClient := test.NewFakeClient(mockT, bannedUser1) + ctx := context.TODO() + + tests := []struct { + name string + toBan *toolchainv1alpha1.BannedUser + wantResult bool + wantError bool + fakeClient *test.FakeClient + }{ + { + name: "user is already banned", + toBan: bannedUser1, + wantResult: true, + wantError: false, + fakeClient: fakeClient, + }, + { + name: "user is not banned", + toBan: bannedUser2, + wantResult: false, + wantError: false, + fakeClient: fakeClient, + }, + { + name: "cannot list banned users because the client does have type v1alpha1.BannedUserList registered in the scheme", + toBan: bannedUser3, + wantResult: false, + wantError: true, + fakeClient: &test.FakeClient{Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(), T: t}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotResult, err := IsAlreadyBanned(ctx, tt.toBan.Labels[toolchainv1alpha1.BannedUserEmailHashLabelKey], tt.fakeClient, test.HostOperatorNs) + + if tt.wantError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantResult, gotResult) + } + }) + } +}