Repository: crossplane-contrib/provider-sql (go)

Issue:
MSSQL database user 
<!--
Thank you for helping to improve Crossplane!

Please be sure to search for open issues before raising a new one. We use issues
for bug reports and feature requests. Please find us at https://slack.crossplane.io
for questions, support, and discussion.
-->

### What happened?
Attempting to create MSSQL User. User fails to create with `managed/user.mssql.sql.crossplane.io  cannot create user: mssql: You can only create a user with a password in a contained database.`


### How can we reproduce it?
Using the following:
```
apiVersion: mssql.sql.crossplane.io/v1alpha1
kind: User
metadata:
  name: user-name
spec:
  deletionPolicy: Delete
  forProvider:
    databaseRef: 
      name: database-name
    passwordSecretRef:
      name: password-secret
      namespace: namespace
      key: password
  providerConfigRef:
    name: provider-config
```

### What environment did it happen in?
Crossplane version: 1.12.2

<!--
Include at least the version or commit of Crossplane you were running. Consider
also including your:

* Cloud provider or hardware configuration Cloud
* Kubernetes version (use `kubectl version`) v1.27.1
* Kubernetes distribution (e.g. Tectonic, GKE, OpenShift) AWS EKS
* OS (e.g. from /etc/os-release)
* Kernel (e.g. `uname -a`)
-->


Pull request:
MSSQL allow to define different databases for login an user
### Description of your changes

When a MSSQL user is created in a database other than the master database. The USER needs to created in the dedicated database, but the LOGIN needs to be created in the master database.

This is also mentioned here: https://github.com/crossplane-contrib/provider-sql/issues/158#issuecomment-1738834942

This pull request adds the possibility to define an additional database, where the login object is created in.
The already existing database fields are used for the user statements.

If the newly introduced loginDatabase is not set, the database field is used for both operations as before.

Fixes #158 

I have:

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `make reviewable` to ensure this PR is ready for review.

### How has this code been tested

The application was built and deployed in an existing cluster. Where the User rewource was configured to use the `loginDatabase: master` and `databaseRef` that is pointing to a Database resource. 

```
apiVersion: mssql.sql.crossplane.io/v1alpha1
kind: User
metadata:
  name: sql-example-user
spec:
  forProvider:
    loginDatabase: master
    databaseRef:
      name: test-db
    passwordSecretRef:
      key: password
      name: sql-example-user-pw
      namespace: crossplane-system
  writeConnectionSecretToRef:
    name: sql-example-user-connection-secret
    namespace: crossplane-system
```

[contribution process]: https://git.io/fj2m9


