Skip to content

Commit 803b7e9

Browse files
committed
feat: Add ability to optionally create Database contained user, instead of Instance level
Signed-off-by: Bogdan-Adrian Burciu <bogdanadrian.burciu@yahoo.com>
1 parent f0cc26e commit 803b7e9

File tree

8 files changed

+170
-54
lines changed

8 files changed

+170
-54
lines changed

apis/cluster/mssql/v1alpha1/user_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type UserStatus struct {
3535
}
3636

3737
// UserParameters define the desired state of a MSSQL user instance.
38+
// +kubebuilder:validation:XValidation:rule="!(has(self.contained) && self.contained == true && (has(self.loginDatabase) || has(self.loginDatabaseRef) || has(self.loginDatabaseSelector)))",message="contained users cannot specify loginDatabase, loginDatabaseRef, or loginDatabaseSelector"
3839
type UserParameters struct {
3940
// Database allows you to specify the name of the Database the USER is created for.
4041
// +crossplane:generate:reference:type=Database
@@ -56,6 +57,11 @@ type UserParameters struct {
5657
LoginDatabaseRef *xpv1.Reference `json:"loginDatabaseRef,omitempty"`
5758
// DatabaseSelector allows you to use selector constraints to select a Database to be used to create the user LOGIN in (normally master).
5859
LoginDatabaseSelector *xpv1.Selector `json:"loginDatabaseSelector,omitempty"`
60+
// Contained specifies whether to create a contained database user (without server-level login).
61+
// When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
62+
// When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
63+
// +optional
64+
Contained *bool `json:"contained,omitempty"`
5965
}
6066

6167
// A UserObservation represents the observed state of a MSSQL user.

apis/cluster/mssql/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/namespaced/mssql/v1alpha1/user_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type UserStatus struct {
3636
}
3737

3838
// UserParameters define the desired state of a MSSQL user instance.
39+
// +kubebuilder:validation:XValidation:rule="!(has(self.contained) && self.contained == true && (has(self.loginDatabase) || has(self.loginDatabaseRef) || has(self.loginDatabaseSelector)))",message="contained users cannot specify loginDatabase, loginDatabaseRef, or loginDatabaseSelector"
3940
type UserParameters struct {
4041
// Database allows you to specify the name of the Database the USER is created for.
4142
// +crossplane:generate:reference:type=Database
@@ -57,6 +58,11 @@ type UserParameters struct {
5758
LoginDatabaseRef *xpv1.NamespacedReference `json:"loginDatabaseRef,omitempty"`
5859
// DatabaseSelector allows you to use selector constraints to select a Database to be used to create the user LOGIN in (normally master).
5960
LoginDatabaseSelector *xpv1.NamespacedSelector `json:"loginDatabaseSelector,omitempty"`
61+
// Contained specifies whether to create a contained database user (without server-level login).
62+
// When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
63+
// When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
64+
// +optional
65+
Contained *bool `json:"contained,omitempty"`
6066
}
6167

6268
// A UserObservation represents the observed state of a MSSQL user.

apis/namespaced/mssql/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package/crds/mssql.sql.crossplane.io_users.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ spec:
7171
description: UserParameters define the desired state of a MSSQL user
7272
instance.
7373
properties:
74+
contained:
75+
description: |-
76+
Contained specifies whether to create a contained database user (without server-level login).
77+
When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
78+
When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
79+
type: boolean
7480
database:
7581
description: Database allows you to specify the name of the Database
7682
the USER is created for.
@@ -254,6 +260,11 @@ spec:
254260
- namespace
255261
type: object
256262
type: object
263+
x-kubernetes-validations:
264+
- message: contained users cannot specify loginDatabase, loginDatabaseRef,
265+
or loginDatabaseSelector
266+
rule: '!(has(self.contained) && self.contained == true && (has(self.loginDatabase)
267+
|| has(self.loginDatabaseRef) || has(self.loginDatabaseSelector)))'
257268
managementPolicies:
258269
default:
259270
- '*'

package/crds/mssql.sql.m.crossplane.io_users.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ spec:
5353
description: UserParameters define the desired state of a MSSQL user
5454
instance.
5555
properties:
56+
contained:
57+
description: |-
58+
Contained specifies whether to create a contained database user (without server-level login).
59+
When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
60+
When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
61+
type: boolean
5662
database:
5763
description: Database allows you to specify the name of the Database
5864
the USER is created for.
@@ -243,6 +249,11 @@ spec:
243249
- name
244250
type: object
245251
type: object
252+
x-kubernetes-validations:
253+
- message: contained users cannot specify loginDatabase, loginDatabaseRef,
254+
or loginDatabaseSelector
255+
rule: '!(has(self.contained) && self.contained == true && (has(self.loginDatabase)
256+
|| has(self.loginDatabaseRef) || has(self.loginDatabaseSelector)))'
246257
managementPolicies:
247258
default:
248259
- '*'

