Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apis/cluster/mssql/v1alpha1/user_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type UserStatus struct {
}

// UserParameters define the desired state of a MSSQL user instance.
// +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"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.contained) || self.contained == oldSelf.contained",message="contained field is immutable after creation"
type UserParameters struct {
// Database allows you to specify the name of the Database the USER is created for.
// +crossplane:generate:reference:type=Database
Expand All @@ -56,6 +58,11 @@ type UserParameters struct {
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"`
// Contained specifies whether to create a contained database user (without server-level login).
// When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
// When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
// +optional
Contained *bool `json:"contained,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this value after creation is not supported, right? Should we have CEL validatioin to ensure it's not mutated?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @chlunde, I've added CEL validation for contained immutability in last commit

}

// A UserObservation represents the observed state of a MSSQL user.
Expand Down
5 changes: 5 additions & 0 deletions apis/cluster/mssql/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apis/namespaced/mssql/v1alpha1/user_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type UserStatus struct {
}

// UserParameters define the desired state of a MSSQL user instance.
// +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"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.contained) || self.contained == oldSelf.contained",message="contained field is immutable after creation"
type UserParameters struct {
// Database allows you to specify the name of the Database the USER is created for.
// +crossplane:generate:reference:type=Database
Expand All @@ -57,6 +59,11 @@ type UserParameters struct {
LoginDatabaseRef *xpv1.NamespacedReference `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.NamespacedSelector `json:"loginDatabaseSelector,omitempty"`
// Contained specifies whether to create a contained database user (without server-level login).
// When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
// When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
// +optional
Contained *bool `json:"contained,omitempty"`
}

// A UserObservation represents the observed state of a MSSQL user.
Expand Down
5 changes: 5 additions & 0 deletions apis/namespaced/mssql/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions package/crds/mssql.sql.crossplane.io_users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ spec:
description: UserParameters define the desired state of a MSSQL user
instance.
properties:
contained:
description: |-
Contained specifies whether to create a contained database user (without server-level login).
When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
type: boolean
database:
description: Database allows you to specify the name of the Database
the USER is created for.
Expand Down Expand Up @@ -254,6 +260,13 @@ spec:
- namespace
type: object
type: object
x-kubernetes-validations:
- message: contained users cannot specify loginDatabase, loginDatabaseRef,
or loginDatabaseSelector
rule: '!(has(self.contained) && self.contained == true && (has(self.loginDatabase)
|| has(self.loginDatabaseRef) || has(self.loginDatabaseSelector)))'
- message: contained field is immutable after creation
rule: '!has(oldSelf.contained) || self.contained == oldSelf.contained'
managementPolicies:
default:
- '*'
Expand Down
13 changes: 13 additions & 0 deletions package/crds/mssql.sql.m.crossplane.io_users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ spec:
description: UserParameters define the desired state of a MSSQL user
instance.
properties:
contained:
description: |-
Contained specifies whether to create a contained database user (without server-level login).
When true, the user will be created directly in the specified database using CREATE USER WITH PASSWORD.
When false (default), a server-level LOGIN will be created first, then a database user mapped to that login.
type: boolean
database:
description: Database allows you to specify the name of the Database
the USER is created for.
Expand Down Expand Up @@ -243,6 +249,13 @@ spec:
- name
type: object
type: object
x-kubernetes-validations:
- message: contained users cannot specify loginDatabase, loginDatabaseRef,
or loginDatabaseSelector
rule: '!(has(self.contained) && self.contained == true && (has(self.loginDatabase)
|| has(self.loginDatabaseRef) || has(self.loginDatabaseSelector)))'
- message: contained field is immutable after creation
rule: '!has(oldSelf.contained) || self.contained == oldSelf.contained'
managementPolicies:
default:
- '*'
Expand Down
89 changes: 62 additions & 27 deletions pkg/controller/cluster/mssql/user/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,18 +185,30 @@ func (c *external) Create(ctx context.Context, mg *v1alpha1.User) (managed.Exter
}
}

loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: loginQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
}
// Check if this should be a contained database user
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
// Create contained database user directly without LOGIN
userQuery := fmt.Sprintf("CREATE USER %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.userDB.Exec(ctx, xsql.Query{
String: userQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
}
} else {
// Create traditional LOGIN + USER approach
loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: loginQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
}

userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
if err := c.userDB.Exec(ctx, xsql.Query{
String: userQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
if err := c.userDB.Exec(ctx, xsql.Query{
String: userQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
}
}

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

if changed {
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: query,
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
// For contained users, use ALTER USER syntax
query := fmt.Sprintf("ALTER USER %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.userDB.Exec(ctx, xsql.Query{
String: query,
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
}
} else {
// For traditional users, use ALTER LOGIN syntax
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: query,
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
}
}

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

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

for rows.Next() {
var sessionID int
if err := rows.Scan(&sessionID); err != nil {
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
return errors.Wrap(err, errCannotGetLogins)
}
if err := c.userDB.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil {
return managed.ExternalDelete{}, errors.Wrapf(err, errCannotKillLoginSession, sessionID, meta.GetExternalName(mg))
return errors.Wrapf(err, errCannotKillLoginSession, sessionID, loginName)
}
}
if err := rows.Err(); err != nil {
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
return rows.Err()
}

func (c *external) Delete(ctx context.Context, mg *v1alpha1.User) (managed.ExternalDelete, error) {
isContained := mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained

// Only kill sessions for traditional users with logins, not contained users
if !isContained {
if err := c.killLoginSessions(ctx, meta.GetExternalName(mg)); err != nil {
return managed.ExternalDelete{}, err
}
}

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

if err := c.loginDB.Exec(ctx, xsql.Query{
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
}); err != nil {
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
// Only drop LOGIN if this is not a contained user
if !isContained {
if err := c.loginDB.Exec(ctx, xsql.Query{
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
}); err != nil {
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
}
}

return managed.ExternalDelete{}, nil
Expand Down
91 changes: 64 additions & 27 deletions pkg/controller/namespaced/mssql/user/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,31 @@ func (c *external) Create(ctx context.Context, mg *namespacedv1alpha1.User) (man
}
}

loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: loginQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
}
// Check if this should be a contained database user
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
// Create contained database user directly without LOGIN
dbName := ptr.Deref(mg.Spec.ForProvider.Database, "")
userQuery := fmt.Sprintf("USE %s; CREATE USER %s WITH PASSWORD = %s", mssql.QuoteIdentifier(dbName), mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.userDB.Exec(ctx, xsql.Query{
String: userQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
}
} else {
// Create traditional LOGIN + USER approach
loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: loginQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(mg))
}

userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
if err := c.userDB.Exec(ctx, xsql.Query{
String: userQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteIdentifier(meta.GetExternalName(mg)))
if err := c.userDB.Exec(ctx, xsql.Query{
String: userQuery,
}); err != nil {
return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(mg))
}
}

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

if changed {
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: query,
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
if mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained {
// For contained users, use ALTER USER syntax with explicit USE statement
dbName := ptr.Deref(mg.Spec.ForProvider.Database, "")
query := fmt.Sprintf("USE %s; ALTER USER %s WITH PASSWORD = %s", mssql.QuoteIdentifier(dbName), mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.userDB.Exec(ctx, xsql.Query{
String: query,
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
}
} else {
// For traditional users, use ALTER LOGIN syntax
query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(mg)), mssql.QuoteValue(pw))
if err := c.loginDB.Exec(ctx, xsql.Query{
String: query,
}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser)
}
}

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

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

for rows.Next() {
var sessionID int
if err := rows.Scan(&sessionID); err != nil {
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
return errors.Wrap(err, errCannotGetLogins)
}
if err := c.userDB.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil {
return managed.ExternalDelete{}, errors.Wrapf(err, errCannotKillLoginSession, sessionID, meta.GetExternalName(mg))
return errors.Wrapf(err, errCannotKillLoginSession, sessionID, loginName)
}
}
if err := rows.Err(); err != nil {
return managed.ExternalDelete{}, errors.Wrap(err, errCannotGetLogins)
return rows.Err()
}

func (c *external) Delete(ctx context.Context, mg *namespacedv1alpha1.User) (managed.ExternalDelete, error) {
isContained := mg.Spec.ForProvider.Contained != nil && *mg.Spec.ForProvider.Contained

// Only kill sessions for traditional users with logins, not contained users
if !isContained {
if err := c.killLoginSessions(ctx, meta.GetExternalName(mg)); err != nil {
return managed.ExternalDelete{}, err
}
}

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

if err := c.loginDB.Exec(ctx, xsql.Query{
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
}); err != nil {
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
// Only drop LOGIN if this is not a contained user
if !isContained {
if err := c.loginDB.Exec(ctx, xsql.Query{
String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(mg))),
}); err != nil {
return managed.ExternalDelete{}, errors.Wrapf(err, errDropLogin, meta.GetExternalName(mg))
}
}

return managed.ExternalDelete{}, nil
Expand Down