Patch:
diff --git a/apis/mssql/v1alpha1/user_types.go b/apis/mssql/v1alpha1/user_types.go
index 846f16c..eb74775 100644
--- a/apis/mssql/v1alpha1/user_types.go
+++ b/apis/mssql/v1alpha1/user_types.go
@@ -36,18 +36,26 @@ type UserStatus struct {
 
 // UserParameters define the desired state of a MSSQL user instance.
 type UserParameters struct {
+	// Database allows you to specify the name of the Database the USER is created for.
 	// +crossplane:generate:reference:type=Database
 	Database *string `json:"database,omitempty"`
-	// DatabaseRef allows you to specify custom resource name of the Database
+	// DatabaseRef allows you to specify custom resource name of the Database the USER is created for.
 	// to fill Database field.
 	DatabaseRef *xpv1.Reference `json:"databaseRef,omitempty"`
-	// DatabaseSelector allows you to use selector constraints to select a
-	// Database.
+	// DatabaseSelector allows you to use selector constraints to select a Database the USER is created for.
 	DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"`
 	// PasswordSecretRef references the secret that contains the password used
 	// for this user. If no reference is given, a password will be auto-generated.
 	// +optional
 	PasswordSecretRef *xpv1.SecretKeySelector `json:"passwordSecretRef,omitempty"`
+	// LoginDatabase allows you to specify the name of the Database to be used to create the user LOGIN in (normally master).
+	// +crossplane:generate:reference:type=Database
+	LoginDatabase *string `json:"loginDatabase,omitempty"`
+	// DatabaseRef allows you to specify custom resource name of the Database to be used to create the user LOGIN in (normally master).
+	// to fill Database field.
+	LoginDatabaseRef *xpv1.Reference `json:"loginDatabaseRef,omitempty"`
+	// DatabaseSelector allows you to use selector constraints to select a Database to be used to create the user LOGIN in (normally master).
+	LoginDatabaseSelector *xpv1.Selector `json:"loginDatabaseSelector,omitempty"`
 }
 
 // A UserObservation represents the observed state of a MSSQL user.
diff --git a/apis/mssql/v1alpha1/zz_generated.deepcopy.go b/apis/mssql/v1alpha1/zz_generated.deepcopy.go
index 729bad3..7ce2c56 100644
--- a/apis/mssql/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/mssql/v1alpha1/zz_generated.deepcopy.go
@@ -543,6 +543,21 @@ func (in *UserParameters) DeepCopyInto(out *UserParameters) {
 		*out = new(v1.SecretKeySelector)
 		**out = **in
 	}
+	if in.LoginDatabase != nil {
+		in, out := &in.LoginDatabase, &out.LoginDatabase
+		*out = new(string)
+		**out = **in
+	}
+	if in.LoginDatabaseRef != nil {
+		in, out := &in.LoginDatabaseRef, &out.LoginDatabaseRef
+		*out = new(v1.Reference)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.LoginDatabaseSelector != nil {
+		in, out := &in.LoginDatabaseSelector, &out.LoginDatabaseSelector
+		*out = new(v1.Selector)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserParameters.
diff --git a/apis/mssql/v1alpha1/zz_generated.resolvers.go b/apis/mssql/v1alpha1/zz_generated.resolvers.go
index 8c077a6..1be1457 100644
--- a/apis/mssql/v1alpha1/zz_generated.resolvers.go
+++ b/apis/mssql/v1alpha1/zz_generated.resolvers.go
@@ -89,5 +89,21 @@ func (mg *User) ResolveReferences(ctx context.Context, c client.Reader) error {
 	mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue)
 	mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference
 
+	rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
+		CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.LoginDatabase),
+		Extract:      reference.ExternalName(),
+		Reference:    mg.Spec.ForProvider.LoginDatabaseRef,
+		Selector:     mg.Spec.ForProvider.LoginDatabaseSelector,
+		To: reference.To{
+			List:    &DatabaseList{},
+			Managed: &Database{},
+		},
+	})
+	if err != nil {
+		return errors.Wrap(err, "mg.Spec.ForProvider.LoginDatabase")
+	}
+	mg.Spec.ForProvider.LoginDatabase = reference.ToPtrValue(rsp.ResolvedValue)
+	mg.Spec.ForProvider.LoginDatabaseRef = rsp.ResolvedReference
+
 	return nil
 }
diff --git a/package/crds/mssql.sql.crossplane.io_users.yaml b/package/crds/mssql.sql.crossplane.io_users.yaml
index 6b6e110..e90fcf9 100644
--- a/package/crds/mssql.sql.crossplane.io_users.yaml
+++ b/package/crds/mssql.sql.crossplane.io_users.yaml
@@ -72,10 +72,12 @@ spec:
                   instance.
                 properties:
                   database:
+                    description: Database allows you to specify the name of the Database
+                      the USER is created for.
                     type: string
                   databaseRef:
                     description: |-
-                      DatabaseRef allows you to specify custom resource name of the Database
+                      DatabaseRef allows you to specify custom resource name of the Database the USER is created for.
                       to fill Database field.
                     properties:
                       name:
@@ -110,9 +112,90 @@ spec:
                     - name
                     type: object
                   databaseSelector:
