Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces support for Microsoft Entra ID authentication to the MSSQL database source, updating the configuration, connection initialization, documentation, and tests. The review feedback highlights two key improvements: adding fail-fast validation to ensure both client ID and client secret are provided when using the service-principal authentication mode, and fixing a bug where the tenant ID is ignored if the client ID is empty.
| workflow, ok := azureFedAuth[azure.Mode] | ||
| if !ok { | ||
| return nil, fmt.Errorf("unsupported azureAuth mode %q", azure.Mode) | ||
| } |
There was a problem hiding this comment.
When using the service-principal authentication mode, both the Client ID (clientId) and the Client Secret (passed via the password field) are strictly required by the Azure AD driver to authenticate.
Since the validator makes password optional when azureAuth is present (required_without=AzureAuth), we should add a fail-fast validation check here to prevent runtime connection failures with cryptic errors.
workflow, ok := azureFedAuth[azure.Mode]
if !ok {
return nil, fmt.Errorf("unsupported azureAuth mode %q", azure.Mode)
}
if azure.Mode == "service-principal" {
if azure.ClientID == "" {
return nil, fmt.Errorf("clientId is required when azureAuth mode is %q", azure.Mode)
}
if pass == "" {
return nil, fmt.Errorf("password (client secret) is required when azureAuth mode is %q", azure.Mode)
}
}There was a problem hiding this comment.
Agreed, done in 0b2c6b2. clientId now carries required_if=Mode service-principal, so a service principal without one fails at parse time; the missing secret is caught in Initialize, since password lives on the outer struct where a tag cannot reach it. The same commit stops sending password in the DSN for modes that do not read it.
| if azure.ClientID != "" { | ||
| userID := azure.ClientID | ||
| if azure.TenantID != "" { | ||
| userID = fmt.Sprintf("%s@%s", azure.ClientID, azure.TenantID) | ||
| } | ||
| query.Add("user id", userID) | ||
| } |
There was a problem hiding this comment.
If azure.ClientID is empty but azure.TenantID is specified, the current implementation completely ignores the tenant ID because the outer if block only executes when azure.ClientID != "" is true.
Updating the condition to azure.ClientID != "" || azure.TenantID != "" ensures that the tenant ID is correctly passed to the driver (as @tenant_id) even when no client ID is specified.
| if azure.ClientID != "" { | |
| userID := azure.ClientID | |
| if azure.TenantID != "" { | |
| userID = fmt.Sprintf("%s@%s", azure.ClientID, azure.TenantID) | |
| } | |
| query.Add("user id", userID) | |
| } | |
| if azure.ClientID != "" || azure.TenantID != "" { | |
| userID := azure.ClientID | |
| if azure.TenantID != "" { | |
| userID = fmt.Sprintf("%s@%s", azure.ClientID, azure.TenantID) | |
| } | |
| query.Add("user id", userID) | |
| } |
There was a problem hiding this comment.
The symptom is real — the tenant was being dropped — but the suggested form does not work in the driver. splitTenantAndClientID returns (user, "") when the @ is at index 0 (if at < 1 || … { return user, "" }), so @<tenant> would be taken as the client id and the tenant dropped anyway. The driver has no way to receive a tenant without a client id.
So rather than emit it, 0b2c6b2 refuses the combination at parse time: clientId is now required_with=TenantID, with a test for the message.
|
@googlebot I signed it! |
…ection
Adds an optional azureAuth block to the mssql source so it can connect to an Azure
SQL server that has SQL authentication disabled, where there are no SQL logins to
hand out. Omitting the block leaves today's behaviour untouched.
azureAuth:
mode: workload-identity # or default | managed-identity | service-principal
clientId: <client-id>
tenantId: <tenant-id>
disableInstanceDiscovery: true
additionallyAllowedTenants: [<tenant-id>]
When set, the connection opens through the azuread driver with the matching fedauth
workflow instead of the base driver, and user/password stop being required
(required_without=AzureAuth). The client id, and optionally its tenant, travel in the
DSN as 'user id' in the clientID@tenantID form the driver parses; a service principal's
secret travels as 'password'. disableInstanceDiscovery and additionallyAllowedTenants
are passed through for network-isolated and sovereign-cloud deployments that cannot
reach the public authority discovery endpoint.
azuread is a package of the already-required github.com/microsoft/go-mssqldb, so no
version change; go mod tidy adds azidentity and MSAL as indirect requirements, which
the issue anticipated.
Encryption toggles are deliberately left out to keep this change to the auth switch;
trustServerCertificate can follow separately.
Fixes googleapis#3571
e259f9c to
0422ff5
Compare
Three cases that used to be accepted and then fail, or silently do the wrong thing, at connect time: - tenantId without clientId was dropped. The driver reads the tenant only from the clientID@tenantID form of 'user id', and splitTenantAndClientID treats a leading '@' as part of the client id, so there is no way to send a tenant on its own. clientId is now required_with=TenantID and the mismatch is reported at parse time instead of being ignored. - service-principal without clientId, or without the client secret, reached the driver and came back as a login failure. Both are now refused with a message that names the missing field. - user alongside azureAuth was silently ignored. It is now refused, matching how the rest of the config treats two identities in one block. Also stops sending 'password' in the DSN for modes that do not read it.
The mssql source connects with one login for every request, so SQL Server sees
the same identity whoever asked. That leaves the executed statement as the only
thing standing between an analyst and data they may not read, and puts the
enforcement in the prompt rather than in the database.
Toolbox already carries the caller's token to the tool, and BigQuery, Spanner
and Looker already use it. This does the same for SQL Server:
useClientOAuth: "true"
azureOnBehalfOf:
clientId: ...
clientSecret: ...
tenantId: ...
With useClientOAuth set, each caller gets their own connection pool, cached by a
digest of their token and closed when the entry expires, and the database applies
that person's permissions and row-level security.
The token the client sent is usually issued for Toolbox rather than for the
database, since that is who the client authenticated against. azureOnBehalfOf
exchanges it for a database-scoped one through the on-behalf-of flow. Omitting
the block passes the caller's token on as it arrived, for clients that already
hold one for the database.
Notes on the edges:
- A request with no token is refused, never downgraded to a shared identity.
'user' and 'password' must be omitted with useClientOAuth, so there is no
fallback identity to reach by accident.
- Pool creation is serialised. Without it a caller's first two concurrent
requests each open a pool, and caching the second closes the first while it is
in use. Neither opening a pool nor building the credential does any I/O, so
the lock is not on the request path in any real sense.
- Cache keys are a SHA-256 of the token rather than the token itself.
- With no identity of its own, this source cannot verify the connection at
startup; the first request carrying a token does that instead.
Tests cover the configuration surface, the refusal of a missing or malformed
Authorization header, pool reuse for one caller and pool separation between two.
They run without a server, since the per-caller path opens nothing until a query
is executed.
Touches the same validate tag as googleapis#4072, which is the only overlap between them;
happy to rebase whichever lands second.
Description
Adds an optional
azureAuthblock to themssqlsource so it can connect to an Azure SQLserver with SQL authentication disabled, where there are no SQL logins to hand out. Omitting
the block leaves current behaviour untouched.
This follows the design proposed in #3571 and the placement @duwenxin99 suggested there
(the switch in
internal/sources/mssql/mssql.go, unit tests inmssql_test.go).Implementation
azureAuthis set the connection opens through theazureaddriver with the matchingfedauthworkflow instead of the base driver.userandpasswordbecomerequired_without=AzureAuth. This changes one existing test'sexpected message from the
requiredtag torequired_without; that is the only change toexisting behaviour.
user idin theclientID@tenantIDform the driver parses. A service principal's secret travels aspassword.disableInstanceDiscoveryandadditionallyAllowedTenantsare passed through fornetwork-isolated and sovereign-cloud deployments that cannot reach the public authority
discovery endpoint.
empty and drives the credential from query parameters, so the two cannot disagree about the
identity.
azureadis a package of the already-requiredgithub.com/microsoft/go-mssqldb, so there isno version change.
go mod tidyaddsazidentityand MSAL as indirect requirements, whichMSSQL source: support Microsoft Entra ID (managed identity) for the database connection #3571 anticipated.
Verification
Unit tests cover parsing and validation. The connection path was also exercised against a real
Entra-enabled Azure SQL Database, querying
SUSER_SNAME()so the server reports the identity itactually served:
connected_asreported by the serverdefaultmanaged-identity<clientId>@<tenantId>of a user-assigned identityThe
managed-identityresult also confirmsclientIdreaches the driver, since that identitywas selected by client id.
Not exercised live, and the same one-line
fedauthswitch in both cases:service-principal— creating an app registration is blocked by policy in the tenant I tested from.workload-identity— needs an AKS pod with federated credentials.Deliberately out of scope
#3571 also asks for
trustServerCertificate. That is an encryption toggle rather than an authmode, so it is left out to keep this change to the auth switch; happy to follow up separately, or
to fold it in here if you would rather have both together.
PR Checklist
CONTRIBUTING.md
bug/issue
before writing your code! That way we can discuss the change, evaluate
designs, and agree on the general idea
review
!if this involve a breaking change🛠️ Fixes #3571