diff --git a/cmd/cluster/checkbanneduser.go b/cmd/cluster/checkbanneduser.go new file mode 100644 index 00000000..830281e0 --- /dev/null +++ b/cmd/cluster/checkbanneduser.go @@ -0,0 +1,72 @@ +package cluster + +import ( + "fmt" + "github.com/openshift/osdctl/pkg/utils" + "github.com/spf13/cobra" + cmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +const BanCodeExportControlCompliance = "export_control_compliance" + +func newCmdCheckBannedUser() *cobra.Command { + return &cobra.Command{ + Use: "check-banned-user [CLUSTER_ID]", + Short: "Checks if the cluster owner is a banned user.", + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(CheckBannedUser(args[0])) + }, + } +} + +func CheckBannedUser(clusterID string) error { + ocm := utils.CreateConnection() + defer func() { + if ocmCloseErr := ocm.Close(); ocmCloseErr != nil { + fmt.Printf("Cannot close the ocm (possible memory leak): %q", ocmCloseErr) + } + }() + + fmt.Print("Finding subscription account: ") + subscription, err := utils.GetSubscription(ocm, clusterID) + if err != nil { + return err + } + + if status := subscription.Status(); status != "Active" { + return fmt.Errorf("Expecting status 'Active' found %v\n", status) + } + + fmt.Printf("Account %v - %v - %v\n", subscription.SupportLevel(), subscription.Creator().HREF(), subscription.Status()) + + fmt.Print("Finding account owner: ") + creator, err := utils.GetAccount(ocm, subscription.Creator().ID()) + if err != nil { + return err + } + + userEmail := creator.Email() + userBanned := creator.Banned() + userBanCode := creator.BanCode() + userBanDescription := creator.BanDescription() + lastUpdate := creator.UpdatedAt() + + fmt.Printf("%v\n-------------------\nLast Update : %v\n", userEmail, lastUpdate) + + if userBanned { + fmt.Println("User is banned") + fmt.Printf("Ban code = %v\n", userBanCode) + fmt.Printf("Ban description = %v\n", userBanDescription) + if userBanCode == BanCodeExportControlCompliance { + fmt.Println("User banned due to export control compliance.\nPlease follow the steps detailed here: https://github.com/openshift/ops-sop/blob/master/v4/alerts/UpgradeConfigSyncFailureOver4HrSRE.md#user-banneddisabled-due-to-export-control-compliance .") + return nil + } + fmt.Println("Recommend sending service log with:") + fmt.Printf("osdctl servicelog post -t https://raw.githubusercontent.com/openshift/managed-notifications/master/ocm/cluster_owner_disabled.json %v\n", clusterID) + return nil + } + fmt.Println("User allowed") + return nil +} diff --git a/cmd/cluster/cmd.go b/cmd/cluster/cmd.go index 42fc5f19..3b6e9579 100644 --- a/cmd/cluster/cmd.go +++ b/cmd/cluster/cmd.go @@ -29,6 +29,8 @@ func NewCmdCluster(streams genericclioptions.IOStreams, flags *genericclioptions clusterCmd.AddCommand(access.NewCmdAccess(streams, flags)) clusterCmd.AddCommand(newCmdResizeControlPlaneNode(streams, flags, globalOpts)) clusterCmd.AddCommand(newCmdCpd()) + clusterCmd.AddCommand(newCmdCheckBannedUser()) + clusterCmd.AddCommand(newCmdValidatePullSecret(client, flags)) return clusterCmd } diff --git a/cmd/cluster/validatepullsecret.go b/cmd/cluster/validatepullsecret.go new file mode 100644 index 00000000..00e91f2a --- /dev/null +++ b/cmd/cluster/validatepullsecret.go @@ -0,0 +1,106 @@ +package cluster + +import ( + "context" + "fmt" + v1 "github.com/openshift-online/ocm-sdk-go/accountsmgmt/v1" + "github.com/openshift/osdctl/pkg/utils" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var BackplaneClusterAdmin = "backplane-cluster-admin" + +func newCmdValidatePullSecret(kubeCli client.Client, flags *genericclioptions.ConfigFlags) *cobra.Command { + return &cobra.Command{ + Use: "validate-pull-secret [CLUSTER_ID]", + Short: "Checks if the pull secret email matches the owner email", + Long: `Checks if the pull secret email matches the owner email. + +The owner's email to check will be determined by the cluster identifier passed to the command, while the pull secret checked will be determined by the cluster that the caller is currently logged in to.`, + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(ValidatePullSecret(args[0], kubeCli, flags)) + }, + } +} + +func ValidatePullSecret(clusterID string, kubeCli client.Client, flags *genericclioptions.ConfigFlags) error { + ocm := utils.CreateConnection() + defer func() { + if ocmCloseErr := ocm.Close(); ocmCloseErr != nil { + fmt.Printf("Cannot close the ocm (possible memory leak): %q", ocmCloseErr) + } + }() + + fmt.Println("Checking if pull secret email matches user email") + + // This is the flagset for the kubeCli object provided from the root command. Set here to retroactively impersonate backplane-cluster-admin + flags.Impersonate = &BackplaneClusterAdmin + secret := &corev1.Secret{} + err := kubeCli.Get(context.TODO(), types.NamespacedName{Namespace: "openshift-config", Name: "pull-secret"}, secret) + if err != nil { + return err + } + + clusterPullSecretEmail, err, done := getPullSecretEmail(clusterID, secret) + if done { + return err + } + + subscription, err := utils.GetSubscription(ocm, clusterID) + if err != nil { + return err + } + + account, err := utils.GetAccount(ocm, subscription.Creator().ID()) + if err != nil { + return err + } + + if account.Email() != clusterPullSecretEmail { + fmt.Println("Pull secret email doesn't match OCM user email. Recommend sending service log with:") + fmt.Printf("osdctl servicelog post -t https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/pull_secret_user_mismatch.json %v\n", clusterID) + return nil + } + + fmt.Println("Email addresses match.") + return nil +} + +func getPullSecretEmail(clusterID string, secret *corev1.Secret) (string, error, bool) { + dockerConfigJsonBytes, found := secret.Data[".dockerconfigjson"] + if !found { + // Indicates issue w/ pull-secret, so we can stop evaluating and specify a more direct course of action + fmt.Println("Secret does not contain expected key '.dockerconfigjson'. Recommend sending a service log with the following command:") + fmt.Printf("osdctl servicelog post -t https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/pull_secret_change_breaking_upgradesync.json %v\n", clusterID) + return "", nil, true + } + + dockerConfigJson, err := v1.UnmarshalAccessToken(dockerConfigJsonBytes) + if err != nil { + return "", err, true + } + + cloudOpenshiftAuth, found := dockerConfigJson.Auths()["cloud.openshift.com"] + if !found { + fmt.Println("Secret does not contain entry for cloud.openshift.com. Recommend sending a service log with the following command:") + fmt.Printf("osdctl servicelog post -t https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/pull_secret_change_breaking_upgradesync.json %v\n", clusterID) + return "", nil, true + } + + clusterPullSecretEmail := cloudOpenshiftAuth.Email() + if clusterPullSecretEmail == "" { + fmt.Printf("%v\n%v\n%v\n", + "Couldn't extract email address from pull secret for cloud.openshift.com", + "This can mean the pull secret is misconfigured. Please verify the pull secret manually:", + " oc get secret -n openshift-config pull-secret -o json | jq -r '.data[\".dockerconfigjson\"]' | base64 -d") + return "", nil, true + } + return clusterPullSecretEmail, nil, false +} diff --git a/cmd/cluster/validatepullsecret_test.go b/cmd/cluster/validatepullsecret_test.go new file mode 100644 index 00000000..a372f948 --- /dev/null +++ b/cmd/cluster/validatepullsecret_test.go @@ -0,0 +1,52 @@ +package cluster + +import ( + corev1 "k8s.io/api/core/v1" + "reflect" + "testing" +) + +func Test_getPullSecretEmail(t *testing.T) { + tests := []struct { + name string + secret *corev1.Secret + expectedEmail string + expectedError error + expectedDone bool + }{ + { + name: "Missing dockerconfigjson", + secret: &corev1.Secret{Data: map[string][]byte{}}, + expectedDone: true, + }, + { + name: "Missing cloud.openshift.com auth", + secret: &corev1.Secret{Data: map[string][]byte{".dockerconfigjson": []byte("{\"auths\":{}}")}}, + expectedDone: true, + }, + { + name: "Missing email", + secret: &corev1.Secret{Data: map[string][]byte{".dockerconfigjson": []byte("{\"auths\":{\"cloud.openshift.com\":{}}}")}}, + expectedDone: true, + }, + { + name: "Valid pull secret", + secret: &corev1.Secret{Data: map[string][]byte{".dockerconfigjson": []byte("{\"auths\":{\"cloud.openshift.com\":{\"email\":\"foo@bar.com\"}}}")}}, + expectedEmail: "foo@bar.com", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + email, err, done := getPullSecretEmail("abc123", tt.secret) + if email != tt.expectedEmail { + t.Errorf("getPullSecretEmail() email = %v, expectedEmail %v", email, tt.expectedEmail) + } + if !reflect.DeepEqual(err, tt.expectedError) { + t.Errorf("getPullSecretEmail() err = %v, expectedEmail %v", err, tt.expectedError) + } + if done != tt.expectedDone { + t.Errorf("getPullSecretEmail() done = %v, expectedEmail %v", done, tt.expectedDone) + } + }) + } +}