+                    description: DatabaseSelector allows you to use selector constraints
+                      to select a Database the USER is created for.
+                    properties:
+                      matchControllerRef:
+                        description: |-
+                          MatchControllerRef ensures an object with the same controller reference
+                          as the selecting object is selected.
+                        type: boolean
+                      matchLabels:
+                        additionalProperties:
+                          type: string
+                        description: MatchLabels ensures an object with matching labels
+                          is selected.
+                        type: object
+                      policy:
+                        description: Policies for selection.
+                        properties:
+                          resolution:
+                            default: Required
+                            description: |-
+                              Resolution specifies whether resolution of this reference is required.
+                              The default is 'Required', which means the reconcile will fail if the
+                              reference cannot be resolved. 'Optional' means this reference will be
+                              a no-op if it cannot be resolved.
+                            enum:
+                            - Required
+                            - Optional
+                            type: string
+                          resolve:
+                            description: |-
+                              Resolve specifies when this reference should be resolved. The default
+                              is 'IfNotPresent', which will attempt to resolve the reference only when
+                              the corresponding field is not present. Use 'Always' to resolve the
+                              reference on every reconcile.
+                            enum:
+                            - Always
+                            - IfNotPresent
+                            type: string
+                        type: object
+                    type: object
+                  loginDatabase:
+                    description: LoginDatabase allows you to specify the name of the
+                      Database to be used to create the user LOGIN in (normally master).
+                    type: string
+                  loginDatabaseRef:
                     description: |-
-                      DatabaseSelector allows you to use selector constraints to select a
-                      Database.
+                      DatabaseRef allows you to specify custom resource name of the Database to be used to create the user LOGIN in (normally master).
+                      to fill Database field.
+                    properties:
+                      name:
+                        description: Name of the referenced object.
+                        type: string
+                      policy:
+                        description: Policies for referencing.
+                        properties:
+                          resolution:
+                            default: Required
+                            description: |-
+                              Resolution specifies whether resolution of this reference is required.
+                              The default is 'Required', which means the reconcile will fail if the
+                              reference cannot be resolved. 'Optional' means this reference will be
+                              a no-op if it cannot be resolved.
+                            enum:
+                            - Required
+                            - Optional
+                            type: string
+                          resolve:
+                            description: |-
+                              Resolve specifies when this reference should be resolved. The default
+                              is 'IfNotPresent', which will attempt to resolve the reference only when
+                              the corresponding field is not present. Use 'Always' to resolve the
+                              reference on every reconcile.
+                            enum:
+                            - Always
+                            - IfNotPresent
+                            type: string
+                        type: object
+                    required:
+                    - name
+                    type: object
+                  loginDatabaseSelector:
+                    description: DatabaseSelector allows you to use selector constraints
+                      to select a Database to be used to create the user LOGIN in
+                      (normally master).
                     properties:
                       matchControllerRef:
                         description: |-
diff --git a/pkg/controller/mssql/user/reconciler.go b/pkg/controller/mssql/user/reconciler.go
index 5ab1d20..054d3b0 100644
--- a/pkg/controller/mssql/user/reconciler.go
+++ b/pkg/controller/mssql/user/reconciler.go
@@ -119,15 +119,23 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E
 		return nil, errors.Wrap(err, errGetSecret)
 	}
 
+	userDB := c.newClient(s.Data, ptr.Deref(cr.Spec.ForProvider.Database, ""))
+	loginDB := userDB
+	if cr.Spec.ForProvider.LoginDatabase != nil {
+		loginDB = c.newClient(s.Data, ptr.Deref(cr.Spec.ForProvider.LoginDatabase, ""))
+	}
+
 	return &external{
-		db:   c.newClient(s.Data, ptr.Deref(cr.Spec.ForProvider.Database, "")),
-		kube: c.kube,
+		userDB:  userDB,
+		loginDB: loginDB,
+		kube:    c.kube,
 	}, nil
 }
 
 type external struct {
-	db   xsql.DB
-	kube client.Client
+	userDB  xsql.DB
+	loginDB xsql.DB
+	kube    client.Client
 }
 
 func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) {
@@ -139,7 +147,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 	var name string
 
 	query := "SELECT name FROM sys.database_principals WHERE type = 'S' AND name = @p1"
-	err := c.db.Scan(ctx, xsql.Query{
+	err := c.userDB.Scan(ctx, xsql.Query{
 		String: query, Parameters: []interface{}{
 			meta.GetExternalName(cr),
 		},
@@ -182,21 +190,21 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext
 	}
 
 	loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(cr)), mssql.QuoteValue(pw))
-	if err := c.db.Exec(ctx, xsql.Query{
+	if err := c.loginDB.Exec(ctx, xsql.Query{
 		String: loginQuery,
 	}); err != nil {
 		return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(cr))
 	}
 
 	userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(cr)), mssql.QuoteIdentifier(meta.GetExternalName(cr)))
-	if err := c.db.Exec(ctx, xsql.Query{
+	if err := c.userDB.Exec(ctx, xsql.Query{
 		String: userQuery,
 	}); err != nil {
 		return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(cr))
 	}
 
 	return managed.ExternalCreation{
-		ConnectionDetails: c.db.GetConnectionDetails(meta.GetExternalName(cr), pw),
+		ConnectionDetails: c.userDB.GetConnectionDetails(meta.GetExternalName(cr), pw),
 	}, nil
 }
 
@@ -213,14 +221,14 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
 
 	if changed {
 		query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(cr)), mssql.QuoteValue(pw))
