mirror of
https://github.com/arangodb/kube-arangodb.git
synced 2024-12-14 11:57:37 +00:00
Feature/change user password (#463)
* Change password in a database for a user * Add unit tests * Change password for user when hash secret has changed * Add integration test for changing root password * Add disclaimer * Change comment * Run unit test for whole pkg/deployment/... * Check password in the database * Fix nil receiver * Fix removing root secret password * Fix unit test definition in Makefile * Don't validate non-existing users' secrets
This commit is contained in:
parent
07f66d7cce
commit
b16029b33c
10 changed files with 467 additions and 39 deletions
3
Makefile
3
Makefile
|
@ -308,8 +308,7 @@ run-unit-tests: $(SOURCES)
|
||||||
$(REPOPATH)/pkg/apis/deployment/v1alpha \
|
$(REPOPATH)/pkg/apis/deployment/v1alpha \
|
||||||
$(REPOPATH)/pkg/apis/replication/v1alpha \
|
$(REPOPATH)/pkg/apis/replication/v1alpha \
|
||||||
$(REPOPATH)/pkg/apis/storage/v1alpha \
|
$(REPOPATH)/pkg/apis/storage/v1alpha \
|
||||||
$(REPOPATH)/pkg/deployment/reconcile \
|
$(REPOPATH)/pkg/deployment/... \
|
||||||
$(REPOPATH)/pkg/deployment/resources \
|
|
||||||
$(REPOPATH)/pkg/storage \
|
$(REPOPATH)/pkg/storage \
|
||||||
$(REPOPATH)/pkg/util/k8sutil \
|
$(REPOPATH)/pkg/util/k8sutil \
|
||||||
$(REPOPATH)/pkg/util/k8sutil/test \
|
$(REPOPATH)/pkg/util/k8sutil/test \
|
||||||
|
|
|
@ -34,6 +34,8 @@ type SecretHashes struct {
|
||||||
TLSCA string `json:"tls-ca,omitempty"`
|
TLSCA string `json:"tls-ca,omitempty"`
|
||||||
// SyncTLSCA contains the hash of the sync.tls.caSecretName secret
|
// SyncTLSCA contains the hash of the sync.tls.caSecretName secret
|
||||||
SyncTLSCA string `json:"sync-tls-ca,omitempty"`
|
SyncTLSCA string `json:"sync-tls-ca,omitempty"`
|
||||||
|
// User's map contains hashes for each user
|
||||||
|
Users map[string]string `json:"users,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equal compares two SecretHashes
|
// Equal compares two SecretHashes
|
||||||
|
@ -47,5 +49,40 @@ func (sh *SecretHashes) Equal(other *SecretHashes) bool {
|
||||||
return sh.AuthJWT == other.AuthJWT &&
|
return sh.AuthJWT == other.AuthJWT &&
|
||||||
sh.RocksDBEncryptionKey == other.RocksDBEncryptionKey &&
|
sh.RocksDBEncryptionKey == other.RocksDBEncryptionKey &&
|
||||||
sh.TLSCA == other.TLSCA &&
|
sh.TLSCA == other.TLSCA &&
|
||||||
sh.SyncTLSCA == other.SyncTLSCA
|
sh.SyncTLSCA == other.SyncTLSCA &&
|
||||||
|
isStringMapEqual(sh.Users, other.Users)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEmptySecretHashes creates new empty structure
|
||||||
|
func NewEmptySecretHashes() *SecretHashes {
|
||||||
|
sh := &SecretHashes{}
|
||||||
|
sh.Users = make(map[string]string)
|
||||||
|
return sh
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStringMapEqual(first map[string]string, second map[string]string) bool {
|
||||||
|
if first == nil && second == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if first == nil || second == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(first) != len(second) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, valueF := range first {
|
||||||
|
valueS, ok := second[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if valueF != valueS {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
164
pkg/apis/deployment/v1alpha/secret_hashes_test.go
Normal file
164
pkg/apis/deployment/v1alpha/secret_hashes_test.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
//
|
||||||
|
// DISCLAIMER
|
||||||
|
//
|
||||||
|
// Copyright 2019 ArangoDB GmbH, Cologne, Germany
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
||||||
|
//
|
||||||
|
// Author tomasz@arangodb.con
|
||||||
|
//
|
||||||
|
|
||||||
|
package v1alpha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/magiconair/properties/assert"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretHashes_Equal(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
sh := SecretHashes{}
|
||||||
|
testCases := []struct {
|
||||||
|
Name string
|
||||||
|
CompareFrom *SecretHashes
|
||||||
|
CompareTo *SecretHashes
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Parameter can not be nil",
|
||||||
|
CompareFrom: &SecretHashes{},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "The addresses are the same",
|
||||||
|
CompareFrom: &sh,
|
||||||
|
CompareTo: &sh,
|
||||||
|
Expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "JWT token is different",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
AuthJWT: "1",
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{
|
||||||
|
AuthJWT: "2",
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Users are different",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "User's table size is different",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "",
|
||||||
|
"user": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "User's table has got different users",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"user": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "User's table has got different hashes for users",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Secret hashes are the same",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
AuthJWT: "1",
|
||||||
|
RocksDBEncryptionKey: "2",
|
||||||
|
TLSCA: "3",
|
||||||
|
SyncTLSCA: "4",
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{
|
||||||
|
AuthJWT: "1",
|
||||||
|
RocksDBEncryptionKey: "2",
|
||||||
|
TLSCA: "3",
|
||||||
|
SyncTLSCA: "4",
|
||||||
|
Users: map[string]string{
|
||||||
|
"root": "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Secret hashes are the same without users",
|
||||||
|
CompareFrom: &SecretHashes{
|
||||||
|
AuthJWT: "1",
|
||||||
|
RocksDBEncryptionKey: "2",
|
||||||
|
TLSCA: "3",
|
||||||
|
SyncTLSCA: "4",
|
||||||
|
},
|
||||||
|
CompareTo: &SecretHashes{
|
||||||
|
AuthJWT: "1",
|
||||||
|
RocksDBEncryptionKey: "2",
|
||||||
|
TLSCA: "3",
|
||||||
|
SyncTLSCA: "4",
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
//nolint:scopelint
|
||||||
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
|
// Act
|
||||||
|
expected := testCase.CompareFrom.Equal(testCase.CompareTo)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, testCase.Expected, expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -367,7 +367,7 @@ func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) {
|
||||||
if in.SecretHashes != nil {
|
if in.SecretHashes != nil {
|
||||||
in, out := &in.SecretHashes, &out.SecretHashes
|
in, out := &in.SecretHashes, &out.SecretHashes
|
||||||
*out = new(SecretHashes)
|
*out = new(SecretHashes)
|
||||||
**out = **in
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
if in.ForceStatusReload != nil {
|
if in.ForceStatusReload != nil {
|
||||||
in, out := &in.ForceStatusReload, &out.ForceStatusReload
|
in, out := &in.ForceStatusReload, &out.ForceStatusReload
|
||||||
|
@ -757,6 +757,13 @@ func (in *RocksDBSpec) DeepCopy() *RocksDBSpec {
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *SecretHashes) DeepCopyInto(out *SecretHashes) {
|
func (in *SecretHashes) DeepCopyInto(out *SecretHashes) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.Users != nil {
|
||||||
|
in, out := &in.Users, &out.Users
|
||||||
|
*out = make(map[string]string, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
(*out)[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,7 @@ import (
|
||||||
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
|
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
driver "github.com/arangodb/go-driver"
|
"github.com/arangodb/go-driver"
|
||||||
|
|
||||||
"github.com/arangodb/kube-arangodb/pkg/util/constants"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
rootUserName = "root"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnsureBootstrap executes the bootstrap once as soon as the deployment becomes ready
|
// EnsureBootstrap executes the bootstrap once as soon as the deployment becomes ready
|
||||||
|
@ -81,7 +75,9 @@ func (d *Deployment) ensureUserPasswordSecret(secrets k8sutil.SecretInterface, u
|
||||||
if auth, err := secrets.Get(secretName, metav1.GetOptions{}); k8sutil.IsNotFound(err) {
|
if auth, err := secrets.Get(secretName, metav1.GetOptions{}); k8sutil.IsNotFound(err) {
|
||||||
// Create new one
|
// Create new one
|
||||||
tokenData := make([]byte, 32)
|
tokenData := make([]byte, 32)
|
||||||
rand.Read(tokenData)
|
if _, err = rand.Read(tokenData); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
token := hex.EncodeToString(tokenData)
|
token := hex.EncodeToString(tokenData)
|
||||||
owner := d.GetAPIObject().AsOwner()
|
owner := d.GetAPIObject().AsOwner()
|
||||||
|
|
||||||
|
@ -91,12 +87,9 @@ func (d *Deployment) ensureUserPasswordSecret(secrets k8sutil.SecretInterface, u
|
||||||
|
|
||||||
return token, nil
|
return token, nil
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
user, ok := auth.Data[constants.SecretUsername]
|
user, pass, err := k8sutil.GetSecretAuthCredentials(auth)
|
||||||
if ok && string(user) == username {
|
if err == nil && user == username {
|
||||||
pass, ok := auth.Data[constants.SecretPassword]
|
return pass, nil
|
||||||
if ok {
|
|
||||||
return string(pass), nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("invalid secret format in secret %s", secretName)
|
return "", fmt.Errorf("invalid secret format in secret %s", secretName)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -24,7 +24,7 @@ package deployment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
||||||
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
|
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
|
||||||
|
|
|
@ -23,12 +23,17 @@
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/arangodb/go-driver"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha"
|
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha"
|
||||||
|
@ -44,10 +49,14 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
// validate performs a secret hash comparison for a single secret.
|
// validate performs a secret hash comparison for a single secret.
|
||||||
// Return true if all is good, false when the SecretChanged condition
|
// Return true if all is good, false when the SecretChanged condition
|
||||||
// must be set.
|
// must be set.
|
||||||
validate := func(secretName string, getExpectedHash func() string, setExpectedHash func(string) error) (bool, error) {
|
validate := func(secretName string,
|
||||||
|
getExpectedHash func() string,
|
||||||
|
setExpectedHash func(string) error,
|
||||||
|
actionHashChanged func(Context, *v1.Secret) error) (bool, error) {
|
||||||
|
|
||||||
log := r.log.With().Str("secret-name", secretName).Logger()
|
log := r.log.With().Str("secret-name", secretName).Logger()
|
||||||
expectedHash := getExpectedHash()
|
expectedHash := getExpectedHash()
|
||||||
hash, err := r.getSecretHash(secretName)
|
secret, hash, err := r.getSecretHash(secretName)
|
||||||
if expectedHash == "" {
|
if expectedHash == "" {
|
||||||
// No hash set yet, try to fill it
|
// No hash set yet, try to fill it
|
||||||
if k8sutil.IsNotFound(err) {
|
if k8sutil.IsNotFound(err) {
|
||||||
|
@ -78,6 +87,18 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
Str("expected-hash", expectedHash).
|
Str("expected-hash", expectedHash).
|
||||||
Str("new-hash", hash).
|
Str("new-hash", hash).
|
||||||
Msg("Secret has changed.")
|
Msg("Secret has changed.")
|
||||||
|
if actionHashChanged != nil {
|
||||||
|
if err := actionHashChanged(r.context, secret); err != nil {
|
||||||
|
log.Debug().Msgf("failed to change secret. hash-changed-action returned error: %v", err)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setExpectedHash(hash); err != nil {
|
||||||
|
log.Debug().Msg("Failed to change secret hash")
|
||||||
|
return true, maskAny(err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
// This is not good, return false so SecretsChanged condition will be set.
|
// This is not good, return false so SecretsChanged condition will be set.
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -91,13 +112,13 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
status, lastVersion := r.context.GetStatus()
|
status, lastVersion := r.context.GetStatus()
|
||||||
getHashes := func() *api.SecretHashes {
|
getHashes := func() *api.SecretHashes {
|
||||||
if status.SecretHashes == nil {
|
if status.SecretHashes == nil {
|
||||||
status.SecretHashes = &api.SecretHashes{}
|
status.SecretHashes = api.NewEmptySecretHashes()
|
||||||
}
|
}
|
||||||
return status.SecretHashes
|
return status.SecretHashes
|
||||||
}
|
}
|
||||||
updateHashes := func(updater func(*api.SecretHashes)) error {
|
updateHashes := func(updater func(*api.SecretHashes)) error {
|
||||||
if status.SecretHashes == nil {
|
if status.SecretHashes == nil {
|
||||||
status.SecretHashes = &api.SecretHashes{}
|
status.SecretHashes = api.NewEmptySecretHashes()
|
||||||
}
|
}
|
||||||
updater(status.SecretHashes)
|
updater(status.SecretHashes)
|
||||||
if err := r.context.UpdateStatus(status, lastVersion); err != nil {
|
if err := r.context.UpdateStatus(status, lastVersion); err != nil {
|
||||||
|
@ -114,7 +135,7 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
setExpectedHash := func(h string) error {
|
setExpectedHash := func(h string) error {
|
||||||
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.AuthJWT = h }))
|
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.AuthJWT = h }))
|
||||||
}
|
}
|
||||||
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash); err != nil {
|
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash, nil); err != nil {
|
||||||
return maskAny(err)
|
return maskAny(err)
|
||||||
} else if !hashOK {
|
} else if !hashOK {
|
||||||
badSecretNames = append(badSecretNames, secretName)
|
badSecretNames = append(badSecretNames, secretName)
|
||||||
|
@ -126,7 +147,7 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
setExpectedHash := func(h string) error {
|
setExpectedHash := func(h string) error {
|
||||||
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.RocksDBEncryptionKey = h }))
|
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.RocksDBEncryptionKey = h }))
|
||||||
}
|
}
|
||||||
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash); err != nil {
|
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash, nil); err != nil {
|
||||||
return maskAny(err)
|
return maskAny(err)
|
||||||
} else if !hashOK {
|
} else if !hashOK {
|
||||||
badSecretNames = append(badSecretNames, secretName)
|
badSecretNames = append(badSecretNames, secretName)
|
||||||
|
@ -138,7 +159,7 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
setExpectedHash := func(h string) error {
|
setExpectedHash := func(h string) error {
|
||||||
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.TLSCA = h }))
|
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.TLSCA = h }))
|
||||||
}
|
}
|
||||||
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash); err != nil {
|
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash, nil); err != nil {
|
||||||
return maskAny(err)
|
return maskAny(err)
|
||||||
} else if !hashOK {
|
} else if !hashOK {
|
||||||
badSecretNames = append(badSecretNames, secretName)
|
badSecretNames = append(badSecretNames, secretName)
|
||||||
|
@ -150,13 +171,40 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
setExpectedHash := func(h string) error {
|
setExpectedHash := func(h string) error {
|
||||||
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.SyncTLSCA = h }))
|
return maskAny(updateHashes(func(dst *api.SecretHashes) { dst.SyncTLSCA = h }))
|
||||||
}
|
}
|
||||||
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash); err != nil {
|
if hashOK, err := validate(secretName, getExpectedHash, setExpectedHash, nil); err != nil {
|
||||||
return maskAny(err)
|
return maskAny(err)
|
||||||
} else if !hashOK {
|
} else if !hashOK {
|
||||||
badSecretNames = append(badSecretNames, secretName)
|
badSecretNames = append(badSecretNames, secretName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for username, secretName := range spec.Bootstrap.PasswordSecretNames {
|
||||||
|
if secretName.IsNone() || secretName.IsAuto() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.context.GetKubeCli().CoreV1().Secrets(r.context.GetNamespace()).Get(string(secretName), metav1.GetOptions{})
|
||||||
|
if k8sutil.IsNotFound(err) {
|
||||||
|
// do nothing when secret was deleted
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
getExpectedHash := func() string {
|
||||||
|
if v, ok := getHashes().Users[username]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
setExpectedHash := func(h string) error {
|
||||||
|
return maskAny(updateHashes(func(dst *api.SecretHashes) {
|
||||||
|
dst.Users[username] = h
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If password changes it should not be set that deployment in 'SecretsChanged' state
|
||||||
|
validate(string(secretName), getExpectedHash, setExpectedHash, changeUserPassword)
|
||||||
|
}
|
||||||
|
|
||||||
if len(badSecretNames) > 0 {
|
if len(badSecretNames) > 0 {
|
||||||
// We have invalid hashes, set the SecretsChanged condition
|
// We have invalid hashes, set the SecretsChanged condition
|
||||||
if status.Conditions.Update(api.ConditionTypeSecretsChanged, true,
|
if status.Conditions.Update(api.ConditionTypeSecretsChanged, true,
|
||||||
|
@ -185,13 +233,47 @@ func (r *Resources) ValidateSecretHashes() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func changeUserPassword(c Context, secret *v1.Secret) error {
|
||||||
|
username, password, err := k8sutil.GetSecretAuthCredentials(secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client, err := c.GetDatabaseClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return maskAny(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := client.User(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
if driver.IsNotFound(err) {
|
||||||
|
options := &driver.UserOptions{
|
||||||
|
Password: password,
|
||||||
|
Active: new(bool),
|
||||||
|
}
|
||||||
|
*options.Active = true
|
||||||
|
|
||||||
|
_, err = client.CreateUser(ctx, username, options)
|
||||||
|
return maskAny(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = user.Update(ctx, driver.UserOptions{
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
|
||||||
|
return maskAny(err)
|
||||||
|
}
|
||||||
|
|
||||||
// getSecretHash fetches a secret with given name and returns a hash over its value.
|
// getSecretHash fetches a secret with given name and returns a hash over its value.
|
||||||
func (r *Resources) getSecretHash(secretName string) (string, error) {
|
func (r *Resources) getSecretHash(secretName string) (*v1.Secret, string, error) {
|
||||||
kubecli := r.context.GetKubeCli()
|
kubecli := r.context.GetKubeCli()
|
||||||
ns := r.context.GetNamespace()
|
ns := r.context.GetNamespace()
|
||||||
s, err := kubecli.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
|
s, err := kubecli.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", maskAny(err)
|
return nil, "", maskAny(err)
|
||||||
}
|
}
|
||||||
// Create hash of value
|
// Create hash of value
|
||||||
rows := make([]string, 0, len(s.Data))
|
rows := make([]string, 0, len(s.Data))
|
||||||
|
@ -203,5 +285,5 @@ func (r *Resources) getSecretHash(secretName string) (string, error) {
|
||||||
data := strings.Join(rows, "\n")
|
data := strings.Join(rows, "\n")
|
||||||
rawHash := sha256.Sum256([]byte(data))
|
rawHash := sha256.Sum256([]byte(data))
|
||||||
hash := fmt.Sprintf("%0x", rawHash)
|
hash := fmt.Sprintf("%0x", rawHash)
|
||||||
return hash, nil
|
return s, hash, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,11 +74,9 @@ func stripArangodPrefix(id string) string {
|
||||||
// complies with kubernetes name requirements.
|
// complies with kubernetes name requirements.
|
||||||
// If the name is to long or contains invalid characters,
|
// If the name is to long or contains invalid characters,
|
||||||
// if will be adjusted and a hash with be added.
|
// if will be adjusted and a hash with be added.
|
||||||
func FixupResourceName(name string, maxLength ...int) string {
|
func FixupResourceName(name string) string {
|
||||||
maxLen := 63
|
maxLen := 63
|
||||||
if len(maxLength) > 0 {
|
|
||||||
maxLen = maxLength[0]
|
|
||||||
}
|
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
needHash := len(name) > maxLen
|
needHash := len(name) > maxLen
|
||||||
for _, ch := range name {
|
for _, ch := range name {
|
||||||
|
|
|
@ -302,14 +302,18 @@ func GetBasicAuthSecret(secrets SecretInterface, secretName string) (string, str
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", maskAny(err)
|
return "", "", maskAny(err)
|
||||||
}
|
}
|
||||||
// Load `ca.crt` field
|
return GetSecretAuthCredentials(s)
|
||||||
username, found := s.Data[constants.SecretUsername]
|
}
|
||||||
|
|
||||||
|
// GetSecretAuthCredentials returns username and password from the secret
|
||||||
|
func GetSecretAuthCredentials(secret *v1.Secret) (string, string, error) {
|
||||||
|
username, found := secret.Data[constants.SecretUsername]
|
||||||
if !found {
|
if !found {
|
||||||
return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretUsername, secretName))
|
return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretUsername, secret.Name))
|
||||||
}
|
}
|
||||||
password, found := s.Data[constants.SecretPassword]
|
password, found := secret.Data[constants.SecretPassword]
|
||||||
if !found {
|
if !found {
|
||||||
return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretPassword, secretName))
|
return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretPassword, secret.Name))
|
||||||
}
|
}
|
||||||
return string(username), string(password), nil
|
return string(username), string(password), nil
|
||||||
}
|
}
|
||||||
|
|
144
tests/secret_hashes_test.go
Normal file
144
tests/secret_hashes_test.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
//
|
||||||
|
// DISCLAIMER
|
||||||
|
//
|
||||||
|
// Copyright 2019 ArangoDB GmbH, Cologne, Germany
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
||||||
|
//
|
||||||
|
// Author tomasz@arangodb.con
|
||||||
|
//
|
||||||
|
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/arangodb/go-driver"
|
||||||
|
|
||||||
|
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha"
|
||||||
|
"github.com/arangodb/kube-arangodb/pkg/client"
|
||||||
|
"github.com/arangodb/kube-arangodb/pkg/util/arangod"
|
||||||
|
"github.com/arangodb/kube-arangodb/pkg/util/constants"
|
||||||
|
"github.com/arangodb/kube-arangodb/pkg/util/retry"
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSecretHashesRootUser checks if Status.SecretHashes.Users[root] changed after request for it
|
||||||
|
func TestSecretHashesRootUser(t *testing.T) {
|
||||||
|
longOrSkip(t)
|
||||||
|
c := client.MustNewInCluster()
|
||||||
|
kubecli := mustNewKubeClient(t)
|
||||||
|
ns := getNamespace(t)
|
||||||
|
|
||||||
|
// Prepare deployment config
|
||||||
|
depl := newDeployment("test-auth-sng-def-" + uniuri.NewLen(4))
|
||||||
|
depl.Spec.Mode = api.NewMode(api.DeploymentModeSingle)
|
||||||
|
depl.Spec.SetDefaults(depl.GetName())
|
||||||
|
depl.Spec.Bootstrap.PasswordSecretNames[api.UserNameRoot] = api.PasswordSecretNameAuto
|
||||||
|
|
||||||
|
// Create deployment
|
||||||
|
apiObject, err := c.DatabaseV1alpha().ArangoDeployments(ns).Create(depl)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create deployment failed: %v", err)
|
||||||
|
}
|
||||||
|
defer deferedCleanupDeployment(c, depl.GetName(), ns)
|
||||||
|
|
||||||
|
// Wait for deployment to be ready
|
||||||
|
depl, err = waitUntilDeployment(c, depl.GetName(), ns, deploymentIsReady())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Deployment not running in time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a database client
|
||||||
|
ctx := arangod.WithRequireAuthentication(context.Background())
|
||||||
|
client := mustNewArangodDatabaseClient(ctx, kubecli, apiObject, t, nil)
|
||||||
|
|
||||||
|
// Wait for single server available
|
||||||
|
if err := waitUntilVersionUp(client, nil); err != nil {
|
||||||
|
t.Fatalf("Single server not running returning version in time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
depl, err = waitUntilDeployment(c, depl.GetName(), ns, func(obj *api.ArangoDeployment) error {
|
||||||
|
// check if root secret password is set
|
||||||
|
secretHashes := obj.Status.SecretHashes
|
||||||
|
if secretHashes == nil {
|
||||||
|
return fmt.Errorf("field Status.SecretHashes is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretHashes.Users == nil {
|
||||||
|
return fmt.Errorf("field Status.SecretHashes.Users is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hash, ok := secretHashes.Users[api.UserNameRoot]; !ok {
|
||||||
|
return fmt.Errorf("field Status.SecretHashes.Users[root] is not set")
|
||||||
|
} else if len(hash) == 0 {
|
||||||
|
return fmt.Errorf("field Status.SecretHashes.Users[root] is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Deployment is not set properly: %v", err)
|
||||||
|
}
|
||||||
|
rootHashSecret := depl.Status.SecretHashes.Users[api.UserNameRoot]
|
||||||
|
|
||||||
|
secretRootName := string(depl.Spec.Bootstrap.PasswordSecretNames[api.UserNameRoot])
|
||||||
|
secretRoot, err := waitUntilSecret(kubecli, secretRootName, ns, nil, time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Root secret '%s' not found: %v", secretRootName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretRoot.Data[constants.SecretPassword] = []byte("1")
|
||||||
|
_, err = kubecli.CoreV1().Secrets(ns).Update(secretRoot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Root secret '%s' has not been changed: %v", secretRootName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = retry.Retry(func() error {
|
||||||
|
// check if root secret hash has changed
|
||||||
|
depl, err = c.DatabaseV1alpha().ArangoDeployments(ns).Get(depl.GetName(), metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get deployment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootHashSecret == depl.Status.SecretHashes.Users[api.UserNameRoot] {
|
||||||
|
return maskAny(errors.New("field Status.SecretHashes.Users[root] has not been changed yet"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, deploymentReadyTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if password changed
|
||||||
|
auth := driver.BasicAuthentication(api.UserNameRoot, "1")
|
||||||
|
_, err = client.Connection().SetAuthentication(auth)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("The password for user '%s' has not been changed: %v", api.UserNameRoot, err)
|
||||||
|
}
|
||||||
|
_, err = client.Version(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can not get version after the password has been changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Cleanup
|
||||||
|
removeDeployment(c, depl.GetName(), ns)
|
||||||
|
}
|
Loading…
Reference in a new issue