pkg/controller/cluster/mssql/user/reconciler.go

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -185,18 +185,30 @@ func (c *external) Create(ctx context.Context, mg *v1alpha1.User) (managed.Exter
185185
}
186186
}
187187

188-
loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
189-
if err := c.loginDB.Exec(ctx, xsql.Query{
190-
String: loginQuery,
191-
}); err != nil {
192-
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
193-
}
188+
// Check if this should be a contained database user
189+
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
190+
// Create contained database user directly without LOGIN
191+
userQuery := fmt.Sprintf("CREATE USER %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
192+
if err := c.userDB.Exec(ctx, xsql.Query{
193+
String: userQuery,
194+
}); err != nil {
195+
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
196+
}
197+
} else {
198+
// Create traditional LOGIN + USER approach
199+
loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
200+
if err := c.loginDB.Exec(ctx, xsql.Query{
201+
String: loginQuery,
202+
}); err != nil {
203+
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
204+
}
194205

195-
userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
196-
if err := c.userDB.Exec(ctx, xsql.Query{
197-
String: userQuery,
198-
}); err != nil {
199-
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
206+
userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
207+
if err := c.userDB.Exec(ctx, xsql.Query{
208+
String: userQuery,
209+
}); err != nil {
210+
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
211+
}
200212
}
201213

202214
return managed.ExternalCreation{
@@ -211,11 +223,22 @@ func (c *external) Update(ctx context.Context, mg *v1alpha1.User) (managed.Exter
211223
}
212224

213225
if changed {
214-
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
215-
if err := c.loginDB.Exec(ctx, xsql.Query{
216-
String: query,
217-
}); err != nil {
218-
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
226+
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
227+
// For contained users, use ALTER USER syntax
228+
query := fmt.Sprintf("ALTER USER %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
229+
if err := c.userDB.Exec(ctx, xsql.Query{
230+
String: query,
231+
}); err != nil {
232+
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
233+
}
234+
} else {
235+
// For traditional users, use ALTER LOGIN syntax
236+
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
237+
if err := c.loginDB.Exec(ctx, xsql.Query{
238+
String: query,
239+
}); err != nil {
240+
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
241+
}
219242
}
220243

221244
return managed.ExternalUpdate{
@@ -229,25 +252,34 @@ func (c *external) Disconnect(ctx context.Context) error {
229252
return nil
230253
}
231254

232-
func (c *external) Delete(ctx context.Context, mg *v1alpha1.User) (managed.ExternalDelete, error) {
233-
query := fmt.Sprintf("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = %s", mssql.QuoteValue(meta.GetExternalName(mg)))
255+
func (c *external) killLoginSessions(ctx context.Context, loginName string) error {
256+
query := fmt.Sprintf("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = %s", mssql.QuoteValue(loginName))
234257
rows, err := c.userDB.Query(ctx, xsql.Query{String: query})
235258
if err != nil {
236-
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
259+
return errors.Wrap(err, errCannotGetLogins)
237260
}
238261
defer rows.Close() //nolint:errcheck
239262

240263
for rows.Next() {
241264
var sessionID int
242265
if err := rows.Scan(&sessionID); err != nil {
243-
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
266+
return errors.Wrap(err, errCannotGetLogins)
244267
}
245268
if err := c.userDB.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil {
246-
return managed.ExternalDelete{}, errors.Wrapf(err, errCannotKillLoginSession, sessionID, meta.GetExternalName(mg))
269+
return errors.Wrapf(err, errCannotKillLoginSession, sessionID, loginName)
247270
}
248271
}
249-
if err := rows.Err(); err != nil {
250-
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
272+
return rows.Err()
273+
}
274+
275+
func (c *external) Delete(ctx context.Context, mg *v1alpha1.User) (managed.ExternalDelete, error) {
276+
isContained := mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained
277+
278+
// Only kill sessions for traditional users with logins, not contained users
279+
if !isContained {
280+
if err := c.killLoginSessions(ctx, meta.GetExternalName(mg)); err != nil {
281+
return managed.ExternalDelete{}, err
282+
}
251283
}
252284

253285
if err := c.userDB.Exec(ctx, xsql.Query{
@@ -256,10 +288,13 @@ func (c *external) Delete(ctx context.Context, mg *v1alpha1.User) (managed.Exter
256288
return managed.ExternalDelete{}, errors.Wrapf(err, errDropUser, meta.GetExternalName(mg))
257289
}
258290

259-
if err := c.loginDB.Exec(ctx, xsql.Query{
260-
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
261-
}); err != nil {
262-
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
291+
// Only drop LOGIN if this is not a contained user
292+
if !isContained {
293+
if err := c.loginDB.Exec(ctx, xsql.Query{
294+
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
295+
}); err != nil {
296+
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
297+
}
263298
}
264299

265300
return managed.ExternalDelete{}, nil

pkg/controller/namespaced/mssql/user/reconciler.go

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,31 @@ func (c *external) Create(ctx context.Context, mg *namespacedv1alpha1.User) (man
168168
}
169169
}
170170

171-
loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
172-
if err := c.loginDB.Exec(ctx, xsql.Query{
173-
String: loginQuery,
174-
}); err != nil {
175-
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
176-
}
171+
// Check if this should be a contained database user
172+
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
173+
// Create contained database user directly without LOGIN
174+
dbName := ptr.Deref(mg.Spec.ForProvider.Database, "")
175+
userQuery := fmt.Sprintf("USE %s; CREATE USER %s WITH PASSWORD = %s", mssql.QuoteIdentifier(dbName), mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
176+
if err := c.userDB.Exec(ctx, xsql.Query{
177+
String: userQuery,
178+
}); err != nil {
179+
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
180+
}
181+
} else {
182+
// Create traditional LOGIN + USER approach
183+
loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
184+
if err := c.loginDB.Exec(ctx, xsql.Query{
185+
String: loginQuery,
186+
}); err != nil {
187+
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
188+
}
177189

178-
userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
179-
if err := c.userDB.Exec(ctx, xsql.Query{
180-
String: userQuery,
181-
}); err != nil {
182-
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
190+
userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
191+
if err := c.userDB.Exec(ctx, xsql.Query{
192+
String: userQuery,
193+
}); err != nil {
194+
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
195+
}
183196
}
184197

185198
return managed.ExternalCreation{
@@ -194,11 +207,23 @@ func (c *external) Update(ctx context.Context, mg *namespacedv1alpha1.User) (man
194207
}
195208

196209
if changed {
197-
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
198-
if err := c.loginDB.Exec(ctx, xsql.Query{
199-
String: query,
200-
}); err != nil {
201-
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
210+
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
211+
// For contained users, use ALTER USER syntax with explicit USE statement
212+
dbName := ptr.Deref(mg.Spec.ForProvider.Database, "")
213+
query := fmt.Sprintf("USE %s; ALTER USER %s WITH PASSWORD = %s", mssql.QuoteIdentifier(dbName), mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
214+
if err := c.userDB.Exec(ctx, xsql.Query{
215+
String: query,
216+
}); err != nil {
217+
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
218+
}
219+
} else {
220+
// For traditional users, use ALTER LOGIN syntax
221+
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
222+
if err := c.loginDB.Exec(ctx, xsql.Query{
223+
String: query,
224+
}); err != nil {
225+
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
226+
}
202227
}
203228

204229
return managed.ExternalUpdate{
@@ -212,25 +237,34 @@ func (c *external) Disconnect(ctx context.Context) error {
212237
return nil
213238
}
214239

215-
func (c *external) Delete(ctx context.Context, mg *namespacedv1alpha1.User) (managed.ExternalDelete, error) {
216-
query := fmt.Sprintf("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = %s", mssql.QuoteValue(meta.GetExternalName(mg)))
240+
func (c *external) killLoginSessions(ctx context.Context, loginName string) error {
241+
query := fmt.Sprintf("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = %s", mssql.QuoteValue(loginName))
217242
rows, err := c.userDB.Query(ctx, xsql.Query{String: query})
218243
if err != nil {
219-
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
244+
return errors.Wrap(err, errCannotGetLogins)
220245
}
221246
defer rows.Close() //nolint:errcheck
222247

223248
for rows.Next() {
224249
var sessionID int
225250
if err := rows.Scan(&sessionID); err != nil {
226-
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
251+
return errors.Wrap(err, errCannotGetLogins)
227252
}
228253
if err := c.userDB.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil {
229-
return managed.ExternalDelete{}, errors.Wrapf(err, errCannotKillLoginSession, sessionID, meta.GetExternalName(mg))
254+
return errors.Wrapf(err, errCannotKillLoginSession, sessionID, loginName)
230255
}
231256
}
232-
if err := rows.Err(); err != nil {
233-
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
257+
return rows.Err()
258+
}
259+
260+
func (c *external) Delete(ctx context.Context, mg *namespacedv1alpha1.User) (managed.ExternalDelete, error) {
261+
isContained := mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained
262+
263+
// Only kill sessions for traditional users with logins, not contained users
264+
if !isContained {
265+
if err := c.killLoginSessions(ctx, meta.GetExternalName(mg)); err != nil {
266+
return managed.ExternalDelete{}, err
267+
}
234268
}
235269

236270
if err := c.userDB.Exec(ctx, xsql.Query{
@@ -239,10 +273,13 @@ func (c *external) Delete(ctx context.Context, mg *namespacedv1alpha1.User) (man
239273
return managed.ExternalDelete{}, errors.Wrapf(err, errDropUser, meta.GetExternalName(mg))
240274
}
241275

242-
if err := c.loginDB.Exec(ctx, xsql.Query{
243-
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
244-
}); err != nil {
245-
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
276+
// Only drop LOGIN if this is not a contained user
277+
if !isContained {
278+
if err := c.loginDB.Exec(ctx, xsql.Query{
279+
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
280+
}); err != nil {
281+
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
282+
}
246283
}
247284

248285
return managed.ExternalDelete{}, nil

0 commit comments

Comments
 (0)