-		if err := c.db.Exec(ctx, xsql.Query{
+		if err := c.loginDB.Exec(ctx, xsql.Query{
 			String: query,
 		}); err != nil {
 			return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
 		}
 
 		return managed.ExternalUpdate{
-			ConnectionDetails: c.db.GetConnectionDetails(meta.GetExternalName(cr), pw),
+			ConnectionDetails: c.userDB.GetConnectionDetails(meta.GetExternalName(cr), pw),
 		}, nil
 	}
 	return managed.ExternalUpdate{}, nil
@@ -233,7 +241,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
 	}
 
 	query := fmt.Sprintf("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = %s", mssql.QuoteValue(meta.GetExternalName(cr)))
-	rows, err := c.db.Query(ctx, xsql.Query{String: query})
+	rows, err := c.userDB.Query(ctx, xsql.Query{String: query})
 	if err != nil {
 		return errors.Wrap(err, errCannotGetLogins)
 	}
@@ -244,7 +252,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
 		if err := rows.Scan(&sessionID); err != nil {
 			return errors.Wrap(err, errCannotGetLogins)
 		}
-		if err := c.db.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil {
+		if err := c.userDB.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil {
 			return errors.Wrapf(err, errCannotKillLoginSession, sessionID, meta.GetExternalName(cr))
 		}
 	}
@@ -252,13 +260,13 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
 		return errors.Wrap(err, errCannotGetLogins)
 	}
 
