...

Source file src/edge-infra.dev/pkg/edge/datasync/controllers/couchctl/user_controller.go

Documentation: edge-infra.dev/pkg/edge/datasync/controllers/couchctl

     1  package couchctl
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"reflect"
     9  	"time"
    10  
    11  	"edge-infra.dev/pkg/k8s/runtime/inventory"
    12  	unstructuredutil "edge-infra.dev/pkg/k8s/unstructured"
    13  
    14  	corev1 "k8s.io/api/core/v1"
    15  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    16  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/client-go/dynamic"
    18  	kuberecorder "k8s.io/client-go/tools/record"
    19  
    20  	"sigs.k8s.io/cli-utils/pkg/kstatus/watcher"
    21  	ctrl "sigs.k8s.io/controller-runtime"
    22  	"sigs.k8s.io/controller-runtime/pkg/builder"
    23  	"sigs.k8s.io/controller-runtime/pkg/client"
    24  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    25  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    26  
    27  	"edge-infra.dev/pkg/edge/api/utils"
    28  	dsapi "edge-infra.dev/pkg/edge/datasync/apis/v1alpha1"
    29  	"edge-infra.dev/pkg/edge/datasync/couchdb"
    30  	"edge-infra.dev/pkg/k8s/meta/status"
    31  	"edge-infra.dev/pkg/k8s/runtime/conditions"
    32  	"edge-infra.dev/pkg/k8s/runtime/controller/metrics"
    33  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
    34  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
    35  	"edge-infra.dev/pkg/k8s/runtime/patch"
    36  	"edge-infra.dev/pkg/k8s/runtime/sap"
    37  
    38  	"github.com/go-logr/logr"
    39  )
    40  
    41  type CouchUserReconciler struct {
    42  	client.Client
    43  	NodeResourcePredicate
    44  	kuberecorder.EventRecorder
    45  	SecretManager   secretManager
    46  	Name            string
    47  	Config          *Config
    48  	Metrics         metrics.Metrics
    49  	patchOptions    []patch.Option
    50  	ResourceManager *sap.ResourceManager
    51  	replicationDB   string
    52  }
    53  
    54  var (
    55  	ErrNewUserNotFoundInDB = errors.New("new user not found in users table")
    56  
    57  	userConditions = reconcile.Conditions{
    58  		Target: status.ReadyCondition,
    59  		Owned: []string{
    60  			dsapi.UserSetupSucceededReason,
    61  			status.ReadyCondition,
    62  			status.ReconcilingCondition,
    63  			status.StalledCondition,
    64  		},
    65  		Summarize: []string{
    66  			dsapi.UserSetupSucceededReason,
    67  			status.StalledCondition,
    68  		},
    69  		NegativePolarity: []string{
    70  			status.ReconcilingCondition,
    71  			status.StalledCondition,
    72  		},
    73  	}
    74  	edgeAdminNamespaces = []string{couchdb.Namespace, "cushion", "edge-iam"}
    75  )
    76  
    77  // SetupWithManager sets up ComputeAddressReconciler with the manager
    78  func (r *CouchUserReconciler) SetupWithManager(mgr ctrl.Manager) error {
    79  	r.replicationDB = r.Config.ReplicationDB()
    80  	r.patchOptions = getPatchOptions(serverConditions.Owned, r.Name)
    81  	d, err := dynamic.NewForConfig(mgr.GetConfig())
    82  	if err != nil {
    83  		return fmt.Errorf("fail to create dynamic client: %w", err)
    84  	}
    85  	r.ResourceManager = sap.NewResourceManager(
    86  		r.Client,
    87  		watcher.NewDefaultStatusWatcher(d, mgr.GetRESTMapper()),
    88  		sap.Owner{Field: r.Name},
    89  	)
    90  	return ctrl.NewControllerManagedBy(mgr).
    91  		For(&dsapi.CouchDBUser{}, r.userPredicates()).
    92  		Owns(&corev1.Secret{}, r.secretPredicates()).
    93  		Complete(r)
    94  }
    95  
    96  func (r *CouchUserReconciler) userPredicates() builder.Predicates {
    97  	return builder.WithPredicates(
    98  		predicate.GenerationChangedPredicate{},
    99  		predicate.NewPredicateFuncs(func(obj client.Object) bool {
   100  			if r.Config.IsDSDS() {
   101  				return r.ShouldReconcile(r.Config, obj)
   102  			}
   103  			return true
   104  		}))
   105  }
   106  
   107  func (r *CouchUserReconciler) secretPredicates() builder.Predicates {
   108  	return builder.WithPredicates(
   109  		predicate.GenerationChangedPredicate{},
   110  		predicate.NewPredicateFuncs(func(obj client.Object) bool {
   111  			if r.Config.IsDSDS() {
   112  				list := dsapi.CouchDBUserList{}
   113  				if err := r.Client.List(context.Background(), &list,
   114  					client.InNamespace(obj.GetNamespace()), client.MatchingLabels{
   115  						couchdb.NodeUIDLabel: r.Config.NodeUID,
   116  					}); err != nil {
   117  					return false
   118  				}
   119  				if len(list.Items) == 0 {
   120  					return false
   121  				}
   122  				return isChild(obj, list.Items)
   123  			}
   124  			return true
   125  		}))
   126  }
   127  
   128  func isChild(obj client.Object, items []dsapi.CouchDBUser) bool {
   129  	// ID: '<namespace>_<name>_<group>_<kind>'
   130  	secretID := fmt.Sprintf("%s_%s__Secret", obj.GetNamespace(), obj.GetName())
   131  	for _, item := range items {
   132  		if item.Status.Inventory == nil || item.Status.Inventory.Entries == nil {
   133  			continue
   134  		}
   135  		for _, entry := range item.Status.Inventory.Entries {
   136  			if entry.ID == secretID {
   137  				return true
   138  			}
   139  		}
   140  	}
   141  	return false
   142  }
   143  
   144  func (r *CouchUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
   145  	reconcileStart := time.Now()
   146  	log := ctrl.LoggerFrom(ctx)
   147  
   148  	user := &dsapi.CouchDBUser{}
   149  	if err := r.Client.Get(ctx, req.NamespacedName, user); err != nil {
   150  		return ctrl.Result{}, client.IgnoreNotFound(err)
   151  	}
   152  	user.WithRetry(r.Config.RequeueTime)
   153  	user.WithInterval(r.Config.PollingInterval)
   154  	withUserType(user)
   155  
   156  	ctx = logr.NewContext(ctx, log)
   157  
   158  	patcher := patch.NewSerialPatcher(user, r.Client)
   159  	if err := reconcile.Progressing(ctx, user, patcher, r.patchOptions...); err != nil {
   160  		log.Error(err, "unable to update status")
   161  		return ctrl.Result{}, err
   162  	}
   163  
   164  	recResult := reconcile.ResultEmpty
   165  	var recErr recerr.Error
   166  
   167  	defer func() {
   168  		summarizer := reconcile.NewSummarizer(patcher)
   169  		res, err = summarizer.SummarizeAndPatch(ctx, user, []reconcile.SummarizeOption{
   170  			reconcile.WithConditions(userConditions),
   171  			reconcile.WithResult(recResult),
   172  			reconcile.WithError(recErr),
   173  			reconcile.WithIgnoreNotFound(),
   174  			reconcile.WithProcessors(
   175  				reconcile.RecordResult,
   176  			),
   177  			reconcile.WithFieldOwner(r.Name),
   178  			reconcile.WithEventRecorder(r.EventRecorder),
   179  		}...)
   180  		r.Metrics.RecordDuration(ctx, user, reconcileStart)
   181  		r.Metrics.RecordReadiness(ctx, user)
   182  	}()
   183  
   184  	// Check if finalizer exists
   185  	if !controllerutil.ContainsFinalizer(user, DatasyncFinalizer) {
   186  		controllerutil.AddFinalizer(user, DatasyncFinalizer)
   187  		// Return immediately so that we requeue and reconcile object with finalizer
   188  		// added.
   189  		recResult = reconcile.ResultRequeue
   190  		return
   191  	}
   192  
   193  	if !user.ObjectMeta.DeletionTimestamp.IsZero() {
   194  		recErr = r.finalize(ctx, user)
   195  		return
   196  	}
   197  
   198  	if recErr = r.reconcile(ctx, user); recErr != nil {
   199  		// CouchDBServer controller is responsible for surfacing couchdb Errors
   200  		if !couchDBNotReadyOrNotFound(recErr) {
   201  			recErr.ToCondition(user, dsapi.UserSetupSucceededReason)
   202  			err = recErr
   203  			return
   204  		}
   205  	}
   206  	recResult = reconcile.ResultSuccess
   207  	conditions.MarkTrue(user, dsapi.UserSetupSucceededReason, status.SucceededReason, "Successfully created CouchDB User")
   208  	log.Info("Successfully created CouchDB User")
   209  
   210  	return
   211  }
   212  
   213  // finalize checks is couchdb node exists removing deleting
   214  func (r *CouchUserReconciler) finalize(ctx context.Context, user *dsapi.CouchDBUser) (recErr recerr.Error) {
   215  	log := logr.FromContextOrDiscard(ctx)
   216  	log.Info("running finalizer")
   217  
   218  	var prune bool
   219  	defer func() {
   220  		if prune {
   221  			log.Info("pruning user's resources")
   222  			err := pruneInventory(ctx, r.ResourceManager, user)
   223  			if err != nil {
   224  				recErr = recerr.New(err, status.DependencyInvalidReason)
   225  			}
   226  			controllerutil.RemoveFinalizer(user, DatasyncFinalizer)
   227  			log.Info("finalizer ran successfully")
   228  		}
   229  	}()
   230  
   231  	nodeFound, err := r.nodeExistsAndSchedulable(ctx, user)
   232  	if err != nil {
   233  		return recerr.New(err, status.DependencyInvalidReason)
   234  	}
   235  
   236  	// node not found, assume couchdb server is gone
   237  	if !nodeFound {
   238  		prune = true
   239  		return
   240  	}
   241  
   242  	// get couchdb client to be able to delete username
   243  	cc, err := couchDBClient(ctx, r.Client, r.Config, user)
   244  	switch {
   245  	case err != nil && serverNotFound(err):
   246  		prune = true
   247  		return
   248  	case err != nil:
   249  		return recerr.New(err, status.DependencyInvalidReason)
   250  	}
   251  
   252  	// Get optional username to delete from couchdb
   253  	username := user.Spec.User.Name
   254  	if username == "" {
   255  		up := &couchdb.UsernamePassword{}
   256  		_, err = up.FromSecret(ctx, r.Client, user.SecretNamespacedName())
   257  		switch {
   258  		case err != nil && kerrors.IsNotFound(err):
   259  			prune = true
   260  			return
   261  		case err != nil:
   262  			return recerr.New(err, status.DependencyInvalidReason)
   263  		}
   264  		username = string(up.Username)
   265  	}
   266  
   267  	err = cc.DeleteUser(ctx, username)
   268  	if err != nil && !couchdb.IsNotFound(err) {
   269  		logError(ctx, err, "fail to delete user from couchdb", "username", username)
   270  		return recerr.New(err, status.DependencyInvalidReason)
   271  	}
   272  	prune = true
   273  	return
   274  }
   275  
   276  // nodeExistsAndSchedulable check for given user if the couchdb server node exists and schedulable
   277  // we could check if the sts/pvc exist instead
   278  func (r *CouchUserReconciler) nodeExistsAndSchedulable(ctx context.Context, user *dsapi.CouchDBUser) (bool, error) {
   279  	var nodeUID string
   280  	_, server, err := checkIfServerIsReady(ctx, r.Client, user)
   281  	switch {
   282  	case err != nil && !kerrors.IsNotFound(err):
   283  		return false, err
   284  	case server != nil:
   285  		nodeUID = labelOrEmpty(server, couchdb.NodeUIDLabel)
   286  	default:
   287  		nodeUID = labelOrEmpty(user, couchdb.NodeUIDLabel)
   288  	}
   289  
   290  	if nodeUID == "" {
   291  		return false, nil
   292  	}
   293  
   294  	nodes := &corev1.NodeList{}
   295  	err = r.Client.List(ctx, nodes, &client.ListOptions{})
   296  	if err != nil {
   297  		return false, err
   298  	}
   299  	for _, node := range nodes.Items {
   300  		if nodeUID == string(node.UID) {
   301  			return !node.Spec.Unschedulable, nil
   302  		}
   303  	}
   304  	return false, nil
   305  }
   306  
   307  func (r *CouchUserReconciler) reconcile(ctx context.Context, user *dsapi.CouchDBUser) recerr.Error {
   308  	log := logr.FromContextOrDiscard(ctx)
   309  
   310  	// attempt to get the couchdbserver
   311  	ready, server, err := checkIfServerIsReady(ctx, r.Client, user)
   312  	if err != nil {
   313  		return recerr.NewWait(err, status.DependencyNotReadyReason, r.Config.ServerNotReady)
   314  	}
   315  
   316  	if !ready {
   317  		err := fmt.Errorf("%w", ErrServerNotReady)
   318  		return recerr.NewWait(err, status.DependencyNotReadyReason, r.Config.ServerNotReady)
   319  	}
   320  
   321  	log = log.WithValues("server", client.ObjectKeyFromObject(server))
   322  	ctx = logr.NewContext(ctx, log)
   323  
   324  	// Errors are always returned
   325  	return r.reconcileUser(ctx, user, server)
   326  }
   327  
   328  func (r *CouchUserReconciler) authorizeUser(user *dsapi.CouchDBUser) (bool, error) {
   329  	if user.IsAdminCredentials() && !utils.Contains(edgeAdminNamespaces, user.Namespace) {
   330  		return false, fmt.Errorf("admin users can only be created in edge admin namespaces: %v", edgeAdminNamespaces)
   331  	}
   332  	return true, nil
   333  }
   334  
   335  func (r *CouchUserReconciler) reconcileUser(ctx context.Context, user *dsapi.CouchDBUser, server *dsapi.CouchDBServer) recerr.Error {
   336  	log := logr.FromContextOrDiscard(ctx)
   337  	// TODO: if the user passed in a secret get that and use it to set the new users username and password
   338  
   339  	changeSet := sap.NewChangeSet()
   340  
   341  	// get / check if the admin creds exist
   342  	cc, err := couchDBClient(ctx, r.Client, r.Config, user)
   343  	switch {
   344  	case err != nil && serverNotFound(err):
   345  		return recerr.NewWait(err, status.DependencyNotReadyReason, r.Config.ServerNotReady)
   346  	case err != nil:
   347  		logError(ctx, err, "error creating couchdb client", "NamespacedName", server.AdminCredentials())
   348  		return recerr.NewWait(err, status.DependencyInvalidReason, r.Config.ServerNotReady)
   349  	}
   350  
   351  	// defer closing the client
   352  	defer cc.Close(ctx)
   353  
   354  	authorized, err := r.authorizeUser(user)
   355  	if !authorized {
   356  		return recerr.NewStalled(err, dsapi.UserCreationFailedReason)
   357  	}
   358  
   359  	// TODO: check if a user already exists and verify the secret is valid
   360  	// and that the creds in the secret actually map to a user with the correct permissions
   361  	// actually maybe this doesnt matter due to the GIGO nature
   362  
   363  	userCreds := r.credentials(user, server)
   364  
   365  	// try to get the user credentials secret
   366  	// if it doesn't exist create it
   367  	var cs *sap.ChangeSetEntry
   368  	secretNN := user.SecretNamespacedName()
   369  	secret, err := userCreds.FromSecret(ctx, r.Client, secretNN)
   370  	switch {
   371  	case err != nil && canMigrate(err):
   372  		var ownerRefs []v1.OwnerReference
   373  		if user.IsUserCredentials() {
   374  			ownerRefs = couchDBUserOwnerReference(user)
   375  		}
   376  		secret, err := userCreds.ToSecret(ctx, r.Client, secretNN, ownerRefs...)
   377  		if err != nil {
   378  			logError(ctx, err, "fail to create couchdb k8 secret", "NamespacedName", secretNN)
   379  			return recerr.New(err, dsapi.UserCreationFailedReason)
   380  		}
   381  		un, err := unstructuredutil.ToUnstructured(secret)
   382  		if err != nil {
   383  			return recerr.New(err, dsapi.UserCreationFailedReason)
   384  		}
   385  		cs, err = r.ResourceManager.Apply(ctx, un, sap.ApplyOptions{})
   386  		if err != nil {
   387  			return recerr.New(err, dsapi.UserCreationFailedReason)
   388  		}
   389  		changeSet.Add(*cs)
   390  	case err != nil:
   391  		log.Error(err, "fail to get couchdb k8 secret", "NamespacedName", secretNN)
   392  		return recerr.New(err, status.DependencyInvalidReason)
   393  	default:
   394  		cs, err = ExistingChangeSetEntry(secret)
   395  		if err != nil {
   396  			return recerr.New(err, status.DependencyInvalidReason)
   397  		}
   398  		changeSet.Add(*cs)
   399  	}
   400  
   401  	usernamePassword := usernamePassword(userCreds, user)
   402  	userName := string(usernamePassword.Username)
   403  	log = log.WithValues("username", userName)
   404  
   405  	// TODO: check if the users creds are defined in the spec via secret
   406  	_, err = cc.CreateNewUser(ctx, userName, string(usernamePassword.Password), user.Spec.User.Roles)
   407  	if err != nil && !errors.Is(err, couchdb.ErrConflict) {
   408  		logError(ctx, err, "fail to create couchdb user", "NamespacedName", secretNN)
   409  		return recerr.NewWait(err, dsapi.UserFailedToAddUserReason, r.Config.ServerNotReady)
   410  	}
   411  	var roles []string
   412  	if !user.IsUserCredentials() {
   413  		roles = user.Spec.User.Roles
   414  	}
   415  	// ensure the user and its roles have been successfully added to couchdb
   416  	userExists, err := cc.CheckUserAndRolesExists(ctx, userName, roles)
   417  	if !userExists || errors.Is(err, couchdb.ErrNotFound) {
   418  		err := fmt.Errorf("%w", ErrNewUserNotFoundInDB)
   419  		logError(ctx, err, "fail to check roles for couchdb user, user or role foes not exists", "NamespacedName", secretNN)
   420  		return recerr.NewWait(err, dsapi.UserFailedToAddUserReason, r.Config.ServerNotReady)
   421  	}
   422  	if err != nil {
   423  		log.Error(err, "fail to check roles for couchdb user", "NamespacedName", secretNN)
   424  		return recerr.NewWait(err, dsapi.UserFailedToAddUserReason, r.Config.ServerNotReady)
   425  	}
   426  
   427  	// TODO: if the user isn't successfully created in couch should we
   428  	// remove the new secret and try again next recon?
   429  
   430  	// Add user to database security if provider is provided or has secure role
   431  	if hasDBSecurityRole(user) { //nolint: nestif
   432  		recErr := r.reconcileRoleBasedUser(ctx, user, cc, userName)
   433  		if recErr != nil {
   434  			return recErr
   435  		}
   436  	}
   437  
   438  	// store secret in secret manager if label exists
   439  	if server.IsCloud() && user.AddToSecretManager() {
   440  		userRef := corev1.SecretReference{Namespace: user.Namespace, Name: user.Name}
   441  		couchReplicationCred := server.ReplicationCredentials() == userRef
   442  		err = r.CredsToSecretManager(ctx, user, userCreds, couchReplicationCred)
   443  		if err != nil {
   444  			return recerr.New(err, string(dsapi.ReplicationCreationFailedStatus))
   445  		}
   446  	}
   447  
   448  	// TODO create service account to read secret
   449  
   450  	i := inventory.New(inventory.FromSapChangeSet(changeSet))
   451  	if err := r.prune(ctx, user, i); err != nil {
   452  		return recerr.New(err, dsapi.PruneFailed)
   453  	}
   454  	user.Status.Inventory = i
   455  
   456  	return nil
   457  }
   458  
   459  func (r *CouchUserReconciler) prune(ctx context.Context, user *dsapi.CouchDBUser, i *inventory.ResourceInventory) error {
   460  	log := logr.FromContextOrDiscard(ctx)
   461  	if user.Status.Inventory != nil {
   462  		diff, err := inventory.Diff(user.Status.Inventory, i)
   463  		if err != nil {
   464  			return nil
   465  		}
   466  		if len(diff) > 0 {
   467  			changeSet, err := r.ResourceManager.DeleteAll(ctx, diff, sap.DefaultDeleteOptions())
   468  			if err != nil {
   469  				return err
   470  			}
   471  			log.Info("pruned objects", "changeset", changeSet.ToMap())
   472  		}
   473  	}
   474  	return nil
   475  }
   476  
   477  // reconcileRoleBasedUser adds the user's roles to replicated databases
   478  func (r *CouchUserReconciler) reconcileRoleBasedUser(
   479  	ctx context.Context,
   480  	user *dsapi.CouchDBUser,
   481  	cc *couchdb.CouchDB,
   482  	userName string) recerr.Error {
   483  	log := logr.FromContextOrDiscard(ctx)
   484  
   485  	replDB := r.Config.ReplicationDB()
   486  	replSet := &dsapi.ReplicationSet{}
   487  	if err := cc.GetReplicationSetDoc(ctx, replDB, replSet); err != nil {
   488  		log.Info("replication doc not found, skipping creation of replication creds", "dbname", replDB)
   489  		return nil
   490  	}
   491  
   492  	// we need to add username to replication db's security, so it can be replicated
   493  	if user.IsReplicationCredentials() {
   494  		if recErr := r.addUsernameToSecurity(ctx, user, cc, userName, replDB); recErr != nil {
   495  			return recErr
   496  		}
   497  	}
   498  
   499  	for _, d := range replSet.Datasets {
   500  		if !shouldReplicate(r.Config, d, r.replicationDB) {
   501  			// if the database is not replicated, it will not exist.
   502  			// TODO add test for user controller
   503  			continue
   504  		}
   505  		if !updateDBSecurityForUser(user, d) {
   506  			continue
   507  		}
   508  		if recErr := r.addUsernameToSecurity(ctx, user, cc, userName, d.Name); recErr != nil {
   509  			return recErr
   510  		}
   511  	}
   512  	return nil
   513  }
   514  
   515  func canMigrate(err error) bool {
   516  	return kerrors.IsNotFound(err) ||
   517  		errors.Is(err, couchdb.ErrCouchDBURIMissing) ||
   518  		errors.Is(err, couchdb.ErrDBNameMissing) ||
   519  		errors.Is(err, couchdb.ErrSecretDataMissing) ||
   520  		errors.Is(err, couchdb.ErrInvalidCookieSecret)
   521  }
   522  
   523  func (r *CouchUserReconciler) addUsernameToSecurity(
   524  	ctx context.Context,
   525  	user *dsapi.CouchDBUser,
   526  	cc *couchdb.CouchDB,
   527  	userName, dbName string) recerr.Error {
   528  	// check that the DB actually exists in couchdb
   529  	exists, err := cc.CheckIfDBExists(ctx, dbName)
   530  	if err != nil {
   531  		// TODO add error to error list and continue
   532  		return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady)
   533  	}
   534  	if !exists {
   535  		err := fmt.Errorf("%w", ErrNewDatabaseNotFound)
   536  		// TODO add error to error list and continue
   537  		return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.DatabaseNotFound)
   538  	}
   539  	security := userToSecurity(user, userName)
   540  
   541  	// verify that the members / roles exist
   542  	exists, err = cc.CheckDBUsersAndRoles(ctx, security, dbName)
   543  	if err != nil {
   544  		err = fmt.Errorf("error checking for couchdb users and roles")
   545  		return recerr.NewWait(err, dsapi.UserCreationFailedReason, r.Config.DatabaseNotFound)
   546  	}
   547  	if exists { // no need to update security doc
   548  		return nil
   549  	}
   550  
   551  	// add the user defined members / roles to the new db
   552  	err = cc.AddMemberUserAndRolesToDB(ctx, security, dbName)
   553  	if err != nil {
   554  		return recerr.NewWait(err, dsapi.DatabaseAddRolesFailedReason, r.Config.DatabaseNotFound)
   555  	}
   556  	return nil
   557  }
   558  
   559  // CredsToSecretManager takes in user and attempts to add the fields as secrets to secretmanager
   560  func (r *CouchUserReconciler) CredsToSecretManager(ctx context.Context, user *dsapi.CouchDBUser, userCreds couchdb.CredentialsManager, couchReplicationCred bool) error {
   561  	sms, err := r.toSecretManagerSecret(userCreds)
   562  	if err != nil {
   563  		return fmt.Errorf("failed to convert secret to secret manager: %w", err)
   564  	}
   565  
   566  	value, err := json.Marshal(sms)
   567  	if err != nil {
   568  		return fmt.Errorf("failed to convert ReplicationCredentials to json string for Secret Manager: %w", err)
   569  	}
   570  
   571  	// create a new instance of secretmanager
   572  	writer, err := r.SecretManager.NewWithOptions(ctx, r.Config.ProjectID)
   573  	if err != nil {
   574  		return err
   575  	}
   576  
   577  	var secretName string // Maximum 255 characters
   578  	if couchReplicationCred {
   579  		secretName = couchdb.ReplicationSMgrSecretName
   580  	} else {
   581  		secretName = fmt.Sprintf("%s-%s", user.Namespace, user.Name)
   582  	}
   583  	return writer.AddSecret(ctx, secretName, value, nil, false, nil, "")
   584  }
   585  
   586  func (r *CouchUserReconciler) toSecretManagerSecret(creds couchdb.CredentialsManager) (*couchdb.SecretManagerSecret, error) {
   587  	sms := &couchdb.SecretManagerSecret{}
   588  	switch s := creds.(type) {
   589  	case *couchdb.AdminCredentials:
   590  		sms.Username = string(s.Username)
   591  		sms.Password = string(s.Password)
   592  	case *couchdb.ReplicationCredentials:
   593  		sms.Username = string(s.Username)
   594  		sms.Password = string(s.Password)
   595  		sms.URI = string(s.URI)
   596  		sms.DBName = string(s.DBName)
   597  	case *couchdb.UserCredentials:
   598  		sms.Username = string(s.Username)
   599  		sms.Password = string(s.Password)
   600  		sms.URI = string(s.URI)
   601  	default:
   602  		return nil, fmt.Errorf("credentials of type %T not implemented for secret manager secret", s)
   603  	}
   604  	return sms, nil
   605  }
   606  
   607  func userToSecurity(user *dsapi.CouchDBUser, userName string) couchdb.Security {
   608  	userRoles := user.Spec.User.Roles
   609  	security := couchdb.Security{}
   610  	for _, role := range userRoles {
   611  		if role == couchdb.CreateViewUser {
   612  			addNameToSecurity(&security, userName, true)
   613  		} else if role == couchdb.ReadOnlyUser {
   614  			addNameToSecurity(&security, userName, false)
   615  		} else {
   616  			addNameToSecurity(&security, userName, false)
   617  			addRoleToSecurity(&security, role, false)
   618  		}
   619  	}
   620  	return security
   621  }
   622  
   623  func addNameToSecurity(security *couchdb.Security, name string, admin bool) {
   624  	if admin {
   625  		if !utils.Contains(security.Admins.Names, name) {
   626  			security.Admins.Names = append(security.Admins.Names, name)
   627  		}
   628  	} else {
   629  		if !utils.Contains(security.Members.Names, name) {
   630  			security.Members.Names = append(security.Members.Names, name)
   631  		}
   632  	}
   633  }
   634  
   635  func addRoleToSecurity(security *couchdb.Security, role string, admin bool) {
   636  	if admin {
   637  		if !utils.Contains(security.Admins.Roles, role) {
   638  			security.Admins.Roles = append(security.Admins.Roles, role)
   639  		}
   640  	} else {
   641  		if !utils.Contains(security.Members.Roles, role) {
   642  			security.Members.Roles = append(security.Members.Roles, role)
   643  		}
   644  	}
   645  }
   646  
   647  func hasDBSecurityRole(user *dsapi.CouchDBUser) bool {
   648  	return !user.Spec.Provider.Empty() || user.IsUserCredentials() || user.IsReplicationCredentials()
   649  }
   650  
   651  // withUserType ensure backward compatibility by explicitly setting the user type
   652  func withUserType(user *dsapi.CouchDBUser) {
   653  	if user.Spec.Type == "" {
   654  		switch {
   655  		case utils.Contains(user.Spec.User.Roles, couchdb.ReplicationUser):
   656  			user.Spec.Type = dsapi.ReplicationCredentials
   657  		case utils.Contains(user.Spec.User.Roles, couchdb.ReadOnlyUser) ||
   658  			utils.Contains(user.Spec.User.Roles, couchdb.CreateViewUser):
   659  			user.Spec.Type = dsapi.UserCredentials
   660  		default:
   661  			user.Spec.Type = dsapi.AdminCredentials
   662  		}
   663  	}
   664  }
   665  
   666  func updateDBSecurityForUser(user *dsapi.CouchDBUser, d dsapi.Dataset) bool {
   667  	if user.Spec.Type == dsapi.ReplicationCredentials {
   668  		return true
   669  	}
   670  	userProvider := user.Spec.Provider
   671  	dbProvider := d.Provider
   672  	if userProvider.Empty() && dbProvider.Empty() {
   673  		return true
   674  	}
   675  	if !userProvider.Empty() && !dbProvider.Empty() {
   676  		return userProvider.Name == dbProvider.Name
   677  	}
   678  	if dbProvider.Empty() {
   679  		return true
   680  	}
   681  	return false
   682  }
   683  
   684  func couchDBUserOwnerReference(user *dsapi.CouchDBUser) []v1.OwnerReference {
   685  	return []v1.OwnerReference{
   686  		*v1.NewControllerRef(user,
   687  			dsapi.GroupVersion.WithKind(reflect.TypeOf(dsapi.CouchDBUser{}).Name())),
   688  	}
   689  }
   690  
   691  func (r *CouchUserReconciler) credentials(user *dsapi.CouchDBUser, server *dsapi.CouchDBServer) couchdb.CredentialsManager {
   692  	up := couchdb.UsernamePassword{Username: []byte(user.Spec.User.Name)}
   693  	switch user.Spec.Type {
   694  	case dsapi.UserCredentials:
   695  		return &couchdb.UserCredentials{
   696  			UsernamePassword: up,
   697  			URI:              []byte(getCouchDBUserURL(r.Config, server)),
   698  		}
   699  	case dsapi.ReplicationCredentials:
   700  		return &couchdb.ReplicationCredentials{
   701  			UserCredentials: couchdb.UserCredentials{
   702  				UsernamePassword: up,
   703  				URI:              []byte(getServerURL(r.Config, server)),
   704  			},
   705  			DBName: []byte(r.Config.ReplicationDB()),
   706  		}
   707  	default:
   708  		return &couchdb.AdminCredentials{UsernamePassword: up}
   709  	}
   710  }
   711  
   712  func usernamePassword(creds couchdb.CredentialsManager, user *dsapi.CouchDBUser) couchdb.UsernamePassword {
   713  	switch user.Spec.Type {
   714  	case dsapi.UserCredentials:
   715  		return creds.(*couchdb.UserCredentials).UsernamePassword
   716  	case dsapi.ReplicationCredentials:
   717  		return creds.(*couchdb.ReplicationCredentials).UsernamePassword
   718  	default:
   719  		return creds.(*couchdb.AdminCredentials).UsernamePassword
   720  	}
   721  }
   722  

View as plain text