From cfc9c5926ded6b7d5f220d49026f6f32a0401079 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 2 May 2024 17:57:40 +0530 Subject: [PATCH 001/103] Add Accounts client Signed-off-by: Mayank Shah --- pkg/kubernetes/accounts.go | 37 +++ pkg/kubernetes/client/accounts/accounts.go | 304 ++++++++++++++++++ pkg/kubernetes/client/configmap.go | 23 ++ pkg/kubernetes/client/gen.go | 2 +- pkg/kubernetes/client/kubeclient_interface.go | 6 + .../client/mock_kube_client_connector.go | 248 +++++++++----- pkg/kubernetes/gen.go | 2 +- pkg/kubernetes/kubernetes_interface.go | 2 + pkg/kubernetes/mock_kubernetes_connector.go | 20 ++ public/dist/index.html | 15 + 10 files changed, 578 insertions(+), 81 deletions(-) create mode 100644 pkg/kubernetes/accounts.go create mode 100644 pkg/kubernetes/client/accounts/accounts.go create mode 100644 pkg/kubernetes/client/configmap.go diff --git a/pkg/kubernetes/accounts.go b/pkg/kubernetes/accounts.go new file mode 100644 index 000000000..b8320defa --- /dev/null +++ b/pkg/kubernetes/accounts.go @@ -0,0 +1,37 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package client ... +package kubernetes + +import ( + "context" + + "github.com/percona/everest/pkg/kubernetes/client/accounts" +) + +// Accounts provides an interface for managing Everest user accounts. +type Accounts interface { + Create(ctx context.Context, username, password string) error + Get(ctx context.Context, username string) (*accounts.Account, error) + List(ctx context.Context) ([]accounts.Account, error) + Delete(ctx context.Context, username string) error + Update(ctx context.Context, username, password string) error +} + +// Accounts returns a new client for managing everest user accounts. +func (c *Kubernetes) Accounts(ctx context.Context) Accounts { + return accounts.New(c.client) +} diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go new file mode 100644 index 000000000..e5e39eb40 --- /dev/null +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -0,0 +1,304 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accounts provides functionality for managing Everest user accounts +package accounts + +import ( + "context" + "crypto/sha256" + "errors" + "time" + + "golang.org/x/crypto/pbkdf2" + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes/client" +) + +type AccountCapability string + +const ( + // AccountsConfigMapName is the name of the ConfigMap that holds account information. + AccountsConfigMapName = "everest-accounts" + + // AccountCapabilityLogin represents capability to create UI session tokens. + AccountCapabilityLogin AccountCapability = "login" + // AccountCapabilityLogin represents capability to generate API auth tokens. + AccountCapabilityApiKey AccountCapability = "apiKey" + + usersFile = "users.yaml" + passwordFile = "passwords.yaml" +) + +// ErrAccountNotFound is returned when an account is not found. +var ErrAccountNotFound = errors.New("account not found") + +// User contains user data. +type User struct { + Enabled bool `yaml:"enabled"` + Capabilities []AccountCapability `yaml:"capabilities"` +} + +// Password contains password data. +type Password struct { + PasswordHash string `yaml:"passwordHash"` + PasswordMTime string `yaml:"passwordMTime"` +} + +// Account contains user and password data. +type Account struct { + ID string + User + Password +} + +type accounts struct { + k client.KubeClientConnector +} + +// New returns a new Kubernetes based account manager for Everest. +func New(k client.KubeClientConnector) *accounts { + return &accounts{k: k} +} + +// Get returns an account by username. +func (a *accounts) Get(ctx context.Context, username string) (*Account, error) { + users, err := a.listAllUsers(ctx) + if err != nil { + return nil, err + } + user, found := users[username] + if !found { + return nil, ErrAccountNotFound + } + passwords, err := a.listAllPasswords(ctx) + if err != nil { + return nil, err + } + pass, found := passwords[username] + if !found { + return nil, ErrAccountNotFound + } + return &Account{ + ID: username, + User: user, + Password: pass, + }, nil +} + +// List returns a list of all accounts. +func (a *accounts) List(ctx context.Context) ([]Account, error) { + users, err := a.listAllUsers(ctx) + if err != nil { + return nil, err + } + passwords, err := a.listAllPasswords(ctx) + if err != nil { + return nil, err + } + return mergeUserPassToAccounts(users, passwords), nil +} + +// Create a new user account. +func (a *accounts) Create(ctx context.Context, username, password string) error { + // Check if this user exists? + users, err := a.listAllUsers(ctx) + if err != nil { + return err + } + if _, found := users[username]; found { + return errors.New("user already exists") + } + salt, err := a.salt() + if err != nil { + return errors.Join(err, errors.New("failed to get salt")) + } + hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + acc := Account{ + ID: username, + User: User{ + Enabled: true, + Capabilities: []AccountCapability{AccountCapabilityLogin}, // XX: for now we only support login + }, + Password: Password{ + PasswordHash: string(hash), + PasswordMTime: time.Now().Format(time.RFC3339), + }, + } + return a.setAccounts(ctx, []Account{acc}) +} + +func (a *accounts) salt() ([]byte, error) { + ns, err := a.k.GetNamespace(context.Background(), common.SystemNamespace) + if err != nil { + return nil, err + } + return []byte(ns.UID), nil +} + +// Delete an existing user account specified by username. +func (a *accounts) Delete(ctx context.Context, username string) error { + // Check if this user exists? + users, err := a.listAllUsers(ctx) + if err != nil { + return err + } + if _, found := users[username]; !found { + return errors.New("user does not exist") + } + // Remove user from the list. + delete(users, username) + passwords, err := a.listAllPasswords(ctx) + if err != nil { + return err + } + delete(passwords, username) + acc := mergeUserPassToAccounts(users, passwords) + return a.setAccounts(ctx, acc) +} + +// Update an existing user account specified by username. +func (a *accounts) Update(ctx context.Context, username, password string) error { + // Check if this user exists? + users, err := a.listAllUsers(ctx) + if err != nil { + return err + } + if _, found := users[username]; !found { + return errors.New("user does not exist") + } + // Update the password. + passwords, err := a.listAllPasswords(ctx) + if err != nil { + return err + } + salt, err := a.salt() + if err != nil { + return errors.Join(err, errors.New("failed to get salt")) + } + hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + passwords[username] = Password{ + PasswordHash: string(hash), + PasswordMTime: time.Now().Format(time.RFC3339), + } + return nil +} + +func mergeUserPassToAccounts(users map[string]User, passwords map[string]Password) []Account { + var accounts []Account + for name, user := range users { + pass, found := passwords[name] + if !found { + continue + } + accounts = append(accounts, Account{ + ID: name, + User: user, + Password: pass, + }) + } + return accounts +} + +func (a *accounts) setAccounts( + ctx context.Context, + accounts []Account, +) error { + // Get existing users and passwords. + users, err := a.listAllUsers(ctx) + if err != nil { + return err + } + passwords, err := a.listAllPasswords(ctx) + if err != nil { + return err + } + // Modify accounts. + for _, acc := range accounts { + users[acc.ID] = acc.User + passwords[acc.ID] = acc.Password + } + // Update accounts ConfigMap. + userB, err := yaml.Marshal(users) + if err != nil { + return err + } + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: AccountsConfigMapName, + Namespace: common.SystemNamespace, + }, + BinaryData: map[string][]byte{ + usersFile: userB, + }, + } + if _, err := a.k.UpdateConfigMap(ctx, cm); err != nil { + return err + } + // Update Accounts Secret. + passB, err := yaml.Marshal(passwords) + if err != nil { + return err + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: AccountsConfigMapName, + Namespace: common.SystemNamespace, + }, + Data: map[string][]byte{ + passwordFile: passB, + }, + } + if _, err := a.k.UpdateSecret(ctx, secret); err != nil { + return err + } + return nil +} + +func (a *accounts) listAllUsers(ctx context.Context) (map[string]User, error) { + cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, AccountsConfigMapName) + if err != nil { + return nil, err + } + usersYaml, found := cm.BinaryData[usersFile] + if !found { + return nil, ErrAccountNotFound + } + var users map[string]User + if err := yaml.Unmarshal(usersYaml, &users); err != nil { + return nil, err + } + return users, nil +} + +func (a *accounts) listAllPasswords(ctx context.Context) (map[string]Password, error) { + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, AccountsConfigMapName) + if err != nil { + return nil, err + } + passwordsYaml, found := secret.Data[passwordFile] + if !found { + return nil, ErrAccountNotFound + } + var passwords map[string]Password + if err := yaml.Unmarshal(passwordsYaml, &passwords); err != nil { + return nil, err + } + return passwords, nil +} diff --git a/pkg/kubernetes/client/configmap.go b/pkg/kubernetes/client/configmap.go new file mode 100644 index 000000000..97dd96f7e --- /dev/null +++ b/pkg/kubernetes/client/configmap.go @@ -0,0 +1,23 @@ +package client + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GetConfigMap returns config map by name and namespace. +func (c *Client) GetConfigMap(ctx context.Context, namespace, name string) (*corev1.ConfigMap, error) { + return c.clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) +} + +// CreateConfigMap creates the provided ConfigMap. +func (c *Client) CreateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) { + return c.clientset.CoreV1().ConfigMaps(configMap.Namespace).Create(ctx, configMap, metav1.CreateOptions{}) +} + +// UpdateConfigMap updates the provided ConfigMap. +func (c *Client) UpdateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) { + return c.clientset.CoreV1().ConfigMaps(configMap.Namespace).Update(ctx, configMap, metav1.UpdateOptions{}) +} diff --git a/pkg/kubernetes/client/gen.go b/pkg/kubernetes/client/gen.go index ea5a38917..ba5793a96 100644 --- a/pkg/kubernetes/client/gen.go +++ b/pkg/kubernetes/client/gen.go @@ -15,5 +15,5 @@ package client -//go:generate ../../../bin/ifacemaker -f backup_storage.go -f client.go -f ctl.go -f database_cluster.go -f database_cluster_backup.go -f database_cluster_restore.go -f database_engine.go -f deployment.go -f install_plan.go -f monitoring.go -f monitoring_config.go -f namespace.go -f olm.go -f node.go -f pod.go -f secret.go -f storage.go -f writer -s Client -i KubeClientConnector -p client -o kubeclient_interface.go +//go:generate ../../../bin/ifacemaker -f backup_storage.go -f configmap.go -f client.go -f ctl.go -f database_cluster.go -f database_cluster_backup.go -f database_cluster_restore.go -f database_engine.go -f deployment.go -f install_plan.go -f monitoring.go -f monitoring_config.go -f namespace.go -f olm.go -f node.go -f pod.go -f secret.go -f storage.go -f writer -s Client -i KubeClientConnector -p client -o kubeclient_interface.go //go:generate ../../../bin/mockery --name=KubeClientConnector --case=snake --inpackage diff --git a/pkg/kubernetes/client/kubeclient_interface.go b/pkg/kubernetes/client/kubeclient_interface.go index 335fa1545..9177c3e45 100644 --- a/pkg/kubernetes/client/kubeclient_interface.go +++ b/pkg/kubernetes/client/kubeclient_interface.go @@ -38,6 +38,12 @@ type KubeClientConnector interface { ListBackupStorages(ctx context.Context, namespace string, options metav1.ListOptions) (*everestv1alpha1.BackupStorageList, error) // DeleteBackupStorage deletes the backupStorage. DeleteBackupStorage(ctx context.Context, namespace, name string) error + // GetConfigMap returns config map by name and namespace. + GetConfigMap(ctx context.Context, namespace, name string) (*corev1.ConfigMap, error) + // CreateConfigMap creates the provided ConfigMap. + CreateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) + // UpdateConfigMap updates the provided ConfigMap. + UpdateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) // Config returns restConfig to the pkg/kubernetes.Kubernetes client. Config() *rest.Config // ClusterName returns the name of the k8s cluster. diff --git a/pkg/kubernetes/client/mock_kube_client_connector.go b/pkg/kubernetes/client/mock_kube_client_connector.go index e3d03607e..079f4e7f6 100644 --- a/pkg/kubernetes/client/mock_kube_client_connector.go +++ b/pkg/kubernetes/client/mock_kube_client_connector.go @@ -5,13 +5,13 @@ package client import ( context "context" - v1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" versioned "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" - operatorsv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/operators/v1" + apisoperatorsv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/operators/v1" mock "github.com/stretchr/testify/mock" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" storagev1 "k8s.io/api/storage/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -141,6 +141,36 @@ func (_m *MockKubeClientConnector) CreateBackupStorage(ctx context.Context, stor return r0 } +// CreateConfigMap provides a mock function with given fields: ctx, configMap +func (_m *MockKubeClientConnector) CreateConfigMap(ctx context.Context, configMap *v1.ConfigMap) (*v1.ConfigMap, error) { + ret := _m.Called(ctx, configMap) + + if len(ret) == 0 { + panic("no return value specified for CreateConfigMap") + } + + var r0 *v1.ConfigMap + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMap) (*v1.ConfigMap, error)); ok { + return rf(ctx, configMap) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMap) *v1.ConfigMap); ok { + r0 = rf(ctx, configMap) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ConfigMap) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.ConfigMap) error); ok { + r1 = rf(ctx, configMap) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateMonitoringConfig provides a mock function with given fields: ctx, config func (_m *MockKubeClientConnector) CreateMonitoringConfig(ctx context.Context, config *v1alpha1.MonitoringConfig) error { ret := _m.Called(ctx, config) @@ -178,23 +208,23 @@ func (_m *MockKubeClientConnector) CreateNamespace(name string) error { } // CreateOperatorGroup provides a mock function with given fields: ctx, namespace, name, targetNamespaces -func (_m *MockKubeClientConnector) CreateOperatorGroup(ctx context.Context, namespace string, name string, targetNamespaces []string) (*v1.OperatorGroup, error) { +func (_m *MockKubeClientConnector) CreateOperatorGroup(ctx context.Context, namespace string, name string, targetNamespaces []string) (*operatorsv1.OperatorGroup, error) { ret := _m.Called(ctx, namespace, name, targetNamespaces) if len(ret) == 0 { panic("no return value specified for CreateOperatorGroup") } - var r0 *v1.OperatorGroup + var r0 *operatorsv1.OperatorGroup var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, []string) (*v1.OperatorGroup, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string, []string) (*operatorsv1.OperatorGroup, error)); ok { return rf(ctx, namespace, name, targetNamespaces) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, []string) *v1.OperatorGroup); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string, []string) *operatorsv1.OperatorGroup); ok { r0 = rf(ctx, namespace, name, targetNamespaces) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.OperatorGroup) + r0 = ret.Get(0).(*operatorsv1.OperatorGroup) } } @@ -208,27 +238,27 @@ func (_m *MockKubeClientConnector) CreateOperatorGroup(ctx context.Context, name } // CreateSecret provides a mock function with given fields: ctx, secret -func (_m *MockKubeClientConnector) CreateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) { +func (_m *MockKubeClientConnector) CreateSecret(ctx context.Context, secret *v1.Secret) (*v1.Secret, error) { ret := _m.Called(ctx, secret) if len(ret) == 0 { panic("no return value specified for CreateSecret") } - var r0 *corev1.Secret + var r0 *v1.Secret var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *corev1.Secret) (*corev1.Secret, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.Secret) (*v1.Secret, error)); ok { return rf(ctx, secret) } - if rf, ok := ret.Get(0).(func(context.Context, *corev1.Secret) *corev1.Secret); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.Secret) *v1.Secret); ok { r0 = rf(ctx, secret) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.Secret) + r0 = ret.Get(0).(*v1.Secret) } } - if rf, ok := ret.Get(1).(func(context.Context, *corev1.Secret) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *v1.Secret) error); ok { r1 = rf(ctx, secret) } else { r1 = ret.Error(1) @@ -532,7 +562,7 @@ func (_m *MockKubeClientConnector) DoRolloutWait(ctx context.Context, key types. } // GenerateKubeConfigWithToken provides a mock function with given fields: user, secret -func (_m *MockKubeClientConnector) GenerateKubeConfigWithToken(user string, secret *corev1.Secret) ([]byte, error) { +func (_m *MockKubeClientConnector) GenerateKubeConfigWithToken(user string, secret *v1.Secret) ([]byte, error) { ret := _m.Called(user, secret) if len(ret) == 0 { @@ -541,10 +571,10 @@ func (_m *MockKubeClientConnector) GenerateKubeConfigWithToken(user string, secr var r0 []byte var r1 error - if rf, ok := ret.Get(0).(func(string, *corev1.Secret) ([]byte, error)); ok { + if rf, ok := ret.Get(0).(func(string, *v1.Secret) ([]byte, error)); ok { return rf(user, secret) } - if rf, ok := ret.Get(0).(func(string, *corev1.Secret) []byte); ok { + if rf, ok := ret.Get(0).(func(string, *v1.Secret) []byte); ok { r0 = rf(user, secret) } else { if ret.Get(0) != nil { @@ -552,7 +582,7 @@ func (_m *MockKubeClientConnector) GenerateKubeConfigWithToken(user string, secr } } - if rf, ok := ret.Get(1).(func(string, *corev1.Secret) error); ok { + if rf, ok := ret.Get(1).(func(string, *v1.Secret) error); ok { r1 = rf(user, secret) } else { r1 = ret.Error(1) @@ -651,6 +681,36 @@ func (_m *MockKubeClientConnector) GetClusterServiceVersion(ctx context.Context, return r0, r1 } +// GetConfigMap provides a mock function with given fields: ctx, namespace, name +func (_m *MockKubeClientConnector) GetConfigMap(ctx context.Context, namespace string, name string) (*v1.ConfigMap, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for GetConfigMap") + } + + var r0 *v1.ConfigMap + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.ConfigMap, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.ConfigMap); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ConfigMap) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDatabaseCluster provides a mock function with given fields: ctx, namespace, name func (_m *MockKubeClientConnector) GetDatabaseCluster(ctx context.Context, namespace string, name string) (*v1alpha1.DatabaseCluster, error) { ret := _m.Called(ctx, namespace, name) @@ -918,23 +978,23 @@ func (_m *MockKubeClientConnector) GetMonitoringConfig(ctx context.Context, name } // GetNamespace provides a mock function with given fields: ctx, name -func (_m *MockKubeClientConnector) GetNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { +func (_m *MockKubeClientConnector) GetNamespace(ctx context.Context, name string) (*v1.Namespace, error) { ret := _m.Called(ctx, name) if len(ret) == 0 { panic("no return value specified for GetNamespace") } - var r0 *corev1.Namespace + var r0 *v1.Namespace var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.Namespace, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.Namespace, error)); ok { return rf(ctx, name) } - if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.Namespace); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.Namespace); ok { r0 = rf(ctx, name) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.Namespace) + r0 = ret.Get(0).(*v1.Namespace) } } @@ -948,23 +1008,23 @@ func (_m *MockKubeClientConnector) GetNamespace(ctx context.Context, name string } // GetNodes provides a mock function with given fields: ctx -func (_m *MockKubeClientConnector) GetNodes(ctx context.Context) (*corev1.NodeList, error) { +func (_m *MockKubeClientConnector) GetNodes(ctx context.Context) (*v1.NodeList, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetNodes") } - var r0 *corev1.NodeList + var r0 *v1.NodeList var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*corev1.NodeList, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) (*v1.NodeList, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) *corev1.NodeList); ok { + if rf, ok := ret.Get(0).(func(context.Context) *v1.NodeList); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.NodeList) + r0 = ret.Get(0).(*v1.NodeList) } } @@ -996,23 +1056,23 @@ func (_m *MockKubeClientConnector) GetObject(gvk schema.GroupVersionKind, name s } // GetOperatorGroup provides a mock function with given fields: ctx, namespace, name -func (_m *MockKubeClientConnector) GetOperatorGroup(ctx context.Context, namespace string, name string) (*v1.OperatorGroup, error) { +func (_m *MockKubeClientConnector) GetOperatorGroup(ctx context.Context, namespace string, name string) (*operatorsv1.OperatorGroup, error) { ret := _m.Called(ctx, namespace, name) if len(ret) == 0 { panic("no return value specified for GetOperatorGroup") } - var r0 *v1.OperatorGroup + var r0 *operatorsv1.OperatorGroup var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.OperatorGroup, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*operatorsv1.OperatorGroup, error)); ok { return rf(ctx, namespace, name) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.OperatorGroup); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *operatorsv1.OperatorGroup); ok { r0 = rf(ctx, namespace, name) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.OperatorGroup) + r0 = ret.Get(0).(*operatorsv1.OperatorGroup) } } @@ -1026,23 +1086,23 @@ func (_m *MockKubeClientConnector) GetOperatorGroup(ctx context.Context, namespa } // GetPackageManifest provides a mock function with given fields: ctx, namespace, name -func (_m *MockKubeClientConnector) GetPackageManifest(ctx context.Context, namespace string, name string) (*operatorsv1.PackageManifest, error) { +func (_m *MockKubeClientConnector) GetPackageManifest(ctx context.Context, namespace string, name string) (*apisoperatorsv1.PackageManifest, error) { ret := _m.Called(ctx, namespace, name) if len(ret) == 0 { panic("no return value specified for GetPackageManifest") } - var r0 *operatorsv1.PackageManifest + var r0 *apisoperatorsv1.PackageManifest var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*operatorsv1.PackageManifest, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*apisoperatorsv1.PackageManifest, error)); ok { return rf(ctx, namespace, name) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *operatorsv1.PackageManifest); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *apisoperatorsv1.PackageManifest); ok { r0 = rf(ctx, namespace, name) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*operatorsv1.PackageManifest) + r0 = ret.Get(0).(*apisoperatorsv1.PackageManifest) } } @@ -1056,23 +1116,23 @@ func (_m *MockKubeClientConnector) GetPackageManifest(ctx context.Context, names } // GetPersistentVolumes provides a mock function with given fields: ctx -func (_m *MockKubeClientConnector) GetPersistentVolumes(ctx context.Context) (*corev1.PersistentVolumeList, error) { +func (_m *MockKubeClientConnector) GetPersistentVolumes(ctx context.Context) (*v1.PersistentVolumeList, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetPersistentVolumes") } - var r0 *corev1.PersistentVolumeList + var r0 *v1.PersistentVolumeList var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*corev1.PersistentVolumeList, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) (*v1.PersistentVolumeList, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) *corev1.PersistentVolumeList); ok { + if rf, ok := ret.Get(0).(func(context.Context) *v1.PersistentVolumeList); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.PersistentVolumeList) + r0 = ret.Get(0).(*v1.PersistentVolumeList) } } @@ -1086,23 +1146,23 @@ func (_m *MockKubeClientConnector) GetPersistentVolumes(ctx context.Context) (*c } // GetPods provides a mock function with given fields: ctx, namespace, labelSelector -func (_m *MockKubeClientConnector) GetPods(ctx context.Context, namespace string, labelSelector *metav1.LabelSelector) (*corev1.PodList, error) { +func (_m *MockKubeClientConnector) GetPods(ctx context.Context, namespace string, labelSelector *metav1.LabelSelector) (*v1.PodList, error) { ret := _m.Called(ctx, namespace, labelSelector) if len(ret) == 0 { panic("no return value specified for GetPods") } - var r0 *corev1.PodList + var r0 *v1.PodList var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, *metav1.LabelSelector) (*corev1.PodList, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, *metav1.LabelSelector) (*v1.PodList, error)); ok { return rf(ctx, namespace, labelSelector) } - if rf, ok := ret.Get(0).(func(context.Context, string, *metav1.LabelSelector) *corev1.PodList); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, *metav1.LabelSelector) *v1.PodList); ok { r0 = rf(ctx, namespace, labelSelector) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.PodList) + r0 = ret.Get(0).(*v1.PodList) } } @@ -1116,23 +1176,23 @@ func (_m *MockKubeClientConnector) GetPods(ctx context.Context, namespace string } // GetSecret provides a mock function with given fields: ctx, namespace, name -func (_m *MockKubeClientConnector) GetSecret(ctx context.Context, namespace string, name string) (*corev1.Secret, error) { +func (_m *MockKubeClientConnector) GetSecret(ctx context.Context, namespace string, name string) (*v1.Secret, error) { ret := _m.Called(ctx, namespace, name) if len(ret) == 0 { panic("no return value specified for GetSecret") } - var r0 *corev1.Secret + var r0 *v1.Secret var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.Secret, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Secret, error)); ok { return rf(ctx, namespace, name) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.Secret); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Secret); ok { r0 = rf(ctx, namespace, name) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.Secret) + r0 = ret.Get(0).(*v1.Secret) } } @@ -1146,23 +1206,23 @@ func (_m *MockKubeClientConnector) GetSecret(ctx context.Context, namespace stri } // GetSecretsForServiceAccount provides a mock function with given fields: ctx, accountName -func (_m *MockKubeClientConnector) GetSecretsForServiceAccount(ctx context.Context, accountName string) (*corev1.Secret, error) { +func (_m *MockKubeClientConnector) GetSecretsForServiceAccount(ctx context.Context, accountName string) (*v1.Secret, error) { ret := _m.Called(ctx, accountName) if len(ret) == 0 { panic("no return value specified for GetSecretsForServiceAccount") } - var r0 *corev1.Secret + var r0 *v1.Secret var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.Secret, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.Secret, error)); ok { return rf(ctx, accountName) } - if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.Secret); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.Secret); ok { r0 = rf(ctx, accountName) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.Secret) + r0 = ret.Get(0).(*v1.Secret) } } @@ -1206,23 +1266,23 @@ func (_m *MockKubeClientConnector) GetServerVersion() (*version.Info, error) { } // GetService provides a mock function with given fields: ctx, namespace, name -func (_m *MockKubeClientConnector) GetService(ctx context.Context, namespace string, name string) (*corev1.Service, error) { +func (_m *MockKubeClientConnector) GetService(ctx context.Context, namespace string, name string) (*v1.Service, error) { ret := _m.Called(ctx, namespace, name) if len(ret) == 0 { panic("no return value specified for GetService") } - var r0 *corev1.Service + var r0 *v1.Service var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*corev1.Service, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Service, error)); ok { return rf(ctx, namespace, name) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *corev1.Service); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Service); ok { r0 = rf(ctx, namespace, name) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.Service) + r0 = ret.Get(0).(*v1.Service) } } @@ -1672,23 +1732,23 @@ func (_m *MockKubeClientConnector) ListObjects(gvk schema.GroupVersionKind, into } // ListPods provides a mock function with given fields: ctx, namespace, options -func (_m *MockKubeClientConnector) ListPods(ctx context.Context, namespace string, options metav1.ListOptions) (*corev1.PodList, error) { +func (_m *MockKubeClientConnector) ListPods(ctx context.Context, namespace string, options metav1.ListOptions) (*v1.PodList, error) { ret := _m.Called(ctx, namespace, options) if len(ret) == 0 { panic("no return value specified for ListPods") } - var r0 *corev1.PodList + var r0 *v1.PodList var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.ListOptions) (*corev1.PodList, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, metav1.ListOptions) (*v1.PodList, error)); ok { return rf(ctx, namespace, options) } - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.ListOptions) *corev1.PodList); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, metav1.ListOptions) *v1.PodList); ok { r0 = rf(ctx, namespace, options) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.PodList) + r0 = ret.Get(0).(*v1.PodList) } } @@ -1702,23 +1762,23 @@ func (_m *MockKubeClientConnector) ListPods(ctx context.Context, namespace strin } // ListSecrets provides a mock function with given fields: ctx, namespace -func (_m *MockKubeClientConnector) ListSecrets(ctx context.Context, namespace string) (*corev1.SecretList, error) { +func (_m *MockKubeClientConnector) ListSecrets(ctx context.Context, namespace string) (*v1.SecretList, error) { ret := _m.Called(ctx, namespace) if len(ret) == 0 { panic("no return value specified for ListSecrets") } - var r0 *corev1.SecretList + var r0 *v1.SecretList var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*corev1.SecretList, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*v1.SecretList, error)); ok { return rf(ctx, namespace) } - if rf, ok := ret.Get(0).(func(context.Context, string) *corev1.SecretList); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *v1.SecretList); ok { r0 = rf(ctx, namespace) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.SecretList) + r0 = ret.Get(0).(*v1.SecretList) } } @@ -1847,6 +1907,36 @@ func (_m *MockKubeClientConnector) UpdateClusterServiceVersion(ctx context.Conte return r0, r1 } +// UpdateConfigMap provides a mock function with given fields: ctx, configMap +func (_m *MockKubeClientConnector) UpdateConfigMap(ctx context.Context, configMap *v1.ConfigMap) (*v1.ConfigMap, error) { + ret := _m.Called(ctx, configMap) + + if len(ret) == 0 { + panic("no return value specified for UpdateConfigMap") + } + + var r0 *v1.ConfigMap + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMap) (*v1.ConfigMap, error)); ok { + return rf(ctx, configMap) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMap) *v1.ConfigMap); ok { + r0 = rf(ctx, configMap) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ConfigMap) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.ConfigMap) error); ok { + r1 = rf(ctx, configMap) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateDatabaseClusterBackup provides a mock function with given fields: ctx, backup func (_m *MockKubeClientConnector) UpdateDatabaseClusterBackup(ctx context.Context, backup *v1alpha1.DatabaseClusterBackup) (*v1alpha1.DatabaseClusterBackup, error) { ret := _m.Called(ctx, backup) @@ -1986,27 +2076,27 @@ func (_m *MockKubeClientConnector) UpdateMonitoringConfig(ctx context.Context, c } // UpdateSecret provides a mock function with given fields: ctx, secret -func (_m *MockKubeClientConnector) UpdateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) { +func (_m *MockKubeClientConnector) UpdateSecret(ctx context.Context, secret *v1.Secret) (*v1.Secret, error) { ret := _m.Called(ctx, secret) if len(ret) == 0 { panic("no return value specified for UpdateSecret") } - var r0 *corev1.Secret + var r0 *v1.Secret var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *corev1.Secret) (*corev1.Secret, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.Secret) (*v1.Secret, error)); ok { return rf(ctx, secret) } - if rf, ok := ret.Get(0).(func(context.Context, *corev1.Secret) *corev1.Secret); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.Secret) *v1.Secret); ok { r0 = rf(ctx, secret) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.Secret) + r0 = ret.Get(0).(*v1.Secret) } } - if rf, ok := ret.Get(1).(func(context.Context, *corev1.Secret) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *v1.Secret) error); ok { r1 = rf(ctx, secret) } else { r1 = ret.Error(1) diff --git a/pkg/kubernetes/gen.go b/pkg/kubernetes/gen.go index 3898060b0..12250d25f 100644 --- a/pkg/kubernetes/gen.go +++ b/pkg/kubernetes/gen.go @@ -15,5 +15,5 @@ package kubernetes -//go:generate ../../bin/ifacemaker -f deployment.go -f install_plan.go -f kubernetes.go -f operator.go -s Kubernetes -i KubernetesConnector -p kubernetes -o kubernetes_interface.go +//go:generate ../../bin/ifacemaker -f accounts.go -f deployment.go -f install_plan.go -f kubernetes.go -f operator.go -s Kubernetes -i KubernetesConnector -p kubernetes -o kubernetes_interface.go //go:generate ../../bin/mockery --name=KubernetesConnector --case=snake --inpackage diff --git a/pkg/kubernetes/kubernetes_interface.go b/pkg/kubernetes/kubernetes_interface.go index ec4b9bacb..8b2cd7ec7 100644 --- a/pkg/kubernetes/kubernetes_interface.go +++ b/pkg/kubernetes/kubernetes_interface.go @@ -20,6 +20,8 @@ import ( // KubernetesConnector ... type KubernetesConnector interface { + // Accounts returns a new client for managing everest user accounts. + Accounts(ctx context.Context) Accounts // GetDeployment returns k8s deployment by provided name and namespace. GetDeployment(ctx context.Context, name, namespace string) (*appsv1.Deployment, error) // UpdateDeployment updates a deployment and returns the updated object. diff --git a/pkg/kubernetes/mock_kubernetes_connector.go b/pkg/kubernetes/mock_kubernetes_connector.go index ead513c76..eb5067144 100644 --- a/pkg/kubernetes/mock_kubernetes_connector.go +++ b/pkg/kubernetes/mock_kubernetes_connector.go @@ -24,6 +24,26 @@ type MockKubernetesConnector struct { mock.Mock } +// Accounts provides a mock function with given fields: ctx +func (_m *MockKubernetesConnector) Accounts(ctx context.Context) Accounts { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Accounts") + } + + var r0 Accounts + if rf, ok := ret.Get(0).(func(context.Context) Accounts); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(Accounts) + } + } + + return r0 +} + // ApplyObject provides a mock function with given fields: obj func (_m *MockKubernetesConnector) ApplyObject(obj runtime.Object) error { ret := _m.Called(obj) diff --git a/public/dist/index.html b/public/dist/index.html index e69de29bb..7a2b0e5a6 100644 --- a/public/dist/index.html +++ b/public/dist/index.html @@ -0,0 +1,15 @@ + + + + + + + Percona Everest + + + + +
+ + + From b9f5cd86270620f3731826ac4d217c6ded719dea Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 13:33:46 +0530 Subject: [PATCH 002/103] add basic unit tests Signed-off-by: Mayank Shah --- pkg/kubernetes/client/accounts/accounts.go | 59 +++++++---- .../client/accounts/accounts_test.go | 97 +++++++++++++++++++ pkg/kubernetes/client/client.go | 14 +++ 3 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 pkg/kubernetes/client/accounts/accounts_test.go diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index e5e39eb40..94c6da5df 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -20,6 +20,8 @@ import ( "context" "crypto/sha256" "errors" + "slices" + "strings" "time" "golang.org/x/crypto/pbkdf2" @@ -141,7 +143,7 @@ func (a *accounts) Create(ctx context.Context, username, password string) error PasswordMTime: time.Now().Format(time.RFC3339), }, } - return a.setAccounts(ctx, []Account{acc}) + return a.setAccounts(ctx, []Account{acc}, true) } func (a *accounts) salt() ([]byte, error) { @@ -160,7 +162,7 @@ func (a *accounts) Delete(ctx context.Context, username string) error { return err } if _, found := users[username]; !found { - return errors.New("user does not exist") + return ErrAccountNotFound } // Remove user from the list. delete(users, username) @@ -170,7 +172,7 @@ func (a *accounts) Delete(ctx context.Context, username string) error { } delete(passwords, username) acc := mergeUserPassToAccounts(users, passwords) - return a.setAccounts(ctx, acc) + return a.setAccounts(ctx, acc, false) } // Update an existing user account specified by username. @@ -197,7 +199,7 @@ func (a *accounts) Update(ctx context.Context, username, password string) error PasswordHash: string(hash), PasswordMTime: time.Now().Format(time.RFC3339), } - return nil + return a.setAccounts(ctx, mergeUserPassToAccounts(users, passwords), true) } func mergeUserPassToAccounts(users map[string]User, passwords map[string]Password) []Account { @@ -213,26 +215,45 @@ func mergeUserPassToAccounts(users map[string]User, passwords map[string]Passwor Password: pass, }) } + slices.SortFunc(accounts, func(a, b Account) int { + return strings.Compare(a.ID, b.ID) + }) return accounts } +// Given a list of accounts, update the ConfigMap and Secret. +// If patch is true, existing all existing accounts are preserved. +// If patch is false, accounts are replaced with the new list. func (a *accounts) setAccounts( ctx context.Context, accounts []Account, + patch bool, ) error { - // Get existing users and passwords. - users, err := a.listAllUsers(ctx) - if err != nil { - return err - } - passwords, err := a.listAllPasswords(ctx) - if err != nil { - return err - } - // Modify accounts. - for _, acc := range accounts { - users[acc.ID] = acc.User - passwords[acc.ID] = acc.Password + var ( + err error + users map[string]User = make(map[string]User) + passwords map[string]Password = make(map[string]Password) + ) + if patch { + // Get existing users and passwords. + users, err = a.listAllUsers(ctx) + if err != nil { + return err + } + passwords, err = a.listAllPasswords(ctx) + if err != nil { + return err + } + // Modify accounts. + for _, acc := range accounts { + users[acc.ID] = acc.User + passwords[acc.ID] = acc.Password + } + } else { + for _, acc := range accounts { + users[acc.ID] = acc.User + passwords[acc.ID] = acc.Password + } } // Update accounts ConfigMap. userB, err := yaml.Marshal(users) @@ -278,7 +299,7 @@ func (a *accounts) listAllUsers(ctx context.Context) (map[string]User, error) { } usersYaml, found := cm.BinaryData[usersFile] if !found { - return nil, ErrAccountNotFound + return make(map[string]User), nil } var users map[string]User if err := yaml.Unmarshal(usersYaml, &users); err != nil { @@ -294,7 +315,7 @@ func (a *accounts) listAllPasswords(ctx context.Context) (map[string]Password, e } passwordsYaml, found := secret.Data[passwordFile] if !found { - return nil, ErrAccountNotFound + return make(map[string]Password), nil } var passwords map[string]Password if err := yaml.Unmarshal(passwordsYaml, &passwords); err != nil { diff --git a/pkg/kubernetes/client/accounts/accounts_test.go b/pkg/kubernetes/client/accounts/accounts_test.go new file mode 100644 index 000000000..84286b99e --- /dev/null +++ b/pkg/kubernetes/client/accounts/accounts_test.go @@ -0,0 +1,97 @@ +package accounts + +import ( + "context" + "testing" + + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAccounts(t *testing.T) { + t.Parallel() + c := client.NewFromFakeClient() + ctx := context.Background() + + // Create system namespace for testing. + _, err := c.Clientset(). + CoreV1(). + Namespaces(). + Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: common.SystemNamespace}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + // Prepare configmap. + _, err = c.Clientset(). + CoreV1(). + ConfigMaps(common.SystemNamespace). + Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: AccountsConfigMapName, + Namespace: common.SystemNamespace, + }, + }, metav1.CreateOptions{}) + // Prepare secret. + _, err = c.Clientset(). + CoreV1(). + Secrets(common.SystemNamespace). + Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: AccountsConfigMapName, + Namespace: common.SystemNamespace, + }, + }, metav1.CreateOptions{}) + + mgr := New(c) + + // Assert that initially there are no accounts. + accounts, err := mgr.List(ctx) + require.NoError(t, err) + assert.Len(t, accounts, 0) + + // Create user1 + err = mgr.Create(ctx, "user1", "password") + require.NoError(t, err) + + // Check that a new account is created. + accounts, err = mgr.List(ctx) + require.NoError(t, err) + assert.Len(t, accounts, 1) + assert.Equal(t, "user1", accounts[0].ID) + assert.True(t, accounts[0].Enabled) + assert.NotEmpty(t, accounts[0].Password.PasswordHash) + assert.NotEmpty(t, accounts[0].Password.PasswordMTime) + user1, err := mgr.Get(ctx, "user1") + require.NoError(t, err) + assert.Equal(t, "user1", user1.ID) + assert.True(t, user1.Enabled) + assert.NotEmpty(t, user1.Password.PasswordHash) + assert.NotEmpty(t, user1.Password.PasswordMTime) + + passwordhash := user1.Password.PasswordHash + + // Update password of user1. + err = mgr.Update(ctx, "user1", "new-password") + require.NoError(t, err) + user1, err = mgr.Get(ctx, "user1") + require.NoError(t, err) + assert.NotEqual(t, passwordhash, user1.Password.PasswordHash) + + // Delete non-existing user. + err = mgr.Delete(ctx, "not-existing") + require.Error(t, err) + assert.ErrorIs(t, err, ErrAccountNotFound) + + // Delete user1. + err = mgr.Delete(ctx, "user1") + require.NoError(t, err) + + // Check that no users exists. + accounts, err = mgr.List(ctx) + require.NoError(t, err) + assert.Len(t, accounts, 0) +} diff --git a/pkg/kubernetes/client/client.go b/pkg/kubernetes/client/client.go index 8da7e3e8a..b89c129a3 100644 --- a/pkg/kubernetes/client/client.go +++ b/pkg/kubernetes/client/client.go @@ -58,6 +58,7 @@ import ( "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" // load all auth plugins @@ -254,6 +255,14 @@ func NewInCluster() (*Client, error) { return c, err } +// NewFromFakeClient returns a Client with a fake (in-memory) clientset. +// This is used only for unit testing. +func NewFromFakeClient() *Client { + return &Client{ + clientset: fake.NewSimpleClientset(), + } +} + func (c *Client) kubeClient() (client.Client, error) { //nolint:ireturn,nolintlint rcl, err := rest.HTTPClientFor(c.restConfig) if err != nil { @@ -295,6 +304,11 @@ func (c *Client) Config() *rest.Config { return c.restConfig } +// Clientset returns the k8s clientset. +func (c *Client) Clientset() kubernetes.Interface { + return c.clientset +} + // ClusterName returns the name of the k8s cluster. func (c *Client) ClusterName() string { return c.clusterName From 04ad1bc79cfaec0afe564ab56ee7115974657273 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 13:39:09 +0530 Subject: [PATCH 003/103] renaming Signed-off-by: Mayank Shah --- pkg/common/constants.go | 3 +++ pkg/kubernetes/client/accounts/accounts.go | 11 ++++------- .../accounts/{accounts_test.go => kubernetes_test.go} | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename pkg/kubernetes/client/accounts/{accounts_test.go => kubernetes_test.go} (96%) diff --git a/pkg/common/constants.go b/pkg/common/constants.go index acd8ff9cc..fa960cb18 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -35,4 +35,7 @@ const ( // EverestOperatorName holds the name for Everest operator. EverestOperatorName = "everest-operator" + + // EverestAccountsConfigName is the name of the ConfigMap that holds account data. + EverestAccountsConfigName = "everest-accounts" ) diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 94c6da5df..d58e40c0f 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -36,9 +36,6 @@ import ( type AccountCapability string const ( - // AccountsConfigMapName is the name of the ConfigMap that holds account information. - AccountsConfigMapName = "everest-accounts" - // AccountCapabilityLogin represents capability to create UI session tokens. AccountCapabilityLogin AccountCapability = "login" // AccountCapabilityLogin represents capability to generate API auth tokens. @@ -262,7 +259,7 @@ func (a *accounts) setAccounts( } cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: AccountsConfigMapName, + Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, BinaryData: map[string][]byte{ @@ -279,7 +276,7 @@ func (a *accounts) setAccounts( } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: AccountsConfigMapName, + Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, Data: map[string][]byte{ @@ -293,7 +290,7 @@ func (a *accounts) setAccounts( } func (a *accounts) listAllUsers(ctx context.Context) (map[string]User, error) { - cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, AccountsConfigMapName) + cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return nil, err } @@ -309,7 +306,7 @@ func (a *accounts) listAllUsers(ctx context.Context) (map[string]User, error) { } func (a *accounts) listAllPasswords(ctx context.Context) (map[string]Password, error) { - secret, err := a.k.GetSecret(ctx, common.SystemNamespace, AccountsConfigMapName) + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return nil, err } diff --git a/pkg/kubernetes/client/accounts/accounts_test.go b/pkg/kubernetes/client/accounts/kubernetes_test.go similarity index 96% rename from pkg/kubernetes/client/accounts/accounts_test.go rename to pkg/kubernetes/client/accounts/kubernetes_test.go index 84286b99e..ef0828b4c 100644 --- a/pkg/kubernetes/client/accounts/accounts_test.go +++ b/pkg/kubernetes/client/accounts/kubernetes_test.go @@ -31,7 +31,7 @@ func TestAccounts(t *testing.T) { ConfigMaps(common.SystemNamespace). Create(ctx, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: AccountsConfigMapName, + Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, }, metav1.CreateOptions{}) @@ -41,7 +41,7 @@ func TestAccounts(t *testing.T) { Secrets(common.SystemNamespace). Create(ctx, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: AccountsConfigMapName, + Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, }, metav1.CreateOptions{}) From fec3548a00d72477164a073d55afb61efe414da7 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 13:44:40 +0530 Subject: [PATCH 004/103] initialize empty secret & cm Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 71bd6ab01..e1798d429 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -133,3 +133,16 @@ spec: ports: - protocol: TCP port: 8080 +--- +apiVersion: v1 +kind: Secret +metadata: + name: everest-accounts + namespace: everest-system +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: everest-accounts + namespace: everest-system +--- From 789d3287f6f3396443e6ecdb7657c36fd33a4265 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 14:35:59 +0530 Subject: [PATCH 005/103] add accounts create cmd Signed-off-by: Mayank Shah --- commands/accounts.go | 34 +++++++++ commands/accounts/create.go | 75 +++++++++++++++++++ commands/root.go | 1 + pkg/accounts/accounts.go | 39 ++++++++++ pkg/kubernetes/accounts.go | 2 +- .../client/accounts/kubernetes_test.go | 5 +- pkg/kubernetes/client/kubeclient_interface.go | 3 + .../client/mock_kube_client_connector.go | 21 ++++++ pkg/kubernetes/kubernetes_interface.go | 2 +- pkg/kubernetes/mock_kubernetes_connector.go | 10 +-- 10 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 commands/accounts.go create mode 100644 commands/accounts/create.go create mode 100644 pkg/accounts/accounts.go diff --git a/commands/accounts.go b/commands/accounts.go new file mode 100644 index 000000000..225e0c8ad --- /dev/null +++ b/commands/accounts.go @@ -0,0 +1,34 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package commands ... +package commands + +import ( + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/percona/everest/commands/accounts" +) + +func newAccountsCmd(l *zap.SugaredLogger) *cobra.Command { + cmd := &cobra.Command{ + Use: "accounts", + } + + cmd.AddCommand(accounts.NewCreateCmd(l)) + + return cmd +} diff --git a/commands/accounts/create.go b/commands/accounts/create.go new file mode 100644 index 000000000..10d66271b --- /dev/null +++ b/commands/accounts/create.go @@ -0,0 +1,75 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accounts holds commands for accounts command. +package accounts + +import ( + "context" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/percona/everest/pkg/accounts" +) + +type CreateConfig struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + KubeconfigPath string `mapstructure:"kubeconfig"` +} + +// NewCreateCmd returns a new create command. +func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Example: "everestctl accounts create --username user1 --password $USER_PASS", + Run: func(cmd *cobra.Command, args []string) { //nolint:revive + initCreateViperFlags(cmd) + c := &CreateConfig{} + err := viper.Unmarshal(c) + if err != nil { + os.Exit(1) + } + + cli, err := accounts.NewCLI(c.KubeconfigPath, l) + if err != nil { + l.Error(err) + os.Exit(1) + } + + if err := cli.Create(context.Background(), c.Username, c.Password); err != nil { + l.Error(err) + os.Exit(1) + } + }, + } + initCreateFlags(cmd) + return cmd +} + +func initCreateFlags(cmd *cobra.Command) { + cmd.Flags().StringP("username", "u", "", "Username of the account") + cmd.Flags().StringP("password", "p", "", "Password of the account") + cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") +} + +func initCreateViperFlags(cmd *cobra.Command) { + viper.BindPFlag("username", cmd.Flags().Lookup("username")) + viper.BindPFlag("password", cmd.Flags().Lookup("password")) + viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec +} diff --git a/commands/root.go b/commands/root.go index da80b79e4..6ad687ea3 100644 --- a/commands/root.go +++ b/commands/root.go @@ -41,6 +41,7 @@ func NewRootCmd(l *zap.SugaredLogger) *cobra.Command { rootCmd.AddCommand(newVersionCmd(l)) rootCmd.AddCommand(newUpgradeCmd(l)) rootCmd.AddCommand(newUninstallCmd(l)) + rootCmd.AddCommand(newAccountsCmd(l)) return rootCmd } diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go new file mode 100644 index 000000000..4ae3d664b --- /dev/null +++ b/pkg/accounts/accounts.go @@ -0,0 +1,39 @@ +package accounts + +import ( + "context" + "errors" + "net/url" + + "go.uber.org/zap" + + "github.com/percona/everest/pkg/kubernetes" +) + +type CLI struct { + kubeClient *kubernetes.Kubernetes + l *zap.SugaredLogger +} + +// NewCLI creates a new CLI for running accounts commands. +func NewCLI(kubeConfigPath string, l *zap.SugaredLogger) (*CLI, error) { + cli := &CLI{ + l: l.With("comoent", "accounts"), + } + k, err := kubernetes.New(kubeConfigPath, l) + if err != nil { + var u *url.Error + if errors.As(err, &u) { + cli.l.Error("Could not connect to Kubernetes. " + + "Make sure Kubernetes is running and is accessible from this computer/server.") + } + return nil, err + } + cli.kubeClient = k + return cli, nil +} + +// Create a new user account. +func (c *CLI) Create(ctx context.Context, username, password string) error { + return c.kubeClient.Accounts().Create(ctx, username, password) +} diff --git a/pkg/kubernetes/accounts.go b/pkg/kubernetes/accounts.go index b8320defa..5b8c10729 100644 --- a/pkg/kubernetes/accounts.go +++ b/pkg/kubernetes/accounts.go @@ -32,6 +32,6 @@ type Accounts interface { } // Accounts returns a new client for managing everest user accounts. -func (c *Kubernetes) Accounts(ctx context.Context) Accounts { +func (c *Kubernetes) Accounts() Accounts { return accounts.New(c.client) } diff --git a/pkg/kubernetes/client/accounts/kubernetes_test.go b/pkg/kubernetes/client/accounts/kubernetes_test.go index ef0828b4c..32a27af92 100644 --- a/pkg/kubernetes/client/accounts/kubernetes_test.go +++ b/pkg/kubernetes/client/accounts/kubernetes_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" - "github.com/percona/everest/pkg/common" - "github.com/percona/everest/pkg/kubernetes/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes/client" ) func TestAccounts(t *testing.T) { diff --git a/pkg/kubernetes/client/kubeclient_interface.go b/pkg/kubernetes/client/kubeclient_interface.go index 9177c3e45..064581320 100644 --- a/pkg/kubernetes/client/kubeclient_interface.go +++ b/pkg/kubernetes/client/kubeclient_interface.go @@ -20,6 +20,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/rest" @@ -46,6 +47,8 @@ type KubeClientConnector interface { UpdateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) // Config returns restConfig to the pkg/kubernetes.Kubernetes client. Config() *rest.Config + // Clientset returns the k8s clientset. + Clientset() kubernetes.Interface // ClusterName returns the name of the k8s cluster. ClusterName() string // Namespace returns the namespace of the k8s cluster. diff --git a/pkg/kubernetes/client/mock_kube_client_connector.go b/pkg/kubernetes/client/mock_kube_client_connector.go index 079f4e7f6..e902c751a 100644 --- a/pkg/kubernetes/client/mock_kube_client_connector.go +++ b/pkg/kubernetes/client/mock_kube_client_connector.go @@ -21,6 +21,7 @@ import ( schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" version "k8s.io/apimachinery/pkg/version" + kubernetes "k8s.io/client-go/kubernetes" rest "k8s.io/client-go/rest" v1alpha1 "github.com/percona/everest-operator/api/v1alpha1" @@ -85,6 +86,26 @@ func (_m *MockKubeClientConnector) ApplyObject(obj runtime.Object) error { return r0 } +// Clientset provides a mock function with given fields: +func (_m *MockKubeClientConnector) Clientset() kubernetes.Interface { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Clientset") + } + + var r0 kubernetes.Interface + if rf, ok := ret.Get(0).(func() kubernetes.Interface); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(kubernetes.Interface) + } + } + + return r0 +} + // ClusterName provides a mock function with given fields: func (_m *MockKubeClientConnector) ClusterName() string { ret := _m.Called() diff --git a/pkg/kubernetes/kubernetes_interface.go b/pkg/kubernetes/kubernetes_interface.go index 8b2cd7ec7..055123f4e 100644 --- a/pkg/kubernetes/kubernetes_interface.go +++ b/pkg/kubernetes/kubernetes_interface.go @@ -21,7 +21,7 @@ import ( // KubernetesConnector ... type KubernetesConnector interface { // Accounts returns a new client for managing everest user accounts. - Accounts(ctx context.Context) Accounts + Accounts() Accounts // GetDeployment returns k8s deployment by provided name and namespace. GetDeployment(ctx context.Context, name, namespace string) (*appsv1.Deployment, error) // UpdateDeployment updates a deployment and returns the updated object. diff --git a/pkg/kubernetes/mock_kubernetes_connector.go b/pkg/kubernetes/mock_kubernetes_connector.go index eb5067144..10d024506 100644 --- a/pkg/kubernetes/mock_kubernetes_connector.go +++ b/pkg/kubernetes/mock_kubernetes_connector.go @@ -24,17 +24,17 @@ type MockKubernetesConnector struct { mock.Mock } -// Accounts provides a mock function with given fields: ctx -func (_m *MockKubernetesConnector) Accounts(ctx context.Context) Accounts { - ret := _m.Called(ctx) +// Accounts provides a mock function with given fields: +func (_m *MockKubernetesConnector) Accounts() Accounts { + ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Accounts") } var r0 Accounts - if rf, ok := ret.Get(0).(func(context.Context) Accounts); ok { - r0 = rf(ctx) + if rf, ok := ret.Get(0).(func() Accounts); ok { + r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(Accounts) From d07875659bbbbe697f186e8fd8d86cc776d98380 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 15:48:04 +0530 Subject: [PATCH 006/103] add list options Signed-off-by: Mayank Shah --- commands/accounts.go | 1 + commands/accounts/create.go | 12 +++---- commands/accounts/list.go | 69 +++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 +++ pkg/accounts/accounts.go | 60 +++++++++++++++++++++++++++++++- 6 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 commands/accounts/list.go diff --git a/commands/accounts.go b/commands/accounts.go index 225e0c8ad..12c597763 100644 --- a/commands/accounts.go +++ b/commands/accounts.go @@ -29,6 +29,7 @@ func newAccountsCmd(l *zap.SugaredLogger) *cobra.Command { } cmd.AddCommand(accounts.NewCreateCmd(l)) + cmd.AddCommand(accounts.NewListCmd(l)) return cmd } diff --git a/commands/accounts/create.go b/commands/accounts/create.go index 10d66271b..56aff49f0 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -27,7 +27,7 @@ import ( "github.com/percona/everest/pkg/accounts" ) -type CreateConfig struct { +type AccountConfig struct { Username string `mapstructure:"username"` Password string `mapstructure:"password"` KubeconfigPath string `mapstructure:"kubeconfig"` @@ -39,8 +39,8 @@ func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { Use: "create", Example: "everestctl accounts create --username user1 --password $USER_PASS", Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initCreateViperFlags(cmd) - c := &CreateConfig{} + initAccountViperFlags(cmd) + c := &AccountConfig{} err := viper.Unmarshal(c) if err != nil { os.Exit(1) @@ -58,17 +58,17 @@ func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { } }, } - initCreateFlags(cmd) + initAccountFlags(cmd) return cmd } -func initCreateFlags(cmd *cobra.Command) { +func initAccountFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") cmd.Flags().StringP("password", "p", "", "Password of the account") cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") } -func initCreateViperFlags(cmd *cobra.Command) { +func initAccountViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) viper.BindPFlag("password", cmd.Flags().Lookup("password")) viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec diff --git a/commands/accounts/list.go b/commands/accounts/list.go new file mode 100644 index 000000000..e55930914 --- /dev/null +++ b/commands/accounts/list.go @@ -0,0 +1,69 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accounts holds commands for accounts command. +package accounts + +import ( + "context" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/percona/everest/pkg/accounts" +) + +// NewListCmd returns a new list command. +func NewListCmd(l *zap.SugaredLogger) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Example: "everestctl accounts list", + Run: func(cmd *cobra.Command, args []string) { //nolint:revive + initListViperFlags(cmd) + o := &accounts.ListOptions{} + err := viper.Unmarshal(o) + if err != nil { + os.Exit(1) + } + + cli, err := accounts.NewCLI(o.KubeconfigPath, l) + if err != nil { + l.Error(err) + os.Exit(1) + } + + if err := cli.List(context.Background(), o); err != nil { + l.Error(err) + os.Exit(1) + } + }, + } + initListFlags(cmd) + return cmd +} + +func initListFlags(cmd *cobra.Command) { + cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") + cmd.Flags().Bool("no-headers", false, "If set, hide table headers") + cmd.Flags().StringSlice("columns", nil, "Comma-separated list of column names to display") +} + +func initListViperFlags(cmd *cobra.Command) { + viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) + viper.BindPFlag("no-headers", cmd.Flags().Lookup("no-headers")) + viper.BindPFlag("columns", cmd.Flags().Lookup("columns")) +} diff --git a/go.mod b/go.mod index b21786d24..fc9026760 100644 --- a/go.mod +++ b/go.mod @@ -122,6 +122,7 @@ require ( github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rodaine/table v1.2.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/go.sum b/go.sum index bd8c642bf..5fba5332f 100644 --- a/go.sum +++ b/go.sum @@ -469,6 +469,7 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -605,6 +606,9 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= +github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index 4ae3d664b..5c0e05ce0 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -3,11 +3,14 @@ package accounts import ( "context" "errors" + "fmt" "net/url" + "strings" "go.uber.org/zap" "github.com/percona/everest/pkg/kubernetes" + "github.com/rodaine/table" ) type CLI struct { @@ -35,5 +38,60 @@ func NewCLI(kubeConfigPath string, l *zap.SugaredLogger) (*CLI, error) { // Create a new user account. func (c *CLI) Create(ctx context.Context, username, password string) error { - return c.kubeClient.Accounts().Create(ctx, username, password) + if err := c.kubeClient.Accounts().Create(ctx, username, password); err != nil { + return err + } + c.l.Infof("User '%s' has been added", username) + return nil +} + +type ListOptions struct { + KubeconfigPath string `mapstructure:"kubeconfig"` + NoHeaders bool `mapstructure:"no-headers"` + Columns []string `mapstructure:"columns"` +} + +// tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { +// return strings.ToUpper(fmt.Sprintf(format, vals...)) +// }) + +// List all user accounts in the system. +func (c *CLI) List(ctx context.Context, opts *ListOptions) error { + accounts, err := c.kubeClient.Accounts().List(ctx) + if err != nil { + return err + } + if opts == nil { + opts = &ListOptions{} + } + headings := []interface{}{"user", "capabilities"} + if len(opts.Columns) > 0 { + headings = []interface{}{} + for _, col := range opts.Columns { + headings = append(headings, col) + } + } + tbl := table.New(headings...) + tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { + if opts.NoHeaders { + return "" + } + return strings.ToUpper(fmt.Sprintf(format, vals...)) + }) + for _, account := range accounts { + row := []any{} + for _, heading := range headings { + switch heading { + case "user": + row = append(row, account.ID) + case "capabilities": + row = append(row, account.Capabilities) + case "enabled": + row = append(row, account.Enabled) + } + } + tbl.AddRow(row...) + } + tbl.Print() + return nil } From bf5f0ddcb4e2d180b3e3be777abf9acbd39ce90a Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 15:48:59 +0530 Subject: [PATCH 007/103] refactor Signed-off-by: Mayank Shah --- pkg/accounts/accounts.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index 5c0e05ce0..2bb615156 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -57,10 +57,6 @@ type ListOptions struct { // List all user accounts in the system. func (c *CLI) List(ctx context.Context, opts *ListOptions) error { - accounts, err := c.kubeClient.Accounts().List(ctx) - if err != nil { - return err - } if opts == nil { opts = &ListOptions{} } @@ -78,6 +74,10 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { } return strings.ToUpper(fmt.Sprintf(format, vals...)) }) + accounts, err := c.kubeClient.Accounts().List(ctx) + if err != nil { + return err + } for _, account := range accounts { row := []any{} for _, heading := range headings { From b898f922f3fc1f75e0e368fbf30a578fc777b7a3 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 16:44:48 +0530 Subject: [PATCH 008/103] add delete cmd Signed-off-by: Mayank Shah --- commands/accounts.go | 1 + commands/accounts/create.go | 27 ++++----- commands/accounts/delete.go | 68 ++++++++++++++++++++++ pkg/accounts/accounts.go | 58 ++++++++++++++++-- pkg/kubernetes/accounts.go | 1 + pkg/kubernetes/client/accounts/accounts.go | 25 +++++--- 6 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 commands/accounts/delete.go diff --git a/commands/accounts.go b/commands/accounts.go index 12c597763..880e74d10 100644 --- a/commands/accounts.go +++ b/commands/accounts.go @@ -30,6 +30,7 @@ func newAccountsCmd(l *zap.SugaredLogger) *cobra.Command { cmd.AddCommand(accounts.NewCreateCmd(l)) cmd.AddCommand(accounts.NewListCmd(l)) + cmd.AddCommand(accounts.NewDeleteCmd(l)) return cmd } diff --git a/commands/accounts/create.go b/commands/accounts/create.go index 56aff49f0..3fd7a1f72 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -27,48 +27,41 @@ import ( "github.com/percona/everest/pkg/accounts" ) -type AccountConfig struct { - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - KubeconfigPath string `mapstructure:"kubeconfig"` -} - // NewCreateCmd returns a new create command. func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { cmd := &cobra.Command{ Use: "create", Example: "everestctl accounts create --username user1 --password $USER_PASS", Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initAccountViperFlags(cmd) - c := &AccountConfig{} - err := viper.Unmarshal(c) - if err != nil { - os.Exit(1) - } + initDeleteViperFlags(cmd) + + kubeconfigPath := viper.GetString("kubeconfig") + username := viper.GetString("username") + password := viper.GetString("password") - cli, err := accounts.NewCLI(c.KubeconfigPath, l) + cli, err := accounts.NewCLI(kubeconfigPath, l) if err != nil { l.Error(err) os.Exit(1) } - if err := cli.Create(context.Background(), c.Username, c.Password); err != nil { + if err := cli.Create(context.Background(), username, password); err != nil { l.Error(err) os.Exit(1) } }, } - initAccountFlags(cmd) + initDeleteFlags(cmd) return cmd } -func initAccountFlags(cmd *cobra.Command) { +func initDeleteFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") cmd.Flags().StringP("password", "p", "", "Password of the account") cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") } -func initAccountViperFlags(cmd *cobra.Command) { +func initDeleteViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) viper.BindPFlag("password", cmd.Flags().Lookup("password")) viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go new file mode 100644 index 000000000..a621d5db2 --- /dev/null +++ b/commands/accounts/delete.go @@ -0,0 +1,68 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accounts holds commands for accounts command. +package accounts + +import ( + "context" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/percona/everest/pkg/accounts" +) + +// NewDeleteCmd returns a new delete command. +func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Example: "everestctl accounts delete --username user1 --password $USER_PASS", + Run: func(cmd *cobra.Command, args []string) { //nolint:revive + initCreateViperFlags(cmd) + + kubeconfigPath := viper.GetString("kubeconfig") + username := viper.GetString("username") + password := viper.GetString("password") + + cli, err := accounts.NewCLI(kubeconfigPath, l) + if err != nil { + l.Error(err) + os.Exit(1) + } + + if err := cli.Delete(context.Background(), username, password); err != nil { + l.Error(err) + os.Exit(1) + } + }, + } + initCreateFlags(cmd) + return cmd +} + +func initCreateFlags(cmd *cobra.Command) { + cmd.Flags().StringP("username", "u", "", "Username of the account") + cmd.Flags().StringP("password", "p", "", "Password of the account") + cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") +} + +func initCreateViperFlags(cmd *cobra.Command) { + viper.BindPFlag("username", cmd.Flags().Lookup("username")) + viper.BindPFlag("password", cmd.Flags().Lookup("password")) + viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec +} diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index 2bb615156..29b260dca 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -9,6 +9,7 @@ import ( "go.uber.org/zap" + "github.com/AlecAivazis/survey/v2" "github.com/percona/everest/pkg/kubernetes" "github.com/rodaine/table" ) @@ -21,7 +22,7 @@ type CLI struct { // NewCLI creates a new CLI for running accounts commands. func NewCLI(kubeConfigPath string, l *zap.SugaredLogger) (*CLI, error) { cli := &CLI{ - l: l.With("comoent", "accounts"), + l: l.With("component", "accounts"), } k, err := kubernetes.New(kubeConfigPath, l) if err != nil { @@ -36,25 +37,70 @@ func NewCLI(kubeConfigPath string, l *zap.SugaredLogger) (*CLI, error) { return cli, nil } +func (c *CLI) runCredentialsWizard(username, password *string) error { + if *username == "" { + pUsername := survey.Input{ + Message: "Enter username", + } + if err := survey.AskOne(&pUsername, username); err != nil { + return err + } + } + if *password == "" { + pPassword := survey.Password{ + Message: "Enter password", + } + if err := survey.AskOne(&pPassword, password); err != nil { + return err + } + } + return nil +} + // Create a new user account. func (c *CLI) Create(ctx context.Context, username, password string) error { + if err := c.runCredentialsWizard(&username, &password); err != nil { + return err + } + if username == "" { + return errors.New("username is required") + } if err := c.kubeClient.Accounts().Create(ctx, username, password); err != nil { return err } - c.l.Infof("User '%s' has been added", username) + c.l.Infof("User '%s' has been created", username) return nil } +// Delete an existing user account. +func (c *CLI) Delete(ctx context.Context, username, password string) error { + if err := c.runCredentialsWizard(&username, &password); err != nil { + return err + } + if username == "" { + return errors.New("username is required") + } + user, err := c.kubeClient.Accounts().Get(ctx, username) + if err != nil { + return err + } + computedHash, err := c.kubeClient.Accounts().ComputePasswordHash(password) + if err != nil { + return err + } + if computedHash != user.PasswordHash { + return errors.New("incorrect password entered") + } + c.l.Infof("User '%s' has been deleted", username) + return c.kubeClient.Accounts().Delete(ctx, username) +} + type ListOptions struct { KubeconfigPath string `mapstructure:"kubeconfig"` NoHeaders bool `mapstructure:"no-headers"` Columns []string `mapstructure:"columns"` } -// tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { -// return strings.ToUpper(fmt.Sprintf(format, vals...)) -// }) - // List all user accounts in the system. func (c *CLI) List(ctx context.Context, opts *ListOptions) error { if opts == nil { diff --git a/pkg/kubernetes/accounts.go b/pkg/kubernetes/accounts.go index 5b8c10729..ffe065104 100644 --- a/pkg/kubernetes/accounts.go +++ b/pkg/kubernetes/accounts.go @@ -29,6 +29,7 @@ type Accounts interface { List(ctx context.Context) ([]accounts.Account, error) Delete(ctx context.Context, username string) error Update(ctx context.Context, username, password string) error + ComputePasswordHash(password string) (string, error) } // Accounts returns a new client for managing everest user accounts. diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index d58e40c0f..7f8b246b6 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -124,11 +124,10 @@ func (a *accounts) Create(ctx context.Context, username, password string) error if _, found := users[username]; found { return errors.New("user already exists") } - salt, err := a.salt() + hash, err := a.computePasswordHash(password) if err != nil { - return errors.Join(err, errors.New("failed to get salt")) + return err } - hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) acc := Account{ ID: username, User: User{ @@ -187,18 +186,30 @@ func (a *accounts) Update(ctx context.Context, username, password string) error if err != nil { return err } - salt, err := a.salt() + hash, err := a.computePasswordHash(password) if err != nil { - return errors.Join(err, errors.New("failed to get salt")) + return err } - hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) passwords[username] = Password{ - PasswordHash: string(hash), + PasswordHash: hash, PasswordMTime: time.Now().Format(time.RFC3339), } return a.setAccounts(ctx, mergeUserPassToAccounts(users, passwords), true) } +func (a *accounts) ComputePasswordHash(password string) (string, error) { + return a.computePasswordHash(password) +} + +func (a *accounts) computePasswordHash(password string) (string, error) { + salt, err := a.salt() + if err != nil { + return "", errors.Join(err, errors.New("failed to get salt")) + } + hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + return string(hash), nil +} + func mergeUserPassToAccounts(users map[string]User, passwords map[string]Password) []Account { var accounts []Account for name, user := range users { From a88cdfbc5414b1f1d587cc15d54fb8e03df9bb44 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 16:45:08 +0530 Subject: [PATCH 009/103] ran format Signed-off-by: Mayank Shah --- pkg/accounts/accounts.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index 29b260dca..79a3feec3 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -7,11 +7,11 @@ import ( "net/url" "strings" + "github.com/AlecAivazis/survey/v2" + "github.com/rodaine/table" "go.uber.org/zap" - "github.com/AlecAivazis/survey/v2" "github.com/percona/everest/pkg/kubernetes" - "github.com/rodaine/table" ) type CLI struct { From f02bc44640af5b2652f4d7994b02889974c5516e Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 16:53:43 +0530 Subject: [PATCH 010/103] add some refactoring for list Signed-off-by: Mayank Shah --- pkg/accounts/accounts.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index 79a3feec3..97f6fc1e3 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -12,6 +12,7 @@ import ( "go.uber.org/zap" "github.com/percona/everest/pkg/kubernetes" + accountsapi "github.com/percona/everest/pkg/kubernetes/client/accounts" ) type CLI struct { @@ -101,30 +102,41 @@ type ListOptions struct { Columns []string `mapstructure:"columns"` } +const ( + columnUser = "user" + columnCapabilities = "capabilities" + columnEnabled = "enabled" +) + // List all user accounts in the system. func (c *CLI) List(ctx context.Context, opts *ListOptions) error { if opts == nil { opts = &ListOptions{} } - headings := []interface{}{"user", "capabilities"} + // Prepare table headings. + headings := []interface{}{columnUser, columnCapabilities, columnEnabled} if len(opts.Columns) > 0 { headings = []interface{}{} for _, col := range opts.Columns { headings = append(headings, col) } } + // Prepare table header. tbl := table.New(headings...) tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { - if opts.NoHeaders { + if opts.NoHeaders { // Skip printing headers. return "" } + // Otherwise print in all caps. return strings.ToUpper(fmt.Sprintf(format, vals...)) }) accounts, err := c.kubeClient.Accounts().List(ctx) if err != nil { return err } - for _, account := range accounts { + + // Return a table row for the given account. + row := func(account *accountsapi.Account) []any { row := []any{} for _, heading := range headings { switch heading { @@ -136,7 +148,10 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { row = append(row, account.Enabled) } } - tbl.AddRow(row...) + return row + } + for _, account := range accounts { + tbl.AddRow(row(&account)...) } tbl.Print() return nil From 0fb662aa00c20d6fc504a22b5b3e46b3ca904690 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Fri, 3 May 2024 17:29:26 +0530 Subject: [PATCH 011/103] fix linting errors Signed-off-by: Mayank Shah --- commands/accounts/create.go | 6 +- commands/accounts/delete.go | 6 +- commands/accounts/list.go | 6 +- pkg/accounts/accounts.go | 24 ++++++- pkg/kubernetes/accounts.go | 6 +- pkg/kubernetes/client/accounts/accounts.go | 69 +++++++++++-------- .../client/accounts/kubernetes_test.go | 17 +++-- 7 files changed, 89 insertions(+), 45 deletions(-) diff --git a/commands/accounts/create.go b/commands/accounts/create.go index 3fd7a1f72..3f4df0b6f 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -14,6 +14,8 @@ // limitations under the License. // Package accounts holds commands for accounts command. +// +//nolint:dupl package accounts import ( @@ -62,7 +64,7 @@ func initDeleteFlags(cmd *cobra.Command) { } func initDeleteViperFlags(cmd *cobra.Command) { - viper.BindPFlag("username", cmd.Flags().Lookup("username")) - viper.BindPFlag("password", cmd.Flags().Lookup("password")) + viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec + viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec } diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go index a621d5db2..eaaf2c57c 100644 --- a/commands/accounts/delete.go +++ b/commands/accounts/delete.go @@ -14,6 +14,8 @@ // limitations under the License. // Package accounts holds commands for accounts command. +// +//nolint:dupl package accounts import ( @@ -62,7 +64,7 @@ func initCreateFlags(cmd *cobra.Command) { } func initCreateViperFlags(cmd *cobra.Command) { - viper.BindPFlag("username", cmd.Flags().Lookup("username")) - viper.BindPFlag("password", cmd.Flags().Lookup("password")) + viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec + viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec } diff --git a/commands/accounts/list.go b/commands/accounts/list.go index e55930914..0ccf80747 100644 --- a/commands/accounts/list.go +++ b/commands/accounts/list.go @@ -63,7 +63,7 @@ func initListFlags(cmd *cobra.Command) { } func initListViperFlags(cmd *cobra.Command) { - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) - viper.BindPFlag("no-headers", cmd.Flags().Lookup("no-headers")) - viper.BindPFlag("columns", cmd.Flags().Lookup("columns")) + viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec + viper.BindPFlag("no-headers", cmd.Flags().Lookup("no-headers")) //nolint:errcheck,gosec + viper.BindPFlag("columns", cmd.Flags().Lookup("columns")) //nolint:errcheck,gosec } diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index 97f6fc1e3..f5df844c5 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -1,3 +1,19 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accounts holds commands for accounts command. package accounts import ( @@ -15,6 +31,7 @@ import ( accountsapi "github.com/percona/everest/pkg/kubernetes/client/accounts" ) +// CLI provides functionality for managing user accounts via the CLI. type CLI struct { kubeClient *kubernetes.Kubernetes l *zap.SugaredLogger @@ -85,7 +102,7 @@ func (c *CLI) Delete(ctx context.Context, username, password string) error { if err != nil { return err } - computedHash, err := c.kubeClient.Accounts().ComputePasswordHash(password) + computedHash, err := c.kubeClient.Accounts().ComputePasswordHash(ctx, password) if err != nil { return err } @@ -96,6 +113,7 @@ func (c *CLI) Delete(ctx context.Context, username, password string) error { return c.kubeClient.Accounts().Delete(ctx, username) } +// ListOptions holds options for listing user accounts. type ListOptions struct { KubeconfigPath string `mapstructure:"kubeconfig"` NoHeaders bool `mapstructure:"no-headers"` @@ -136,7 +154,7 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { } // Return a table row for the given account. - row := func(account *accountsapi.Account) []any { + row := func(account accountsapi.Account) []any { row := []any{} for _, heading := range headings { switch heading { @@ -151,7 +169,7 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { return row } for _, account := range accounts { - tbl.AddRow(row(&account)...) + tbl.AddRow(row(account)...) } tbl.Print() return nil diff --git a/pkg/kubernetes/accounts.go b/pkg/kubernetes/accounts.go index ffe065104..086f2035c 100644 --- a/pkg/kubernetes/accounts.go +++ b/pkg/kubernetes/accounts.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package client ... +// Package kubernetes ... package kubernetes import ( @@ -29,10 +29,12 @@ type Accounts interface { List(ctx context.Context) ([]accounts.Account, error) Delete(ctx context.Context, username string) error Update(ctx context.Context, username, password string) error - ComputePasswordHash(password string) (string, error) + ComputePasswordHash(ctx context.Context, password string) (string, error) } // Accounts returns a new client for managing everest user accounts. +// +//nolint:ireturn,stylecheck func (c *Kubernetes) Accounts() Accounts { return accounts.New(c.client) } diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 7f8b246b6..a61e28cb2 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -33,13 +33,14 @@ import ( "github.com/percona/everest/pkg/kubernetes/client" ) +// AccountCapability represents a capability of an account. type AccountCapability string const ( // AccountCapabilityLogin represents capability to create UI session tokens. AccountCapabilityLogin AccountCapability = "login" - // AccountCapabilityLogin represents capability to generate API auth tokens. - AccountCapabilityApiKey AccountCapability = "apiKey" + // AccountCapabilityAPIKey represents capability to generate API auth tokens. + AccountCapabilityAPIKey AccountCapability = "apiKey" usersFile = "users.yaml" passwordFile = "passwords.yaml" @@ -67,17 +68,18 @@ type Account struct { Password } -type accounts struct { +// Client provides functionality for managing user accounts on Kubernetes. +type Client struct { k client.KubeClientConnector } // New returns a new Kubernetes based account manager for Everest. -func New(k client.KubeClientConnector) *accounts { - return &accounts{k: k} +func New(k client.KubeClientConnector) *Client { + return &Client{k: k} } // Get returns an account by username. -func (a *accounts) Get(ctx context.Context, username string) (*Account, error) { +func (a *Client) Get(ctx context.Context, username string) (*Account, error) { users, err := a.listAllUsers(ctx) if err != nil { return nil, err @@ -102,7 +104,7 @@ func (a *accounts) Get(ctx context.Context, username string) (*Account, error) { } // List returns a list of all accounts. -func (a *accounts) List(ctx context.Context) ([]Account, error) { +func (a *Client) List(ctx context.Context) ([]Account, error) { users, err := a.listAllUsers(ctx) if err != nil { return nil, err @@ -115,7 +117,7 @@ func (a *accounts) List(ctx context.Context) ([]Account, error) { } // Create a new user account. -func (a *accounts) Create(ctx context.Context, username, password string) error { +func (a *Client) Create(ctx context.Context, username, password string) error { // Check if this user exists? users, err := a.listAllUsers(ctx) if err != nil { @@ -124,7 +126,7 @@ func (a *accounts) Create(ctx context.Context, username, password string) error if _, found := users[username]; found { return errors.New("user already exists") } - hash, err := a.computePasswordHash(password) + hash, err := a.computePasswordHash(ctx, password) if err != nil { return err } @@ -135,15 +137,15 @@ func (a *accounts) Create(ctx context.Context, username, password string) error Capabilities: []AccountCapability{AccountCapabilityLogin}, // XX: for now we only support login }, Password: Password{ - PasswordHash: string(hash), + PasswordHash: hash, PasswordMTime: time.Now().Format(time.RFC3339), }, } return a.setAccounts(ctx, []Account{acc}, true) } -func (a *accounts) salt() ([]byte, error) { - ns, err := a.k.GetNamespace(context.Background(), common.SystemNamespace) +func (a *Client) salt(ctx context.Context) ([]byte, error) { + ns, err := a.k.GetNamespace(ctx, common.SystemNamespace) if err != nil { return nil, err } @@ -151,7 +153,7 @@ func (a *accounts) salt() ([]byte, error) { } // Delete an existing user account specified by username. -func (a *accounts) Delete(ctx context.Context, username string) error { +func (a *Client) Delete(ctx context.Context, username string) error { // Check if this user exists? users, err := a.listAllUsers(ctx) if err != nil { @@ -172,7 +174,7 @@ func (a *accounts) Delete(ctx context.Context, username string) error { } // Update an existing user account specified by username. -func (a *accounts) Update(ctx context.Context, username, password string) error { +func (a *Client) Update(ctx context.Context, username, password string) error { // Check if this user exists? users, err := a.listAllUsers(ctx) if err != nil { @@ -186,7 +188,7 @@ func (a *accounts) Update(ctx context.Context, username, password string) error if err != nil { return err } - hash, err := a.computePasswordHash(password) + hash, err := a.computePasswordHash(ctx, password) if err != nil { return err } @@ -197,12 +199,13 @@ func (a *accounts) Update(ctx context.Context, username, password string) error return a.setAccounts(ctx, mergeUserPassToAccounts(users, passwords), true) } -func (a *accounts) ComputePasswordHash(password string) (string, error) { - return a.computePasswordHash(password) +// ComputePasswordHash computes the password hash for a given password. +func (a *Client) ComputePasswordHash(ctx context.Context, password string) (string, error) { + return a.computePasswordHash(ctx, password) } -func (a *accounts) computePasswordHash(password string) (string, error) { - salt, err := a.salt() +func (a *Client) computePasswordHash(ctx context.Context, password string) (string, error) { + salt, err := a.salt(ctx) if err != nil { return "", errors.Join(err, errors.New("failed to get salt")) } @@ -211,7 +214,7 @@ func (a *accounts) computePasswordHash(password string) (string, error) { } func mergeUserPassToAccounts(users map[string]User, passwords map[string]Password) []Account { - var accounts []Account + accounts := make([]Account, 0, len(users)) for name, user := range users { pass, found := passwords[name] if !found { @@ -232,15 +235,15 @@ func mergeUserPassToAccounts(users map[string]User, passwords map[string]Passwor // Given a list of accounts, update the ConfigMap and Secret. // If patch is true, existing all existing accounts are preserved. // If patch is false, accounts are replaced with the new list. -func (a *accounts) setAccounts( +func (a *Client) setAccounts( ctx context.Context, accounts []Account, patch bool, ) error { var ( err error - users map[string]User = make(map[string]User) - passwords map[string]Password = make(map[string]Password) + users = make(map[string]User) + passwords = make(map[string]Password) ) if patch { // Get existing users and passwords. @@ -263,7 +266,16 @@ func (a *accounts) setAccounts( passwords[acc.ID] = acc.Password } } - // Update accounts ConfigMap. + if err := a.updateConfigMap(ctx, users); err != nil { + return err + } + if err := a.updateSecret(ctx, passwords); err != nil { + return err + } + return nil +} + +func (a *Client) updateConfigMap(ctx context.Context, users map[string]User) error { userB, err := yaml.Marshal(users) if err != nil { return err @@ -280,7 +292,10 @@ func (a *accounts) setAccounts( if _, err := a.k.UpdateConfigMap(ctx, cm); err != nil { return err } - // Update Accounts Secret. + return nil +} + +func (a *Client) updateSecret(ctx context.Context, passwords map[string]Password) error { passB, err := yaml.Marshal(passwords) if err != nil { return err @@ -300,7 +315,7 @@ func (a *accounts) setAccounts( return nil } -func (a *accounts) listAllUsers(ctx context.Context) (map[string]User, error) { +func (a *Client) listAllUsers(ctx context.Context) (map[string]User, error) { cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return nil, err @@ -316,7 +331,7 @@ func (a *accounts) listAllUsers(ctx context.Context) (map[string]User, error) { return users, nil } -func (a *accounts) listAllPasswords(ctx context.Context) (map[string]Password, error) { +func (a *Client) listAllPasswords(ctx context.Context) (map[string]Password, error) { secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return nil, err diff --git a/pkg/kubernetes/client/accounts/kubernetes_test.go b/pkg/kubernetes/client/accounts/kubernetes_test.go index 32a27af92..92721e39d 100644 --- a/pkg/kubernetes/client/accounts/kubernetes_test.go +++ b/pkg/kubernetes/client/accounts/kubernetes_test.go @@ -24,7 +24,8 @@ func TestAccounts(t *testing.T) { Namespaces(). Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: common.SystemNamespace}, - }, metav1.CreateOptions{}) + }, metav1.CreateOptions{}, + ) require.NoError(t, err) // Prepare configmap. _, err = c.Clientset(). @@ -35,7 +36,9 @@ func TestAccounts(t *testing.T) { Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, - }, metav1.CreateOptions{}) + }, metav1.CreateOptions{}, + ) + require.NoError(t, err) // Prepare secret. _, err = c.Clientset(). CoreV1(). @@ -45,14 +48,16 @@ func TestAccounts(t *testing.T) { Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, - }, metav1.CreateOptions{}) + }, metav1.CreateOptions{}, + ) + require.NoError(t, err) mgr := New(c) // Assert that initially there are no accounts. accounts, err := mgr.List(ctx) require.NoError(t, err) - assert.Len(t, accounts, 0) + assert.Empty(t, accounts) // Create user1 err = mgr.Create(ctx, "user1", "password") @@ -85,7 +90,7 @@ func TestAccounts(t *testing.T) { // Delete non-existing user. err = mgr.Delete(ctx, "not-existing") require.Error(t, err) - assert.ErrorIs(t, err, ErrAccountNotFound) + require.ErrorIs(t, err, ErrAccountNotFound) // Delete user1. err = mgr.Delete(ctx, "user1") @@ -94,5 +99,5 @@ func TestAccounts(t *testing.T) { // Check that no users exists. accounts, err = mgr.List(ctx) require.NoError(t, err) - assert.Len(t, accounts, 0) + assert.Empty(t, accounts) } From 014605087a40df6f0da4b360c54e28e0d597b752 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 10:25:46 +0530 Subject: [PATCH 012/103] refactor: --kubeconfig is a global flag Signed-off-by: Mayank Shah --- commands/accounts/create.go | 2 +- commands/accounts/delete.go | 2 +- commands/accounts/list.go | 2 +- commands/install.go | 1 - commands/root.go | 1 + commands/token/reset.go | 6 ------ commands/uninstall.go | 1 - commands/upgrade.go | 1 - pkg/kubernetes/client/accounts/accounts.go | 2 +- 9 files changed, 5 insertions(+), 13 deletions(-) diff --git a/commands/accounts/create.go b/commands/accounts/create.go index 3f4df0b6f..462b11697 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -60,11 +60,11 @@ func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { func initDeleteFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") cmd.Flags().StringP("password", "p", "", "Password of the account") - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") } func initDeleteViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec + viper.BindEnv("kubeconfig") //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec } diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go index eaaf2c57c..52cd3628e 100644 --- a/commands/accounts/delete.go +++ b/commands/accounts/delete.go @@ -60,11 +60,11 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { func initCreateFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") cmd.Flags().StringP("password", "p", "", "Password of the account") - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") } func initCreateViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec + viper.BindEnv("kubeconfig") //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec } diff --git a/commands/accounts/list.go b/commands/accounts/list.go index 0ccf80747..1e8971353 100644 --- a/commands/accounts/list.go +++ b/commands/accounts/list.go @@ -57,12 +57,12 @@ func NewListCmd(l *zap.SugaredLogger) *cobra.Command { } func initListFlags(cmd *cobra.Command) { - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") cmd.Flags().Bool("no-headers", false, "If set, hide table headers") cmd.Flags().StringSlice("columns", nil, "Comma-separated list of column names to display") } func initListViperFlags(cmd *cobra.Command) { + viper.BindEnv("kubeconfig") //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec viper.BindPFlag("no-headers", cmd.Flags().Lookup("no-headers")) //nolint:errcheck,gosec viper.BindPFlag("columns", cmd.Flags().Lookup("columns")) //nolint:errcheck,gosec diff --git a/commands/install.go b/commands/install.go index b49859d5d..d91d505a6 100644 --- a/commands/install.go +++ b/commands/install.go @@ -63,7 +63,6 @@ func newInstallCmd(l *zap.SugaredLogger) *cobra.Command { } func initInstallFlags(cmd *cobra.Command) { - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") cmd.Flags().String("namespaces", install.DefaultEverestNamespace, "Comma-separated namespaces list Percona Everest can manage") cmd.Flags().Bool("skip-wizard", false, "Skip installation wizard") cmd.Flags().String("version-metadata-url", "https://check.percona.com", "URL to retrieve version metadata information from") diff --git a/commands/root.go b/commands/root.go index 6ad687ea3..e111b2502 100644 --- a/commands/root.go +++ b/commands/root.go @@ -35,6 +35,7 @@ func NewRootCmd(l *zap.SugaredLogger) *cobra.Command { rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose mode") rootCmd.PersistentFlags().Bool("json", false, "Set output type to JSON") + rootCmd.PersistentFlags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") rootCmd.AddCommand(newInstallCmd(l)) rootCmd.AddCommand(newTokenCmd(l)) diff --git a/commands/token/reset.go b/commands/token/reset.go index 0e836a884..b77f5c0a9 100644 --- a/commands/token/reset.go +++ b/commands/token/reset.go @@ -57,15 +57,9 @@ func NewResetCmd(l *zap.SugaredLogger) *cobra.Command { }, } - initResetFlags(cmd) - return cmd } -func initResetFlags(cmd *cobra.Command) { - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") -} - func initResetViperFlags(cmd *cobra.Command) { viper.BindEnv("kubeconfig") //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec diff --git a/commands/uninstall.go b/commands/uninstall.go index 68715e34b..4fd679eea 100644 --- a/commands/uninstall.go +++ b/commands/uninstall.go @@ -57,7 +57,6 @@ func newUninstallCmd(l *zap.SugaredLogger) *cobra.Command { } func initUninstallFlags(cmd *cobra.Command) { - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") cmd.Flags().BoolP("assume-yes", "y", false, "Assume yes to all questions") cmd.Flags().BoolP("force", "f", false, "Force removal in case there are database clusters running") } diff --git a/commands/upgrade.go b/commands/upgrade.go index ceea93f4d..76b6c7d08 100644 --- a/commands/upgrade.go +++ b/commands/upgrade.go @@ -63,7 +63,6 @@ func newUpgradeCmd(l *zap.SugaredLogger) *cobra.Command { } func initUpgradeFlags(cmd *cobra.Command) { - cmd.Flags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") cmd.Flags().String("version-metadata-url", "https://check.percona.com", "URL to retrieve version metadata information from") } diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index a61e28cb2..b5f7dafa6 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -181,7 +181,7 @@ func (a *Client) Update(ctx context.Context, username, password string) error { return err } if _, found := users[username]; !found { - return errors.New("user does not exist") + return ErrAccountNotFound } // Update the password. passwords, err := a.listAllPasswords(ctx) From e19128dff73473c996c510e9a3c1c60146dda91c Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:35:26 +0530 Subject: [PATCH 013/103] create JWT secret Signed-off-by: Mayank Shah --- cmd/config/config.go | 2 ++ pkg/common/constants.go | 3 ++ pkg/install/install.go | 5 +++ pkg/kubernetes/gen.go | 2 +- pkg/kubernetes/jwt.go | 50 ++++++++++++++++++++++++++ pkg/kubernetes/kubernetes_interface.go | 5 +++ 6 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 pkg/kubernetes/jwt.go diff --git a/cmd/config/config.go b/cmd/config/config.go index 8f54ba74f..85d3f1d36 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -52,6 +52,8 @@ type EverestConfig struct { APIRequestsRateLimit int `default:"100" envconfig:"API_REQUESTS_RATE_LIMIT"` // VersionServiceURL contains the URL of the version service. VersionServiceURL string `default:"https://check.percona.com" envconfig:"VERSION_SERVICE_URL"` + // JWTSigningKey is the key used to sign JWT tokens. + JWTSigningKey string `envconfig:"JWT_SIGNING_KEY"` } // ParseConfig parses env vars and fills EverestConfig. diff --git a/pkg/common/constants.go b/pkg/common/constants.go index fa960cb18..e06f86b0b 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -38,4 +38,7 @@ const ( // EverestAccountsConfigName is the name of the ConfigMap that holds account data. EverestAccountsConfigName = "everest-accounts" + + // EverestJWTSecretName is the name of the secret that holds JWT secret. + EverestJWTSecretName = "everest-jwt" ) diff --git a/pkg/install/install.go b/pkg/install/install.go index 7bf0300e3..d6f5ef36b 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -366,6 +366,11 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er everestExists = true } + o.l.Info("Creating JWT Secret") + if err := o.kubeClient.CreateJWTSecret(ctx, !everestExists); err != nil { + return err + } + if !everestExists { o.l.Info(fmt.Sprintf("Deploying Everest to %s", common.SystemNamespace)) if err = o.kubeClient.InstallEverest(ctx, common.SystemNamespace, v); err != nil { diff --git a/pkg/kubernetes/gen.go b/pkg/kubernetes/gen.go index 12250d25f..44e84acaf 100644 --- a/pkg/kubernetes/gen.go +++ b/pkg/kubernetes/gen.go @@ -15,5 +15,5 @@ package kubernetes -//go:generate ../../bin/ifacemaker -f accounts.go -f deployment.go -f install_plan.go -f kubernetes.go -f operator.go -s Kubernetes -i KubernetesConnector -p kubernetes -o kubernetes_interface.go +//go:generate ../../bin/ifacemaker -f accounts.go -f deployment.go -f install_plan.go -f kubernetes.go -f operator.go -f jwt.go -s Kubernetes -i KubernetesConnector -p kubernetes -o kubernetes_interface.go //go:generate ../../bin/mockery --name=KubernetesConnector --case=snake --inpackage diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go new file mode 100644 index 000000000..9bf055d2f --- /dev/null +++ b/pkg/kubernetes/jwt.go @@ -0,0 +1,50 @@ +package kubernetes + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/percona/everest/pkg/common" +) + +// CreateJWTSecret creates a new secret with the JWT singing key. +// If `force` is set to true, the secret will be re-created with a new key. +func (k *Kubernetes) CreateJWTSecret(ctx context.Context, force bool) error { + if _, err := k.client.GetSecret(ctx, common.SystemNamespace, common.EverestJWTSecretName); client.IgnoreNotFound(err) != nil { + return err + } else if !force { + // Secret already exists, and we don't want to overwrite it. + return nil + } + token, err := genJWTToken() + if err != nil { + return errors.Join(err, errors.New("failed to generate JWT token")) + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.EverestJWTSecretName, + Namespace: common.SystemNamespace, + }, + Data: map[string][]byte{ + "signing_key": []byte(token), + }, + } + if _, err := k.client.CreateSecret(ctx, secret); err != nil { + return err + } + return nil +} + +func genJWTToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/pkg/kubernetes/kubernetes_interface.go b/pkg/kubernetes/kubernetes_interface.go index 055123f4e..de87a178f 100644 --- a/pkg/kubernetes/kubernetes_interface.go +++ b/pkg/kubernetes/kubernetes_interface.go @@ -21,6 +21,8 @@ import ( // KubernetesConnector ... type KubernetesConnector interface { // Accounts returns a new client for managing everest user accounts. + // + //nolint:ireturn,stylecheck Accounts() Accounts // GetDeployment returns k8s deployment by provided name and namespace. GetDeployment(ctx context.Context, name, namespace string) (*appsv1.Deployment, error) @@ -108,4 +110,7 @@ type KubernetesConnector interface { UpdateClusterRoleBinding(ctx context.Context, name string, namespaces []string) error // OperatorInstalledVersion returns the installed version of operator by name. OperatorInstalledVersion(ctx context.Context, namespace, name string) (*goversion.Version, error) + // CreateJWTSecret creates a new secret with the JWT singing key. + // If `force` is set to true, the secret will be re-created with a new key. + CreateJWTSecret(ctx context.Context, force bool) error } From 90a951a556b31ac5eb21ff9451fd49235c3b4605 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:39:03 +0530 Subject: [PATCH 014/103] ran make gen Signed-off-by: Mayank Shah --- pkg/kubernetes/jwt.go | 4 ++-- pkg/kubernetes/mock_kubernetes_connector.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go index 9bf055d2f..f42d99f62 100644 --- a/pkg/kubernetes/jwt.go +++ b/pkg/kubernetes/jwt.go @@ -8,7 +8,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/percona/everest/pkg/common" ) @@ -16,7 +16,7 @@ import ( // CreateJWTSecret creates a new secret with the JWT singing key. // If `force` is set to true, the secret will be re-created with a new key. func (k *Kubernetes) CreateJWTSecret(ctx context.Context, force bool) error { - if _, err := k.client.GetSecret(ctx, common.SystemNamespace, common.EverestJWTSecretName); client.IgnoreNotFound(err) != nil { + if _, err := k.client.GetSecret(ctx, common.SystemNamespace, common.EverestJWTSecretName); ctrlclient.IgnoreNotFound(err) != nil { return err } else if !force { // Secret already exists, and we don't want to overwrite it. diff --git a/pkg/kubernetes/mock_kubernetes_connector.go b/pkg/kubernetes/mock_kubernetes_connector.go index 10d024506..9ba455f1a 100644 --- a/pkg/kubernetes/mock_kubernetes_connector.go +++ b/pkg/kubernetes/mock_kubernetes_connector.go @@ -128,6 +128,24 @@ func (_m *MockKubernetesConnector) Config() *rest.Config { return r0 } +// CreateJWTSecret provides a mock function with given fields: ctx, force +func (_m *MockKubernetesConnector) CreateJWTSecret(ctx context.Context, force bool) error { + ret := _m.Called(ctx, force) + + if len(ret) == 0 { + panic("no return value specified for CreateJWTSecret") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, bool) error); ok { + r0 = rf(ctx, force) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // CreateNamespace provides a mock function with given fields: name func (_m *MockKubernetesConnector) CreateNamespace(name string) error { ret := _m.Called(name) From 22625f9fb3285e56ace684474e8ac4ba39095d09 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:39:56 +0530 Subject: [PATCH 015/103] ensure JWT secret during upgrade Signed-off-by: Mayank Shah --- pkg/upgrade/upgrade.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index d373ecc98..1ee363890 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -114,6 +114,10 @@ func (u *Upgrade) Run(ctx context.Context) error { return err } + if err := u.kubeClient.CreateJWTSecret(ctx, false); err != nil { + return err + } + // We cannot use the latest version of catalog yet since // at the time of writing, each catalog version supports only one Everest version. catalogVersion := recVer.Catalog From cfc896b7e0c68bd7e1a77605af3897c0a49de518 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:42:03 +0530 Subject: [PATCH 016/103] mount JWT secret Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index e1798d429..122871b4a 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -96,6 +96,12 @@ spec: containers: - name: everest image: perconalab/everest:0.0.0 + env: + - name: JWT_SIGNING_KEY + valueFrom: + secretKeyRef: + name: everest-jwt + key: signing_key ports: - containerPort: 8080 readinessProbe: From 7cab15b6ba178ce3b51573fed3767cb56ea5267e Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:50:16 +0530 Subject: [PATCH 017/103] update RBAC Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 122871b4a..024548a38 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -41,6 +41,9 @@ rules: - apiGroups: ["everest.percona.com"] resources: ["*"] verbs: ["*"] + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["create", "upgrade", "get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding From edf569c4d1808435739bd2f3f52c0d422ccc3a36 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:51:54 +0530 Subject: [PATCH 018/103] fix rbac Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 024548a38..d7739aaff 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -27,10 +27,7 @@ rules: resources: ["databaseclusters", "databaseclusterbackups", "databaseclusterrestores", "backupstorages", "monitoringconfigs"] verbs: ["*"] - apiGroups: [""] - resources: ["secrets"] - verbs: ["*"] - - apiGroups: [""] - resources: ["secrets"] + resources: ["secrets", "configmaps"] verbs: ["*"] - apiGroups: [""] resources: ["nodes", "pods", "persistentvolumes"] @@ -41,9 +38,6 @@ rules: - apiGroups: ["everest.percona.com"] resources: ["*"] verbs: ["*"] - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["create", "upgrade", "get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding From d60ab309b266d9b158f113a140e40ee1fbba2cce Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 11:55:43 +0530 Subject: [PATCH 019/103] fix linting Signed-off-by: Mayank Shah --- go.mod | 4 ++-- go.sum | 3 +++ pkg/kubernetes/client/client.go | 2 ++ pkg/kubernetes/client/kubeclient_interface.go | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index fc9026760..2764bd862 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,8 @@ require ( github.com/oapi-codegen/runtime v1.1.1 github.com/operator-framework/api v0.23.0 github.com/operator-framework/operator-lifecycle-manager v0.27.0 - github.com/percona/everest-operator v0.6.0-dev1.0.20240514121858-9c270ed11e06 + github.com/percona/everest-operator v0.6.0-dev1.0.20240426070203-91f4233b9320 + github.com/rodaine/table v1.2.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 @@ -122,7 +123,6 @@ require ( github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rodaine/table v1.2.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/go.sum b/go.sum index 5fba5332f..80c283bbf 100644 --- a/go.sum +++ b/go.sum @@ -469,6 +469,7 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -607,6 +608,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= diff --git a/pkg/kubernetes/client/client.go b/pkg/kubernetes/client/client.go index b89c129a3..221f6c3d7 100644 --- a/pkg/kubernetes/client/client.go +++ b/pkg/kubernetes/client/client.go @@ -305,6 +305,8 @@ func (c *Client) Config() *rest.Config { } // Clientset returns the k8s clientset. +// +//nolint:ireturn func (c *Client) Clientset() kubernetes.Interface { return c.clientset } diff --git a/pkg/kubernetes/client/kubeclient_interface.go b/pkg/kubernetes/client/kubeclient_interface.go index 064581320..caead0380 100644 --- a/pkg/kubernetes/client/kubeclient_interface.go +++ b/pkg/kubernetes/client/kubeclient_interface.go @@ -48,6 +48,8 @@ type KubeClientConnector interface { // Config returns restConfig to the pkg/kubernetes.Kubernetes client. Config() *rest.Config // Clientset returns the k8s clientset. + // + //nolint:ireturn Clientset() kubernetes.Interface // ClusterName returns the name of the k8s cluster. ClusterName() string From df0f99bc1f84b80b5fa095dcd6470a7a0058fc6e Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 12:32:40 +0530 Subject: [PATCH 020/103] add session manager Signed-off-by: Mayank Shah --- api/everest.go | 9 +- go.mod | 3 + go.sum | 4 + pkg/kubernetes/client/accounts/accounts.go | 5 + pkg/session/manager.go | 123 +++++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 pkg/session/manager.go diff --git a/api/everest.go b/api/everest.go index f35f119a3..9286cfc16 100644 --- a/api/everest.go +++ b/api/everest.go @@ -25,7 +25,9 @@ import ( "fmt" "io/fs" "net/http" + "strings" + echojwt "github.com/labstack/echo-jwt/v4" "github.com/labstack/echo/v4" echomiddleware "github.com/labstack/echo/v4/middleware" middleware "github.com/oapi-codegen/echo-middleware" @@ -120,7 +122,12 @@ func (e *EverestServer) initHTTPServer() error { // Use our validation middleware to check all requests against the OpenAPI schema. apiGroup := e.echo.Group(basePath) - apiGroup.Use(e.authenticate) + apiGroup.Use(echojwt.WithConfig(echojwt.Config{ + Skipper: func(c echo.Context) bool { + return strings.Contains(c.Request().URL.Path, "session") + }, + SigningKey: []byte(e.config.JWTSigningKey), + })) apiGroup.Use(middleware.OapiRequestValidatorWithOptions(swagger, &middleware.Options{ SilenceServersWarning: true, })) diff --git a/go.mod b/go.mod index 2764bd862..2589bd89d 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,8 @@ require ( github.com/go-test/deep v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -93,6 +95,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/labstack/echo-jwt/v4 v4.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/go.sum b/go.sum index 80c283bbf..5df7cfb70 100644 --- a/go.sum +++ b/go.sum @@ -284,6 +284,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= @@ -443,6 +445,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c= +github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index b5f7dafa6..31c8124dc 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -68,6 +68,11 @@ type Account struct { Password } +// HasCapability returns true if the given account has the specified capability. +func (a Account) HasCapability(cap AccountCapability) bool { + return slices.Contains(a.Capabilities, cap) +} + // Client provides functionality for managing user accounts on Kubernetes. type Client struct { k client.KubeClientConnector diff --git a/pkg/session/manager.go b/pkg/session/manager.go new file mode 100644 index 000000000..c5871b220 --- /dev/null +++ b/pkg/session/manager.go @@ -0,0 +1,123 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package session provides a session manager for creating and verifying JWT tokens. +package session + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" + + "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/kubernetes/client/accounts" +) + +const ( + // SessionManagerClaimsIssuer fills the "iss" field of the token. + SessionManagerClaimsIssuer = "everest" +) + +// SessionManager provides functionality for creating and managing JWT tokens. +type SessionManager struct { + accountManager kubernetes.Accounts + signingKey []byte +} + +// Option is a function that modifies a SessionManager. +type Option func(*SessionManager) + +// New creates a new session manager with the given options. +func New(options ...Option) *SessionManager { + m := &SessionManager{} + for _, opt := range options { + opt(m) + } + return m +} + +// WithAccountManager sets the account manager to use for verifying user credentials. +func WithAccountManager(am kubernetes.Accounts) Option { + return func(m *SessionManager) { + m.accountManager = am + } +} + +// WithSigningKey sets the signing key to use for managing JWT tokens. +func WithSigningKey(key []byte) Option { + return func(m *SessionManager) { + m.signingKey = key + } +} + +// Create creates a new token for a given subject (user) and returns it as a string. +// Passing a value of `0` for secondsBeforeExpiry creates a token that never expires. +// The id parameter holds an optional unique JWT token identifier and stored as a standard claim "jti" in the JWT token. +func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64, id string) (string, error) { + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + now := time.Now().UTC() + claims := jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(now), + Issuer: SessionManagerClaimsIssuer, + NotBefore: jwt.NewNumericDate(now), + Subject: subject, + ID: id, + } + if secondsBeforeExpiry > 0 { + expires := now.Add(time.Duration(secondsBeforeExpiry) * time.Second) + claims.ExpiresAt = jwt.NewNumericDate(expires) + } + + return mgr.signClaims(claims) +} + +func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(mgr.signingKey) +} + +// Authenticate verifies the given username and password. +func (mgr *SessionManager) Authenticate(ctx context.Context, username string, password string) error { + if password == "" { + return fmt.Errorf("blank passwords are not allowed") + } + + account, err := mgr.accountManager.Get(ctx, username) + if err != nil { + return err + } + + computedHash, err := mgr.accountManager.ComputePasswordHash(ctx, password) + if err != nil { + return errors.Join(err, errors.New("failed to compute password hash")) + } + + if computedHash != account.PasswordHash { + return errors.New("invalid password") + } + + if !account.Enabled { + return fmt.Errorf("account disabled") + } + + if !account.HasCapability(accounts.AccountCapabilityLogin) { + return fmt.Errorf("user does not have capability to login") + } + return nil +} From 17193d85cba19885e87f2aefa890218205173881 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 12:34:15 +0530 Subject: [PATCH 021/103] add session spec Signed-off-by: Mayank Shah --- docs/spec/openapi.yml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/spec/openapi.yml b/docs/spec/openapi.yml index b847bd9ca..fe158286f 100644 --- a/docs/spec/openapi.yml +++ b/docs/spec/openapi.yml @@ -24,6 +24,40 @@ tags: description: Everything related to the Backup storage paths: + '/session': + post: + summary: Create a new session + description: Create a new session + operationId: createSession + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + token: + type: string + '400': + description: Unsuccessful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + requestBody: + description: The user credentials + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserCredentials' '/namespaces': get: summary: Get all namespaces managed by Everest @@ -1359,6 +1393,13 @@ components: type: array items: type: string + UserCredentials: + type: object + properties: + username: + type: string + password: + type: string CreateBackupStorageParams: type: object description: Backup storage parameters From 6a5040b577374d2b660ad9f0f7237035b8af6d20 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 12:37:33 +0530 Subject: [PATCH 022/103] ran automake Signed-off-by: Mayank Shah --- api/everest-server.gen.go | 321 +++++++++++++----------- client/everest-client.gen.go | 467 ++++++++++++++++++++++++----------- docs/spec/openapi.yml | 2 +- 3 files changed, 491 insertions(+), 299 deletions(-) diff --git a/api/everest-server.gen.go b/api/everest-server.gen.go index 797dfe6a5..16c4c836a 100644 --- a/api/everest-server.gen.go +++ b/api/everest-server.gen.go @@ -854,6 +854,12 @@ type UpdateBackupStorageParams struct { VerifyTLS *bool `json:"verifyTLS,omitempty"` } +// UserCredentials defines model for UserCredentials. +type UserCredentials struct { + Password *string `json:"password,omitempty"` + Username *string `json:"username,omitempty"` +} + // Version Everest version info type Version struct { FullCommit string `json:"fullCommit"` @@ -993,6 +999,9 @@ type UpdateDatabaseEngineJSONRequestBody = DatabaseEngine // UpgradeDatabaseEngineOperatorJSONRequestBody defines body for UpgradeDatabaseEngineOperator for application/json ContentType. type UpgradeDatabaseEngineOperatorJSONRequestBody = DatabaseEngineOperatorUpgradeParams +// CreateSessionJSONRequestBody defines body for CreateSession for application/json ContentType. +type CreateSessionJSONRequestBody = UserCredentials + // AsDatabaseClusterSpecEngineResourcesCpu0 returns the union data inside the DatabaseCluster_Spec_Engine_Resources_Cpu as a DatabaseClusterSpecEngineResourcesCpu0 func (t DatabaseCluster_Spec_Engine_Resources_Cpu) AsDatabaseClusterSpecEngineResourcesCpu0() (DatabaseClusterSpecEngineResourcesCpu0, error) { var body DatabaseClusterSpecEngineResourcesCpu0 @@ -1531,6 +1540,9 @@ type ServerInterface interface { // Get the capacity and available resources of a kubernetes cluster // (GET /resources) GetKubernetesClusterResources(ctx echo.Context) error + // Create a new session + // (POST /session) + CreateSession(ctx echo.Context) error // Get Everest API Server version info // (GET /version) VersionInfo(ctx echo.Context) error @@ -2191,6 +2203,15 @@ func (w *ServerInterfaceWrapper) GetKubernetesClusterResources(ctx echo.Context) return err } +// CreateSession converts echo context to params. +func (w *ServerInterfaceWrapper) CreateSession(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.CreateSession(ctx) + return err +} + // VersionInfo converts echo context to params. func (w *ServerInterfaceWrapper) VersionInfo(ctx echo.Context) error { var err error @@ -2261,160 +2282,162 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PUT(baseURL+"/namespaces/:namespace/database-engines/:name/operator-version", wrapper.UpgradeDatabaseEngineOperator) router.GET(baseURL+"/namespaces/:namespace/database-engines/:name/operator-version/preflight", wrapper.GetOperatorUpgradePreflight) router.GET(baseURL+"/resources", wrapper.GetKubernetesClusterResources) + router.POST(baseURL+"/session", wrapper.CreateSession) router.GET(baseURL+"/version", wrapper.VersionInfo) } // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "03169+l/AwAA//8XJY34sBQBAA==", + "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", + "xnjZvFHR9ly/UPHz+8//GwAA//96qXHm0xcBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/client/everest-client.gen.go b/client/everest-client.gen.go index a7078248b..c399fb5d4 100644 --- a/client/everest-client.gen.go +++ b/client/everest-client.gen.go @@ -855,6 +855,12 @@ type UpdateBackupStorageParams struct { VerifyTLS *bool `json:"verifyTLS,omitempty"` } +// UserCredentials defines model for UserCredentials. +type UserCredentials struct { + Password *string `json:"password,omitempty"` + Username *string `json:"username,omitempty"` +} + // Version Everest version info type Version struct { FullCommit string `json:"fullCommit"` @@ -994,6 +1000,9 @@ type UpdateDatabaseEngineJSONRequestBody = DatabaseEngine // UpgradeDatabaseEngineOperatorJSONRequestBody defines body for UpgradeDatabaseEngineOperator for application/json ContentType. type UpgradeDatabaseEngineOperatorJSONRequestBody = DatabaseEngineOperatorUpgradeParams +// CreateSessionJSONRequestBody defines body for CreateSession for application/json ContentType. +type CreateSessionJSONRequestBody = UserCredentials + // AsDatabaseClusterSpecEngineResourcesCpu0 returns the union data inside the DatabaseCluster_Spec_Engine_Resources_Cpu as a DatabaseClusterSpecEngineResourcesCpu0 func (t DatabaseCluster_Spec_Engine_Resources_Cpu) AsDatabaseClusterSpecEngineResourcesCpu0() (DatabaseClusterSpecEngineResourcesCpu0, error) { var body DatabaseClusterSpecEngineResourcesCpu0 @@ -1625,6 +1634,11 @@ type ClientInterface interface { // GetKubernetesClusterResources request GetKubernetesClusterResources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateSessionWithBody request with any body + CreateSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateSession(ctx context.Context, body CreateSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // VersionInfo request VersionInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) } @@ -2169,6 +2183,30 @@ func (c *Client) GetKubernetesClusterResources(ctx context.Context, reqEditors . return c.Client.Do(req) } +func (c *Client) CreateSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateSessionRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateSession(ctx context.Context, body CreateSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateSessionRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) VersionInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewVersionInfoRequest(c.Server) if err != nil { @@ -3601,6 +3639,46 @@ func NewGetKubernetesClusterResourcesRequest(server string) (*http.Request, erro return req, nil } +// NewCreateSessionRequest calls the generic CreateSession builder with application/json body +func NewCreateSessionRequest(server string, body CreateSessionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateSessionRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreateSessionRequestWithBody generates requests for CreateSession with any type of body +func NewCreateSessionRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/session") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewVersionInfoRequest generates requests for VersionInfo func NewVersionInfoRequest(server string) (*http.Request, error) { var err error @@ -3795,6 +3873,11 @@ type ClientWithResponsesInterface interface { // GetKubernetesClusterResourcesWithResponse request GetKubernetesClusterResourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetKubernetesClusterResourcesResponse, error) + // CreateSessionWithBodyWithResponse request with any body + CreateSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSessionResponse, error) + + CreateSessionWithResponse(ctx context.Context, body CreateSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSessionResponse, error) + // VersionInfoWithResponse request VersionInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*VersionInfoResponse, error) } @@ -4620,6 +4703,32 @@ func (r GetKubernetesClusterResourcesResponse) StatusCode() int { return 0 } +type CreateSessionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Token *string `json:"token,omitempty"` + } + JSON400 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r CreateSessionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateSessionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type VersionInfoResponse struct { Body []byte HTTPResponse *http.Response @@ -5036,6 +5145,23 @@ func (c *ClientWithResponses) GetKubernetesClusterResourcesWithResponse(ctx cont return ParseGetKubernetesClusterResourcesResponse(rsp) } +// CreateSessionWithBodyWithResponse request with arbitrary body returning *CreateSessionResponse +func (c *ClientWithResponses) CreateSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSessionResponse, error) { + rsp, err := c.CreateSessionWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateSessionResponse(rsp) +} + +func (c *ClientWithResponses) CreateSessionWithResponse(ctx context.Context, body CreateSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSessionResponse, error) { + rsp, err := c.CreateSession(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateSessionResponse(rsp) +} + // VersionInfoWithResponse request returning *VersionInfoResponse func (c *ClientWithResponses) VersionInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*VersionInfoResponse, error) { rsp, err := c.VersionInfo(ctx, reqEditors...) @@ -6439,6 +6565,48 @@ func ParseGetKubernetesClusterResourcesResponse(rsp *http.Response) (*GetKuberne return response, nil } +// ParseCreateSessionResponse parses an HTTP response from a CreateSessionWithResponse call +func ParseCreateSessionResponse(rsp *http.Response) (*CreateSessionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateSessionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Token *string `json:"token,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseVersionInfoResponse parses an HTTP response from a VersionInfoWithResponse call func ParseVersionInfoResponse(rsp *http.Response) (*VersionInfoResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -6466,155 +6634,156 @@ func ParseVersionInfoResponse(rsp *http.Response) (*VersionInfoResponse, error) // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "03169+l/AwAA//8XJY34sBQBAA==", + "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", + "xnjZvFHR9ly/UPHz+8//GwAA//96qXHm0xcBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/docs/spec/openapi.yml b/docs/spec/openapi.yml index fe158286f..aed73b829 100644 --- a/docs/spec/openapi.yml +++ b/docs/spec/openapi.yml @@ -24,7 +24,7 @@ tags: description: Everything related to the Backup storage paths: - '/session': + '/session': post: summary: Create a new session description: Create a new session From e21981ca03860829f28272b36050980a4eb5dc44 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 12:43:37 +0530 Subject: [PATCH 023/103] add session api Signed-off-by: Mayank Shah --- api/everest.go | 8 ++++++ api/session.go | 56 ++++++++++++++++++++++++++++++++++++++++++ pkg/session/manager.go | 20 +++++++-------- 3 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 api/session.go diff --git a/api/everest.go b/api/everest.go index 9286cfc16..da986f5ad 100644 --- a/api/everest.go +++ b/api/everest.go @@ -37,6 +37,7 @@ import ( "github.com/percona/everest/cmd/config" "github.com/percona/everest/pkg/auth" "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/session" "github.com/percona/everest/public" ) @@ -47,6 +48,7 @@ type EverestServer struct { l *zap.SugaredLogger echo *echo.Echo kubeClient *kubernetes.Kubernetes + sessionMgr *session.Manager } type authValidator interface { @@ -69,12 +71,18 @@ func NewEverestServer(c *config.EverestConfig, l *zap.SugaredLogger) (*EverestSe echoServer := echo.New() echoServer.Use(echomiddleware.RateLimiter(echomiddleware.NewRateLimiterMemoryStore(rate.Limit(c.APIRequestsRateLimit)))) + sessMgr := session.New( + session.WithAccountManager(kubeClient.Accounts()), + session.WithSigningKey([]byte(c.JWTSigningKey)), + ) + e := &EverestServer{ config: c, l: l, echo: echoServer, kubeClient: kubeClient, auth: auth.NewToken(kubeClient, l, []byte(ns.UID)), + sessionMgr: sessMgr, } if err := e.initHTTPServer(); err != nil { diff --git a/api/session.go b/api/session.go new file mode 100644 index 000000000..a2641782c --- /dev/null +++ b/api/session.go @@ -0,0 +1,56 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package api ... +package api + +import ( + "fmt" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + + "github.com/percona/everest/pkg/kubernetes/client/accounts" +) + +// CreateSession creates a new session. +func (e *EverestServer) CreateSession(ctx echo.Context) error { + var params UserCredentials + if err := ctx.Bind(¶ms); err != nil { + return err + } + + c := ctx.Request().Context() + err := e.sessionMgr.Authenticate(c, *params.Username, *params.Password) + if err != nil { + return err + } + + uniqueId, err := uuid.NewRandom() + if err != nil { + return err + } + jwtToken, err := e.sessionMgr.Create( + fmt.Sprintf("%s:%s", *params.Username, accounts.AccountCapabilityLogin), + int64((time.Hour * 24).Seconds()), + uniqueId.String()) + if err != nil { + return err + } + + return ctx.JSON(http.StatusOK, map[string]string{"token": jwtToken}) +} diff --git a/pkg/session/manager.go b/pkg/session/manager.go index c5871b220..4d6c740ca 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -33,18 +33,18 @@ const ( SessionManagerClaimsIssuer = "everest" ) -// SessionManager provides functionality for creating and managing JWT tokens. -type SessionManager struct { +// Manager provides functionality for creating and managing JWT tokens. +type Manager struct { accountManager kubernetes.Accounts signingKey []byte } // Option is a function that modifies a SessionManager. -type Option func(*SessionManager) +type Option func(*Manager) // New creates a new session manager with the given options. -func New(options ...Option) *SessionManager { - m := &SessionManager{} +func New(options ...Option) *Manager { + m := &Manager{} for _, opt := range options { opt(m) } @@ -53,14 +53,14 @@ func New(options ...Option) *SessionManager { // WithAccountManager sets the account manager to use for verifying user credentials. func WithAccountManager(am kubernetes.Accounts) Option { - return func(m *SessionManager) { + return func(m *Manager) { m.accountManager = am } } // WithSigningKey sets the signing key to use for managing JWT tokens. func WithSigningKey(key []byte) Option { - return func(m *SessionManager) { + return func(m *Manager) { m.signingKey = key } } @@ -68,7 +68,7 @@ func WithSigningKey(key []byte) Option { // Create creates a new token for a given subject (user) and returns it as a string. // Passing a value of `0` for secondsBeforeExpiry creates a token that never expires. // The id parameter holds an optional unique JWT token identifier and stored as a standard claim "jti" in the JWT token. -func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64, id string) (string, error) { +func (mgr *Manager) Create(subject string, secondsBeforeExpiry int64, id string) (string, error) { // Create a new token object, specifying signing method and the claims // you would like it to contain. now := time.Now().UTC() @@ -87,13 +87,13 @@ func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64, id return mgr.signClaims(claims) } -func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) { +func (mgr *Manager) signClaims(claims jwt.Claims) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(mgr.signingKey) } // Authenticate verifies the given username and password. -func (mgr *SessionManager) Authenticate(ctx context.Context, username string, password string) error { +func (mgr *Manager) Authenticate(ctx context.Context, username string, password string) error { if password == "" { return fmt.Errorf("blank passwords are not allowed") } From 600a52334ac695a767c55f6b2d55d5367bb8196b Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 12:44:45 +0530 Subject: [PATCH 024/103] rename params Signed-off-by: Mayank Shah --- pkg/kubernetes/client/accounts/accounts.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 31c8124dc..9a6685b48 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -69,8 +69,8 @@ type Account struct { } // HasCapability returns true if the given account has the specified capability. -func (a Account) HasCapability(cap AccountCapability) bool { - return slices.Contains(a.Capabilities, cap) +func (a Account) HasCapability(c AccountCapability) bool { + return slices.Contains(a.Capabilities, c) } // Client provides functionality for managing user accounts on Kubernetes. From fa4829337a8181c6d435ddd0fbf74daff1c86ed3 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 12:51:16 +0530 Subject: [PATCH 025/103] clean-up Signed-off-by: Mayank Shah --- commands/accounts/list.go | 3 ++- pkg/accounts/accounts.go | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/commands/accounts/list.go b/commands/accounts/list.go index 1e8971353..baa2ceab3 100644 --- a/commands/accounts/list.go +++ b/commands/accounts/list.go @@ -39,8 +39,9 @@ func NewListCmd(l *zap.SugaredLogger) *cobra.Command { if err != nil { os.Exit(1) } + kubeconfigPath := viper.GetString("kubeconfig") - cli, err := accounts.NewCLI(o.KubeconfigPath, l) + cli, err := accounts.NewCLI(kubeconfigPath, l) if err != nil { l.Error(err) os.Exit(1) diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go index f5df844c5..8de383593 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/accounts.go @@ -115,9 +115,8 @@ func (c *CLI) Delete(ctx context.Context, username, password string) error { // ListOptions holds options for listing user accounts. type ListOptions struct { - KubeconfigPath string `mapstructure:"kubeconfig"` - NoHeaders bool `mapstructure:"no-headers"` - Columns []string `mapstructure:"columns"` + NoHeaders bool `mapstructure:"no-headers"` + Columns []string `mapstructure:"columns"` } const ( From 611073342badc9f1bb4f87a41c47e42d56bcc938 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 13:12:55 +0530 Subject: [PATCH 026/103] clean-up and linting Signed-off-by: Mayank Shah --- api/auth.go | 72 ------------------------------------------ api/session.go | 14 +++++--- pkg/upgrade/upgrade.go | 2 ++ 3 files changed, 12 insertions(+), 76 deletions(-) delete mode 100644 api/auth.go diff --git a/api/auth.go b/api/auth.go deleted file mode 100644 index 843301936..000000000 --- a/api/auth.go +++ /dev/null @@ -1,72 +0,0 @@ -// everest -// Copyright (C) 2023 Percona LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package api - -import ( - "errors" - "net/http" - "strings" - - "github.com/AlekSi/pointer" - "github.com/labstack/echo/v4" -) - -// authenticate is a middleware which authenticates a user by checking if the provided token is valid. -// If the user cannot be authenticated, the middleware returns "Unauthorized" response to the user. -func (e *EverestServer) authenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - token, err := e.authToken(c) - if err != nil { - e.l.Error(err) - return err - } - - valid, err := e.auth.Valid(c.Request().Context(), token) - if err != nil { - e.l.Error(err) - return c.JSON(http.StatusInternalServerError, Error{ - Message: pointer.ToString("Could not verify authentication token"), - }) - } - - if !valid { - return c.JSON(http.StatusUnauthorized, Error{ - Message: pointer.ToString("Unauthorized"), - }) - } - - return next(c) - } -} - -func (e *EverestServer) authToken(c echo.Context) (string, error) { - var token string - - header := c.Request().Header.Get("Authorization") - if s, found := strings.CutPrefix(header, "Bearer "); found && header != "" { - token = s - } else { - cookie, err := c.Cookie("everest_token") - if err != nil && !errors.Is(err, http.ErrNoCookie) { - return "", errors.New("could not parse everest_token cookie") - } - if cookie != nil { - token = cookie.Value - } - } - - return token, nil -} diff --git a/api/session.go b/api/session.go index a2641782c..99f2c5d01 100644 --- a/api/session.go +++ b/api/session.go @@ -27,6 +27,11 @@ import ( "github.com/percona/everest/pkg/kubernetes/client/accounts" ) +const ( + jwtSubjectTml = "%s:%s" // username:capability + jwtDefaultExpiry = time.Hour * 24 +) + // CreateSession creates a new session. func (e *EverestServer) CreateSession(ctx echo.Context) error { var params UserCredentials @@ -40,14 +45,15 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { return err } - uniqueId, err := uuid.NewRandom() + uniqueID, err := uuid.NewRandom() if err != nil { return err } jwtToken, err := e.sessionMgr.Create( - fmt.Sprintf("%s:%s", *params.Username, accounts.AccountCapabilityLogin), - int64((time.Hour * 24).Seconds()), - uniqueId.String()) + fmt.Sprintf(jwtSubjectTml, *params.Username, accounts.AccountCapabilityLogin), + int64(jwtDefaultExpiry.Seconds()), + uniqueID.String(), + ) if err != nil { return err } diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index 1ee363890..2d2409ab2 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -92,6 +92,8 @@ func NewUpgrade(cfg *Config, l *zap.SugaredLogger) (*Upgrade, error) { } // Run runs the operators installation process. +// +//nolint:funlen func (u *Upgrade) Run(ctx context.Context) error { // Get Everest version. everestVersion, err := cliVersion.EverestVersionFromDeployment(ctx, u.kubeClient) From 72e807c7b4f475b72a35ae1fc728b165ec35b9aa Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 13:19:48 +0530 Subject: [PATCH 027/103] go mod tidy Signed-off-by: Mayank Shah --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 2589bd89d..19e4918bd 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,11 @@ require ( github.com/dchest/uniuri v1.2.0 github.com/getkin/kin-openapi v0.124.0 github.com/go-logr/zapr v1.3.0 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.6.0 github.com/kelseyhightower/envconfig v1.4.0 + github.com/labstack/echo-jwt/v4 v4.2.0 github.com/labstack/echo/v4 v4.11.4 github.com/oapi-codegen/echo-middleware v1.0.1 github.com/oapi-codegen/runtime v1.1.1 @@ -69,7 +71,6 @@ require ( github.com/go-test/deep v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -95,7 +96,6 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/labstack/echo-jwt/v4 v4.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect From bccf83b827bffc3201f8c3fc78efeb3e79c02c5a Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 15:01:42 +0530 Subject: [PATCH 028/103] add post install step Signed-off-by: Mayank Shah --- pkg/common/constants.go | 3 ++ pkg/install/install.go | 60 ++++++++++------------ pkg/kubernetes/accounts.go | 1 + pkg/kubernetes/client/accounts/accounts.go | 20 ++++++++ 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/pkg/common/constants.go b/pkg/common/constants.go index e06f86b0b..06a8cc2b3 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -41,4 +41,7 @@ const ( // EverestJWTSecretName is the name of the secret that holds JWT secret. EverestJWTSecretName = "everest-jwt" + + // EverestAdminUser is the name of the admin user. + EverestAdminUser = "admin" ) diff --git a/pkg/install/install.go b/pkg/install/install.go index d6f5ef36b..cf9e41477 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -38,7 +38,6 @@ import ( "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes" - "github.com/percona/everest/pkg/token" "github.com/percona/everest/pkg/version" versionservice "github.com/percona/everest/pkg/version_service" ) @@ -48,6 +47,19 @@ const ( DefaultEverestNamespace = "everest" ) +const postInstallMessage = ` +Everest has been successfully installed! + +To view the password for the 'admin' user, run the following command: + +kubectl get secret -n everest-system everest-accounts -o jsonpath='{.data.passwords\.yaml}' | base64 --decode | yq .admin.passwordHash + + +To create a new user, run the following command: + +everestctl accounts create +` + // Install implements the main logic for commands. type Install struct { l *zap.SugaredLogger @@ -200,17 +212,7 @@ func (o *Install) Run(ctx context.Context) error { return err } - _, err = o.kubeClient.GetSecret(ctx, common.SystemNamespace, token.SecretName) - if err != nil && !k8serrors.IsNotFound(err) { - return errors.Join(err, errors.New("could not get the everest token secret")) - } - if err != nil && k8serrors.IsNotFound(err) { - pwd, err := o.generateToken(ctx) - if err != nil { - return err - } - o.l.Info("\n" + pwd.String() + "\n\n") - } + o.l.Info("\n" + postInstallMessage + "\n") return nil } @@ -376,6 +378,18 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er if err = o.kubeClient.InstallEverest(ctx, common.SystemNamespace, v); err != nil { return err } + + // Create admin user for Everest. + // We set the password to "password" here, however we reset it in the next step. + // The reason is that `Create` hashes the password, but during the initial installation, + // we want the user to be able to view a plain text password which they will reset later. + if err := o.kubeClient.Accounts().Create(ctx, common.EverestAdminUser, "password"); err != nil { + return err + } + // Reset the password and store it in plain text for the user to view it. + if err := o.kubeClient.Accounts().ResetAdminPassword(ctx); err != nil { + return err + } } else { o.l.Info("Restarting Everest") if err := o.kubeClient.RestartOperator(ctx, common.PerconaEverestOperatorDeploymentName, common.SystemNamespace); err != nil { @@ -686,28 +700,6 @@ func (o *Install) serviceAccountRolePolicyRules() []rbacv1.PolicyRule { } } -func (o *Install) generateToken(ctx context.Context) (*token.ResetResponse, error) { - o.l.Info("Creating token for Everest") - - r, err := token.NewReset( - token.ResetConfig{ - KubeconfigPath: o.config.KubeconfigPath, - Namespace: common.SystemNamespace, - }, - o.l, - ) - if err != nil { - return nil, errors.Join(err, errors.New("could not initialize reset token")) - } - - res, err := r.Run(ctx) - if err != nil { - return nil, errors.Join(err, errors.New("could not create token")) - } - - return res, nil -} - // ValidateNamespaces validates a comma-separated namespaces string. func ValidateNamespaces(str string) ([]string, error) { nsList := strings.Split(str, ",") diff --git a/pkg/kubernetes/accounts.go b/pkg/kubernetes/accounts.go index 086f2035c..4fd0c09b6 100644 --- a/pkg/kubernetes/accounts.go +++ b/pkg/kubernetes/accounts.go @@ -30,6 +30,7 @@ type Accounts interface { Delete(ctx context.Context, username string) error Update(ctx context.Context, username, password string) error ComputePasswordHash(ctx context.Context, password string) (string, error) + ResetAdminPassword(ctx context.Context) error } // Accounts returns a new client for managing everest user accounts. diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 9a6685b48..ae1e04ebd 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -18,7 +18,9 @@ package accounts import ( "context" + "crypto/rand" "crypto/sha256" + "encoding/hex" "errors" "slices" "strings" @@ -108,6 +110,24 @@ func (a *Client) Get(ctx context.Context, username string) (*Account, error) { }, nil } +// ResetAdminPassword sets a new password for the admin account. +// This password will not be hashed, so that the user can view, login and reset it. +func (a *Client) ResetAdminPassword(ctx context.Context) error { + admin, err := a.Get(ctx, common.EverestAdminUser) + if err != nil { + return err + } + b := make([]byte, 64) + if _, err := rand.Read(b); err != nil { + return errors.Join(err, errors.New("failed to generate random password")) + } + admin.Password = Password{ + PasswordHash: hex.EncodeToString(b), + PasswordMTime: time.Now().Format(time.RFC3339), + } + return a.setAccounts(ctx, []Account{*admin}, true) +} + // List returns a list of all accounts. func (a *Client) List(ctx context.Context) ([]Account, error) { users, err := a.listAllUsers(ctx) From aab3c2e087c301265c27bcafe49d59642cacc88a Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 15:21:34 +0530 Subject: [PATCH 029/103] use awk instead of yq Signed-off-by: Mayank Shah --- pkg/install/install.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/install/install.go b/pkg/install/install.go index cf9e41477..393d18df6 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -47,12 +47,13 @@ const ( DefaultEverestNamespace = "everest" ) +// XXX: once `everestctl accounts update` is added, update this message to include instructions on how to reset password. const postInstallMessage = ` Everest has been successfully installed! To view the password for the 'admin' user, run the following command: -kubectl get secret -n everest-system everest-accounts -o jsonpath='{.data.passwords\.yaml}' | base64 --decode | yq .admin.passwordHash +kubectl get secret -n everest-system everest-accounts -o jsonpath='{.data.passwords\.yaml}' | base64 --decode | awk '/admin:/ {getline; print $2}' To create a new user, run the following command: From e478dbdddccde92b4059411cf7f0fc77d76ede35 Mon Sep 17 00:00:00 2001 From: Diogo Recharte Date: Thu, 11 Apr 2024 18:41:40 +0100 Subject: [PATCH 030/103] Update login page to use username/password instead of token --- .../src/contexts/auth/auth.context.types.ts | 2 +- .../src/contexts/auth/auth.provider.tsx | 59 ++++++------------- .../src/pages/login/Login.constants.ts | 6 +- .../everest/src/pages/login/Login.messages.ts | 8 +-- ui/apps/everest/src/pages/login/Login.tsx | 59 +++++++------------ 5 files changed, 46 insertions(+), 88 deletions(-) diff --git a/ui/apps/everest/src/contexts/auth/auth.context.types.ts b/ui/apps/everest/src/contexts/auth/auth.context.types.ts index 2022184ec..9e63e55b7 100644 --- a/ui/apps/everest/src/contexts/auth/auth.context.types.ts +++ b/ui/apps/everest/src/contexts/auth/auth.context.types.ts @@ -6,7 +6,7 @@ export type UserAuthStatus = | 'unknown'; export interface AuthContextProps { - login: (token: string) => void; + login: (username: string, password: string) => void; logout: () => void; setRedirectRoute: (route: string) => void; authStatus: UserAuthStatus; diff --git a/ui/apps/everest/src/contexts/auth/auth.provider.tsx b/ui/apps/everest/src/contexts/auth/auth.provider.tsx index 1d09aefae..e321246c0 100644 --- a/ui/apps/everest/src/contexts/auth/auth.provider.tsx +++ b/ui/apps/everest/src/contexts/auth/auth.provider.tsx @@ -1,5 +1,4 @@ -import { addApiInterceptors, api, removeApiInterceptors } from 'api/api'; -import { useVersion } from 'hooks/api/version/useVersion'; +import { api, addApiInterceptors, removeApiInterceptors } from 'api/api'; import { ReactNode, useEffect, useState } from 'react'; import { enqueueSnackbar } from 'notistack'; import AuthContext from './auth.context'; @@ -10,25 +9,22 @@ const setApiBearerToken = (token: string) => const AuthProvider = ({ children }: { children: ReactNode }) => { const [authStatus, setAuthStatus] = useState('unknown'); - const [token, setToken] = useState(''); const [redirect, setRedirect] = useState(null); - // We use the "/version" API call just to make sure the token works - // At this point, there's not really a login flow, per se - const { - status: queryStatus, - fetchStatus, - refetch, - } = useVersion({ - enabled: false, - retry: false, - }); - - const login = (token: string) => { - setAuthStatus('loggingIn'); - setApiBearerToken(token); - setToken(token); - refetch(); + const login = async (username: string, password: string) => { + try { + const response = await api.post('/session', { username, password }); + const token = response.data.token; // Assuming the response structure has a token field + setAuthStatus('loggedIn'); + setApiBearerToken(token); + localStorage.setItem('pwd', token); + addApiInterceptors(); + } catch (error) { + setAuthStatus('loggedOut'); + enqueueSnackbar('Invalid credentials', { + variant: 'error', + }); + } }; const logout = () => { @@ -45,33 +41,14 @@ const AuthProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { const savedToken = localStorage.getItem('pwd'); - if (savedToken) { - login(savedToken); - } else { - setAuthStatus('loggedOut'); - } - }, []); - - useEffect(() => { - if (fetchStatus === 'fetching') { - return; - } - if (queryStatus === 'success') { setAuthStatus('loggedIn'); - localStorage.setItem('pwd', token); + setApiBearerToken(savedToken); addApiInterceptors(); - } else if (queryStatus === 'error') { + } else { setAuthStatus('loggedOut'); - // This means the request was triggered by clicking the button, not an auto login - if (!localStorage.getItem('pwd')) { - enqueueSnackbar('Invalid authorization token', { - variant: 'error', - }); - } - localStorage.removeItem('pwd'); } - }, [fetchStatus, queryStatus, token]); + }, []); return ( ; diff --git a/ui/apps/everest/src/pages/login/Login.messages.ts b/ui/apps/everest/src/pages/login/Login.messages.ts index 3b70467ae..2bd23211e 100644 --- a/ui/apps/everest/src/pages/login/Login.messages.ts +++ b/ui/apps/everest/src/pages/login/Login.messages.ts @@ -4,10 +4,8 @@ export const Messages = { intro: `Percona Everest accelerates code deployment and scaling, and reduces database administration overhead. With the help of Percona Everest, you can regain control over data access, database configuration, and cloud database costs. Provision your first database cluster with Percona Everest today!`, login: 'Log in', - insertToken: - 'Enter the authorization token that you received during the installation process.', - resetToken: 'Reset token', - useTerminal: 'Reset your authorization token using the Everest CLI.', + insertCredentials: 'Enter your username and password to log in.', ok: 'OK', - token: 'Token', + username: 'Username', + password: 'Password', }; diff --git a/ui/apps/everest/src/pages/login/Login.tsx b/ui/apps/everest/src/pages/login/Login.tsx index 67d5a8be6..3e19823e1 100644 --- a/ui/apps/everest/src/pages/login/Login.tsx +++ b/ui/apps/everest/src/pages/login/Login.tsx @@ -2,35 +2,28 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Box, Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, Stack, Typography, } from '@mui/material'; -import { Card, DialogTitle, EverestMainIcon, TextInput } from '@percona/ui-lib'; -import { CodeCopyBlock } from 'components/code-copy-block/code-copy-block'; +import { Card, EverestMainIcon, TextInput } from '@percona/ui-lib'; import { AuthContext } from 'contexts/auth'; -import { useContext, useState } from 'react'; +import { useContext } from 'react'; import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; import { Navigate } from 'react-router-dom'; -import { LoginFields, LoginFormType, loginSchema } from './Login.constants'; +import { LoginFormType, loginSchema } from './Login.constants'; import { Messages } from './Login.messages'; const Login = () => { const methods = useForm({ mode: 'onChange', - defaultValues: { token: '' }, + defaultValues: { username: '', password: '' }, resolver: zodResolver(loginSchema), }); - const [modalOpen, setModalOpen] = useState(false); const { login, authStatus, redirectRoute } = useContext(AuthContext); - const handleClick = () => setModalOpen(true); - const handleClose = () => setModalOpen(false); - - const handleLogin: SubmitHandler = ({ token }) => login(token); + const handleLogin: SubmitHandler = ({ username, password }) => { + login(username, password); + }; if (authStatus === 'unknown') { return <>; @@ -73,19 +66,29 @@ const Login = () => { {Messages.login} - {Messages.insertToken} + {Messages.insertCredentials}
+
- } /> - - {Messages.resetToken} - - - {Messages.useTerminal} - - - - - - - ); }; From bf596a98a432ff741c6ffbad161995b5a5780265 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 16:25:59 +0530 Subject: [PATCH 031/103] authenticate admin insecure Signed-off-by: Mayank Shah --- dev/Tiltfile | 5 +++-- dev/jwt-secret.yaml | 6 ++++++ pkg/kubernetes/client/accounts/accounts.go | 6 ++++++ pkg/session/manager.go | 11 ++++++++++- public/dist/index.html | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 dev/jwt-secret.yaml diff --git a/dev/Tiltfile b/dev/Tiltfile index 865f5a10a..16f3303e3 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -14,8 +14,8 @@ print('Using PG operator version: %s' % pg_operator_version) # External resources set up # uncomment the settings below and insert your k8s context & registry names # to get your current context name run `kubectl config view` -#allow_k8s_contexts("gke_percona-everest_europe-west1-c_everest-dev") -#default_registry("us-central1-docker.pkg.dev/percona-everest/quickstart-docker-repo") +allow_k8s_contexts("k3d-everest-dev") +default_registry("localhost:61430") # Check for required env vars @@ -396,6 +396,7 @@ docker_build_with_restart('perconalab/everest', ] ) +k8s_yaml(namespace_inject('jwt-secret.yaml', everest_namespace)) k8s_yaml(namespace_inject('%s/deploy/quickstart-k8s.yaml' % backend_dir, everest_namespace)) k8s_resource( workload='percona-everest', diff --git a/dev/jwt-secret.yaml b/dev/jwt-secret.yaml new file mode 100644 index 000000000..5e28c41fe --- /dev/null +++ b/dev/jwt-secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: everest-jwt +data: + signing_key: aGVsbG8gZXZlcmVzdAo= diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index ae1e04ebd..1db9d9519 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -31,6 +31,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/AlekSi/pointer" "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes/client" ) @@ -61,6 +62,10 @@ type User struct { type Password struct { PasswordHash string `yaml:"passwordHash"` PasswordMTime string `yaml:"passwordMTime"` + + // Insecure is set to true if the password is not hashed. + // Generally this is set for the admin password after calling ResetAdminPassword. + Insecure *bool `yaml:"insecure,omitempty"` } // Account contains user and password data. @@ -124,6 +129,7 @@ func (a *Client) ResetAdminPassword(ctx context.Context) error { admin.Password = Password{ PasswordHash: hex.EncodeToString(b), PasswordMTime: time.Now().Format(time.RFC3339), + Insecure: pointer.To(true), } return a.setAccounts(ctx, []Account{*admin}, true) } diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 4d6c740ca..99c139f6f 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -22,6 +22,7 @@ import ( "fmt" "time" + "github.com/AlekSi/pointer" "github.com/golang-jwt/jwt/v4" "github.com/percona/everest/pkg/kubernetes" @@ -108,7 +109,15 @@ func (mgr *Manager) Authenticate(ctx context.Context, username string, password return errors.Join(err, errors.New("failed to compute password hash")) } - if computedHash != account.PasswordHash { + // For secure accounts, compare the computed hash with the stored hash. + if !pointer.GetBool(account.Insecure) && + computedHash != account.PasswordHash { + return errors.New("invalid password") + } + + // For insecure accounts, compare the password with the stored hash. + if pointer.GetBool(account.Insecure) && + password != account.PasswordHash { return errors.New("invalid password") } diff --git a/public/dist/index.html b/public/dist/index.html index 7a2b0e5a6..36368f1e4 100644 --- a/public/dist/index.html +++ b/public/dist/index.html @@ -5,7 +5,7 @@ Percona Everest - + From c2fca1d5e7b3e64f545670048a7f432bef2a805c Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Mon, 6 May 2024 18:34:42 +0530 Subject: [PATCH 032/103] add everest accounts Signed-off-by: Mayank Shah --- dev/everest-accounts.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 dev/everest-accounts.yaml diff --git a/dev/everest-accounts.yaml b/dev/everest-accounts.yaml new file mode 100644 index 000000000..02e981e4c --- /dev/null +++ b/dev/everest-accounts.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: everest-accounts +data: + users.yaml : | + admin: + enabled: true +--- +apiVersion: v1 +kind: Secret +metadata: + name: everest-accounts +data: + passwords.yaml: YWRtaW46CiAgaW5zZWN1cmU6IHRydWUKICBwYXNzd29yZEhhc2g6IGFkbWluCg== +--- From f8f5408ffe94f481fe03e9fafb12d4661a5e4ce6 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 11:19:20 +0530 Subject: [PATCH 033/103] big refactor Signed-off-by: Mayank Shah --- api/session.go | 2 +- commands/accounts/create.go | 20 +- commands/accounts/delete.go | 20 +- commands/accounts/list.go | 20 +- pkg/accounts/{ => cli}/accounts.go | 59 +-- pkg/accounts/tests.go | 61 +++ pkg/accounts/types.go | 57 +++ pkg/install/install.go | 34 +- pkg/kubernetes/accounts.go | 20 +- pkg/kubernetes/client/accounts/accounts.go | 389 +++++++----------- .../client/accounts/accounts_test.go | 59 +++ .../client/accounts/kubernetes_test.go | 103 ----- pkg/kubernetes/kubernetes_interface.go | 3 +- pkg/kubernetes/mock_kubernetes_connector.go | 9 +- pkg/session/manager.go | 30 +- 15 files changed, 424 insertions(+), 462 deletions(-) rename pkg/accounts/{ => cli}/accounts.go (70%) create mode 100644 pkg/accounts/tests.go create mode 100644 pkg/accounts/types.go create mode 100644 pkg/kubernetes/client/accounts/accounts_test.go delete mode 100644 pkg/kubernetes/client/accounts/kubernetes_test.go diff --git a/api/session.go b/api/session.go index 99f2c5d01..cf7d0b65f 100644 --- a/api/session.go +++ b/api/session.go @@ -24,7 +24,7 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" - "github.com/percona/everest/pkg/kubernetes/client/accounts" + "github.com/percona/everest/pkg/accounts" ) const ( diff --git a/commands/accounts/create.go b/commands/accounts/create.go index 462b11697..f3571a667 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -14,19 +14,20 @@ // limitations under the License. // Package accounts holds commands for accounts command. -// -//nolint:dupl package accounts import ( "context" + "errors" + "net/url" "os" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" - "github.com/percona/everest/pkg/accounts" + accountscli "github.com/percona/everest/pkg/accounts/cli" + "github.com/percona/everest/pkg/kubernetes" ) // NewCreateCmd returns a new create command. @@ -41,7 +42,18 @@ func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { username := viper.GetString("username") password := viper.GetString("password") - cli, err := accounts.NewCLI(kubeconfigPath, l) + k, err := kubernetes.New(kubeconfigPath, l) + if err != nil { + var u *url.Error + if errors.As(err, &u) { + l.Error("Could not connect to Kubernetes. " + + "Make sure Kubernetes is running and is accessible from this computer/server.") + } + os.Exit(0) + } + + cli := accountscli.New(l) + cli.WithAccountManager(k.Accounts()) if err != nil { l.Error(err) os.Exit(1) diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go index 52cd3628e..298183bbf 100644 --- a/commands/accounts/delete.go +++ b/commands/accounts/delete.go @@ -14,19 +14,20 @@ // limitations under the License. // Package accounts holds commands for accounts command. -// -//nolint:dupl package accounts import ( "context" + "errors" + "net/url" "os" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" - "github.com/percona/everest/pkg/accounts" + accountscli "github.com/percona/everest/pkg/accounts/cli" + "github.com/percona/everest/pkg/kubernetes" ) // NewDeleteCmd returns a new delete command. @@ -41,12 +42,19 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { username := viper.GetString("username") password := viper.GetString("password") - cli, err := accounts.NewCLI(kubeconfigPath, l) + k, err := kubernetes.New(kubeconfigPath, l) if err != nil { - l.Error(err) - os.Exit(1) + var u *url.Error + if errors.As(err, &u) { + l.Error("Could not connect to Kubernetes. " + + "Make sure Kubernetes is running and is accessible from this computer/server.") + } + os.Exit(0) } + cli := accountscli.New(l) + cli.WithAccountManager(k.Accounts()) + if err := cli.Delete(context.Background(), username, password); err != nil { l.Error(err) os.Exit(1) diff --git a/commands/accounts/list.go b/commands/accounts/list.go index baa2ceab3..8620d6731 100644 --- a/commands/accounts/list.go +++ b/commands/accounts/list.go @@ -18,13 +18,16 @@ package accounts import ( "context" + "errors" + "net/url" "os" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" - "github.com/percona/everest/pkg/accounts" + accountscli "github.com/percona/everest/pkg/accounts/cli" + "github.com/percona/everest/pkg/kubernetes" ) // NewListCmd returns a new list command. @@ -34,19 +37,26 @@ func NewListCmd(l *zap.SugaredLogger) *cobra.Command { Example: "everestctl accounts list", Run: func(cmd *cobra.Command, args []string) { //nolint:revive initListViperFlags(cmd) - o := &accounts.ListOptions{} + o := &accountscli.ListOptions{} err := viper.Unmarshal(o) if err != nil { os.Exit(1) } kubeconfigPath := viper.GetString("kubeconfig") - cli, err := accounts.NewCLI(kubeconfigPath, l) + k, err := kubernetes.New(kubeconfigPath, l) if err != nil { - l.Error(err) - os.Exit(1) + var u *url.Error + if errors.As(err, &u) { + l.Error("Could not connect to Kubernetes. " + + "Make sure Kubernetes is running and is accessible from this computer/server.") + } + os.Exit(0) } + cli := accountscli.New(l) + cli.WithAccountManager(k.Accounts()) + if err := cli.List(context.Background(), o); err != nil { l.Error(err) os.Exit(1) diff --git a/pkg/accounts/accounts.go b/pkg/accounts/cli/accounts.go similarity index 70% rename from pkg/accounts/accounts.go rename to pkg/accounts/cli/accounts.go index 8de383593..6fcde586e 100644 --- a/pkg/accounts/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -13,46 +13,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package accounts holds commands for accounts command. -package accounts +// Package cli holds commands for accounts command. +package cli import ( "context" "errors" "fmt" - "net/url" "strings" "github.com/AlecAivazis/survey/v2" "github.com/rodaine/table" "go.uber.org/zap" - "github.com/percona/everest/pkg/kubernetes" - accountsapi "github.com/percona/everest/pkg/kubernetes/client/accounts" + "github.com/percona/everest/pkg/accounts" ) // CLI provides functionality for managing user accounts via the CLI. type CLI struct { - kubeClient *kubernetes.Kubernetes - l *zap.SugaredLogger + accountManager accounts.Interface + l *zap.SugaredLogger } -// NewCLI creates a new CLI for running accounts commands. -func NewCLI(kubeConfigPath string, l *zap.SugaredLogger) (*CLI, error) { - cli := &CLI{ +// New creates a new CLI for running accounts commands. +func New(l *zap.SugaredLogger) *CLI { + return &CLI{ l: l.With("component", "accounts"), } - k, err := kubernetes.New(kubeConfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - cli.l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - return nil, err - } - cli.kubeClient = k - return cli, nil +} + +// WithAccountManager sets the account manager for the CLI. +func (c *CLI) WithAccountManager(m accounts.Interface) { + c.accountManager = m } func (c *CLI) runCredentialsWizard(username, password *string) error { @@ -83,7 +75,7 @@ func (c *CLI) Create(ctx context.Context, username, password string) error { if username == "" { return errors.New("username is required") } - if err := c.kubeClient.Accounts().Create(ctx, username, password); err != nil { + if err := c.accountManager.Create(ctx, username, password); err != nil { return err } c.l.Infof("User '%s' has been created", username) @@ -98,19 +90,10 @@ func (c *CLI) Delete(ctx context.Context, username, password string) error { if username == "" { return errors.New("username is required") } - user, err := c.kubeClient.Accounts().Get(ctx, username) - if err != nil { + if err := c.accountManager.Verify(ctx, username, password); err != nil { return err } - computedHash, err := c.kubeClient.Accounts().ComputePasswordHash(ctx, password) - if err != nil { - return err - } - if computedHash != user.PasswordHash { - return errors.New("incorrect password entered") - } - c.l.Infof("User '%s' has been deleted", username) - return c.kubeClient.Accounts().Delete(ctx, username) + return c.accountManager.Delete(ctx, username) } // ListOptions holds options for listing user accounts. @@ -147,18 +130,18 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { // Otherwise print in all caps. return strings.ToUpper(fmt.Sprintf(format, vals...)) }) - accounts, err := c.kubeClient.Accounts().List(ctx) + accountsList, err := c.accountManager.List(ctx) if err != nil { return err } // Return a table row for the given account. - row := func(account accountsapi.Account) []any { + row := func(user string, account *accounts.Account) []any { row := []any{} for _, heading := range headings { switch heading { case "user": - row = append(row, account.ID) + row = append(row, user) case "capabilities": row = append(row, account.Capabilities) case "enabled": @@ -167,8 +150,8 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { } return row } - for _, account := range accounts { - tbl.AddRow(row(account)...) + for user, a := range accountsList { + tbl.AddRow(row(user, a)...) } tbl.Print() return nil diff --git a/pkg/accounts/tests.go b/pkg/accounts/tests.go new file mode 100644 index 000000000..d3a61f347 --- /dev/null +++ b/pkg/accounts/tests.go @@ -0,0 +1,61 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package accounts + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Tests runs a series of tests on the provided accounts provider. +func Tests(t *testing.T, p Interface) { + ctx := context.Background() + // Check that there are currently no accounts. + accounts, err := p.List(ctx) + require.NoError(t, err) + assert.Empty(t, accounts) + + // Create a new account. + err = p.Create(ctx, "user1", "password1") + require.NoError(t, err) + + // Get user1. + user1, err := p.Get(ctx, "user1") + require.NoError(t, err) + assert.NotNil(t, user1) + assert.True(t, user1.Enabled) + + // List accounts. + accounts, err = p.List(ctx) + require.NoError(t, err) + assert.Len(t, accounts, 1) + + // Verify user1. + err = p.Verify(ctx, "user1", "password1") + require.NoError(t, err) + + // Delete user1. + err = p.Delete(ctx, "user1") + require.NoError(t, err) + + // Check that no accounts exist. + accounts, err = p.List(ctx) + require.NoError(t, err) + assert.Empty(t, accounts) +} diff --git a/pkg/accounts/types.go b/pkg/accounts/types.go new file mode 100644 index 000000000..c2429be9f --- /dev/null +++ b/pkg/accounts/types.go @@ -0,0 +1,57 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accounts ... +package accounts + +import ( + "context" + "errors" + "slices" +) + +// AccountCapability represents a capability of an account. +type AccountCapability string + +// ErrAccountNotFound is returned when an account is not found. +var ErrAccountNotFound = errors.New("account not found") + +const ( + // AccountCapabilityLogin represents capability to create UI session tokens. + AccountCapabilityLogin AccountCapability = "login" + // AccountCapabilityAPIKey represents capability to generate API auth tokens. + AccountCapabilityAPIKey AccountCapability = "apiKey" +) + +// Account is an internal representation of an Everest user account. +type Account struct { + Enabled bool `yaml:"enabled"` + Capabilities []AccountCapability `yaml:"capabilities"` + PasswordMtime string `yaml:"passwordMtime"` +} + +// HasCapability returns true if the given account has the specified capability. +func (a Account) HasCapability(c AccountCapability) bool { + return slices.Contains(a.Capabilities, c) +} + +// Interface provides the methods for managing Everest user accounts. +type Interface interface { + Create(ctx context.Context, username, password string) error + Get(ctx context.Context, username string) (*Account, error) + List(ctx context.Context) (map[string]*Account, error) + Delete(ctx context.Context, username string) error + Verify(ctx context.Context, username, password string) error +} diff --git a/pkg/install/install.go b/pkg/install/install.go index 393d18df6..ae85edaf2 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -36,6 +36,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/percona/everest/pkg/accounts" "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes" "github.com/percona/everest/pkg/version" @@ -53,7 +54,7 @@ Everest has been successfully installed! To view the password for the 'admin' user, run the following command: -kubectl get secret -n everest-system everest-accounts -o jsonpath='{.data.passwords\.yaml}' | base64 --decode | awk '/admin:/ {getline; print $2}' +kubectl get secret -n everest-system everest-accounts -o jsonpath='{.data.admin}' | base64 --decode To create a new user, run the following command: @@ -379,18 +380,6 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er if err = o.kubeClient.InstallEverest(ctx, common.SystemNamespace, v); err != nil { return err } - - // Create admin user for Everest. - // We set the password to "password" here, however we reset it in the next step. - // The reason is that `Create` hashes the password, but during the initial installation, - // we want the user to be able to view a plain text password which they will reset later. - if err := o.kubeClient.Accounts().Create(ctx, common.EverestAdminUser, "password"); err != nil { - return err - } - // Reset the password and store it in plain text for the user to view it. - if err := o.kubeClient.Accounts().ResetAdminPassword(ctx); err != nil { - return err - } } else { o.l.Info("Restarting Everest") if err := o.kubeClient.RestartOperator(ctx, common.PerconaEverestOperatorDeploymentName, common.SystemNamespace); err != nil { @@ -401,6 +390,10 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er } } + if err := o.createEverestAdminAccount(ctx); err != nil { + return err + } + o.l.Info("Updating cluster role bindings for everest-admin") if err := o.kubeClient.UpdateClusterRoleBinding(ctx, everestServiceAccountClusterRoleBinding, o.config.NamespacesList); err != nil { return err @@ -743,3 +736,18 @@ func validateRFC1035(s string) error { return nil } + +func (o *Install) createEverestAdminAccount(ctx context.Context) error { + o.l.Info("Creating Everest admin account") + // Check if admin already exists? + if _, err := o.kubeClient.Accounts().Get(ctx, common.EverestAdminUser); err == nil { + return nil + } else if !errors.Is(err, accounts.ErrAccountNotFound) { + return err + } + + if err := o.kubeClient.Accounts().Create(ctx, common.EverestAdminUser, ""); err != nil { + return err + } + return nil +} diff --git a/pkg/kubernetes/accounts.go b/pkg/kubernetes/accounts.go index 4fd0c09b6..601e9a879 100644 --- a/pkg/kubernetes/accounts.go +++ b/pkg/kubernetes/accounts.go @@ -17,25 +17,13 @@ package kubernetes import ( - "context" - - "github.com/percona/everest/pkg/kubernetes/client/accounts" + "github.com/percona/everest/pkg/accounts" + k8sAccounts "github.com/percona/everest/pkg/kubernetes/client/accounts" ) -// Accounts provides an interface for managing Everest user accounts. -type Accounts interface { - Create(ctx context.Context, username, password string) error - Get(ctx context.Context, username string) (*accounts.Account, error) - List(ctx context.Context) ([]accounts.Account, error) - Delete(ctx context.Context, username string) error - Update(ctx context.Context, username, password string) error - ComputePasswordHash(ctx context.Context, password string) (string, error) - ResetAdminPassword(ctx context.Context) error -} - // Accounts returns a new client for managing everest user accounts. // //nolint:ireturn,stylecheck -func (c *Kubernetes) Accounts() Accounts { - return accounts.New(c.client) +func (c *Kubernetes) Accounts() accounts.Interface { + return k8sAccounts.New(c.client) } diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 1db9d9519..f39a45867 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -22,358 +22,251 @@ import ( "crypto/sha256" "encoding/hex" "errors" - "slices" - "strings" "time" "golang.org/x/crypto/pbkdf2" - "gopkg.in/yaml.v3" + "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/AlekSi/pointer" + "github.com/percona/everest/pkg/accounts" "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes/client" ) -// AccountCapability represents a capability of an account. -type AccountCapability string - const ( - // AccountCapabilityLogin represents capability to create UI session tokens. - AccountCapabilityLogin AccountCapability = "login" - // AccountCapabilityAPIKey represents capability to generate API auth tokens. - AccountCapabilityAPIKey AccountCapability = "apiKey" + usersFile = "users.yaml" - usersFile = "users.yaml" - passwordFile = "passwords.yaml" + tempAdminPasswordSecret = "everest-admin-temp" ) -// ErrAccountNotFound is returned when an account is not found. -var ErrAccountNotFound = errors.New("account not found") - -// User contains user data. -type User struct { - Enabled bool `yaml:"enabled"` - Capabilities []AccountCapability `yaml:"capabilities"` -} - -// Password contains password data. -type Password struct { - PasswordHash string `yaml:"passwordHash"` - PasswordMTime string `yaml:"passwordMTime"` - - // Insecure is set to true if the password is not hashed. - // Generally this is set for the admin password after calling ResetAdminPassword. - Insecure *bool `yaml:"insecure,omitempty"` -} - -// Account contains user and password data. -type Account struct { - ID string - User - Password -} - -// HasCapability returns true if the given account has the specified capability. -func (a Account) HasCapability(c AccountCapability) bool { - return slices.Contains(a.Capabilities, c) -} - -// Client provides functionality for managing user accounts on Kubernetes. -type Client struct { +type configMapsClient struct { k client.KubeClientConnector } -// New returns a new Kubernetes based account manager for Everest. -func New(k client.KubeClientConnector) *Client { - return &Client{k: k} +// New returns an implementation of the accounts interface that +// manages everest accounts directly via ConfigMaps. +// +//nolint:ireturn +func New(k client.KubeClientConnector) accounts.Interface { + return &configMapsClient{k: k} } // Get returns an account by username. -func (a *Client) Get(ctx context.Context, username string) (*Account, error) { - users, err := a.listAllUsers(ctx) +func (a *configMapsClient) Get(ctx context.Context, username string) (*accounts.Account, error) { + users, err := a.listAllAccounts(ctx) if err != nil { return nil, err } user, found := users[username] if !found { - return nil, ErrAccountNotFound - } - passwords, err := a.listAllPasswords(ctx) - if err != nil { - return nil, err - } - pass, found := passwords[username] - if !found { - return nil, ErrAccountNotFound + return nil, accounts.ErrAccountNotFound } - return &Account{ - ID: username, - User: user, - Password: pass, - }, nil + return user, nil } -// ResetAdminPassword sets a new password for the admin account. -// This password will not be hashed, so that the user can view, login and reset it. -func (a *Client) ResetAdminPassword(ctx context.Context) error { - admin, err := a.Get(ctx, common.EverestAdminUser) - if err != nil { - return err - } - b := make([]byte, 64) - if _, err := rand.Read(b); err != nil { - return errors.Join(err, errors.New("failed to generate random password")) - } - admin.Password = Password{ - PasswordHash: hex.EncodeToString(b), - PasswordMTime: time.Now().Format(time.RFC3339), - Insecure: pointer.To(true), - } - return a.setAccounts(ctx, []Account{*admin}, true) +// List returns a list of all accounts. +func (a *configMapsClient) List(ctx context.Context) (map[string]*accounts.Account, error) { + return a.listAllAccounts(ctx) } -// List returns a list of all accounts. -func (a *Client) List(ctx context.Context) ([]Account, error) { - users, err := a.listAllUsers(ctx) +func (a *configMapsClient) listAllAccounts(ctx context.Context) (map[string]*accounts.Account, error) { + result := make(map[string]*accounts.Account) + cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return nil, err } - passwords, err := a.listAllPasswords(ctx) - if err != nil { + if err := yaml.Unmarshal([]byte(cm.Data[usersFile]), result); err != nil { return nil, err } - return mergeUserPassToAccounts(users, passwords), nil + return result, nil } // Create a new user account. -func (a *Client) Create(ctx context.Context, username, password string) error { - // Check if this user exists? - users, err := a.listAllUsers(ctx) - if err != nil { +func (a *configMapsClient) Create(ctx context.Context, username, password string) error { + if username == common.EverestAdminUser && password == "" { + return a.createAdminWithTempPassword(ctx) + } + account := &accounts.Account{ + Enabled: true, + Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, + PasswordMtime: time.Now().Format(time.RFC3339), + } + if err := a.setAccount(ctx, username, account); err != nil { return err } - if _, found := users[username]; found { - return errors.New("user already exists") + if err := a.setPassword(ctx, username, password); err != nil { + return err } - hash, err := a.computePasswordHash(ctx, password) - if err != nil { + return nil +} + +func (a *configMapsClient) createAdminWithTempPassword(ctx context.Context) error { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { return err } - acc := Account{ - ID: username, - User: User{ - Enabled: true, - Capabilities: []AccountCapability{AccountCapabilityLogin}, // XX: for now we only support login + password := hex.EncodeToString(b) + tempAdminSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: tempAdminPasswordSecret, + Namespace: common.SystemNamespace, }, - Password: Password{ - PasswordHash: hash, - PasswordMTime: time.Now().Format(time.RFC3339), + Data: map[string][]byte{ + "password": []byte(password), }, } - return a.setAccounts(ctx, []Account{acc}, true) -} - -func (a *Client) salt(ctx context.Context) ([]byte, error) { - ns, err := a.k.GetNamespace(ctx, common.SystemNamespace) - if err != nil { - return nil, err + if _, err := a.k.CreateSecret(ctx, tempAdminSecret); err != nil { + return err } - return []byte(ns.UID), nil + if err := a.setAccount(ctx, common.EverestAdminUser, &accounts.Account{ + Enabled: true, + Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, + PasswordMtime: time.Now().Format(time.RFC3339), + }); err != nil { + return err + } + return nil } -// Delete an existing user account specified by username. -func (a *Client) Delete(ctx context.Context, username string) error { - // Check if this user exists? - users, err := a.listAllUsers(ctx) +func (a *configMapsClient) setPassword(ctx context.Context, username, password string) error { + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return err } - if _, found := users[username]; !found { - return ErrAccountNotFound - } - // Remove user from the list. - delete(users, username) - passwords, err := a.listAllPasswords(ctx) + hash, err := a.computePasswordHash(ctx, password) if err != nil { + return errors.Join(err, errors.New("failed to compute hash")) + } + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + secret.Data[username] = []byte(hash) + if _, err := a.k.UpdateSecret(ctx, secret); err != nil { return err } - delete(passwords, username) - acc := mergeUserPassToAccounts(users, passwords) - return a.setAccounts(ctx, acc, false) + return nil } -// Update an existing user account specified by username. -func (a *Client) Update(ctx context.Context, username, password string) error { - // Check if this user exists? - users, err := a.listAllUsers(ctx) +func (a *configMapsClient) setAccount(ctx context.Context, username string, account *accounts.Account) error { + cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return err } - if _, found := users[username]; !found { - return ErrAccountNotFound - } - // Update the password. - passwords, err := a.listAllPasswords(ctx) - if err != nil { + accounts := make(map[string]*accounts.Account) + if err := yaml.Unmarshal([]byte(cm.Data[usersFile]), &accounts); err != nil { return err } - hash, err := a.computePasswordHash(ctx, password) + accounts[username] = account + data, err := yaml.Marshal(accounts) if err != nil { return err } - passwords[username] = Password{ - PasswordHash: hash, - PasswordMTime: time.Now().Format(time.RFC3339), + if cm.Data == nil { + cm.Data = make(map[string]string) } - return a.setAccounts(ctx, mergeUserPassToAccounts(users, passwords), true) -} - -// ComputePasswordHash computes the password hash for a given password. -func (a *Client) ComputePasswordHash(ctx context.Context, password string) (string, error) { - return a.computePasswordHash(ctx, password) + cm.Data[usersFile] = string(data) + if _, err := a.k.UpdateConfigMap(ctx, cm); err != nil { + return err + } + return nil } -func (a *Client) computePasswordHash(ctx context.Context, password string) (string, error) { - salt, err := a.salt(ctx) +func (a *configMapsClient) salt(ctx context.Context) ([]byte, error) { + ns, err := a.k.GetNamespace(ctx, common.SystemNamespace) if err != nil { - return "", errors.Join(err, errors.New("failed to get salt")) + return nil, err } - hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) - return string(hash), nil -} - -func mergeUserPassToAccounts(users map[string]User, passwords map[string]Password) []Account { - accounts := make([]Account, 0, len(users)) - for name, user := range users { - pass, found := passwords[name] - if !found { - continue - } - accounts = append(accounts, Account{ - ID: name, - User: user, - Password: pass, - }) - } - slices.SortFunc(accounts, func(a, b Account) int { - return strings.Compare(a.ID, b.ID) - }) - return accounts + return []byte(ns.UID), nil } -// Given a list of accounts, update the ConfigMap and Secret. -// If patch is true, existing all existing accounts are preserved. -// If patch is false, accounts are replaced with the new list. -func (a *Client) setAccounts( - ctx context.Context, - accounts []Account, - patch bool, -) error { - var ( - err error - users = make(map[string]User) - passwords = make(map[string]Password) - ) - if patch { - // Get existing users and passwords. - users, err = a.listAllUsers(ctx) - if err != nil { - return err - } - passwords, err = a.listAllPasswords(ctx) - if err != nil { - return err - } - // Modify accounts. - for _, acc := range accounts { - users[acc.ID] = acc.User - passwords[acc.ID] = acc.Password - } - } else { - for _, acc := range accounts { - users[acc.ID] = acc.User - passwords[acc.ID] = acc.Password - } - } - if err := a.updateConfigMap(ctx, users); err != nil { - return err - } - if err := a.updateSecret(ctx, passwords); err != nil { +// Delete an existing user account specified by username. +func (a *configMapsClient) Delete(ctx context.Context, username string) error { + users, err := a.listAllAccounts(ctx) + if err != nil { return err } - return nil -} - -func (a *Client) updateConfigMap(ctx context.Context, users map[string]User) error { - userB, err := yaml.Marshal(users) + // Update ConfigMap. + delete(users, username) + b, err := yaml.Marshal(users) if err != nil { return err } + cmData := map[string]string{ + usersFile: string(b), + } cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: common.EverestAccountsConfigName, Namespace: common.SystemNamespace, }, - BinaryData: map[string][]byte{ - usersFile: userB, - }, + Data: cmData, } if _, err := a.k.UpdateConfigMap(ctx, cm); err != nil { return err } - return nil -} - -func (a *Client) updateSecret(ctx context.Context, passwords map[string]Password) error { - passB, err := yaml.Marshal(passwords) + // Update Secret. + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return err } - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestAccountsConfigName, - Namespace: common.SystemNamespace, - }, - Data: map[string][]byte{ - passwordFile: passB, - }, - } + secretData := secret.Data + delete(secretData, username) + secret.Data = secretData if _, err := a.k.UpdateSecret(ctx, secret); err != nil { return err } return nil } -func (a *Client) listAllUsers(ctx context.Context) (map[string]User, error) { - cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) +func (a *configMapsClient) Verify(ctx context.Context, username, password string) error { + if username == common.EverestAdminUser { + if err := a.tryVerifyTempAdminPassword(ctx, password); err == nil { + return nil + } + } + users, err := a.listAllAccounts(ctx) if err != nil { - return nil, err + return err } - usersYaml, found := cm.BinaryData[usersFile] + _, found := users[username] if !found { - return make(map[string]User), nil + return accounts.ErrAccountNotFound } - var users map[string]User - if err := yaml.Unmarshal(usersYaml, &users); err != nil { - return nil, err + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) + if err != nil { + return err } - return users, nil + computedHash, err := a.computePasswordHash(ctx, password) + if err != nil { + return err + } + storedhash, found := secret.Data[username] + if !found { + return accounts.ErrAccountNotFound + } + if string(storedhash) != computedHash { + return errors.New("incorrect password provided") + } + return nil } -func (a *Client) listAllPasswords(ctx context.Context) (map[string]Password, error) { - secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) +func (a *configMapsClient) tryVerifyTempAdminPassword(ctx context.Context, password string) error { + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, tempAdminPasswordSecret) if err != nil { - return nil, err + return err } - passwordsYaml, found := secret.Data[passwordFile] - if !found { - return make(map[string]Password), nil + if string(secret.Data["password"]) != password { + return errors.New("incorrect password provided") } - var passwords map[string]Password - if err := yaml.Unmarshal(passwordsYaml, &passwords); err != nil { - return nil, err + return nil +} + +func (a *configMapsClient) computePasswordHash(ctx context.Context, password string) (string, error) { + salt, err := a.salt(ctx) + if err != nil { + return "", errors.Join(err, errors.New("failed to get salt")) } - return passwords, nil + hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + return string(hash), nil } diff --git a/pkg/kubernetes/client/accounts/accounts_test.go b/pkg/kubernetes/client/accounts/accounts_test.go new file mode 100644 index 000000000..94abd4acd --- /dev/null +++ b/pkg/kubernetes/client/accounts/accounts_test.go @@ -0,0 +1,59 @@ +package accounts + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/percona/everest/pkg/accounts" + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes/client" +) + +func TestAccounts(t *testing.T) { + t.Parallel() + ctx := context.Background() + + c := client.NewFromFakeClient() + + // Create system namespace for testing. + _, err := c.Clientset(). + CoreV1(). + Namespaces(). + Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: common.SystemNamespace}, + }, metav1.CreateOptions{}, + ) + require.NoError(t, err) + + // Prepare configmap. + _, err = c.Clientset(). + CoreV1(). + ConfigMaps(common.SystemNamespace). + Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.EverestAccountsConfigName, + Namespace: common.SystemNamespace, + }, + }, metav1.CreateOptions{}, + ) + require.NoError(t, err) + + // Prepare secret. + _, err = c.Clientset(). + CoreV1(). + Secrets(common.SystemNamespace). + Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.EverestAccountsConfigName, + Namespace: common.SystemNamespace, + }, + }, metav1.CreateOptions{}, + ) + require.NoError(t, err) + + accounts.Tests(t, New(c)) +} diff --git a/pkg/kubernetes/client/accounts/kubernetes_test.go b/pkg/kubernetes/client/accounts/kubernetes_test.go deleted file mode 100644 index 92721e39d..000000000 --- a/pkg/kubernetes/client/accounts/kubernetes_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package accounts - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/percona/everest/pkg/common" - "github.com/percona/everest/pkg/kubernetes/client" -) - -func TestAccounts(t *testing.T) { - t.Parallel() - c := client.NewFromFakeClient() - ctx := context.Background() - - // Create system namespace for testing. - _, err := c.Clientset(). - CoreV1(). - Namespaces(). - Create(ctx, &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{Name: common.SystemNamespace}, - }, metav1.CreateOptions{}, - ) - require.NoError(t, err) - // Prepare configmap. - _, err = c.Clientset(). - CoreV1(). - ConfigMaps(common.SystemNamespace). - Create(ctx, &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestAccountsConfigName, - Namespace: common.SystemNamespace, - }, - }, metav1.CreateOptions{}, - ) - require.NoError(t, err) - // Prepare secret. - _, err = c.Clientset(). - CoreV1(). - Secrets(common.SystemNamespace). - Create(ctx, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestAccountsConfigName, - Namespace: common.SystemNamespace, - }, - }, metav1.CreateOptions{}, - ) - require.NoError(t, err) - - mgr := New(c) - - // Assert that initially there are no accounts. - accounts, err := mgr.List(ctx) - require.NoError(t, err) - assert.Empty(t, accounts) - - // Create user1 - err = mgr.Create(ctx, "user1", "password") - require.NoError(t, err) - - // Check that a new account is created. - accounts, err = mgr.List(ctx) - require.NoError(t, err) - assert.Len(t, accounts, 1) - assert.Equal(t, "user1", accounts[0].ID) - assert.True(t, accounts[0].Enabled) - assert.NotEmpty(t, accounts[0].Password.PasswordHash) - assert.NotEmpty(t, accounts[0].Password.PasswordMTime) - user1, err := mgr.Get(ctx, "user1") - require.NoError(t, err) - assert.Equal(t, "user1", user1.ID) - assert.True(t, user1.Enabled) - assert.NotEmpty(t, user1.Password.PasswordHash) - assert.NotEmpty(t, user1.Password.PasswordMTime) - - passwordhash := user1.Password.PasswordHash - - // Update password of user1. - err = mgr.Update(ctx, "user1", "new-password") - require.NoError(t, err) - user1, err = mgr.Get(ctx, "user1") - require.NoError(t, err) - assert.NotEqual(t, passwordhash, user1.Password.PasswordHash) - - // Delete non-existing user. - err = mgr.Delete(ctx, "not-existing") - require.Error(t, err) - require.ErrorIs(t, err, ErrAccountNotFound) - - // Delete user1. - err = mgr.Delete(ctx, "user1") - require.NoError(t, err) - - // Check that no users exists. - accounts, err = mgr.List(ctx) - require.NoError(t, err) - assert.Empty(t, accounts) -} diff --git a/pkg/kubernetes/kubernetes_interface.go b/pkg/kubernetes/kubernetes_interface.go index de87a178f..8e7ab96d1 100644 --- a/pkg/kubernetes/kubernetes_interface.go +++ b/pkg/kubernetes/kubernetes_interface.go @@ -15,6 +15,7 @@ import ( "k8s.io/client-go/rest" everestv1alpha1 "github.com/percona/everest-operator/api/v1alpha1" + "github.com/percona/everest/pkg/accounts" "github.com/percona/everest/pkg/kubernetes/client" ) @@ -23,7 +24,7 @@ type KubernetesConnector interface { // Accounts returns a new client for managing everest user accounts. // //nolint:ireturn,stylecheck - Accounts() Accounts + Accounts() accounts.Interface // GetDeployment returns k8s deployment by provided name and namespace. GetDeployment(ctx context.Context, name, namespace string) (*appsv1.Deployment, error) // UpdateDeployment updates a deployment and returns the updated object. diff --git a/pkg/kubernetes/mock_kubernetes_connector.go b/pkg/kubernetes/mock_kubernetes_connector.go index 9ba455f1a..1968caec9 100644 --- a/pkg/kubernetes/mock_kubernetes_connector.go +++ b/pkg/kubernetes/mock_kubernetes_connector.go @@ -16,6 +16,7 @@ import ( rest "k8s.io/client-go/rest" v1alpha1 "github.com/percona/everest-operator/api/v1alpha1" + accounts "github.com/percona/everest/pkg/accounts" client "github.com/percona/everest/pkg/kubernetes/client" ) @@ -25,19 +26,19 @@ type MockKubernetesConnector struct { } // Accounts provides a mock function with given fields: -func (_m *MockKubernetesConnector) Accounts() Accounts { +func (_m *MockKubernetesConnector) Accounts() accounts.Interface { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Accounts") } - var r0 Accounts - if rf, ok := ret.Get(0).(func() Accounts); ok { + var r0 accounts.Interface + if rf, ok := ret.Get(0).(func() accounts.Interface); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(Accounts) + r0 = ret.Get(0).(accounts.Interface) } } diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 99c139f6f..b12393c6e 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -18,15 +18,12 @@ package session import ( "context" - "errors" "fmt" "time" - "github.com/AlekSi/pointer" "github.com/golang-jwt/jwt/v4" - "github.com/percona/everest/pkg/kubernetes" - "github.com/percona/everest/pkg/kubernetes/client/accounts" + "github.com/percona/everest/pkg/accounts" ) const ( @@ -36,7 +33,7 @@ const ( // Manager provides functionality for creating and managing JWT tokens. type Manager struct { - accountManager kubernetes.Accounts + accountManager accounts.Interface signingKey []byte } @@ -53,9 +50,9 @@ func New(options ...Option) *Manager { } // WithAccountManager sets the account manager to use for verifying user credentials. -func WithAccountManager(am kubernetes.Accounts) Option { +func WithAccountManager(i accounts.Interface) Option { return func(m *Manager) { - m.accountManager = am + m.accountManager = i } } @@ -99,26 +96,13 @@ func (mgr *Manager) Authenticate(ctx context.Context, username string, password return fmt.Errorf("blank passwords are not allowed") } - account, err := mgr.accountManager.Get(ctx, username) - if err != nil { + if err := mgr.accountManager.Verify(ctx, username, password); err != nil { return err } - computedHash, err := mgr.accountManager.ComputePasswordHash(ctx, password) + account, err := mgr.accountManager.Get(ctx, username) if err != nil { - return errors.Join(err, errors.New("failed to compute password hash")) - } - - // For secure accounts, compare the computed hash with the stored hash. - if !pointer.GetBool(account.Insecure) && - computedHash != account.PasswordHash { - return errors.New("invalid password") - } - - // For insecure accounts, compare the password with the stored hash. - if pointer.GetBool(account.Insecure) && - password != account.PasswordHash { - return errors.New("invalid password") + return err } if !account.Enabled { From 330c23c8d51aa1b8740828ceae707a823380dfa0 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 12:01:20 +0530 Subject: [PATCH 034/103] go mod tidy Signed-off-by: Mayank Shah --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 19e4918bd..6149c6534 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( golang.org/x/sync v0.7.0 golang.org/x/time v0.5.0 google.golang.org/protobuf v1.33.0 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.3 k8s.io/apiextensions-apiserver v0.29.3 @@ -161,7 +162,6 @@ require ( google.golang.org/grpc v1.61.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240221221325-2ac9dc51f3f1 // indirect k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect From 4752d08684488835f1b8471365f6f8ff46962c90 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 12:14:56 +0530 Subject: [PATCH 035/103] update dev Signed-off-by: Mayank Shah --- dev/Tiltfile | 1 + dev/everest-accounts.yaml | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dev/Tiltfile b/dev/Tiltfile index 16f3303e3..a82ce3cfe 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -397,6 +397,7 @@ docker_build_with_restart('perconalab/everest', ) k8s_yaml(namespace_inject('jwt-secret.yaml', everest_namespace)) +k8s_yaml(namespace_inject('everest-accounts.yaml', everest_namespace)) k8s_yaml(namespace_inject('%s/deploy/quickstart-k8s.yaml' % backend_dir, everest_namespace)) k8s_resource( workload='percona-everest', diff --git a/dev/everest-accounts.yaml b/dev/everest-accounts.yaml index 02e981e4c..c70b4a3a3 100644 --- a/dev/everest-accounts.yaml +++ b/dev/everest-accounts.yaml @@ -11,6 +11,11 @@ apiVersion: v1 kind: Secret metadata: name: everest-accounts +--- +apiVersion: v1 +kind: Secret +metadata: + name: everest-admin-temp data: - passwords.yaml: YWRtaW46CiAgaW5zZWN1cmU6IHRydWUKICBwYXNzd29yZEhhc2g6IGFkbWluCg== + password: ZXZlcmVzdGFkbWluCg== # everestadmin --- From d2b8e4494cd542343308d8b35636ae89ac2d59bd Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 12:43:45 +0530 Subject: [PATCH 036/103] more fixes Signed-off-by: Mayank Shah --- dev/Tiltfile | 4 ++-- dev/everest-accounts.yaml | 4 +++- pkg/accounts/types.go | 8 ++++++-- pkg/install/install.go | 2 +- pkg/kubernetes/client/accounts/accounts.go | 24 ++++++++++++++-------- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/dev/Tiltfile b/dev/Tiltfile index a82ce3cfe..81aa6ef50 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -396,9 +396,9 @@ docker_build_with_restart('perconalab/everest', ] ) -k8s_yaml(namespace_inject('jwt-secret.yaml', everest_namespace)) -k8s_yaml(namespace_inject('everest-accounts.yaml', everest_namespace)) k8s_yaml(namespace_inject('%s/deploy/quickstart-k8s.yaml' % backend_dir, everest_namespace)) +k8s_yaml(namespace_inject('jwt-secret.yaml', everest_namespace)) +k8s_yaml(namespace_inject('everest-accounts.yaml', everest_namespace),allow_duplicates=True) k8s_resource( workload='percona-everest', objects=[ diff --git a/dev/everest-accounts.yaml b/dev/everest-accounts.yaml index c70b4a3a3..b57cdd03b 100644 --- a/dev/everest-accounts.yaml +++ b/dev/everest-accounts.yaml @@ -6,6 +6,8 @@ data: users.yaml : | admin: enabled: true + capabilities: + - login --- apiVersion: v1 kind: Secret @@ -17,5 +19,5 @@ kind: Secret metadata: name: everest-admin-temp data: - password: ZXZlcmVzdGFkbWluCg== # everestadmin + password: ZXZlcmVzdGFkbWlu # everestadmin --- diff --git a/pkg/accounts/types.go b/pkg/accounts/types.go index c2429be9f..13b0061e9 100644 --- a/pkg/accounts/types.go +++ b/pkg/accounts/types.go @@ -25,8 +25,12 @@ import ( // AccountCapability represents a capability of an account. type AccountCapability string -// ErrAccountNotFound is returned when an account is not found. -var ErrAccountNotFound = errors.New("account not found") +var ( + // ErrAccountNotFound is returned when an account is not found. + ErrAccountNotFound = errors.New("account not found") + // ErrIncorrectPassword is returned when the password is invalid. + ErrIncorrectPassword = errors.New("invalid password") +) const ( // AccountCapabilityLogin represents capability to create UI session tokens. diff --git a/pkg/install/install.go b/pkg/install/install.go index ae85edaf2..6093209bd 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -54,7 +54,7 @@ Everest has been successfully installed! To view the password for the 'admin' user, run the following command: -kubectl get secret -n everest-system everest-accounts -o jsonpath='{.data.admin}' | base64 --decode +kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode To create a new user, run the following command: diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index f39a45867..e064c100a 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -27,6 +27,7 @@ import ( "golang.org/x/crypto/pbkdf2" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/percona/everest/pkg/accounts" @@ -35,8 +36,7 @@ import ( ) const ( - usersFile = "users.yaml" - + usersFile = "users.yaml" tempAdminPasswordSecret = "everest-admin-temp" ) @@ -116,6 +116,7 @@ func (a *configMapsClient) createAdminWithTempPassword(ctx context.Context) erro "password": []byte(password), }, } + // This temporary secret is deleted once the admin password is reset. if _, err := a.k.CreateSecret(ctx, tempAdminSecret); err != nil { return err } @@ -221,8 +222,8 @@ func (a *configMapsClient) Delete(ctx context.Context, username string) error { func (a *configMapsClient) Verify(ctx context.Context, username, password string) error { if username == common.EverestAdminUser { - if err := a.tryVerifyTempAdminPassword(ctx, password); err == nil { - return nil + if shouldSkip, err := a.tryVerifyTempAdminPassword(ctx, password); !shouldSkip { + return err } } users, err := a.listAllAccounts(ctx) @@ -246,20 +247,25 @@ func (a *configMapsClient) Verify(ctx context.Context, username, password string return accounts.ErrAccountNotFound } if string(storedhash) != computedHash { - return errors.New("incorrect password provided") + return accounts.ErrIncorrectPassword } return nil } -func (a *configMapsClient) tryVerifyTempAdminPassword(ctx context.Context, password string) error { +// try to check with the temporary password. +// Returns: [skip(bool), error] +func (a *configMapsClient) tryVerifyTempAdminPassword(ctx context.Context, password string) (bool, error) { secret, err := a.k.GetSecret(ctx, common.SystemNamespace, tempAdminPasswordSecret) if err != nil { - return err + if k8serrors.IsNotFound(err) { + return true, nil + } + return false, err } if string(secret.Data["password"]) != password { - return errors.New("incorrect password provided") + return false, accounts.ErrIncorrectPassword } - return nil + return false, nil } func (a *configMapsClient) computePasswordHash(ctx context.Context, password string) (string, error) { From d1c34835dccf008bf16f647977b983f1e16ccc0c Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 13:09:48 +0530 Subject: [PATCH 037/103] revert Signed-off-by: Mayank Shah --- pkg/kubernetes/client/accounts/accounts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index e064c100a..baa031369 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -253,7 +253,7 @@ func (a *configMapsClient) Verify(ctx context.Context, username, password string } // try to check with the temporary password. -// Returns: [skip(bool), error] +// Returns: [skip(bool), error]. func (a *configMapsClient) tryVerifyTempAdminPassword(ctx context.Context, password string) (bool, error) { secret, err := a.k.GetSecret(ctx, common.SystemNamespace, tempAdminPasswordSecret) if err != nil { From 35c4810a37e8697406ef927c596380f738b13207 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 13:23:49 +0530 Subject: [PATCH 038/103] clean-up and set cookie Signed-off-by: Mayank Shah --- api/everest.go | 20 ++++--- api/session.go | 5 ++ commands/root.go | 1 - commands/token.go | 34 ------------ commands/token/reset.go | 72 ------------------------- pkg/common/constants.go | 3 ++ pkg/token/reset.go | 117 ---------------------------------------- 7 files changed, 22 insertions(+), 230 deletions(-) delete mode 100644 commands/token.go delete mode 100644 commands/token/reset.go delete mode 100644 pkg/token/reset.go diff --git a/api/everest.go b/api/everest.go index da986f5ad..033c5578f 100644 --- a/api/everest.go +++ b/api/everest.go @@ -36,6 +36,7 @@ import ( "github.com/percona/everest/cmd/config" "github.com/percona/everest/pkg/auth" + "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes" "github.com/percona/everest/pkg/session" "github.com/percona/everest/public" @@ -130,21 +131,28 @@ func (e *EverestServer) initHTTPServer() error { // Use our validation middleware to check all requests against the OpenAPI schema. apiGroup := e.echo.Group(basePath) - apiGroup.Use(echojwt.WithConfig(echojwt.Config{ - Skipper: func(c echo.Context) bool { - return strings.Contains(c.Request().URL.Path, "session") - }, - SigningKey: []byte(e.config.JWTSigningKey), - })) apiGroup.Use(middleware.OapiRequestValidatorWithOptions(swagger, &middleware.Options{ SilenceServersWarning: true, })) + apiGroup.Use(e.jwtMiddleWare()) apiGroup.Use(e.checkOperatorUpgradeState) RegisterHandlers(apiGroup, e) return nil } +func (e *EverestServer) jwtMiddleWare() echo.MiddlewareFunc { + tokenLookup := "header:Authorization:Bearer " + tokenLookup = tokenLookup + ",cookie:" + common.EverestTokenCookie + return echojwt.WithConfig(echojwt.Config{ + Skipper: func(c echo.Context) bool { + return strings.Contains(c.Request().URL.Path, "session") + }, + SigningKey: []byte(e.config.JWTSigningKey), + TokenLookup: tokenLookup, + }) +} + // Start starts everest server. func (e *EverestServer) Start() error { return e.echo.Start(fmt.Sprintf("0.0.0.0:%d", e.config.HTTPPort)) diff --git a/api/session.go b/api/session.go index cf7d0b65f..766bc2c30 100644 --- a/api/session.go +++ b/api/session.go @@ -25,6 +25,7 @@ import ( "github.com/labstack/echo/v4" "github.com/percona/everest/pkg/accounts" + "github.com/percona/everest/pkg/common" ) const ( @@ -58,5 +59,9 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { return err } + ctx.SetCookie(&http.Cookie{ + Name: common.EverestTokenCookie, + Value: jwtToken, + }) return ctx.JSON(http.StatusOK, map[string]string{"token": jwtToken}) } diff --git a/commands/root.go b/commands/root.go index e111b2502..58b7e582b 100644 --- a/commands/root.go +++ b/commands/root.go @@ -38,7 +38,6 @@ func NewRootCmd(l *zap.SugaredLogger) *cobra.Command { rootCmd.PersistentFlags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") rootCmd.AddCommand(newInstallCmd(l)) - rootCmd.AddCommand(newTokenCmd(l)) rootCmd.AddCommand(newVersionCmd(l)) rootCmd.AddCommand(newUpgradeCmd(l)) rootCmd.AddCommand(newUninstallCmd(l)) diff --git a/commands/token.go b/commands/token.go deleted file mode 100644 index 98a6a3581..000000000 --- a/commands/token.go +++ /dev/null @@ -1,34 +0,0 @@ -// everest -// Copyright (C) 2023 Percona LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package commands ... -package commands - -import ( - "github.com/spf13/cobra" - "go.uber.org/zap" - - "github.com/percona/everest/commands/token" -) - -func newTokenCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "token", - } - - cmd.AddCommand(token.NewResetCmd(l)) - - return cmd -} diff --git a/commands/token/reset.go b/commands/token/reset.go deleted file mode 100644 index b77f5c0a9..000000000 --- a/commands/token/reset.go +++ /dev/null @@ -1,72 +0,0 @@ -// everest -// Copyright (C) 2023 Percona LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package token holds commands for token command. -package token - -import ( - "os" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" - - "github.com/percona/everest/pkg/common" - "github.com/percona/everest/pkg/output" - "github.com/percona/everest/pkg/token" -) - -// NewResetCmd returns a new versions command. -func NewResetCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "reset", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initResetViperFlags(cmd) - - c, err := parseResetConfig() - if err != nil { - os.Exit(1) - } - - c.Namespace = common.SystemNamespace - command, err := token.NewReset(*c, l) - if err != nil { - output.PrintError(err, l) - os.Exit(1) - } - - res, err := command.Run(cmd.Context()) - if err != nil { - output.PrintError(err, l) - os.Exit(1) - } - - output.PrintOutput(cmd, l, res) - }, - } - - return cmd -} - -func initResetViperFlags(cmd *cobra.Command) { - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec -} - -func parseResetConfig() (*token.ResetConfig, error) { - c := &token.ResetConfig{} - err := viper.Unmarshal(c) - return c, err -} diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 06a8cc2b3..7b3c1f895 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -44,4 +44,7 @@ const ( // EverestAdminUser is the name of the admin user. EverestAdminUser = "admin" + + // EverestTokenCookie is the name of the cookie that holds the token. + EverestTokenCookie = "everest_token" ) diff --git a/pkg/token/reset.go b/pkg/token/reset.go deleted file mode 100644 index 77e0c3eb9..000000000 --- a/pkg/token/reset.go +++ /dev/null @@ -1,117 +0,0 @@ -// everest -// Copyright (C) 2023 Percona LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package token holds the main logic for token commands. -package token - -import ( - "context" - "crypto/sha256" - "errors" - "fmt" - "net/url" - - "github.com/dchest/uniuri" - "go.uber.org/zap" - "golang.org/x/crypto/pbkdf2" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/percona/everest/pkg/kubernetes" -) - -// Reset implements the main logic for command. -type Reset struct { - config ResetConfig - l *zap.SugaredLogger - - kubeClient *kubernetes.Kubernetes -} - -type ( - // ResetConfig stores configuration for the reset command. - ResetConfig struct { - // KubeconfigPath is a path to a kubeconfig - KubeconfigPath string `mapstructure:"kubeconfig"` - // Namespace defines the namespace token shall be reset in. - Namespace string - } - - // ResetResponse is a response from the reset command. - ResetResponse struct { - // Token is plain-text token generated by the command. - Token string `json:"token,omitempty"` - } -) - -// SecretName stores the name of the secret to store Everest token. -const SecretName = "everest-token" - -func (r ResetResponse) String() string { - return fmt.Sprintf("Here's your authorization token for accessing the Everest UI and API:\n\n\033[1m%s\033[0m\n\nStore this token securely as you will not be able to retrieve it later. If you ever need to reset it, use the following command:\neverestctl token reset", r.Token) -} - -// NewReset returns a new Reset struct. -func NewReset(c ResetConfig, l *zap.SugaredLogger) (*Reset, error) { - cli := &Reset{ - config: c, - l: l.With("component", "token/reset"), - } - - k, err := kubernetes.New(c.KubeconfigPath, cli.l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - cli.l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - return nil, err - } - cli.kubeClient = k - - return cli, nil -} - -// Run runs the reset command. -func (r *Reset) Run(ctx context.Context) (*ResetResponse, error) { - ns, err := r.kubeClient.GetNamespace(ctx, r.config.Namespace) - if err != nil { - return nil, errors.Join(err, errors.New("could not get namespace from Kubernetes")) - } - - newToken := uniuri.NewLen(128) - salt := []byte(ns.UID) - hash := pbkdf2.Key([]byte(newToken), salt, 4096, 32, sha256.New) - - err = r.kubeClient.SetSecret(&corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: SecretName, - Namespace: r.config.Namespace, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "token": hash, - }, - }) - if err != nil { - return nil, errors.Join(err, errors.New("could not update token in Kubernetes")) - } - - return &ResetResponse{Token: newToken}, nil -} From 3f5f7da99c27db4bcaab24366e62a6d115701311 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 14:43:43 +0530 Subject: [PATCH 039/103] fix CI Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index d6726c029..efbd53700 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -332,10 +332,6 @@ jobs: --skip-wizard \ --namespaces everest - # API_TOKEN is used later by "make test" - echo "API_TOKEN=$(./bin/everestctl token reset --json | jq .token -r)" >> $GITHUB_ENV - - - name: Patch Everest Deployment to use the PR image run: | kubectl -n everest-system patch deployment percona-everest --type strategic --patch-file dev/patch-deployment-image.yaml @@ -345,6 +341,12 @@ jobs: run: | kubectl port-forward --namespace everest-system deployment/percona-everest 8080:8080 & + - name: Log into Everest as admin + run: | + EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode) + echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"admin\",\"password\": \"'\"$EVEREST_PASS\"'\"}' | jq -r .token)" >> $GITHUB_ENV + echo $GITHUB_ENV # todo: remove + - name: Run integration tests run: | cd api-tests From ad435ac366511352533c8a339e9e2e9365e339ac Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 15:21:02 +0530 Subject: [PATCH 040/103] go mod tidy Signed-off-by: Mayank Shah --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 6149c6534..7ce332f70 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/Percona-Lab/percona-version-service v0.0.0-20240311164804-ffbc02387a1b github.com/aws/aws-sdk-go v1.51.18 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/dchest/uniuri v1.2.0 github.com/getkin/kin-openapi v0.124.0 github.com/go-logr/zapr v1.3.0 github.com/golang-jwt/jwt/v4 v4.5.0 diff --git a/go.sum b/go.sum index 5df7cfb70..9429ca8c6 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= -github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= From 8d55107292de73f389edb6a3dddf825e46ee10f1 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 16:25:34 +0530 Subject: [PATCH 041/103] remove todo line Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index efbd53700..c8ee22049 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -345,7 +345,6 @@ jobs: run: | EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode) echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"admin\",\"password\": \"'\"$EVEREST_PASS\"'\"}' | jq -r .token)" >> $GITHUB_ENV - echo $GITHUB_ENV # todo: remove - name: Run integration tests run: | From 255dbe560d47a91091fd53ab02b74c6225e86a32 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 7 May 2024 21:17:58 +0530 Subject: [PATCH 042/103] change JWT logic Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 9 +++++++++ pkg/install/install.go | 2 +- pkg/kubernetes/jwt.go | 27 ++++++++++++++++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index d7739aaff..6a92a2082 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -72,6 +72,15 @@ metadata: kubernetes.io/service-account.name: everest-admin type: kubernetes.io/service-account-token --- +apiVersion: v1 +kind: Secret +metadata: + name: everest-accounts + namespace: everest-jwt +data: + # use your own signing key here. + signing_key: eW91ci1ldmVyZXN0LWp3dC1zaWduaW5nLWtleQo= +--- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/pkg/install/install.go b/pkg/install/install.go index 6093209bd..ca5558213 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -371,7 +371,7 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er } o.l.Info("Creating JWT Secret") - if err := o.kubeClient.CreateJWTSecret(ctx, !everestExists); err != nil { + if err := o.kubeClient.CreateJWTSecret(ctx, true); err != nil { return err } diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go index f42d99f62..e15d1670a 100644 --- a/pkg/kubernetes/jwt.go +++ b/pkg/kubernetes/jwt.go @@ -16,12 +16,6 @@ import ( // CreateJWTSecret creates a new secret with the JWT singing key. // If `force` is set to true, the secret will be re-created with a new key. func (k *Kubernetes) CreateJWTSecret(ctx context.Context, force bool) error { - if _, err := k.client.GetSecret(ctx, common.SystemNamespace, common.EverestJWTSecretName); ctrlclient.IgnoreNotFound(err) != nil { - return err - } else if !force { - // Secret already exists, and we don't want to overwrite it. - return nil - } token, err := genJWTToken() if err != nil { return errors.Join(err, errors.New("failed to generate JWT token")) @@ -35,7 +29,26 @@ func (k *Kubernetes) CreateJWTSecret(ctx context.Context, force bool) error { "signing_key": []byte(token), }, } - if _, err := k.client.CreateSecret(ctx, secret); err != nil { + + exists := false + if _, err := k.client.GetSecret(ctx, + common.SystemNamespace, + common.EverestJWTSecretName, + ); err == nil { + exists = true + } else if ctrlclient.IgnoreNotFound(err) != nil { + return err + } + + if !exists { + if _, err := k.client.CreateSecret(ctx, secret); err != nil { + return err + } + return nil + } + + if force { + _, err = k.client.UpdateSecret(ctx, secret) return err } return nil From 1478a3891473caf8f5e2aa1213fd103fecfe3ef9 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 09:25:03 +0530 Subject: [PATCH 043/103] update JWT logic Signed-off-by: Mayank Shah --- pkg/common/constants.go | 2 + pkg/install/install.go | 25 +++++++-- pkg/kubernetes/jwt.go | 58 ++++++++------------- pkg/kubernetes/kubernetes_interface.go | 5 +- pkg/kubernetes/mock_kubernetes_connector.go | 36 ++++++------- pkg/upgrade/upgrade.go | 4 -- 6 files changed, 63 insertions(+), 67 deletions(-) diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 7b3c1f895..3f25cff3b 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -41,6 +41,8 @@ const ( // EverestJWTSecretName is the name of the secret that holds JWT secret. EverestJWTSecretName = "everest-jwt" + // EverestJWTSecretKey is the key in the secret that holds JWT secret. + EverestJWTSecretKey = "signing_secret" // EverestAdminUser is the name of the admin user. EverestAdminUser = "admin" diff --git a/pkg/install/install.go b/pkg/install/install.go index ca5558213..5c7f26bd4 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -19,6 +19,8 @@ package install import ( "context" + "crypto/rand" + "encoding/hex" "errors" "fmt" "net/url" @@ -370,11 +372,6 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er everestExists = true } - o.l.Info("Creating JWT Secret") - if err := o.kubeClient.CreateJWTSecret(ctx, true); err != nil { - return err - } - if !everestExists { o.l.Info(fmt.Sprintf("Deploying Everest to %s", common.SystemNamespace)) if err = o.kubeClient.InstallEverest(ctx, common.SystemNamespace, v); err != nil { @@ -390,6 +387,10 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er } } + if err := o.createEverestJWTToken(ctx); err != nil { + return err + } + if err := o.createEverestAdminAccount(ctx); err != nil { return err } @@ -751,3 +752,17 @@ func (o *Install) createEverestAdminAccount(ctx context.Context) error { } return nil } + +func (o *Install) createEverestJWTToken(ctx context.Context) error { + o.l.Info("Creating JWT token for Everest") + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return err + } + token := hex.EncodeToString(b) + + if err := o.kubeClient.SetJWTToken(ctx, token); err != nil { + return err + } + return nil +} diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go index e15d1670a..ef55ac472 100644 --- a/pkg/kubernetes/jwt.go +++ b/pkg/kubernetes/jwt.go @@ -2,62 +2,46 @@ package kubernetes import ( "context" - "crypto/rand" - "encoding/hex" - "errors" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/percona/everest/pkg/common" ) -// CreateJWTSecret creates a new secret with the JWT singing key. -// If `force` is set to true, the secret will be re-created with a new key. -func (k *Kubernetes) CreateJWTSecret(ctx context.Context, force bool) error { - token, err := genJWTToken() - if err != nil { - return errors.Join(err, errors.New("failed to generate JWT token")) - } - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestJWTSecretName, - Namespace: common.SystemNamespace, - }, - Data: map[string][]byte{ - "signing_key": []byte(token), - }, - } - +// SetJWTToken sets the provided JWT token in the everest-jwt secret. +func (k *Kubernetes) SetJWTToken(ctx context.Context, token string) error { + // Check if the secret exists? exists := false - if _, err := k.client.GetSecret(ctx, - common.SystemNamespace, - common.EverestJWTSecretName, - ); err == nil { + secret, err := k.GetSecret(ctx, common.SystemNamespace, common.EverestJWTSecretName) + if err == nil { exists = true - } else if ctrlclient.IgnoreNotFound(err) != nil { + } else if !errors.IsNotFound(err) { return err } + // Create secret if it doesn't exists. if !exists { - if _, err := k.client.CreateSecret(ctx, secret); err != nil { + secret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.EverestJWTSecretName, + Namespace: common.SystemNamespace, + }, + Data: map[string][]byte{ + common.EverestJWTSecretKey: []byte(token), + }, + } + if _, err := k.CreateSecret(ctx, secret); err != nil { return err } return nil } - if force { - _, err = k.client.UpdateSecret(ctx, secret) + // Otherwise, update the secret. + secret.Data[common.EverestJWTSecretKey] = []byte(token) + if _, err := k.UpdateSecret(ctx, secret); err != nil { return err } return nil } - -func genJWTToken() (string, error) { - b := make([]byte, 32) - if _, err := rand.Read(b); err != nil { - return "", err - } - return hex.EncodeToString(b), nil -} diff --git a/pkg/kubernetes/kubernetes_interface.go b/pkg/kubernetes/kubernetes_interface.go index 8e7ab96d1..6e1c2ec90 100644 --- a/pkg/kubernetes/kubernetes_interface.go +++ b/pkg/kubernetes/kubernetes_interface.go @@ -111,7 +111,6 @@ type KubernetesConnector interface { UpdateClusterRoleBinding(ctx context.Context, name string, namespaces []string) error // OperatorInstalledVersion returns the installed version of operator by name. OperatorInstalledVersion(ctx context.Context, namespace, name string) (*goversion.Version, error) - // CreateJWTSecret creates a new secret with the JWT singing key. - // If `force` is set to true, the secret will be re-created with a new key. - CreateJWTSecret(ctx context.Context, force bool) error + // SetJWTToken sets the provided JWT token in the everest-jwt secret. + SetJWTToken(ctx context.Context, token string) error } diff --git a/pkg/kubernetes/mock_kubernetes_connector.go b/pkg/kubernetes/mock_kubernetes_connector.go index 1968caec9..ce66172e4 100644 --- a/pkg/kubernetes/mock_kubernetes_connector.go +++ b/pkg/kubernetes/mock_kubernetes_connector.go @@ -129,24 +129,6 @@ func (_m *MockKubernetesConnector) Config() *rest.Config { return r0 } -// CreateJWTSecret provides a mock function with given fields: ctx, force -func (_m *MockKubernetesConnector) CreateJWTSecret(ctx context.Context, force bool) error { - ret := _m.Called(ctx, force) - - if len(ret) == 0 { - panic("no return value specified for CreateJWTSecret") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, bool) error); ok { - r0 = rf(ctx, force) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // CreateNamespace provides a mock function with given fields: name func (_m *MockKubernetesConnector) CreateNamespace(name string) error { ret := _m.Called(name) @@ -885,6 +867,24 @@ func (_m *MockKubernetesConnector) RestartOperator(ctx context.Context, name str return r0 } +// SetJWTToken provides a mock function with given fields: ctx, token +func (_m *MockKubernetesConnector) SetJWTToken(ctx context.Context, token string) error { + ret := _m.Called(ctx, token) + + if len(ret) == 0 { + panic("no return value specified for SetJWTToken") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, token) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateClusterRoleBinding provides a mock function with given fields: ctx, name, namespaces func (_m *MockKubernetesConnector) UpdateClusterRoleBinding(ctx context.Context, name string, namespaces []string) error { ret := _m.Called(ctx, name, namespaces) diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index 2d2409ab2..bda9bc5a8 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -116,10 +116,6 @@ func (u *Upgrade) Run(ctx context.Context) error { return err } - if err := u.kubeClient.CreateJWTSecret(ctx, false); err != nil { - return err - } - // We cannot use the latest version of catalog yet since // at the time of writing, each catalog version supports only one Everest version. catalogVersion := recVer.Catalog From f5333a06f03cb1926f36a329794cc142a0d75ef3 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 09:42:24 +0530 Subject: [PATCH 044/103] fix secret Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 6a92a2082..ba76c5fb4 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -66,14 +66,6 @@ subjects: --- apiVersion: v1 kind: Secret -metadata: - name: everest-admin-token - annotations: - kubernetes.io/service-account.name: everest-admin -type: kubernetes.io/service-account-token ---- -apiVersion: v1 -kind: Secret metadata: name: everest-accounts namespace: everest-jwt From 60235d7b48b7fe0a923180d091ba3c0e76656b49 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 09:48:29 +0530 Subject: [PATCH 045/103] fix secret Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index ba76c5fb4..47d9c8aac 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -67,8 +67,8 @@ subjects: apiVersion: v1 kind: Secret metadata: - name: everest-accounts - namespace: everest-jwt + name: everest-jwt + namespace: everest-system data: # use your own signing key here. signing_key: eW91ci1ldmVyZXN0LWp3dC1zaWduaW5nLWtleQo= From 0f21e437fa00dc21d82340a956fc44c88f0e074f Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 09:56:43 +0530 Subject: [PATCH 046/103] fix typo Signed-off-by: Mayank Shah --- pkg/common/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 3f25cff3b..4b293d1ee 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -42,7 +42,7 @@ const ( // EverestJWTSecretName is the name of the secret that holds JWT secret. EverestJWTSecretName = "everest-jwt" // EverestJWTSecretKey is the key in the secret that holds JWT secret. - EverestJWTSecretKey = "signing_secret" + EverestJWTSecretKey = "signing_key" // EverestAdminUser is the name of the admin user. EverestAdminUser = "admin" From 9d9e08a279e352d6f6c8a272b777e1c6cc078e34 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 10:02:47 +0530 Subject: [PATCH 047/103] fix token logic in upgrade Signed-off-by: Mayank Shah --- pkg/kubernetes/jwt.go | 9 +++++++ pkg/kubernetes/kubernetes_interface.go | 2 ++ pkg/kubernetes/mock_kubernetes_connector.go | 28 +++++++++++++++++++++ pkg/upgrade/upgrade.go | 21 ++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go index ef55ac472..44cc79475 100644 --- a/pkg/kubernetes/jwt.go +++ b/pkg/kubernetes/jwt.go @@ -45,3 +45,12 @@ func (k *Kubernetes) SetJWTToken(ctx context.Context, token string) error { } return nil } + +// GetJWTToken returns the JWT token from the everest-jwt secret. +func (k *Kubernetes) GetJWTToken(ctx context.Context) (string, error) { + secret, err := k.GetSecret(ctx, common.SystemNamespace, common.EverestJWTSecretName) + if err != nil { + return "", err + } + return string(secret.Data[common.EverestJWTSecretKey]), nil +} diff --git a/pkg/kubernetes/kubernetes_interface.go b/pkg/kubernetes/kubernetes_interface.go index 6e1c2ec90..eb08193cb 100644 --- a/pkg/kubernetes/kubernetes_interface.go +++ b/pkg/kubernetes/kubernetes_interface.go @@ -113,4 +113,6 @@ type KubernetesConnector interface { OperatorInstalledVersion(ctx context.Context, namespace, name string) (*goversion.Version, error) // SetJWTToken sets the provided JWT token in the everest-jwt secret. SetJWTToken(ctx context.Context, token string) error + // GetJWTToken returns the JWT token from the everest-jwt secret. + GetJWTToken(ctx context.Context) (string, error) } diff --git a/pkg/kubernetes/mock_kubernetes_connector.go b/pkg/kubernetes/mock_kubernetes_connector.go index ce66172e4..ad3a779fd 100644 --- a/pkg/kubernetes/mock_kubernetes_connector.go +++ b/pkg/kubernetes/mock_kubernetes_connector.go @@ -487,6 +487,34 @@ func (_m *MockKubernetesConnector) GetEverestID(ctx context.Context) (string, er return r0, r1 } +// GetJWTToken provides a mock function with given fields: ctx +func (_m *MockKubernetesConnector) GetJWTToken(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetJWTToken") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetLogs provides a mock function with given fields: ctx, containerStatuses, pod, container func (_m *MockKubernetesConnector) GetLogs(ctx context.Context, containerStatuses []corev1.ContainerStatus, pod string, container string) ([]string, error) { ret := _m.Called(ctx, containerStatuses, pod, container) diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index bda9bc5a8..2ab5e04a5 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -18,6 +18,8 @@ package upgrade import ( "context" + "crypto/rand" + "encoding/hex" "errors" "fmt" "net/url" @@ -141,11 +143,30 @@ func (u *Upgrade) Run(ctx context.Context) error { return errors.Join(err, errors.New("could not find install plan")) } + // We get the JWT token so that we can preserve it, since InstallEverest() + // will overwrite it. + jwtToken, err := u.kubeClient.GetJWTToken(ctx) + if err != nil { + return err + } + u.l.Infof("Upgrading Everest to %s in namespace %s", upgradeEverestTo, common.SystemNamespace) if err := u.kubeClient.InstallEverest(ctx, common.SystemNamespace, upgradeEverestTo); err != nil { return err } + // Restore the JWT token. + if jwtToken == "" { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return err + } + jwtToken = hex.EncodeToString(b) + } + if err := u.kubeClient.SetJWTToken(ctx, jwtToken); err != nil { + return err + } + if err := u.upgradeEverestOperator(ctx, ip.Name); err != nil { return err } From 3f2d5f38d3e86dd515cd81fc46a918b27ac1983b Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 10:05:46 +0530 Subject: [PATCH 048/103] linters Signed-off-by: Mayank Shah --- pkg/upgrade/upgrade.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index 2ab5e04a5..39d066205 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -94,8 +94,6 @@ func NewUpgrade(cfg *Config, l *zap.SugaredLogger) (*Upgrade, error) { } // Run runs the operators installation process. -// -//nolint:funlen func (u *Upgrade) Run(ctx context.Context) error { // Get Everest version. everestVersion, err := cliVersion.EverestVersionFromDeployment(ctx, u.kubeClient) From 8f46757a1fa4fe735364396d1cb4d3d72f5c5bc3 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:16:32 +0530 Subject: [PATCH 049/103] improve error handling Signed-off-by: Mayank Shah --- api/session.go | 19 +++++++++++++++++++ pkg/accounts/types.go | 4 ++++ pkg/session/manager.go | 5 +++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/api/session.go b/api/session.go index 766bc2c30..9fa85c3e7 100644 --- a/api/session.go +++ b/api/session.go @@ -17,10 +17,12 @@ package api import ( + "errors" "fmt" "net/http" "time" + "github.com/AlekSi/pointer" "github.com/google/uuid" "github.com/labstack/echo/v4" @@ -43,6 +45,23 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { c := ctx.Request().Context() err := e.sessionMgr.Authenticate(c, *params.Username, *params.Password) if err != nil { + // Return appropriate error messages based on the error type. + if errors.Is(err, accounts.ErrAccountNotFound) || + errors.Is(err, accounts.ErrIncorrectPassword) { + return ctx.JSON(http.StatusUnauthorized, Error{ + Message: pointer.To("Incorrect username or password provided"), + }) + } + if errors.Is(err, accounts.ErrAccountDisabled) { + return ctx.JSON(http.StatusForbidden, Error{ + Message: pointer.To("User account is disabled"), + }) + } + if errors.Is(err, accounts.ErrInsufficientCapabilities) { + return ctx.JSON(http.StatusForbidden, Error{ + Message: pointer.To("User account lacks required capabilities"), + }) + } return err } diff --git a/pkg/accounts/types.go b/pkg/accounts/types.go index 13b0061e9..271bdef13 100644 --- a/pkg/accounts/types.go +++ b/pkg/accounts/types.go @@ -30,6 +30,10 @@ var ( ErrAccountNotFound = errors.New("account not found") // ErrIncorrectPassword is returned when the password is invalid. ErrIncorrectPassword = errors.New("invalid password") + // ErrInsufficientCapabilities is returned when the account does not have the required capabilities. + ErrInsufficientCapabilities = errors.New("insufficient capabilities") + // ErrAccountDisabled is returned when the account is disabled. + ErrAccountDisabled = errors.New("account disabled") ) const ( diff --git a/pkg/session/manager.go b/pkg/session/manager.go index b12393c6e..490f00a4a 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -18,6 +18,7 @@ package session import ( "context" + "errors" "fmt" "time" @@ -106,11 +107,11 @@ func (mgr *Manager) Authenticate(ctx context.Context, username string, password } if !account.Enabled { - return fmt.Errorf("account disabled") + return accounts.ErrAccountDisabled } if !account.HasCapability(accounts.AccountCapabilityLogin) { - return fmt.Errorf("user does not have capability to login") + return errors.Join(accounts.ErrInsufficientCapabilities, errors.New("user does not have capability to login")) } return nil } From fa755bb23eddb67b15871377eccf4f5a63a0e5f7 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:21:28 +0530 Subject: [PATCH 050/103] allow using manifest from current commit Signed-off-by: Mayank Shah --- Makefile | 1 + pkg/version/version.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 9e2f9bcd1..b567639bd 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ LD_FLAGS_API = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.Pr LD_FLAGS_CLI = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl'" LD_FLAGS_CLI_TEST = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl' \ -X 'github.com/percona/everest/pkg/version.EverestChannelOverride=fast-v0'" + -X 'github.com/percona/everest/pkg/version.Debug=true'" default: help diff --git a/pkg/version/version.go b/pkg/version/version.go index 1887d42bc..0928fa4a0 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -47,6 +47,8 @@ var ( FullCommit string //nolint:gochecknoglobals // EverestChannelOverride overrides the default olm channel for Everest operator. EverestChannelOverride string //nolint:gochecknoglobals + // Debug is set to true if this is a debug build. + Debug bool //nolint:gochecknoglobals rcSuffix = regexp.MustCompile(`rc\d+$`) ) From b023501d571835c6e1202b475333d92ff108a5c0 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:23:18 +0530 Subject: [PATCH 051/103] fix dev setup Signed-off-by: Mayank Shah --- dev/Tiltfile | 1 - dev/jwt-secret.yaml | 6 ------ 2 files changed, 7 deletions(-) delete mode 100644 dev/jwt-secret.yaml diff --git a/dev/Tiltfile b/dev/Tiltfile index 81aa6ef50..0dca9f591 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -397,7 +397,6 @@ docker_build_with_restart('perconalab/everest', ) k8s_yaml(namespace_inject('%s/deploy/quickstart-k8s.yaml' % backend_dir, everest_namespace)) -k8s_yaml(namespace_inject('jwt-secret.yaml', everest_namespace)) k8s_yaml(namespace_inject('everest-accounts.yaml', everest_namespace),allow_duplicates=True) k8s_resource( workload='percona-everest', diff --git a/dev/jwt-secret.yaml b/dev/jwt-secret.yaml deleted file mode 100644 index 5e28c41fe..000000000 --- a/dev/jwt-secret.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: everest-jwt -data: - signing_key: aGVsbG8gZXZlcmVzdAo= From 669c9895d3dccf7ad100c103892d20c00104127b Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:28:45 +0530 Subject: [PATCH 052/103] allow using local manifests during install Signed-off-by: Mayank Shah --- Makefile | 5 +++-- pkg/version/version.go | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index b567639bd..5262b4d55 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,12 @@ RELEASE_FULLCOMMIT ?= $(shell git rev-parse HEAD) FLAGS = -X 'github.com/percona/everest/pkg/version.Version=$(RELEASE_VERSION)' \ -X 'github.com/percona/everest/pkg/version.FullCommit=$(RELEASE_FULLCOMMIT)' \ +USE_LOCAL_MANIFEST ?= false LD_FLAGS_API = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=Everest API Server'" LD_FLAGS_CLI = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl'" LD_FLAGS_CLI_TEST = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl' \ - -X 'github.com/percona/everest/pkg/version.EverestChannelOverride=fast-v0'" - -X 'github.com/percona/everest/pkg/version.Debug=true'" + -X 'github.com/percona/everest/pkg/version.EverestChannelOverride=fast-v0' \ + -X 'github.com/percona/everest/pkg/version.UseLocalManifest=$(USE_LOCAL_MANIFEST)' \" default: help diff --git a/pkg/version/version.go b/pkg/version/version.go index 0928fa4a0..cf2f81589 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -32,7 +32,7 @@ const ( releaseCatalogImage = "docker.io/percona/everest-catalog:%s" devManifestURL = "https://raw.githubusercontent.com/percona/everest/main/deploy/quickstart-k8s.yaml" releaseManifestURL = "https://raw.githubusercontent.com/percona/everest/v%s/deploy/quickstart-k8s.yaml" - debugManifestURL = "https://raw.githubusercontent.com/percona/everest/%s/deploy/quickstart-k8s.yaml" + localManifestURL = "https://raw.githubusercontent.com/percona/everest/%s/deploy/quickstart-k8s.yaml" everestOperatorChannelStable = "stable-v0" everestOperatorChannelFast = "fast-v0" @@ -47,8 +47,8 @@ var ( FullCommit string //nolint:gochecknoglobals // EverestChannelOverride overrides the default olm channel for Everest operator. EverestChannelOverride string //nolint:gochecknoglobals - // Debug is set to true if this is a debug build. - Debug bool //nolint:gochecknoglobals + // UseLocalManifest is set to "true" if the local manifest should be used. + UseLocalManifest string //nolint:gochecknoglobals rcSuffix = regexp.MustCompile(`rc\d+$`) ) From 3c8d02214573ea759ed6fddfa3382c29bbcadd3e Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:30:56 +0530 Subject: [PATCH 053/103] typo Signed-off-by: Mayank Shah --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5262b4d55..f99edfa6e 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,12 @@ RELEASE_FULLCOMMIT ?= $(shell git rev-parse HEAD) FLAGS = -X 'github.com/percona/everest/pkg/version.Version=$(RELEASE_VERSION)' \ -X 'github.com/percona/everest/pkg/version.FullCommit=$(RELEASE_FULLCOMMIT)' \ -USE_LOCAL_MANIFEST ?= false +USE_LOCAL_MANIFEST ?=false LD_FLAGS_API = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=Everest API Server'" LD_FLAGS_CLI = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl'" LD_FLAGS_CLI_TEST = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl' \ -X 'github.com/percona/everest/pkg/version.EverestChannelOverride=fast-v0' \ - -X 'github.com/percona/everest/pkg/version.UseLocalManifest=$(USE_LOCAL_MANIFEST)' \" + -X 'github.com/percona/everest/pkg/version.UseLocalManifest=$(USE_LOCAL_MANIFEST)'" default: help From b0afe57ed58d0c2e93f4bfd058ad750f55fd3e09 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:38:33 +0530 Subject: [PATCH 054/103] fix tests Signed-off-by: Mayank Shah --- .github/workflows/cli-tests.yml | 2 +- .github/workflows/dev-be-ci.yaml | 4 ++-- .github/workflows/dev-fe-ci.yaml | 2 +- pkg/install/install.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cli-tests.yml b/.github/workflows/cli-tests.yml index 619a9a763..d31c8bd33 100644 --- a/.github/workflows/cli-tests.yml +++ b/.github/workflows/cli-tests.yml @@ -73,7 +73,7 @@ jobs: - name: Build CLI binary run: | make init - make build-cli + USE_LOCAL_MANIFEST=true make build-cli - name: Run ${{ env.MAKE_TARGET }} integration tests working-directory: cli-tests diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index c8ee22049..c914b3f5c 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -322,7 +322,7 @@ jobs: - name: Provision Everest using CLI shell: bash run: | - make build-cli + USE_LOCAL_MANIFEST=true make build-cli ./bin/everestctl install -v \ --version 0.0.0 \ --version-metadata-url https://check-dev.percona.com \ @@ -419,7 +419,7 @@ jobs: - name: Build CLI binary run: | make init - make build-cli + USE_LOCAL_MANIFEST=true make build-cli - name: Create KIND cluster uses: helm/kind-action@v1.9.0 diff --git a/.github/workflows/dev-fe-ci.yaml b/.github/workflows/dev-fe-ci.yaml index d8d466400..716de0659 100644 --- a/.github/workflows/dev-fe-ci.yaml +++ b/.github/workflows/dev-fe-ci.yaml @@ -170,7 +170,7 @@ jobs: - name: Run Provisioning run: | - make build-cli + USE_LOCAL_MANIFEST=true make build-cli ./bin/everestctl install -v \ --version 0.0.0 \ --version-metadata-url https://check-dev.percona.com \ diff --git a/pkg/install/install.go b/pkg/install/install.go index 5c7f26bd4..e09cd8195 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -56,7 +56,7 @@ Everest has been successfully installed! To view the password for the 'admin' user, run the following command: -kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode +kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode && echo To create a new user, run the following command: From a84efd39c8a1ac21b6226ca488952a66a87fca1c Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 12:40:40 +0530 Subject: [PATCH 055/103] linting Signed-off-by: Mayank Shah --- pkg/upgrade/upgrade.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index 39d066205..2ab5e04a5 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -94,6 +94,8 @@ func NewUpgrade(cfg *Config, l *zap.SugaredLogger) (*Upgrade, error) { } // Run runs the operators installation process. +// +//nolint:funlen func (u *Upgrade) Run(ctx context.Context) error { // Get Everest version. everestVersion, err := cliVersion.EverestVersionFromDeployment(ctx, u.kubeClient) From e568a56cd01bc4da48c3b36643e62277c525d832 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 14:14:47 +0530 Subject: [PATCH 056/103] CI testing Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index c914b3f5c..7e81269f1 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -345,6 +345,7 @@ jobs: run: | EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode) echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"admin\",\"password\": \"'\"$EVEREST_PASS\"'\"}' | jq -r .token)" >> $GITHUB_ENV + echo $GITHUB_ENV - name: Run integration tests run: | From 0539a6dd865054b12440b403b149533664d11aef Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 14:33:36 +0530 Subject: [PATCH 057/103] fix CI Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 7e81269f1..3dd813221 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -343,9 +343,8 @@ jobs: - name: Log into Everest as admin run: | - EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode) + echo "EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode)" >> $GITHUB_ENV echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"admin\",\"password\": \"'\"$EVEREST_PASS\"'\"}' | jq -r .token)" >> $GITHUB_ENV - echo $GITHUB_ENV - name: Run integration tests run: | From 925df47b65cd811a1d4833b4686bb2e66d17b83d Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 15:09:45 +0530 Subject: [PATCH 058/103] fix CI Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 2 +- deploy/quickstart-k8s.yaml | 3 --- dev/Tiltfile | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 3dd813221..2cddda6ae 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -344,7 +344,7 @@ jobs: - name: Log into Everest as admin run: | echo "EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode)" >> $GITHUB_ENV - echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"admin\",\"password\": \"'\"$EVEREST_PASS\"'\"}' | jq -r .token)" >> $GITHUB_ENV + echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' | jq -r .token)" >> $GITHUB_ENV - name: Run integration tests run: | diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 47d9c8aac..bc7d8e014 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -68,7 +68,6 @@ apiVersion: v1 kind: Secret metadata: name: everest-jwt - namespace: everest-system data: # use your own signing key here. signing_key: eW91ci1ldmVyZXN0LWp3dC1zaWduaW5nLWtleQo= @@ -142,11 +141,9 @@ apiVersion: v1 kind: Secret metadata: name: everest-accounts - namespace: everest-system --- apiVersion: v1 kind: ConfigMap metadata: name: everest-accounts - namespace: everest-system --- diff --git a/dev/Tiltfile b/dev/Tiltfile index 0dca9f591..423b3ab07 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -406,7 +406,6 @@ k8s_resource( 'everest-admin-role-binding:rolebinding', 'everest-admin-cluster-role:clusterrole', 'everest-admin-cluster-role-binding:clusterrolebinding', - 'everest-admin-token:secret', ], new_name='everest', port_forwards=8080, From 2377a5fe9665bf44f129ecbf94dfa91ba3790d34 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 15:23:26 +0530 Subject: [PATCH 059/103] test CI Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 2cddda6ae..550accffd 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -345,6 +345,7 @@ jobs: run: | echo "EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode)" >> $GITHUB_ENV echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' | jq -r .token)" >> $GITHUB_ENV + curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' | jq -r .token - name: Run integration tests run: | From 4f1df0090def772cad4a72fb68457b2b444fc8e5 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 15:35:07 +0530 Subject: [PATCH 060/103] test CI Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 550accffd..ed49745a1 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -345,7 +345,7 @@ jobs: run: | echo "EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode)" >> $GITHUB_ENV echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' | jq -r .token)" >> $GITHUB_ENV - curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' | jq -r .token + curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' - name: Run integration tests run: | From 308000f37c36918f7a858e203299b6b166b34d24 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 16:31:08 +0530 Subject: [PATCH 061/103] create CI user Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index ed49745a1..18559616c 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -341,11 +341,10 @@ jobs: run: | kubectl port-forward --namespace everest-system deployment/percona-everest 8080:8080 & - - name: Log into Everest as admin + - name: Create Everest test user run: | - echo "EVEREST_PASS=$(kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode)" >> $GITHUB_ENV - echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' | jq -r .token)" >> $GITHUB_ENV - curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "admin","password": "'"$EVEREST_PASS"'"}' + ./bin/everestctl accounts create -u everest-ci -p password + echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"everest-ci\",\"password\": \"password\"}' | jq -r .token)" >> $GITHUB_ENV - name: Run integration tests run: | From 53293409cb35b92fda354c805e1ac948693c3bd5 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 16:34:28 +0530 Subject: [PATCH 062/103] deployment must be restarted Signed-off-by: Mayank Shah --- pkg/kubernetes/jwt.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go index 44cc79475..b241d4865 100644 --- a/pkg/kubernetes/jwt.go +++ b/pkg/kubernetes/jwt.go @@ -35,7 +35,8 @@ func (k *Kubernetes) SetJWTToken(ctx context.Context, token string) error { if _, err := k.CreateSecret(ctx, secret); err != nil { return err } - return nil + // Restart the deployment to pick up the new secret. + return k.RestartDeployment(ctx, common.SystemNamespace, common.PerconaEverestDeploymentName) } // Otherwise, update the secret. @@ -43,7 +44,8 @@ func (k *Kubernetes) SetJWTToken(ctx context.Context, token string) error { if _, err := k.UpdateSecret(ctx, secret); err != nil { return err } - return nil + // Restart the deployment to pick up the new secret. + return k.RestartDeployment(ctx, common.SystemNamespace, common.PerconaEverestDeploymentName) } // GetJWTToken returns the JWT token from the everest-jwt secret. From cc3f2ef9315d786385401c4fb9c4f87ab05aedeb Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 19:04:47 +0530 Subject: [PATCH 063/103] typo fix Signed-off-by: Mayank Shah --- pkg/kubernetes/jwt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/kubernetes/jwt.go b/pkg/kubernetes/jwt.go index b241d4865..799c3b2f3 100644 --- a/pkg/kubernetes/jwt.go +++ b/pkg/kubernetes/jwt.go @@ -36,7 +36,7 @@ func (k *Kubernetes) SetJWTToken(ctx context.Context, token string) error { return err } // Restart the deployment to pick up the new secret. - return k.RestartDeployment(ctx, common.SystemNamespace, common.PerconaEverestDeploymentName) + return k.RestartDeployment(ctx, common.PerconaEverestDeploymentName, common.SystemNamespace) } // Otherwise, update the secret. @@ -45,7 +45,7 @@ func (k *Kubernetes) SetJWTToken(ctx context.Context, token string) error { return err } // Restart the deployment to pick up the new secret. - return k.RestartDeployment(ctx, common.SystemNamespace, common.PerconaEverestDeploymentName) + return k.RestartDeployment(ctx, common.PerconaEverestDeploymentName, common.SystemNamespace) } // GetJWTToken returns the JWT token from the everest-jwt secret. From 211dfe103e6f99cc5f4481b5cea6650ccc16010a Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 19:21:03 +0530 Subject: [PATCH 064/103] fix ci Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 2 +- public/dist/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 18559616c..dc99f1ffd 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -344,7 +344,7 @@ jobs: - name: Create Everest test user run: | ./bin/everestctl accounts create -u everest-ci -p password - echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{\"username\": \"everest-ci\",\"password\": \"password\"}' | jq -r .token)" >> $GITHUB_ENV + echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest-ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV - name: Run integration tests run: | diff --git a/public/dist/index.html b/public/dist/index.html index 36368f1e4..4f6cc8747 100644 --- a/public/dist/index.html +++ b/public/dist/index.html @@ -5,7 +5,7 @@ Percona Everest - + From 2a30cf6d8e1ba8e5f0e12b879f552336329dc6c6 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 8 May 2024 20:06:18 +0530 Subject: [PATCH 065/103] use bash shell Signed-off-by: Mayank Shah --- .github/workflows/dev-fe-ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev-fe-ci.yaml b/.github/workflows/dev-fe-ci.yaml index 716de0659..bf436d156 100644 --- a/.github/workflows/dev-fe-ci.yaml +++ b/.github/workflows/dev-fe-ci.yaml @@ -169,6 +169,7 @@ jobs: echo -n "MONITORING_URL=http://$url" >> $GITHUB_ENV - name: Run Provisioning + shell: bash run: | USE_LOCAL_MANIFEST=true make build-cli ./bin/everestctl install -v \ From 13fef28f76c4b532f5000726eb8df21cdb677849 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 10:00:26 +0530 Subject: [PATCH 066/103] undo CI changes Signed-off-by: Mayank Shah --- .github/workflows/cli-tests.yml | 2 +- .github/workflows/dev-be-ci.yaml | 2 +- .github/workflows/dev-fe-ci.yaml | 3 +-- Makefile | 3 +-- pkg/version/version.go | 3 --- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cli-tests.yml b/.github/workflows/cli-tests.yml index d31c8bd33..619a9a763 100644 --- a/.github/workflows/cli-tests.yml +++ b/.github/workflows/cli-tests.yml @@ -73,7 +73,7 @@ jobs: - name: Build CLI binary run: | make init - USE_LOCAL_MANIFEST=true make build-cli + make build-cli - name: Run ${{ env.MAKE_TARGET }} integration tests working-directory: cli-tests diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index dc99f1ffd..315af27b3 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -322,7 +322,7 @@ jobs: - name: Provision Everest using CLI shell: bash run: | - USE_LOCAL_MANIFEST=true make build-cli + make build-cli ./bin/everestctl install -v \ --version 0.0.0 \ --version-metadata-url https://check-dev.percona.com \ diff --git a/.github/workflows/dev-fe-ci.yaml b/.github/workflows/dev-fe-ci.yaml index bf436d156..d8d466400 100644 --- a/.github/workflows/dev-fe-ci.yaml +++ b/.github/workflows/dev-fe-ci.yaml @@ -169,9 +169,8 @@ jobs: echo -n "MONITORING_URL=http://$url" >> $GITHUB_ENV - name: Run Provisioning - shell: bash run: | - USE_LOCAL_MANIFEST=true make build-cli + make build-cli ./bin/everestctl install -v \ --version 0.0.0 \ --version-metadata-url https://check-dev.percona.com \ diff --git a/Makefile b/Makefile index f99edfa6e..a9be01cdd 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,7 @@ USE_LOCAL_MANIFEST ?=false LD_FLAGS_API = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=Everest API Server'" LD_FLAGS_CLI = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl'" LD_FLAGS_CLI_TEST = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl' \ - -X 'github.com/percona/everest/pkg/version.EverestChannelOverride=fast-v0' \ - -X 'github.com/percona/everest/pkg/version.UseLocalManifest=$(USE_LOCAL_MANIFEST)'" + -X 'github.com/percona/everest/pkg/version.EverestChannelOverride=fast-v0'" default: help diff --git a/pkg/version/version.go b/pkg/version/version.go index cf2f81589..95df2c488 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -32,7 +32,6 @@ const ( releaseCatalogImage = "docker.io/percona/everest-catalog:%s" devManifestURL = "https://raw.githubusercontent.com/percona/everest/main/deploy/quickstart-k8s.yaml" releaseManifestURL = "https://raw.githubusercontent.com/percona/everest/v%s/deploy/quickstart-k8s.yaml" - localManifestURL = "https://raw.githubusercontent.com/percona/everest/%s/deploy/quickstart-k8s.yaml" everestOperatorChannelStable = "stable-v0" everestOperatorChannelFast = "fast-v0" @@ -47,8 +46,6 @@ var ( FullCommit string //nolint:gochecknoglobals // EverestChannelOverride overrides the default olm channel for Everest operator. EverestChannelOverride string //nolint:gochecknoglobals - // UseLocalManifest is set to "true" if the local manifest should be used. - UseLocalManifest string //nolint:gochecknoglobals rcSuffix = regexp.MustCompile(`rc\d+$`) ) From 1bf8879b3fb7126356880fb258938ae8bc69f9ca Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 10:35:49 +0530 Subject: [PATCH 067/103] handle plain-text admin without the use of separate secret Signed-off-by: Mayank Shah --- dev/everest-accounts.yaml | 8 +- pkg/accounts/types.go | 2 +- pkg/kubernetes/client/accounts/accounts.go | 124 +++++++++++---------- 3 files changed, 69 insertions(+), 65 deletions(-) diff --git a/dev/everest-accounts.yaml b/dev/everest-accounts.yaml index b57cdd03b..4fed7f0ec 100644 --- a/dev/everest-accounts.yaml +++ b/dev/everest-accounts.yaml @@ -13,11 +13,7 @@ apiVersion: v1 kind: Secret metadata: name: everest-accounts ---- -apiVersion: v1 -kind: Secret -metadata: - name: everest-admin-temp data: - password: ZXZlcmVzdGFkbWlu # everestadmin + passwords.yaml: | + admin: YWRtaW4= #admin --- diff --git a/pkg/accounts/types.go b/pkg/accounts/types.go index 271bdef13..5917d945a 100644 --- a/pkg/accounts/types.go +++ b/pkg/accounts/types.go @@ -29,7 +29,7 @@ var ( // ErrAccountNotFound is returned when an account is not found. ErrAccountNotFound = errors.New("account not found") // ErrIncorrectPassword is returned when the password is invalid. - ErrIncorrectPassword = errors.New("invalid password") + ErrIncorrectPassword = errors.New("incorrect password") // ErrInsufficientCapabilities is returned when the account does not have the required capabilities. ErrInsufficientCapabilities = errors.New("insufficient capabilities") // ErrAccountDisabled is returned when the account is disabled. diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index baa031369..558270efd 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -22,12 +22,12 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "fmt" "time" "golang.org/x/crypto/pbkdf2" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/percona/everest/pkg/accounts" @@ -38,6 +38,9 @@ import ( const ( usersFile = "users.yaml" tempAdminPasswordSecret = "everest-admin-temp" + + // We set this annotation on the secret to indicate which passwords are stored in plain text. + insecurePasswordAnnotation = "insecure-password/%s" ) type configMapsClient struct { @@ -84,9 +87,6 @@ func (a *configMapsClient) listAllAccounts(ctx context.Context) (map[string]*acc // Create a new user account. func (a *configMapsClient) Create(ctx context.Context, username, password string) error { - if username == common.EverestAdminUser && password == "" { - return a.createAdminWithTempPassword(ctx) - } account := &accounts.Account{ Enabled: true, Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, @@ -95,50 +95,63 @@ func (a *configMapsClient) Create(ctx context.Context, username, password string if err := a.setAccount(ctx, username, account); err != nil { return err } - if err := a.setPassword(ctx, username, password); err != nil { + + storeHashed := true + // If an admin account is created without a password, we will generate a random password + // and store it in plain text. + if username == common.EverestAdminUser && password == "" { + storeHashed = false + randPassword, err := a.generateRandomPassword() + if err != nil { + return err + } + password = randPassword + } + + if err := a.setPassword(ctx, username, password, storeHashed); err != nil { return err } return nil } -func (a *configMapsClient) createAdminWithTempPassword(ctx context.Context) error { +func (a *configMapsClient) generateRandomPassword() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { - return err + return "", err } - password := hex.EncodeToString(b) - tempAdminSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: tempAdminPasswordSecret, - Namespace: common.SystemNamespace, - }, - Data: map[string][]byte{ - "password": []byte(password), - }, - } - // This temporary secret is deleted once the admin password is reset. - if _, err := a.k.CreateSecret(ctx, tempAdminSecret); err != nil { - return err - } - if err := a.setAccount(ctx, common.EverestAdminUser, &accounts.Account{ - Enabled: true, - Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, - PasswordMtime: time.Now().Format(time.RFC3339), - }); err != nil { - return err - } - return nil + return hex.EncodeToString(b), nil } -func (a *configMapsClient) setPassword(ctx context.Context, username, password string) error { +func (a *configMapsClient) setPassword( + ctx context.Context, + username, password string, + ensureHash bool, +) error { secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return err } - hash, err := a.computePasswordHash(ctx, password) - if err != nil { - return errors.Join(err, errors.New("failed to compute hash")) + + hash := password + if !ensureHash { + // Add an annotation so that we know which passwords are stored as plain text. + annotations := secret.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[fmt.Sprintf(insecurePasswordAnnotation, username)] = "true" + secret.SetAnnotations(annotations) + } else { + hash, err = a.computePasswordHash(ctx, password) + if err != nil { + return errors.Join(err, errors.New("failed to compute hash")) + } + // Remove the annotation as the password is hashed. + annotations := secret.GetAnnotations() + delete(annotations, fmt.Sprintf(insecurePasswordAnnotation, username)) + secret.SetAnnotations(annotations) } + if secret.Data == nil { secret.Data = make(map[string][]byte) } @@ -221,11 +234,6 @@ func (a *configMapsClient) Delete(ctx context.Context, username string) error { } func (a *configMapsClient) Verify(ctx context.Context, username, password string) error { - if username == common.EverestAdminUser { - if shouldSkip, err := a.tryVerifyTempAdminPassword(ctx, password); !shouldSkip { - return err - } - } users, err := a.listAllAccounts(ctx) if err != nil { return err @@ -234,38 +242,38 @@ func (a *configMapsClient) Verify(ctx context.Context, username, password string if !found { return accounts.ErrAccountNotFound } + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) if err != nil { return err } - computedHash, err := a.computePasswordHash(ctx, password) - if err != nil { - return err + + // helper to check if a password should be compared as a hash. + shouldCompareAsHash := func() bool { + annotations := secret.GetAnnotations() + _, found := annotations[fmt.Sprintf(insecurePasswordAnnotation, username)] + return !found } - storedhash, found := secret.Data[username] + + storedB, found := secret.Data[username] if !found { return accounts.ErrAccountNotFound } - if string(storedhash) != computedHash { - return accounts.ErrIncorrectPassword - } - return nil -} + stored := string(storedB) -// try to check with the temporary password. -// Returns: [skip(bool), error]. -func (a *configMapsClient) tryVerifyTempAdminPassword(ctx context.Context, password string) (bool, error) { - secret, err := a.k.GetSecret(ctx, common.SystemNamespace, tempAdminPasswordSecret) - if err != nil { - if k8serrors.IsNotFound(err) { - return true, nil + provided := password + if shouldCompareAsHash() { + computedHash, err := a.computePasswordHash(ctx, password) + if err != nil { + return err } - return false, err + provided = computedHash } - if string(secret.Data["password"]) != password { - return false, accounts.ErrIncorrectPassword + + if stored != provided { + return accounts.ErrIncorrectPassword } - return false, nil + return nil } func (a *configMapsClient) computePasswordHash(ctx context.Context, password string) (string, error) { From c3b78db6c236adb63a0326d23ec906e6f407bccf Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 10:42:56 +0530 Subject: [PATCH 068/103] update dev setup Signed-off-by: Mayank Shah --- dev/everest-accounts.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev/everest-accounts.yaml b/dev/everest-accounts.yaml index 4fed7f0ec..3354eb70a 100644 --- a/dev/everest-accounts.yaml +++ b/dev/everest-accounts.yaml @@ -13,7 +13,8 @@ apiVersion: v1 kind: Secret metadata: name: everest-accounts + annotations: + insecure-password/admin: "true" data: - passwords.yaml: | - admin: YWRtaW4= #admin + admin: YWRtaW4= #admin --- From 2dc03005b203b243255311db81c84aea4d50b1b0 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 10:43:39 +0530 Subject: [PATCH 069/103] clean-up ci Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 315af27b3..cb061b1bf 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -419,7 +419,7 @@ jobs: - name: Build CLI binary run: | make init - USE_LOCAL_MANIFEST=true make build-cli + make build-cli - name: Create KIND cluster uses: helm/kind-action@v1.9.0 From db973a6ed5b2220f0c21ede810fe33c4805cc62a Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 11:25:23 +0530 Subject: [PATCH 070/103] update configs and dev setup Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 8 ++++++++ dev/Tiltfile | 5 ++++- dev/everest-accounts.yaml | 20 -------------------- pkg/kubernetes/client/accounts/accounts.go | 4 +++- 4 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 dev/everest-accounts.yaml diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index bc7d8e014..5fc329d54 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -141,9 +141,17 @@ apiVersion: v1 kind: Secret metadata: name: everest-accounts +data: + admin: YWRtaW4= #admin --- apiVersion: v1 kind: ConfigMap metadata: name: everest-accounts +data: + users.yaml : | + admin: + enabled: true + capabilities: + - login --- diff --git a/dev/Tiltfile b/dev/Tiltfile index 423b3ab07..e7c89d320 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -260,6 +260,7 @@ k8s_resource( 'vmauths.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, 'vmclusters.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, 'vmnodescrapes.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, + 'vmscrapeconfigs.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, 'vmpodscrapes.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, 'vmprobes.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, 'vmrules.operator.victoriametrics.com:customresourcedefinition:%s' % everest_monitoring_namespace, @@ -397,7 +398,6 @@ docker_build_with_restart('perconalab/everest', ) k8s_yaml(namespace_inject('%s/deploy/quickstart-k8s.yaml' % backend_dir, everest_namespace)) -k8s_yaml(namespace_inject('everest-accounts.yaml', everest_namespace),allow_duplicates=True) k8s_resource( workload='percona-everest', objects=[ @@ -406,6 +406,9 @@ k8s_resource( 'everest-admin-role-binding:rolebinding', 'everest-admin-cluster-role:clusterrole', 'everest-admin-cluster-role-binding:clusterrolebinding', + 'everest-jwt:secret', + 'everest-accounts:secret', + 'everest-accounts:configmap', ], new_name='everest', port_forwards=8080, diff --git a/dev/everest-accounts.yaml b/dev/everest-accounts.yaml deleted file mode 100644 index 3354eb70a..000000000 --- a/dev/everest-accounts.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: everest-accounts -data: - users.yaml : | - admin: - enabled: true - capabilities: - - login ---- -apiVersion: v1 -kind: Secret -metadata: - name: everest-accounts - annotations: - insecure-password/admin: "true" -data: - admin: YWRtaW4= #admin ---- diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 558270efd..2855b3bef 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -92,13 +92,15 @@ func (a *configMapsClient) Create(ctx context.Context, username, password string Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, PasswordMtime: time.Now().Format(time.RFC3339), } + + // XX: once we allow updating password, Create should fail if the user already exists. if err := a.setAccount(ctx, username, account); err != nil { return err } - storeHashed := true // If an admin account is created without a password, we will generate a random password // and store it in plain text. + storeHashed := true if username == common.EverestAdminUser && password == "" { storeHashed = false randPassword, err := a.generateRandomPassword() From 51708d21727916e65cbd7ad09b6dc9ded5244ae0 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 11:32:31 +0530 Subject: [PATCH 071/103] clean-up makefile Signed-off-by: Mayank Shah --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index a9be01cdd..9e2f9bcd1 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,6 @@ RELEASE_FULLCOMMIT ?= $(shell git rev-parse HEAD) FLAGS = -X 'github.com/percona/everest/pkg/version.Version=$(RELEASE_VERSION)' \ -X 'github.com/percona/everest/pkg/version.FullCommit=$(RELEASE_FULLCOMMIT)' \ -USE_LOCAL_MANIFEST ?=false LD_FLAGS_API = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=Everest API Server'" LD_FLAGS_CLI = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl'" LD_FLAGS_CLI_TEST = -ldflags " $(FLAGS) -X 'github.com/percona/everest/pkg/version.ProjectName=everestctl' \ From 9e36580a4422b2f81e951f695a8061fb13ad1692 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 11:37:28 +0530 Subject: [PATCH 072/103] refactor Signed-off-by: Mayank Shah --- api/session.go | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/api/session.go b/api/session.go index 9fa85c3e7..55fec9cb6 100644 --- a/api/session.go +++ b/api/session.go @@ -45,35 +45,17 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { c := ctx.Request().Context() err := e.sessionMgr.Authenticate(c, *params.Username, *params.Password) if err != nil { - // Return appropriate error messages based on the error type. - if errors.Is(err, accounts.ErrAccountNotFound) || - errors.Is(err, accounts.ErrIncorrectPassword) { - return ctx.JSON(http.StatusUnauthorized, Error{ - Message: pointer.To("Incorrect username or password provided"), - }) - } - if errors.Is(err, accounts.ErrAccountDisabled) { - return ctx.JSON(http.StatusForbidden, Error{ - Message: pointer.To("User account is disabled"), - }) - } - if errors.Is(err, accounts.ErrInsufficientCapabilities) { - return ctx.JSON(http.StatusForbidden, Error{ - Message: pointer.To("User account lacks required capabilities"), - }) - } - return err + return sessionErrToHttpResp(ctx, err) } uniqueID, err := uuid.NewRandom() if err != nil { return err } - jwtToken, err := e.sessionMgr.Create( - fmt.Sprintf(jwtSubjectTml, *params.Username, accounts.AccountCapabilityLogin), - int64(jwtDefaultExpiry.Seconds()), - uniqueID.String(), - ) + subject := fmt.Sprintf(jwtSubjectTml, *params.Username, accounts.AccountCapabilityLogin) + secondsBeforeExpiry := int64(jwtDefaultExpiry.Seconds()) + + jwtToken, err := e.sessionMgr.Create(subject, secondsBeforeExpiry, uniqueID.String()) if err != nil { return err } @@ -84,3 +66,25 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { }) return ctx.JSON(http.StatusOK, map[string]string{"token": jwtToken}) } + +func sessionErrToHttpResp(ctx echo.Context, err error) error { + if errors.Is(err, accounts.ErrAccountNotFound) || + errors.Is(err, accounts.ErrIncorrectPassword) { + return ctx.JSON(http.StatusUnauthorized, Error{ + Message: pointer.To("Incorrect username or password provided"), + }) + } + + if errors.Is(err, accounts.ErrAccountDisabled) { + return ctx.JSON(http.StatusForbidden, Error{ + Message: pointer.To("User account is disabled"), + }) + } + + if errors.Is(err, accounts.ErrInsufficientCapabilities) { + return ctx.JSON(http.StatusForbidden, Error{ + Message: pointer.To("User account lacks required capabilities"), + }) + } + return err +} From cc97d71360fe06b12d990117170cb18748f072c1 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 11:39:36 +0530 Subject: [PATCH 073/103] fix secret Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 5fc329d54..238db9eb6 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -141,6 +141,8 @@ apiVersion: v1 kind: Secret metadata: name: everest-accounts + annotations: + insecure-password/admin: "true" data: admin: YWRtaW4= #admin --- From 11cc7ca730737adc4593e8ebb35b5bbf87af84d8 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 11:44:29 +0530 Subject: [PATCH 074/103] fix post install message Signed-off-by: Mayank Shah --- pkg/install/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/install/install.go b/pkg/install/install.go index e09cd8195..fdc4e7814 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -56,7 +56,7 @@ Everest has been successfully installed! To view the password for the 'admin' user, run the following command: -kubectl get secret -n everest-system everest-admin-temp -o jsonpath='{.data.password}' | base64 --decode && echo +kubectl get secret everest-accounts -n everest-system -o jsonpath='{.data.admin}' | base64 --decode && echo To create a new user, run the following command: From 88b9503e84d213287d1ee58d9f2f06d84dac8eb8 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 11:47:42 +0530 Subject: [PATCH 075/103] print message to stdout Signed-off-by: Mayank Shah --- pkg/install/install.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/install/install.go b/pkg/install/install.go index fdc4e7814..9cd9e423d 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -216,8 +216,7 @@ func (o *Install) Run(ctx context.Context) error { return err } - o.l.Info("\n" + postInstallMessage + "\n") - + fmt.Fprint(os.Stdout, postInstallMessage) return nil } From f7fdf2f0b09a93c1014dc7c5d4650b02c9697506 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 12:03:08 +0530 Subject: [PATCH 076/103] Fix JWT and admin reset logic --- pkg/install/install.go | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/pkg/install/install.go b/pkg/install/install.go index 9cd9e423d..123782154 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -38,7 +38,6 @@ import ( rbacv1 "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - "github.com/percona/everest/pkg/accounts" "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes" "github.com/percona/everest/pkg/version" @@ -376,6 +375,12 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er if err = o.kubeClient.InstallEverest(ctx, common.SystemNamespace, v); err != nil { return err } + if err := o.createEverestJWTToken(ctx); err != nil { + return err + } + if err := o.resetEverestAdminPassword(ctx); err != nil { + return err + } } else { o.l.Info("Restarting Everest") if err := o.kubeClient.RestartOperator(ctx, common.PerconaEverestOperatorDeploymentName, common.SystemNamespace); err != nil { @@ -386,14 +391,6 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er } } - if err := o.createEverestJWTToken(ctx); err != nil { - return err - } - - if err := o.createEverestAdminAccount(ctx); err != nil { - return err - } - o.l.Info("Updating cluster role bindings for everest-admin") if err := o.kubeClient.UpdateClusterRoleBinding(ctx, everestServiceAccountClusterRoleBinding, o.config.NamespacesList); err != nil { return err @@ -737,15 +734,8 @@ func validateRFC1035(s string) error { return nil } -func (o *Install) createEverestAdminAccount(ctx context.Context) error { - o.l.Info("Creating Everest admin account") - // Check if admin already exists? - if _, err := o.kubeClient.Accounts().Get(ctx, common.EverestAdminUser); err == nil { - return nil - } else if !errors.Is(err, accounts.ErrAccountNotFound) { - return err - } - +func (o *Install) resetEverestAdminPassword(ctx context.Context) error { + o.l.Info("Resetting admin password") if err := o.kubeClient.Accounts().Create(ctx, common.EverestAdminUser, ""); err != nil { return err } From e6ffbd0b8591fa74f54f7c94362152f4656cfd98 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 9 May 2024 12:05:55 +0530 Subject: [PATCH 077/103] fix linting Signed-off-by: Mayank Shah --- api/session.go | 4 ++-- pkg/install/install.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/session.go b/api/session.go index 55fec9cb6..4effb4b46 100644 --- a/api/session.go +++ b/api/session.go @@ -45,7 +45,7 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { c := ctx.Request().Context() err := e.sessionMgr.Authenticate(c, *params.Username, *params.Password) if err != nil { - return sessionErrToHttpResp(ctx, err) + return sessionErrToHTTPRes(ctx, err) } uniqueID, err := uuid.NewRandom() @@ -67,7 +67,7 @@ func (e *EverestServer) CreateSession(ctx echo.Context) error { return ctx.JSON(http.StatusOK, map[string]string{"token": jwtToken}) } -func sessionErrToHttpResp(ctx echo.Context, err error) error { +func sessionErrToHTTPRes(ctx echo.Context, err error) error { if errors.Is(err, accounts.ErrAccountNotFound) || errors.Is(err, accounts.ErrIncorrectPassword) { return ctx.JSON(http.StatusUnauthorized, Error{ diff --git a/pkg/install/install.go b/pkg/install/install.go index 123782154..df598f136 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -370,7 +370,7 @@ func (o *Install) provisionEverest(ctx context.Context, v *goversion.Version) er everestExists = true } - if !everestExists { + if !everestExists { //nolint:nestif o.l.Info(fmt.Sprintf("Deploying Everest to %s", common.SystemNamespace)) if err = o.kubeClient.InstallEverest(ctx, common.SystemNamespace, v); err != nil { return err From 88171ce34468fb1cb4c5639bb67f74e6e0fdd3fe Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 14 May 2024 13:48:47 +0530 Subject: [PATCH 078/103] undo some changes Signed-off-by: Mayank Shah --- dev/Tiltfile | 4 ++-- public/dist/index.html | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/dev/Tiltfile b/dev/Tiltfile index e7c89d320..e83559394 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -14,8 +14,8 @@ print('Using PG operator version: %s' % pg_operator_version) # External resources set up # uncomment the settings below and insert your k8s context & registry names # to get your current context name run `kubectl config view` -allow_k8s_contexts("k3d-everest-dev") -default_registry("localhost:61430") +#allow_k8s_contexts("gke_percona-everest_europe-west1-c_everest-dev") +#default_registry("us-central1-docker.pkg.dev/percona-everest/quickstart-docker-repo") # Check for required env vars diff --git a/public/dist/index.html b/public/dist/index.html index 4f6cc8747..e69de29bb 100644 --- a/public/dist/index.html +++ b/public/dist/index.html @@ -1,15 +0,0 @@ - - - - - - - Percona Everest - - - - -
- - - From 62710ada201f2bd1049bbbec94349b15f27164d4 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 14:12:31 +0530 Subject: [PATCH 079/103] go mod tidy Signed-off-by: Mayank Shah --- go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.sum b/go.sum index 9429ca8c6..e9ccee187 100644 --- a/go.sum +++ b/go.sum @@ -559,8 +559,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/percona/everest-operator v0.6.0-dev1.0.20240514121858-9c270ed11e06 h1:oVCfexKFqKnRyDRWVyAEfMj6s6adUpF7Zgq28KSBcfs= -github.com/percona/everest-operator v0.6.0-dev1.0.20240514121858-9c270ed11e06/go.mod h1:TOvVDtoaChBQBZRMRQwcWs89fA6UuR9huFxxV11Ww3I= +github.com/percona/everest-operator v0.6.0-dev1.0.20240426070203-91f4233b9320 h1:xvYCCmA450nEm0o5eYAgkYaRwN0hat8XGkgtUCHzXwI= +github.com/percona/everest-operator v0.6.0-dev1.0.20240426070203-91f4233b9320/go.mod h1:TOvVDtoaChBQBZRMRQwcWs89fA6UuR9huFxxV11Ww3I= github.com/percona/percona-backup-mongodb v1.8.1-0.20230920143330-3b1c2e263901 h1:BDgsZRCjEuxl2/z4yWBqB0s8d20shuIDks7/RVdZiLs= github.com/percona/percona-backup-mongodb v1.8.1-0.20230920143330-3b1c2e263901/go.mod h1:fZRCMpUqkWlLVdRKqqaj001LoVP2eo6F0ZhoMPeXDng= github.com/percona/percona-postgresql-operator v0.0.0-20231220140959-ad5eef722609 h1:+UOK4gcHrRgqjo4smgfwT7/0apF6PhAJdQIdAV4ub/M= From adc010e4ef3f6628fc8223e35e318c8228b1545a Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 14:14:02 +0530 Subject: [PATCH 080/103] typos Signed-off-by: Mayank Shah --- commands/accounts/create.go | 8 ++++---- commands/accounts/delete.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/commands/accounts/create.go b/commands/accounts/create.go index f3571a667..d3eddc1f7 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -36,7 +36,7 @@ func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { Use: "create", Example: "everestctl accounts create --username user1 --password $USER_PASS", Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initDeleteViperFlags(cmd) + initCreateViperFlags(cmd) kubeconfigPath := viper.GetString("kubeconfig") username := viper.GetString("username") @@ -65,16 +65,16 @@ func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { } }, } - initDeleteFlags(cmd) + initCreateFlags(cmd) return cmd } -func initDeleteFlags(cmd *cobra.Command) { +func initCreateFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") cmd.Flags().StringP("password", "p", "", "Password of the account") } -func initDeleteViperFlags(cmd *cobra.Command) { +func initCreateViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec viper.BindEnv("kubeconfig") //nolint:errcheck,gosec diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go index 298183bbf..f2c08a33a 100644 --- a/commands/accounts/delete.go +++ b/commands/accounts/delete.go @@ -36,7 +36,7 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { Use: "delete", Example: "everestctl accounts delete --username user1 --password $USER_PASS", Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initCreateViperFlags(cmd) + initDeleteViperFlags(cmd) kubeconfigPath := viper.GetString("kubeconfig") username := viper.GetString("username") @@ -61,16 +61,16 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { } }, } - initCreateFlags(cmd) + initDeleteFlags(cmd) return cmd } -func initCreateFlags(cmd *cobra.Command) { +func initDeleteFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") cmd.Flags().StringP("password", "p", "", "Password of the account") } -func initCreateViperFlags(cmd *cobra.Command) { +func initDeleteViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec viper.BindEnv("kubeconfig") //nolint:errcheck,gosec From 3e00c9e5e26d1d3225e7ed02ab825e9c64ca4d23 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 14:17:33 +0530 Subject: [PATCH 081/103] Update go mod --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7ce332f70..f7ab8a251 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/oapi-codegen/runtime v1.1.1 github.com/operator-framework/api v0.23.0 github.com/operator-framework/operator-lifecycle-manager v0.27.0 - github.com/percona/everest-operator v0.6.0-dev1.0.20240426070203-91f4233b9320 + github.com/percona/everest-operator v0.6.0-dev1.0.20240514121858-9c270ed11e06 github.com/rodaine/table v1.2.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 diff --git a/go.sum b/go.sum index e9ccee187..9429ca8c6 100644 --- a/go.sum +++ b/go.sum @@ -559,8 +559,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/percona/everest-operator v0.6.0-dev1.0.20240426070203-91f4233b9320 h1:xvYCCmA450nEm0o5eYAgkYaRwN0hat8XGkgtUCHzXwI= -github.com/percona/everest-operator v0.6.0-dev1.0.20240426070203-91f4233b9320/go.mod h1:TOvVDtoaChBQBZRMRQwcWs89fA6UuR9huFxxV11Ww3I= +github.com/percona/everest-operator v0.6.0-dev1.0.20240514121858-9c270ed11e06 h1:oVCfexKFqKnRyDRWVyAEfMj6s6adUpF7Zgq28KSBcfs= +github.com/percona/everest-operator v0.6.0-dev1.0.20240514121858-9c270ed11e06/go.mod h1:TOvVDtoaChBQBZRMRQwcWs89fA6UuR9huFxxV11Ww3I= github.com/percona/percona-backup-mongodb v1.8.1-0.20230920143330-3b1c2e263901 h1:BDgsZRCjEuxl2/z4yWBqB0s8d20shuIDks7/RVdZiLs= github.com/percona/percona-backup-mongodb v1.8.1-0.20230920143330-3b1c2e263901/go.mod h1:fZRCMpUqkWlLVdRKqqaj001LoVP2eo6F0ZhoMPeXDng= github.com/percona/percona-postgresql-operator v0.0.0-20231220140959-ad5eef722609 h1:+UOK4gcHrRgqjo4smgfwT7/0apF6PhAJdQIdAV4ub/M= From 5a5dbe39181419d2690273b7167cf8faf955b9c8 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 14:17:49 +0530 Subject: [PATCH 082/103] Use constant time compare when checking password hashed --- pkg/kubernetes/client/accounts/accounts.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 2855b3bef..1e2d7e911 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -20,6 +20,7 @@ import ( "context" "crypto/rand" "crypto/sha256" + "crypto/subtle" "encoding/hex" "errors" "fmt" @@ -272,7 +273,7 @@ func (a *configMapsClient) Verify(ctx context.Context, username, password string provided = computedHash } - if stored != provided { + if subtle.ConstantTimeCompare([]byte(stored), []byte(provided)) == 0 { return accounts.ErrIncorrectPassword } return nil From a462cc5422aad2486a2b86517e77d69703d358c8 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 14:44:38 +0530 Subject: [PATCH 083/103] Add username/password sanity checks --- pkg/accounts/cli/accounts.go | 35 ++++++++++++++++++++++++ pkg/accounts/cli/accounts_test.go | 45 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 pkg/accounts/cli/accounts_test.go diff --git a/pkg/accounts/cli/accounts.go b/pkg/accounts/cli/accounts.go index 6fcde586e..f2a8dfe7d 100644 --- a/pkg/accounts/cli/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" "github.com/AlecAivazis/survey/v2" @@ -27,6 +28,7 @@ import ( "go.uber.org/zap" "github.com/percona/everest/pkg/accounts" + "github.com/percona/everest/pkg/common" ) // CLI provides functionality for managing user accounts via the CLI. @@ -75,6 +77,17 @@ func (c *CLI) Create(ctx context.Context, username, password string) error { if username == "" { return errors.New("username is required") } + + if username == common.EverestAdminUser && password == "" { + // fallthrough, this is allowed. + } else if !validateUsername(username) { + c.l.Error("Username must contain only letters, numbers, and underscores, and must be at least 3 characters long") + return errors.New("username is invalid") + } else if !validatePassword(password) { + c.l.Error("Password must contain only letters, numbers and specific special characters (@#$%^&+=!_), and must be at least 6 characters long") + return errors.New("password is invalid") + } + if err := c.accountManager.Create(ctx, username, password); err != nil { return err } @@ -156,3 +169,25 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { tbl.Print() return nil } + +func validateUsername(username string) bool { + // Regular expression to validate username. + // [a-zA-Z0-9_] - Allowed characters (letters, digits, underscore) + // {3,} - Length of the username (minimum 3 characters) + pattern := "^[a-zA-Z0-9_]{3,}$" + regex := regexp.MustCompile(pattern) + return regex.MatchString(username) +} + +func validatePassword(password string) bool { + if strings.Contains(password, " ") { + return false + } + // Regular expression to validate password. + // [a-zA-Z0-9@#$%^&+=!] - Allowed characters (letters, numbers, and specified special characters) + // {6,} - Minimum six characters + // $ - End of the string + pattern := `^[a-zA-Z0-9@#$%^&+=!_]{6,}$` + regex := regexp.MustCompile(pattern) + return regex.MatchString(password) +} diff --git a/pkg/accounts/cli/accounts_test.go b/pkg/accounts/cli/accounts_test.go new file mode 100644 index 000000000..3bd1d8f39 --- /dev/null +++ b/pkg/accounts/cli/accounts_test.go @@ -0,0 +1,45 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUsernamePasswordSanitation(t *testing.T) { + t.Parallel() + t.Run("Username", func(t *testing.T) { + t.Parallel() + testCases := []struct { + username string + allowed bool + }{ + {"alice", true}, + {"bob!!", false}, + {"f", false}, + {"hello@@", false}, + {"bruce_wayne11", true}, + } + for _, tc := range testCases { + result := validateUsername(tc.username) + assert.Equal(t, tc.allowed, result) + } + }) + t.Run("Password", func(t *testing.T) { + t.Parallel() + testCases := []struct { + password string + allowed bool + }{ + {"select *;", false}, + {"DROP TABLE IF EXISTS", false}, + {"pass", false}, + {"verysecurepassword123!!", true}, + {"d@rtH_vad3r_88", true}, + } + for _, tc := range testCases { + result := validatePassword(tc.password) + assert.Equal(t, tc.allowed, result) + } + }) +} From 406f39a9c39e5c018b7978213bd191b31221a653 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 16:00:57 +0530 Subject: [PATCH 084/103] refactor Signed-off-by: Mayank Shah --- pkg/accounts/cli/accounts.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/accounts/cli/accounts.go b/pkg/accounts/cli/accounts.go index f2a8dfe7d..95f7c6158 100644 --- a/pkg/accounts/cli/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -78,14 +78,9 @@ func (c *CLI) Create(ctx context.Context, username, password string) error { return errors.New("username is required") } - if username == common.EverestAdminUser && password == "" { - // fallthrough, this is allowed. - } else if !validateUsername(username) { - c.l.Error("Username must contain only letters, numbers, and underscores, and must be at least 3 characters long") - return errors.New("username is invalid") - } else if !validatePassword(password) { - c.l.Error("Password must contain only letters, numbers and specific special characters (@#$%^&+=!_), and must be at least 6 characters long") - return errors.New("password is invalid") + if ok, msg := validateCredentials(username, password); !ok { + c.l.Error("Invalid credentials", "msg", msg) + return errors.New("invalid credentials") } if err := c.accountManager.Create(ctx, username, password); err != nil { @@ -170,6 +165,21 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { return nil } +func validateCredentials(username, password string) (bool, string) { + if username == common.EverestAdminUser && password == "" { + return true, "" + } + if !validateUsername(username) { + return false, + "Username must contain only letters, numbers, and underscores, and must be at least 3 characters long" + } + if !validatePassword(password) { + return false, + "Password must contain only letters, numbers and specific special characters (@#$%^&+=!_), and must be at least 6 characters long" + } + return true, "" +} + func validateUsername(username string) bool { // Regular expression to validate username. // [a-zA-Z0-9_] - Allowed characters (letters, digits, underscore) From 142fd77abd4e46e7c332326ecc303f5790ba607e Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 16:15:51 +0530 Subject: [PATCH 085/103] change username Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 2 +- pkg/accounts/cli/accounts.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index cb061b1bf..887d5b382 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -343,7 +343,7 @@ jobs: - name: Create Everest test user run: | - ./bin/everestctl accounts create -u everest-ci -p password + ./bin/everestctl accounts create -u everest_ci -p password echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest-ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV - name: Run integration tests diff --git a/pkg/accounts/cli/accounts.go b/pkg/accounts/cli/accounts.go index 95f7c6158..932def02a 100644 --- a/pkg/accounts/cli/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -79,7 +79,7 @@ func (c *CLI) Create(ctx context.Context, username, password string) error { } if ok, msg := validateCredentials(username, password); !ok { - c.l.Error("Invalid credentials", "msg", msg) + c.l.Error(msg) return errors.New("invalid credentials") } From c940fef1cd9052bbf50b2088fa4a196bbbc54d70 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 16:28:08 +0530 Subject: [PATCH 086/103] fix token Signed-off-by: Mayank Shah --- .github/workflows/dev-be-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-be-ci.yaml b/.github/workflows/dev-be-ci.yaml index 887d5b382..6ea88d3a7 100644 --- a/.github/workflows/dev-be-ci.yaml +++ b/.github/workflows/dev-be-ci.yaml @@ -344,7 +344,7 @@ jobs: - name: Create Everest test user run: | ./bin/everestctl accounts create -u everest_ci -p password - echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest-ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV + echo "API_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest_ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV - name: Run integration tests run: | From f2d999b762029d3364db5220b8cbc6e79089aee9 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 16:34:50 +0530 Subject: [PATCH 087/103] remove regex for password Signed-off-by: Mayank Shah --- pkg/accounts/cli/accounts.go | 11 ++++------- pkg/accounts/cli/accounts_test.go | 6 ++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/pkg/accounts/cli/accounts.go b/pkg/accounts/cli/accounts.go index 932def02a..4998378aa 100644 --- a/pkg/accounts/cli/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -193,11 +193,8 @@ func validatePassword(password string) bool { if strings.Contains(password, " ") { return false } - // Regular expression to validate password. - // [a-zA-Z0-9@#$%^&+=!] - Allowed characters (letters, numbers, and specified special characters) - // {6,} - Minimum six characters - // $ - End of the string - pattern := `^[a-zA-Z0-9@#$%^&+=!_]{6,}$` - regex := regexp.MustCompile(pattern) - return regex.MatchString(password) + if len(password) < 6 { + return false + } + return true } diff --git a/pkg/accounts/cli/accounts_test.go b/pkg/accounts/cli/accounts_test.go index 3bd1d8f39..877956764 100644 --- a/pkg/accounts/cli/accounts_test.go +++ b/pkg/accounts/cli/accounts_test.go @@ -31,11 +31,9 @@ func TestUsernamePasswordSanitation(t *testing.T) { password string allowed bool }{ - {"select *;", false}, - {"DROP TABLE IF EXISTS", false}, {"pass", false}, - {"verysecurepassword123!!", true}, - {"d@rtH_vad3r_88", true}, + {"password with spaces", false}, + {"verysecurepassword!", true}, } for _, tc := range testCases { result := validatePassword(tc.password) From a196621cc95ac4e5bd9439285bd1742ea5969c98 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Wed, 15 May 2024 17:32:51 +0530 Subject: [PATCH 088/103] fix FE tests Signed-off-by: Mayank Shah --- .github/workflows/dev-fe-ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-fe-ci.yaml b/.github/workflows/dev-fe-ci.yaml index d8d466400..52ef9b3e8 100644 --- a/.github/workflows/dev-fe-ci.yaml +++ b/.github/workflows/dev-fe-ci.yaml @@ -213,9 +213,10 @@ jobs: --skip-wizard \ --namespaces pg-only - - name: Change token + - name: Create Everest test user run: | - echo "EVEREST_K8_TOKEN=$(./bin/everestctl token reset --json | jq .token -r)" >> $GITHUB_ENV + ./bin/everestctl accounts create -u everest_ci -p password + echo "EVEREST_K8_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest_ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV - name: Expose Everest API Server run: | From afd04b7699d49af074f4d5a75b162625c89963c3 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 16 May 2024 10:52:31 +0530 Subject: [PATCH 089/103] Store everything in a single secret --- pkg/accounts/types.go | 1 + pkg/common/constants.go | 5 +- pkg/kubernetes/client/accounts/accounts.go | 164 +++++++----------- .../client/accounts/accounts_test.go | 15 +- 4 files changed, 64 insertions(+), 121 deletions(-) diff --git a/pkg/accounts/types.go b/pkg/accounts/types.go index 5917d945a..4f0d208ac 100644 --- a/pkg/accounts/types.go +++ b/pkg/accounts/types.go @@ -48,6 +48,7 @@ type Account struct { Enabled bool `yaml:"enabled"` Capabilities []AccountCapability `yaml:"capabilities"` PasswordMtime string `yaml:"passwordMtime"` + PasswordHash string `yaml:"passwordHash"` } // HasCapability returns true if the given account has the specified capability. diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 4b293d1ee..9ded6299e 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -36,9 +36,8 @@ const ( // EverestOperatorName holds the name for Everest operator. EverestOperatorName = "everest-operator" - // EverestAccountsConfigName is the name of the ConfigMap that holds account data. - EverestAccountsConfigName = "everest-accounts" - + // EverestAccountsSecretName is the name of the secret that holds accounts. + EverestAccountsSecretName = "everest-accounts" // EverestJWTSecretName is the name of the secret that holds JWT secret. EverestJWTSecretName = "everest-jwt" // EverestJWTSecretKey is the key in the secret that holds JWT secret. diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 1e2d7e911..38052004c 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -28,8 +28,6 @@ import ( "golang.org/x/crypto/pbkdf2" "gopkg.in/yaml.v2" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/percona/everest/pkg/accounts" "github.com/percona/everest/pkg/common" @@ -37,9 +35,7 @@ import ( ) const ( - usersFile = "users.yaml" - tempAdminPasswordSecret = "everest-admin-temp" - + usersFile = "users.yaml" // We set this annotation on the secret to indicate which passwords are stored in plain text. insecurePasswordAnnotation = "insecure-password/%s" ) @@ -76,11 +72,11 @@ func (a *configMapsClient) List(ctx context.Context) (map[string]*accounts.Accou func (a *configMapsClient) listAllAccounts(ctx context.Context) (map[string]*accounts.Account, error) { result := make(map[string]*accounts.Account) - cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsSecretName) if err != nil { return nil, err } - if err := yaml.Unmarshal([]byte(cm.Data[usersFile]), result); err != nil { + if err := yaml.Unmarshal(secret.Data[usersFile], result); err != nil { return nil, err } return result, nil @@ -88,33 +84,31 @@ func (a *configMapsClient) listAllAccounts(ctx context.Context) (map[string]*acc // Create a new user account. func (a *configMapsClient) Create(ctx context.Context, username, password string) error { - account := &accounts.Account{ - Enabled: true, - Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, - PasswordMtime: time.Now().Format(time.RFC3339), - } - - // XX: once we allow updating password, Create should fail if the user already exists. - if err := a.setAccount(ctx, username, account); err != nil { - return err + // Compute a hash for the password. + hash, err := a.computePasswordHash(ctx, password) + if err != nil { + return errors.Join(err, errors.New("failed to compute hash")) } + secure := true - // If an admin account is created without a password, we will generate a random password - // and store it in plain text. - storeHashed := true + // Initial admin account is stored in plain text. if username == common.EverestAdminUser && password == "" { - storeHashed = false - randPassword, err := a.generateRandomPassword() + randomPass, err := a.generateRandomPassword() if err != nil { - return err + return errors.Join(err, errors.New("failed to generate random password")) } - password = randPassword + hash = randomPass + secure = false } - if err := a.setPassword(ctx, username, password, storeHashed); err != nil { - return err + account := &accounts.Account{ + Enabled: true, + Capabilities: []accounts.AccountCapability{accounts.AccountCapabilityLogin}, + PasswordMtime: time.Now().Format(time.RFC3339), + PasswordHash: hash, } - return nil + + return a.insertOrUpdateAccount(ctx, username, account, secure) } func (a *configMapsClient) generateRandomPassword() (string, error) { @@ -125,65 +119,43 @@ func (a *configMapsClient) generateRandomPassword() (string, error) { return hex.EncodeToString(b), nil } -func (a *configMapsClient) setPassword( +func (a *configMapsClient) insertOrUpdateAccount( ctx context.Context, - username, password string, - ensureHash bool, + username string, + account *accounts.Account, + secure bool, ) error { - secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsSecretName) if err != nil { return err } - hash := password - if !ensureHash { - // Add an annotation so that we know which passwords are stored as plain text. - annotations := secret.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[fmt.Sprintf(insecurePasswordAnnotation, username)] = "true" - secret.SetAnnotations(annotations) - } else { - hash, err = a.computePasswordHash(ctx, password) - if err != nil { - return errors.Join(err, errors.New("failed to compute hash")) - } - // Remove the annotation as the password is hashed. - annotations := secret.GetAnnotations() - delete(annotations, fmt.Sprintf(insecurePasswordAnnotation, username)) - secret.SetAnnotations(annotations) - } - - if secret.Data == nil { - secret.Data = make(map[string][]byte) - } - secret.Data[username] = []byte(hash) - if _, err := a.k.UpdateSecret(ctx, secret); err != nil { - return err - } - return nil -} - -func (a *configMapsClient) setAccount(ctx context.Context, username string, account *accounts.Account) error { - cm, err := a.k.GetConfigMap(ctx, common.SystemNamespace, common.EverestAccountsConfigName) - if err != nil { - return err - } accounts := make(map[string]*accounts.Account) - if err := yaml.Unmarshal([]byte(cm.Data[usersFile]), &accounts); err != nil { + if err := yaml.Unmarshal([]byte(secret.Data[usersFile]), &accounts); err != nil { return err } + accounts[username] = account data, err := yaml.Marshal(accounts) if err != nil { return err } - if cm.Data == nil { - cm.Data = make(map[string]string) + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + secret.Data[usersFile] = data + + annotations := secret.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) } - cm.Data[usersFile] = string(data) - if _, err := a.k.UpdateConfigMap(ctx, cm); err != nil { + delete(annotations, fmt.Sprintf(insecurePasswordAnnotation, username)) + if !secure { + annotations[fmt.Sprintf(insecurePasswordAnnotation, username)] = "true" + } + + if _, err := a.k.UpdateSecret(ctx, secret); err != nil { return err } return nil @@ -203,33 +175,21 @@ func (a *configMapsClient) Delete(ctx context.Context, username string) error { if err != nil { return err } - // Update ConfigMap. + + if _, found := users[username]; !found { + return accounts.ErrAccountNotFound + } + delete(users, username) - b, err := yaml.Marshal(users) + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsSecretName) if err != nil { return err } - cmData := map[string]string{ - usersFile: string(b), - } - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestAccountsConfigName, - Namespace: common.SystemNamespace, - }, - Data: cmData, - } - if _, err := a.k.UpdateConfigMap(ctx, cm); err != nil { - return err - } - // Update Secret. - secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) + data, err := yaml.Marshal(users) if err != nil { return err } - secretData := secret.Data - delete(secretData, username) - secret.Data = secretData + secret.Data[usersFile] = data if _, err := a.k.UpdateSecret(ctx, secret); err != nil { return err } @@ -237,19 +197,19 @@ func (a *configMapsClient) Delete(ctx context.Context, username string) error { } func (a *configMapsClient) Verify(ctx context.Context, username, password string) error { - users, err := a.listAllAccounts(ctx) + secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsSecretName) if err != nil { return err } - _, found := users[username] - if !found { - return accounts.ErrAccountNotFound - } - secret, err := a.k.GetSecret(ctx, common.SystemNamespace, common.EverestAccountsConfigName) - if err != nil { + users := make(map[string]*accounts.Account) + if err := yaml.Unmarshal(secret.Data[usersFile], users); err != nil { return err } + user, found := users[username] + if !found { + return accounts.ErrAccountNotFound + } // helper to check if a password should be compared as a hash. shouldCompareAsHash := func() bool { @@ -258,13 +218,9 @@ func (a *configMapsClient) Verify(ctx context.Context, username, password string return !found } - storedB, found := secret.Data[username] - if !found { - return accounts.ErrAccountNotFound - } - stored := string(storedB) - + actual := user.PasswordHash provided := password + if shouldCompareAsHash() { computedHash, err := a.computePasswordHash(ctx, password) if err != nil { @@ -273,7 +229,7 @@ func (a *configMapsClient) Verify(ctx context.Context, username, password string provided = computedHash } - if subtle.ConstantTimeCompare([]byte(stored), []byte(provided)) == 0 { + if subtle.ConstantTimeCompare([]byte(actual), []byte(provided)) == 0 { return accounts.ErrIncorrectPassword } return nil diff --git a/pkg/kubernetes/client/accounts/accounts_test.go b/pkg/kubernetes/client/accounts/accounts_test.go index 94abd4acd..4a255655b 100644 --- a/pkg/kubernetes/client/accounts/accounts_test.go +++ b/pkg/kubernetes/client/accounts/accounts_test.go @@ -29,26 +29,13 @@ func TestAccounts(t *testing.T) { ) require.NoError(t, err) - // Prepare configmap. - _, err = c.Clientset(). - CoreV1(). - ConfigMaps(common.SystemNamespace). - Create(ctx, &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestAccountsConfigName, - Namespace: common.SystemNamespace, - }, - }, metav1.CreateOptions{}, - ) - require.NoError(t, err) - // Prepare secret. _, err = c.Clientset(). CoreV1(). Secrets(common.SystemNamespace). Create(ctx, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: common.EverestAccountsConfigName, + Name: common.EverestAccountsSecretName, Namespace: common.SystemNamespace, }, }, metav1.CreateOptions{}, From ba733f0e8d1a84a9fc802217762f100592bd205c Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 16 May 2024 11:10:52 +0530 Subject: [PATCH 090/103] fix manifest Signed-off-by: Mayank Shah --- deploy/quickstart-k8s.yaml | 30 ++++++++-------------- pkg/kubernetes/client/accounts/accounts.go | 2 +- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/deploy/quickstart-k8s.yaml b/deploy/quickstart-k8s.yaml index 238db9eb6..99c47d545 100644 --- a/deploy/quickstart-k8s.yaml +++ b/deploy/quickstart-k8s.yaml @@ -72,6 +72,16 @@ data: # use your own signing key here. signing_key: eW91ci1ldmVyZXN0LWp3dC1zaWduaW5nLWtleQo= --- +apiVersion: v1 +kind: Secret +metadata: + name: everest-accounts + annotations: + insecure-password/admin: "true" +data: + # username: admin | password: admin + users.yaml: YWRtaW46CiAgcGFzc3dvcmRIYXNoOiBhZG1pbgogIGVuYWJsZWQ6IHRydWUKICBjYXBhYmlsaXRpZXM6CiAgICAtIGxvZ2lu +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -137,23 +147,3 @@ spec: - protocol: TCP port: 8080 --- -apiVersion: v1 -kind: Secret -metadata: - name: everest-accounts - annotations: - insecure-password/admin: "true" -data: - admin: YWRtaW4= #admin ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: everest-accounts -data: - users.yaml : | - admin: - enabled: true - capabilities: - - login ---- diff --git a/pkg/kubernetes/client/accounts/accounts.go b/pkg/kubernetes/client/accounts/accounts.go index 38052004c..7dff01b68 100644 --- a/pkg/kubernetes/client/accounts/accounts.go +++ b/pkg/kubernetes/client/accounts/accounts.go @@ -131,7 +131,7 @@ func (a *configMapsClient) insertOrUpdateAccount( } accounts := make(map[string]*accounts.Account) - if err := yaml.Unmarshal([]byte(secret.Data[usersFile]), &accounts); err != nil { + if err := yaml.Unmarshal(secret.Data[usersFile], &accounts); err != nil { return err } From 5e84ab298c657a045cbd5f56c5a80f413d154388 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 16 May 2024 11:42:13 +0530 Subject: [PATCH 091/103] update install message Signed-off-by: Mayank Shah --- pkg/install/install.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/install/install.go b/pkg/install/install.go index df598f136..77ef4b6ec 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -53,9 +53,14 @@ const ( const postInstallMessage = ` Everest has been successfully installed! + To view the password for the 'admin' user, run the following command: -kubectl get secret everest-accounts -n everest-system -o jsonpath='{.data.admin}' | base64 --decode && echo +kubectl get secret everest-accounts -n everest-system -o jsonpath='{.data.users\.yaml}' \ + | base64 --decode \ + | grep -A 5 '^admin:' \ + | grep 'passwordHash:' \ + | awk '{print $2}' To create a new user, run the following command: From d664eacbdd0c91287b69e0e8766fad962c54a0cf Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 16 May 2024 12:16:19 +0530 Subject: [PATCH 092/103] revert version.go changes Signed-off-by: Mayank Shah --- pkg/version/version.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/version/version.go b/pkg/version/version.go index 95df2c488..1887d42bc 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -32,6 +32,7 @@ const ( releaseCatalogImage = "docker.io/percona/everest-catalog:%s" devManifestURL = "https://raw.githubusercontent.com/percona/everest/main/deploy/quickstart-k8s.yaml" releaseManifestURL = "https://raw.githubusercontent.com/percona/everest/v%s/deploy/quickstart-k8s.yaml" + debugManifestURL = "https://raw.githubusercontent.com/percona/everest/%s/deploy/quickstart-k8s.yaml" everestOperatorChannelStable = "stable-v0" everestOperatorChannelFast = "fast-v0" From 68d7de6931243cb1a0e7035410a349a6652c992d Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 16 May 2024 14:52:24 +0530 Subject: [PATCH 093/103] clean-up tiltfile Signed-off-by: Mayank Shah --- dev/Tiltfile | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/Tiltfile b/dev/Tiltfile index e83559394..acd3ea88c 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -408,7 +408,6 @@ k8s_resource( 'everest-admin-cluster-role-binding:clusterrolebinding', 'everest-jwt:secret', 'everest-accounts:secret', - 'everest-accounts:configmap', ], new_name='everest', port_forwards=8080, From 10763f160dbc4669a76fda0a196e372c183f6663 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 16 May 2024 17:33:56 +0530 Subject: [PATCH 094/103] re-arrange steps Signed-off-by: Mayank Shah --- .github/workflows/dev-fe-ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-fe-ci.yaml b/.github/workflows/dev-fe-ci.yaml index 52ef9b3e8..3f24dddbc 100644 --- a/.github/workflows/dev-fe-ci.yaml +++ b/.github/workflows/dev-fe-ci.yaml @@ -213,15 +213,15 @@ jobs: --skip-wizard \ --namespaces pg-only + - name: Expose Everest API Server + run: | + kubectl port-forward -n everest-system deployment/percona-everest 8080:8080 & + - name: Create Everest test user run: | ./bin/everestctl accounts create -u everest_ci -p password echo "EVEREST_K8_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest_ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV - - name: Expose Everest API Server - run: | - kubectl port-forward -n everest-system deployment/percona-everest 8080:8080 & - - name: Run Everest run: | cd ui From b56b4755c0e030b87e8131bd71898b5183cfa0b3 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Thu, 16 May 2024 13:12:11 +0100 Subject: [PATCH 095/103] chore: add user/pasword to e2e tests --- .github/workflows/dev-fe-ci.yaml | 8 ++++++-- ui/apps/everest/.e2e/auth.setup.ts | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-fe-ci.yaml b/.github/workflows/dev-fe-ci.yaml index 52ef9b3e8..315ae0f8c 100644 --- a/.github/workflows/dev-fe-ci.yaml +++ b/.github/workflows/dev-fe-ci.yaml @@ -214,9 +214,11 @@ jobs: --namespaces pg-only - name: Create Everest test user + env: + CI_USER: '${{ secrets.CI_USER }}' + CI_PASSWORD: '${{ secrets.CI_PASSWORD }}' run: | - ./bin/everestctl accounts create -u everest_ci -p password - echo "EVEREST_K8_TOKEN=$(curl --location -s 'localhost:8080/v1/session' --header 'Content-Type: application/json' --data '{"username": "everest_ci","password": "password"}' | jq -r .token)" >> $GITHUB_ENV + ./bin/everestctl accounts create -u ${CI_USER} -p ${CI_PASSWORD} - name: Expose Everest API Server run: | @@ -235,6 +237,8 @@ jobs: - name: Run integration tests env: + CI_USER: '${{ secrets.CI_USER }}' + CI_PASSWORD: '${{ secrets.CI_PASSWORD }}' EVEREST_LOCATION_BUCKET_NAME: '${{ secrets.BACKUP_LOCATION_BUCKET_NAME }}' EVEREST_LOCATION_ACCESS_KEY: '${{ secrets.BACKUP_LOCATION_ACCESS_KEY }}' EVEREST_LOCATION_SECRET_KEY: '${{ secrets.BACKUP_LOCATION_SECRET_KEY }}' diff --git a/ui/apps/everest/.e2e/auth.setup.ts b/ui/apps/everest/.e2e/auth.setup.ts index e4f1d60b6..db875ca46 100644 --- a/ui/apps/everest/.e2e/auth.setup.ts +++ b/ui/apps/everest/.e2e/auth.setup.ts @@ -15,11 +15,12 @@ import { expect, test as setup } from '@playwright/test'; import { STORAGE_STATE_FILE } from './constants'; -const { EVEREST_K8_TOKEN } = process.env; +const { CI_USER, CI_PASSWORD } = process.env; setup('Login', async ({ page }) => { page.goto('/login'); - await page.getByTestId('text-input-token').fill(EVEREST_K8_TOKEN); + await page.getByTestId('text-input-username').fill(CI_USER); + await page.getByTestId('text-input-password').fill(CI_PASSWORD); await page.getByTestId('login-button').click(); await expect(page.getByText('Create database')).toBeVisible(); From f7a38270e8a9d12497a3b00efeab83dc9e3555fd Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Fri, 17 May 2024 03:26:41 +0100 Subject: [PATCH 096/103] chore: add missing secrets --- .github/actions/fe-e2e/action.yml | 8 ++++++-- .github/workflows/dev-fe-gatekeeper.yaml | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/actions/fe-e2e/action.yml b/.github/actions/fe-e2e/action.yml index feb7252e8..618e1c0d1 100644 --- a/.github/actions/fe-e2e/action.yml +++ b/.github/actions/fe-e2e/action.yml @@ -11,6 +11,10 @@ inputs: type: string BACKUP_LOCATION_URL: type: string + CI_USER: + type: string + CI_PASSWORD: + type: string runs: using: 'composite' @@ -142,8 +146,8 @@ runs: - name: Run integration tests shell: bash env: - CI_USER: '${{ secrets.CI_USER }}' - CI_PASSWORD: '${{ secrets.CI_PASSWORD }}' + CI_USER: '${{ inputs.CI_USER }}' + CI_PASSWORD: '${{ inputs.CI_PASSWORD }}' EVEREST_LOCATION_BUCKET_NAME: '${{ inputs.BACKUP_LOCATION_BUCKET_NAME }}' EVEREST_LOCATION_ACCESS_KEY: '${{ inputs.BACKUP_LOCATION_ACCESS_KEY }}' EVEREST_LOCATION_SECRET_KEY: '${{ inputs.BACKUP_LOCATION_SECRET_KEY }}' diff --git a/.github/workflows/dev-fe-gatekeeper.yaml b/.github/workflows/dev-fe-gatekeeper.yaml index 123f034a7..f268028eb 100644 --- a/.github/workflows/dev-fe-gatekeeper.yaml +++ b/.github/workflows/dev-fe-gatekeeper.yaml @@ -137,6 +137,8 @@ jobs: BACKUP_LOCATION_SECRET_KEY: ${{ secrets.BACKUP_LOCATION_SECRET_KEY }} BACKUP_LOCATION_REGION: ${{ secrets.BACKUP_LOCATION_REGION }} BACKUP_LOCATION_URL: ${{ secrets.BACKUP_LOCATION_URL }} + CI_USER: ${{ secrets.CI_USER }} + CI_PASSWORD: ${{ secrets.CI_PASSWORD }} merge-gatekeeper: needs: [CI_checks, e2e_execution] @@ -152,4 +154,4 @@ jobs: interval: 45 timeout: 300 ignored: 'license/snyk (Percona Github Org), security/snyk (Percona Github Org)' - ref: ${{ github.event.pull_request.head.sha || github.sha }} \ No newline at end of file + ref: ${{ github.event.pull_request.head.sha || github.sha }} From 71616e5f1732984a789d0e88e22784852bbfe967 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Fri, 17 May 2024 03:27:45 +0100 Subject: [PATCH 097/103] fix: secrets -> inputs --- .github/actions/fe-e2e/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/fe-e2e/action.yml b/.github/actions/fe-e2e/action.yml index 618e1c0d1..4e824e7c7 100644 --- a/.github/actions/fe-e2e/action.yml +++ b/.github/actions/fe-e2e/action.yml @@ -125,8 +125,8 @@ runs: - name: Create Everest test user shell: bash env: - CI_USER: '${{ secrets.CI_USER }}' - CI_PASSWORD: '${{ secrets.CI_PASSWORD }}' + CI_USER: '${{ inputs.CI_USER }}' + CI_PASSWORD: '${{ inputs.CI_PASSWORD }}' run: | ./bin/everestctl accounts create -u ${CI_USER} -p ${CI_PASSWORD} From 5d36e416d71ec8fab993f3ceb03bef5b5a126f2e Mon Sep 17 00:00:00 2001 From: fabio-silva Date: Fri, 17 May 2024 11:30:01 +0000 Subject: [PATCH 098/103] chore: lint/format --- ui/apps/everest/src/pages/login/Login.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ui/apps/everest/src/pages/login/Login.tsx b/ui/apps/everest/src/pages/login/Login.tsx index 3e19823e1..c79e1733c 100644 --- a/ui/apps/everest/src/pages/login/Login.tsx +++ b/ui/apps/everest/src/pages/login/Login.tsx @@ -1,10 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { - Box, - Button, - Stack, - Typography, -} from '@mui/material'; +import { Box, Button, Stack, Typography } from '@mui/material'; import { Card, EverestMainIcon, TextInput } from '@percona/ui-lib'; import { AuthContext } from 'contexts/auth'; import { useContext } from 'react'; @@ -21,7 +16,10 @@ const Login = () => { }); const { login, authStatus, redirectRoute } = useContext(AuthContext); - const handleLogin: SubmitHandler = ({ username, password }) => { + const handleLogin: SubmitHandler = ({ + username, + password, + }) => { login(username, password); }; From 78c32d2fef4a9bd47993fc4fb28fe2cf96ad37f0 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Fri, 17 May 2024 12:33:57 +0100 Subject: [PATCH 099/103] chore: deploy From 37b41d6b23d2c812cdfa16ffb2ffb7968c414ca7 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Fri, 17 May 2024 13:47:10 +0100 Subject: [PATCH 100/103] feat: add CI_USER/PASSWORD --- .github/workflows/dev-fe-e2e.yaml | 8 +++++++- .github/workflows/dev-fe-gatekeeper.yaml | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev-fe-e2e.yaml b/.github/workflows/dev-fe-e2e.yaml index 37ef9de68..218cdeccc 100644 --- a/.github/workflows/dev-fe-e2e.yaml +++ b/.github/workflows/dev-fe-e2e.yaml @@ -13,6 +13,10 @@ on: required: true BACKUP_LOCATION_URL: required: true + CI_USER: + required: true + CI_PASSWORD: + required: true jobs: e2e: @@ -146,8 +150,10 @@ jobs: EVEREST_LOCATION_SECRET_KEY: '${{ secrets.BACKUP_LOCATION_SECRET_KEY }}' EVEREST_LOCATION_REGION: '${{ secrets.BACKUP_LOCATION_REGION }}' EVEREST_LOCATION_URL: '${{ secrets.BACKUP_LOCATION_URL }}' + CI_USER: '${{ secrets.CI_USER }}' + CI_PASSWORD: '${{ secrets.CI_PASSWORD }}' MONITORING_USER: 'admin' MONITORING_PASSWORD: 'admin' run: | cd ui - pnpm --filter "@percona/everest" e2e \ No newline at end of file + pnpm --filter "@percona/everest" e2e diff --git a/.github/workflows/dev-fe-gatekeeper.yaml b/.github/workflows/dev-fe-gatekeeper.yaml index 15addbe55..eda10a004 100644 --- a/.github/workflows/dev-fe-gatekeeper.yaml +++ b/.github/workflows/dev-fe-gatekeeper.yaml @@ -123,7 +123,7 @@ jobs: run: | echo "${{ github.triggering_actor }} does not have permissions on this repo." exit 1 - + E2E_tests_workflow: uses: ./.github/workflows/dev-fe-e2e.yaml secrets: @@ -132,7 +132,8 @@ jobs: BACKUP_LOCATION_SECRET_KEY: ${{ secrets.BACKUP_LOCATION_SECRET_KEY }} BACKUP_LOCATION_REGION: ${{ secrets.BACKUP_LOCATION_REGION }} BACKUP_LOCATION_URL: ${{ secrets.BACKUP_LOCATION_URL }} - + CI_USER: ${{ secrets.CI_USER }} + CI_PASSWORD: ${{ secrets.CI_PASSWORD }} # e2e_execution: # needs: permission_checks From 886120cb1705122b6af5740f937f09cc5391f2f1 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Fri, 17 May 2024 14:03:20 +0100 Subject: [PATCH 101/103] chore: create user on CI --- .github/workflows/dev-fe-e2e.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dev-fe-e2e.yaml b/.github/workflows/dev-fe-e2e.yaml index 218cdeccc..1cc1eed4a 100644 --- a/.github/workflows/dev-fe-e2e.yaml +++ b/.github/workflows/dev-fe-e2e.yaml @@ -124,6 +124,11 @@ jobs: run: | echo "EVEREST_K8_TOKEN=$(./bin/everestctl token reset --json | jq .token -r)" >> $GITHUB_ENV + - name: Create Everest test user + shell: bash + run: | + ./bin/everestctl accounts create -u ${{ secrets.CI_USER }} -p ${{ secrets.CI_USER }} + - name: Expose Everest API Server shell: bash run: | From eb91fec900dd26c83f8074749564933e8412d90c Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Sat, 18 May 2024 20:51:05 +0100 Subject: [PATCH 102/103] fix: bad password set --- .github/workflows/dev-fe-e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-fe-e2e.yaml b/.github/workflows/dev-fe-e2e.yaml index 1cc1eed4a..f26983c6e 100644 --- a/.github/workflows/dev-fe-e2e.yaml +++ b/.github/workflows/dev-fe-e2e.yaml @@ -127,7 +127,7 @@ jobs: - name: Create Everest test user shell: bash run: | - ./bin/everestctl accounts create -u ${{ secrets.CI_USER }} -p ${{ secrets.CI_USER }} + ./bin/everestctl accounts create -u ${{ secrets.CI_USER }} -p ${{ secrets.CI_PASSWORD }} - name: Expose Everest API Server shell: bash From 6b0f1fe02b0526afe48a3400607e5d75b64a2462 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Tue, 21 May 2024 15:33:36 +0100 Subject: [PATCH 103/103] remove authentication from Delete Signed-off-by: Mayank Shah --- commands/accounts/delete.go | 5 +---- pkg/accounts/cli/accounts.go | 13 +++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go index f2c08a33a..24687a2d2 100644 --- a/commands/accounts/delete.go +++ b/commands/accounts/delete.go @@ -40,7 +40,6 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { kubeconfigPath := viper.GetString("kubeconfig") username := viper.GetString("username") - password := viper.GetString("password") k, err := kubernetes.New(kubeconfigPath, l) if err != nil { @@ -55,7 +54,7 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { cli := accountscli.New(l) cli.WithAccountManager(k.Accounts()) - if err := cli.Delete(context.Background(), username, password); err != nil { + if err := cli.Delete(context.Background(), username); err != nil { l.Error(err) os.Exit(1) } @@ -67,12 +66,10 @@ func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { func initDeleteFlags(cmd *cobra.Command) { cmd.Flags().StringP("username", "u", "", "Username of the account") - cmd.Flags().StringP("password", "p", "", "Password of the account") } func initDeleteViperFlags(cmd *cobra.Command) { viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec - viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec viper.BindEnv("kubeconfig") //nolint:errcheck,gosec viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec } diff --git a/pkg/accounts/cli/accounts.go b/pkg/accounts/cli/accounts.go index 4998378aa..2faf969db 100644 --- a/pkg/accounts/cli/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -91,16 +91,17 @@ func (c *CLI) Create(ctx context.Context, username, password string) error { } // Delete an existing user account. -func (c *CLI) Delete(ctx context.Context, username, password string) error { - if err := c.runCredentialsWizard(&username, &password); err != nil { - return err +func (c *CLI) Delete(ctx context.Context, username string) error { + if username == "" { + if err := survey.AskOne(&survey.Input{ + Message: "Enter username", + }, username); err != nil { + return err + } } if username == "" { return errors.New("username is required") } - if err := c.accountManager.Verify(ctx, username, password); err != nil { - return err - } return c.accountManager.Delete(ctx, username) }