-	if err := c.db.Exec(ctx, xsql.Query{
+	if err := c.userDB.Exec(ctx, xsql.Query{
 		String: fmt.Sprintf("DROP USER IF EXISTS %s", mssql.QuoteIdentifier(meta.GetExternalName(cr))),
 	}); err != nil {
 		return errors.Wrapf(err, errDropUser, meta.GetExternalName(cr))
 	}
 
-	if err := c.db.Exec(ctx, xsql.Query{
+	if err := c.loginDB.Exec(ctx, xsql.Query{
 		String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(cr))),
 	}); err != nil {
 		return errors.Wrapf(err, errDropLogin, meta.GetExternalName(cr))


Test patch:
diff --git a/pkg/controller/mssql/user/reconciler_test.go b/pkg/controller/mssql/user/reconciler_test.go
index a0a3a74..022eceb 100644
--- a/pkg/controller/mssql/user/reconciler_test.go
+++ b/pkg/controller/mssql/user/reconciler_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
@@ -40,6 +41,7 @@ import (
 )
 
 type mockDB struct {
+	database   string
 	MockExec   func(ctx context.Context, q xsql.Query) error
 	MockExecTx func(ctx context.Context, ql []xsql.Query) error
 	MockScan   func(ctx context.Context, q xsql.Query, dest ...interface{}) error
@@ -92,18 +94,25 @@ func TestConnect(t *testing.T) {
 		mg  resource.Managed
 	}
 
+	type want struct {
+		sameClient *bool
+		err        error
+	}
+
 	cases := map[string]struct {
 		reason string
 		fields fields
 		args   args
-		want   error
+		want   want
 	}{
 		"ErrNotUser": {
 			reason: "An error should be returned if the managed resource is not a *User",
 			args: args{
 				mg: nil,
 			},
-			want: errors.New(errNotUser),
+			want: want{
+				err: errors.New(errNotUser),
+			},
 		},
 		"ErrTrackProviderConfigUsage": {
 			reason: "An error should be returned if we can't track our ProviderConfig usage",
@@ -113,7 +122,9 @@ func TestConnect(t *testing.T) {
 			args: args{
 				mg: &v1alpha1.User{},
 			},
-			want: errors.Wrap(errBoom, errTrackPCUsage),
+			want: want{
+				err: errors.Wrap(errBoom, errTrackPCUsage),
+			},
 		},
 		"ErrGetProviderConfig": {
 			reason: "An error should be returned if we can't get our ProviderConfig",
@@ -132,7 +143,9 @@ func TestConnect(t *testing.T) {
 					},
 				},
 			},
-			want: errors.Wrap(errBoom, errGetPC),
+			want: want{
+				err: errors.Wrap(errBoom, errGetPC),
+			},
 		},
 		"ErrMissingConnectionSecret": {
 			reason: "An error should be returned if our ProviderConfig doesn't specify a connection secret",
@@ -154,7 +167,9 @@ func TestConnect(t *testing.T) {
 					},
 				},
 			},
-			want: errors.New(errNoSecretRef),
+			want: want{
+				err: errors.New(errNoSecretRef),
+			},
 		},
 		"ErrGetConnectionSecret": {
 			reason: "An error should be returned if we can't get our ProviderConfig's connection secret",
@@ -181,17 +196,106 @@ func TestConnect(t *testing.T) {
 					},
 				},
 			},
-			want: errors.Wrap(errBoom, errGetSecret),
+			want: want{
+				err: errors.Wrap(errBoom, errGetSecret),
+			},
+		},
+		"Success": {
+			reason: "With NO login database defined, the clients should be the same",
+			fields: fields{
+				kube: &test.MockClient{
+					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
+						switch o := obj.(type) {
+						case *v1alpha1.ProviderConfig:
+							o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{}
+						case *corev1.Secret:
+							secret := corev1.Secret{
+								Data: map[string][]byte{},
+							}
+							secret.DeepCopyInto(obj.(*corev1.Secret))
+						}
+						return nil
+					}),
+				},
+				usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }),
+				newDB: func(creds map[string][]byte, database string) xsql.DB { return mockDB{database: database} },
+			},
+			args: args{
+				mg: &v1alpha1.User{
+					Spec: v1alpha1.UserSpec{
+						ResourceSpec: xpv1.ResourceSpec{
+							ProviderConfigReference: &xpv1.Reference{},
+						},
+						ForProvider: v1alpha1.UserParameters{
+							Database: ptr.To("success-database"),
+						},
+					},
+				},
+			},
+			want: want{
+				err:        nil,
+				sameClient: ptr.To(true),
+			},
+		},
+		"SuccessLoginDB": {
+			reason: "With the login database defined, the clients should differ",
+			fields: fields{
+				kube: &test.MockClient{
+					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
+						switch o := obj.(type) {
+						case *v1alpha1.ProviderConfig:
+							o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{}
+						case *corev1.Secret:
+							secret := corev1.Secret{
+								Data: map[string][]byte{},
+							}
+							secret.DeepCopyInto(obj.(*corev1.Secret))
+						}
+						return nil
+					}),
+				},
+				usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }),
+				newDB: func(creds map[string][]byte, database string) xsql.DB { return mockDB{database: database} },
+			},
+			args: args{
+				mg: &v1alpha1.User{
+					Spec: v1alpha1.UserSpec{
+						ResourceSpec: xpv1.ResourceSpec{
+							ProviderConfigReference: &xpv1.Reference{},
+						},
+						ForProvider: v1alpha1.UserParameters{
+							Database:      ptr.To("success-database"),
+							LoginDatabase: ptr.To("success-login-database"),
+						},
+					},
+				},
+			},
+			want: want{
+				err:        nil,
+				sameClient: ptr.To(false),
+			},
 		},
 	}
 
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
 			e := &connector{kube: tc.fields.kube, usage: tc.fields.usage, newClient: tc.fields.newDB}
