Skip to content

Conversation

@baburciu
Copy link

@baburciu baburciu commented Dec 4, 2025

Description of your changes

This PR adds support for MSSQL contained database users, for scenarios where users need to be scoped only to specific databases without requiring server-level login creation.

Problem Statement:
AFAIU the existing loginDatabase functionality only allows creating LOGINs in one database (typically master). However, this traditional LOGIN+USER pattern has critical limitations:

  • Geo-replication incompatibility: LOGINs are server-level objects that are not replicated to Azure SQL Database read replicas, thus users created with CREATE USER FOR LOGIN become orphaned on read replicas since the referenced LOGIN doesn't exist on the replica instance/server
  • Multi-tenant isolation: Traditional users have server-level visibility rather than being truly database-scoped.

Key Changes:

  1. New contained field for the User.mssql.sql.crossplane.io resource: Added optional boolean field to both cluster and namespaced MSSQL User CRDs
  2. Conditional user creation:
    • When contained: true: Uses CREATE USER [username] WITH PASSWORD = 'password' syntax directly in target database
    • When contained: false or unset: Uses traditional CREATE LOGIN + CREATE USER FOR LOGIN approach
  3. Authentication-aware updates: Password updates use appropriate SQL syntax based on user type
  4. Session handling: Skips session killing for contained users (no server-level login to kill)
  5. CEL validation: Ensures mutual exclusion between contained: true and loginDatabase fields

Default behavior is unchanged when contained field is not specified, ensuring existing deployments continue to work.

Disclaimer: AI was also used in generating this patch.

Fixes #298

I have:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable to ensure this PR is ready for review.

How has this code been tested

Built the package docker.io/bogdanadrianburciu/provider-sql:v0.13.0-issue298-amd64 and tested with a Crossplane 1.20.0 install in AKS.
With

apiVersion: mssql.sql.crossplane.io/v1alpha1
kind: User
metadata:
  name: foo-dev-read-replica
spec:
  deletionPolicy: Orphan
  forProvider:
    contained: true
    database: FOO-DB
    passwordSecretRef:
      key: password
      name: foo-dev-secret-db-creds-read-replica
      namespace: dev
  managementPolicies:
  - '*'
  providerConfigRef:
    name: foo-dev-read-replica-db-connection
  writeConnectionSecretToRef:
    name: foo-dev-read-replica-db-user-state
    namespace: dev
---
apiVersion: mssql.sql.crossplane.io/v1alpha1
kind: User
metadata:
  name: foo-dev
spec:
  deletionPolicy: Orphan
  forProvider:
    database: FOO-DB
    loginDatabase: master
    passwordSecretRef:
      key: password
      name: foo-dev-secret-db-creds
      namespace: dev
  managementPolicies:
  - '*'
  providerConfigRef:
    name: foo-dev-db-connection
  writeConnectionSecretToRef:
    name: foo-dev-db-user-state
    namespace: dev

we see (in Azure SQL):

root@mssql-test:/# sqlcmd -S foo-db-instace.database.windows.net,1433   -d FOO-DB   -U foodbadmin   -P '********************'  -C   -Q "SELECT name AS user_name, type_desc AS user_type, authentication_type_desc FROM sys.database_principals WHERE type IN ('S', 'E', 'X', 'G', 'U') AND name NOT LIKE '##%';"
user_name                                                                                                                        user_type                                                    authentication_type_desc
-------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------ ------------------------------------------------------------
dbo                                                                                                                              SQL_USER                                                     INSTANCE
guest                                                                                                                            SQL_USER                                                     NONE
INFORMATION_SCHEMA                                                                                                               SQL_USER                                                     NONE
sys                                                                                                                              SQL_USER                                                     NONE
foo-dev-read-replica                                                                                                             SQL_USER                                                     DATABASE
foo-dev                                                                                                                          SQL_USER                                                     INSTANCE

(6 rows affected)
root@mssql-test:/# sqlcmd -S foo-db-instace.database.windows.net,1433   -d FOO-DB   -U foo-dev-read-replica   -P '**************'  -C   -Q "SELECT DB_NAME() AS DatabaseName"
DatabaseName
--------------------------------------------------------------------------------------------------------------------------------
FOO-DB

(1 rows affected)
root@mssql-test:/#

…ad of Instance level

Signed-off-by: Bogdan-Adrian Burciu <[email protected]>
@baburciu baburciu force-pushed the feat-298/mssql-contained-database-user branch from 871aa38 to 803b7e9 Compare December 4, 2025 18:00
// 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

@baburciu baburciu force-pushed the feat-298/mssql-contained-database-user branch from 0f47bf8 to a1060e6 Compare December 5, 2025 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support MSSQL contained database user

2 participants