-			_, err := e.Connect(tc.args.ctx, tc.args.mg)
-			if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
+			ec, err := e.Connect(tc.args.ctx, tc.args.mg)
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
 				t.Errorf("\n%s\ne.Connect(...): -want error, +got error:\n%s\n", tc.reason, diff)
 			}
+			if tc.want.sameClient != nil {
+				ext := ec.(*external)
+				db1 := ext.userDB.(mockDB).database
+				db2 := ext.loginDB.(mockDB).database
+				if *tc.want.sameClient && db1 != db2 {
+					t.Errorf("\n%s\ne.Connect(...): want clients to be on the same database\n%s / %s\n",
+						tc.reason, db1, db2)
+				} else if !*tc.want.sameClient && db1 == db2 {
+					t.Errorf("\n%s\ne.Connect(...): want clients NOT to be the same instance\n%s\n",
+						tc.reason, db1)
+				}
+			}
 		})
 	}
 }
@@ -325,8 +429,9 @@ func TestObserve(t *testing.T) {
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
 			e := external{
-				db:   tc.fields.db,
-				kube: tc.fields.kube,
+				userDB:  tc.fields.db,
+				loginDB: tc.fields.db,
+				kube:    tc.fields.kube,
 			}
 			got, err := e.Observe(tc.args.ctx, tc.args.mg)
 			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
@@ -474,8 +579,9 @@ func TestCreate(t *testing.T) {
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
 			e := external{
-				db:   tc.fields.db,
-				kube: tc.fields.kube,
+				userDB:  tc.fields.db,
+				loginDB: tc.fields.db,
+				kube:    tc.fields.kube,
 			}
 			got, err := e.Create(tc.args.ctx, tc.args.mg)
 			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
@@ -692,8 +798,9 @@ func TestUpdate(t *testing.T) {
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
 			e := external{
-				db:   tc.fields.db,
-				kube: tc.args.kube,
+				userDB:  tc.fields.db,
+				loginDB: tc.fields.db,
+				kube:    tc.args.kube,
 			}
 			got, err := e.Update(tc.args.ctx, tc.args.mg)
 			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
@@ -710,7 +817,8 @@ func TestDelete(t *testing.T) {
 	errBoom := errors.New("boom")
 
 	type fields struct {
-		db xsql.DB
+		userDB  xsql.DB
+		loginDB xsql.DB
 	}
 
 	type args struct {
@@ -734,7 +842,7 @@ func TestDelete(t *testing.T) {
 		"ErrDropDB": {
 			reason: "Errors dropping a user should be returned",
 			fields: fields{
-				db: &mockDB{
+				userDB: &mockDB{
 					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
 						return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil
 					},
@@ -742,6 +850,7 @@ func TestDelete(t *testing.T) {
 						return errBoom
 					},
 				},
+				loginDB: &mockDB{},
 			},
 			args: args{
 				mg: &v1alpha1.User{},
@@ -751,7 +860,7 @@ func TestDelete(t *testing.T) {
 		"Success": {
 			reason: "No error should be returned",
 			fields: fields{
-				db: &mockDB{
+				userDB: &mockDB{
 					MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
 						return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil
 					},
@@ -759,6 +868,12 @@ func TestDelete(t *testing.T) {
 						return nil
 					},
 				},
+				loginDB: &mockDB{
+
+					MockExec: func(ctx context.Context, q xsql.Query) error {
+						return nil
+					},
+				},
 			},
 			args: args{
 				mg: &v1alpha1.User{},
@@ -768,7 +883,7 @@ func TestDelete(t *testing.T) {
 
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
-			e := external{db: tc.fields.db}
+			e := external{userDB: tc.fields.userDB, loginDB: tc.fields.loginDB}
 			err := e.Delete(tc.args.ctx, tc.args.mg)
 			if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
 				t.Errorf("\n%s\ne.Delete(...): -want error, +got error:\n%s\n", tc.reason, diff)