Skip to content

Commit 8869e80

Browse files
committed
Merged PR 7167: Merge from development to main
Related work items: #7086, #40377, #40829, #40924, #41302, #41412, #41497, #41728, #41762, #41830, #41851, #42218, #42220, #42221, #42278, #42830, #43124, #43143, #43144, #43259, #43282
2 parents 825bfee + 57619a2 commit 8869e80

44 files changed

Lines changed: 767 additions & 458 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gdn/.gdnsuppress

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
{
2-
"hydrated": false,
2+
"hydrated": true,
33
"properties": {
44
"helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/suppressions"
55
},
66
"version": "1.0.0",
77
"suppressionSets": {
88
"default": {
99
"name": "default",
10-
"createdDate": "2026-02-19 21:04:38Z",
11-
"lastUpdatedDate": "2026-02-19 21:04:38Z"
10+
"createdDate": "2026-02-26 00:51:41Z",
11+
"lastUpdatedDate": "2026-02-26 00:51:41Z"
1212
}
1313
},
1414
"results": {
1515
"bd1ce7227025d22a25d3d98df0aef3b236e0aa4987f80c43874a2afc3e3713a0": {
1616
"signature": "bd1ce7227025d22a25d3d98df0aef3b236e0aa4987f80c43874a2afc3e3713a0",
1717
"alternativeSignatures": [],
18+
"target": "mssql-py-core/tests/mssql_python/test_bulkcopy_access_token.py",
19+
"line": 443,
20+
"uriBaseId": "file:///D:/a/_work/1/s/",
1821
"memberOf": [
1922
"default"
2023
],
21-
"createdDate": "2026-02-19 21:04:38Z"
24+
"tool": "credscan",
25+
"ruleId": "CSCAN-GENERAL0030",
26+
"createdDate": "2026-02-26 00:51:41Z"
2227
}
2328
}
2429
}

.github/prompts/plan-documentMssqlTdsPublicApi.prompt.md

Lines changed: 0 additions & 342 deletions
This file was deleted.

.pipeline/benchmark-pipeline.yml

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Performance benchmark pipeline
2+
#
3+
# Compares Criterion benchmark results between the PR branch and
4+
# the target branch to detect performance regressions.
5+
# Runs on Linux x64 only.
6+
7+
trigger: none
8+
9+
pr:
10+
branches:
11+
include:
12+
- main
13+
- development
14+
15+
parameters:
16+
- name: baselineBranch
17+
displayName: 'Baseline branch (for manual runs; PR builds auto-detect target)'
18+
type: string
19+
default: main
20+
21+
stages:
22+
- stage: Benchmark
23+
displayName: Performance Benchmark
24+
jobs:
25+
- job: Benchmark
26+
displayName: 'Criterion Benchmark'
27+
timeoutInMinutes: 30
28+
pool:
29+
name: RUST-1ES-POOL-WUS3
30+
demands:
31+
- imageOverride -equals RUST-1ES-UBUSLIM
32+
variables:
33+
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
34+
35+
steps:
36+
- checkout: self
37+
fetchDepth: 0
38+
persistCredentials: true
39+
40+
- template: templates/install-dependencies.yml
41+
parameters:
42+
osType: Linux
43+
enableJsDeps: false
44+
45+
- template: templates/cargo-authenticate-template.yml
46+
parameters:
47+
osType: Linux
48+
49+
- template: templates/sql-setup-template.yml
50+
parameters:
51+
buildTarget: Linux
52+
53+
- script: |
54+
set -euo pipefail
55+
CONTAINER_ID=$(docker ps -q --filter ancestor=mcr.microsoft.com/mssql/server:2025-latest)
56+
if [ -z "$CONTAINER_ID" ]; then
57+
echo "##vso[task.logissue type=error]No SQL Server container found"
58+
exit 1
59+
fi
60+
if [ "$(echo "$CONTAINER_ID" | wc -l)" -ne 1 ]; then
61+
echo "##vso[task.logissue type=error]Multiple SQL Server containers found"
62+
exit 1
63+
fi
64+
echo "SQL Server container: $CONTAINER_ID"
65+
for i in $(seq 1 30); do
66+
if docker exec "$CONTAINER_ID" /opt/mssql-tools18/bin/sqlcmd \
67+
-S localhost -U sa -P "$SQL_PASSWORD" -C -Q "SELECT 1" > /dev/null 2>&1; then
68+
echo "SQL Server ready after $((i * 2))s"
69+
exit 0
70+
fi
71+
echo "Waiting for SQL Server... ($i/30)"
72+
sleep 2
73+
done
74+
echo "##vso[task.logissue type=error]SQL Server did not become ready in time"
75+
exit 1
76+
displayName: Wait for SQL Server
77+
env:
78+
SQL_PASSWORD: $(SQL_PASSWORD)
79+
80+
- script: cargo install critcmp --version 0.1.8 --locked
81+
displayName: Install critcmp
82+
83+
- script: cargo fetch
84+
displayName: Fetch crates
85+
86+
- script: |
87+
set -euo pipefail
88+
PR_SHA=$(git rev-parse HEAD)
89+
echo "PR commit: $PR_SHA"
90+
echo "##vso[task.setvariable variable=PR_SHA]$PR_SHA"
91+
92+
if [ "$(Build.Reason)" = "PullRequest" ]; then
93+
RAW="$(System.PullRequest.TargetBranch)"
94+
BASELINE="${RAW#refs/heads/}"
95+
else
96+
BASELINE="${{ parameters.baselineBranch }}"
97+
fi
98+
echo "Baseline branch: $BASELINE"
99+
echo "##vso[task.setvariable variable=BASELINE_BRANCH]$BASELINE"
100+
displayName: Resolve branches
101+
102+
- script: |
103+
set -euo pipefail
104+
echo "=== Checking out baseline: $(BASELINE_BRANCH) ==="
105+
git fetch origin "$(BASELINE_BRANCH)"
106+
git checkout FETCH_HEAD
107+
108+
echo "=== Running baseline benchmarks ==="
109+
cargo bench --package mssql-tds --bench perf -- --save-baseline base
110+
displayName: 'Baseline benchmarks ($(BASELINE_BRANCH))'
111+
env:
112+
SQL_PASSWORD: $(SQL_PASSWORD)
113+
DB_HOST: localhost
114+
DB_PORT: '1433'
115+
DB_USERNAME: sa
116+
TRUST_SERVER_CERTIFICATE: 'true'
117+
CERT_HOST_NAME: sql1
118+
119+
- script: |
120+
set -euo pipefail
121+
echo "=== Checking out PR commit: $(PR_SHA) ==="
122+
git checkout "$(PR_SHA)"
123+
124+
echo "=== Running PR benchmarks ==="
125+
cargo bench --package mssql-tds --bench perf -- --save-baseline pr
126+
displayName: 'PR benchmarks'
127+
env:
128+
SQL_PASSWORD: $(SQL_PASSWORD)
129+
DB_HOST: localhost
130+
DB_PORT: '1433'
131+
DB_USERNAME: sa
132+
TRUST_SERVER_CERTIFICATE: 'true'
133+
CERT_HOST_NAME: sql1
134+
135+
- script: |
136+
set -euo pipefail
137+
echo ""
138+
echo "============================================"
139+
echo " Benchmark Comparison: base → pr"
140+
echo "============================================"
141+
echo ""
142+
mkdir -p "$(Build.ArtifactStagingDirectory)/benchmarks"
143+
critcmp base pr | tee "$(Build.ArtifactStagingDirectory)/benchmarks/comparison.txt"
144+
displayName: Compare results
145+
146+
- task: PublishPipelineArtifact@1
147+
inputs:
148+
targetPath: '$(Build.ArtifactStagingDirectory)/benchmarks'
149+
artifactName: benchmark-results
150+
displayName: Publish benchmark results
151+
condition: always()

mssql-py-core/src/connection.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,25 @@ impl PyCoreConnection {
428428
context.ipaddress_preference = ipaddress_preference;
429429
context.keep_alive_in_ms = keep_alive_in_ms;
430430
context.keep_alive_interval_in_ms = keep_alive_interval_in_ms;
431+
// Block authentication methods not yet supported through py-core.
432+
// ActiveDirectoryIntegrated requires Kerberos ticket forwarding via
433+
// Entra ID which has no token-provider wired up yet.
434+
// ActiveDirectoryPassword requires an Entra ID token provider callback
435+
// that py-core does not register.
436+
match &transformed.method {
437+
mssql_tds::connection::client_context::TdsAuthenticationMethod::ActiveDirectoryIntegrated => {
438+
return Err(PyRuntimeError::new_err(
439+
"Authentication method 'ActiveDirectoryIntegrated' is not currently supported by mssql-py-core."
440+
));
441+
}
442+
mssql_tds::connection::client_context::TdsAuthenticationMethod::ActiveDirectoryPassword => {
443+
return Err(PyRuntimeError::new_err(
444+
"Authentication method 'ActiveDirectoryPassword' is not currently supported by mssql-py-core."
445+
));
446+
}
447+
_ => {}
448+
}
449+
431450
context.tds_authentication_method = transformed.method;
432451
context.access_token = transformed.access_token;
433452

mssql-py-core/tests/test_auth_resolution.py

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -201,26 +201,17 @@ def test_07_sqlpassword_uid_pwd(self):
201201
ctx["authentication"] = "SqlPassword"
202202
_connect_ok(ctx)
203203

204-
# #8 — ADPassword + UID + PWD → AAD Password
205-
# (will fail against local SQL Server — that's fine, validates pipeline)
204+
# #8 — ADPassword + UID + PWD → blocked (not supported by mssql-py-core)
206205
def test_08_ad_password_uid_pwd(self):
207206
ctx = _base_ctx()
208207
ctx["authentication"] = "ActiveDirectoryPassword"
209-
try:
210-
_connect_ok(ctx)
211-
except RuntimeError as e:
212-
assert "Unsupported Authentication" not in str(e)
213-
assert "Both User and Password" not in str(e)
208+
_expect_validation_error(ctx, "not currently supported by mssql-py-core")
214209

215-
# #9 — ADIntegrated, no creds
210+
# #9 — ADIntegrated, no creds → blocked (not supported by mssql-py-core)
216211
def test_09_ad_integrated_alone(self):
217212
ctx = _bare_ctx()
218213
ctx["authentication"] = "ActiveDirectoryIntegrated"
219-
try:
220-
_connect_ok(ctx)
221-
except RuntimeError as e:
222-
assert "Unsupported Authentication" not in str(e)
223-
assert "User or Password" not in str(e)
214+
_expect_validation_error(ctx, "not currently supported by mssql-py-core")
224215

225216
# #10 — ADInteractive + UID (as hint)
226217
def test_10_ad_interactive_with_hint(self):
@@ -354,39 +345,27 @@ def test_20_admsi_system_clears_pwd(self):
354345
except RuntimeError as e:
355346
assert "Both User and Password" not in str(e)
356347

357-
# #21 — ADIntegrated + UID → UID silently cleared (ODBC dialog behavior)
348+
# #21 — ADIntegrated + UID → blocked (not supported by mssql-py-core)
358349
def test_21_ad_integrated_clears_uid(self):
359350
ctx = _bare_ctx()
360351
ctx["authentication"] = "ActiveDirectoryIntegrated"
361352
ctx["user_name"] = "user@domain.com"
362-
try:
363-
_connect_ok(ctx)
364-
except RuntimeError as e:
365-
assert "ActiveDirectoryIntegrated" not in str(e)
366-
assert "User or Password" not in str(e)
353+
_expect_validation_error(ctx, "not currently supported by mssql-py-core")
367354

368-
# #22 — ADIntegrated + PWD → PWD silently cleared
355+
# #22 — ADIntegrated + PWD → blocked (not supported by mssql-py-core)
369356
def test_22_ad_integrated_clears_pwd(self):
370357
ctx = _bare_ctx()
371358
ctx["authentication"] = "ActiveDirectoryIntegrated"
372359
ctx["password"] = "secret"
373-
try:
374-
_connect_ok(ctx)
375-
except RuntimeError as e:
376-
assert "ActiveDirectoryIntegrated" not in str(e)
377-
assert "User or Password" not in str(e)
360+
_expect_validation_error(ctx, "not currently supported by mssql-py-core")
378361

379-
# #23 — ADIntegrated + UID + PWD → both silently cleared
362+
# #23 — ADIntegrated + UID + PWD → blocked (not supported by mssql-py-core)
380363
def test_23_ad_integrated_clears_both(self):
381364
ctx = _bare_ctx()
382365
ctx["authentication"] = "ActiveDirectoryIntegrated"
383366
ctx["user_name"] = "user@domain.com"
384367
ctx["password"] = "secret"
385-
try:
386-
_connect_ok(ctx)
387-
except RuntimeError as e:
388-
assert "ActiveDirectoryIntegrated" not in str(e)
389-
assert "User or Password" not in str(e)
368+
_expect_validation_error(ctx, "not currently supported by mssql-py-core")
390369

391370

392371
# ═══════════════════════════════════════════════════════════════════════════
@@ -521,7 +500,13 @@ def test_38_adspa_pwd_only(self):
521500
# ═══════════════════════════════════════════════════════════════════════════
522501

523502
class TestAccessTokenClashes:
524-
"""ODBC conflict matrix §3.5: rows #39–#43."""
503+
"""ODBC conflict matrix §3.5: rows #39–#43.
504+
505+
validate_auth enforces strict ODBC-parity: access_token must be
506+
the sole credential — any auth keyword, UID, or PWD is a conflict.
507+
The Python layer (cursor.py) is responsible for stripping the
508+
authentication keyword after acquiring a token, before calling py-core.
509+
"""
525510

526511
# #39 — Access Token + TC=Yes
527512
def test_39_token_tc(self):
@@ -671,10 +656,7 @@ def test_case_insensitive_sqlpassword(self):
671656
def test_case_insensitive_ad_password(self):
672657
ctx = _base_ctx()
673658
ctx["authentication"] = "activedirectorypassword"
674-
try:
675-
_connect_ok(ctx)
676-
except RuntimeError as e:
677-
assert "Unsupported Authentication" not in str(e)
659+
_expect_validation_error(ctx, "not currently supported by mssql-py-core")
678660

679661
# Bogus auth keyword
680662
def test_bogus_auth_keyword_rejected(self):
@@ -754,7 +736,11 @@ def test_ad_managed_identity_tc(self):
754736
# ═══════════════════════════════════════════════════════════════════════════
755737

756738
class TestAccessTokenClashesExtended:
757-
"""Access Token conflicts with each AD auth keyword."""
739+
"""Access Token conflicts with each AD auth keyword.
740+
741+
validate_auth rejects access_token combined with any other credential.
742+
Python's cursor.py must strip stale fields before calling py-core.
743+
"""
758744

759745
def test_token_ad_password(self):
760746
ctx = _bare_ctx()
@@ -790,3 +776,49 @@ def test_token_uid_pwd_combined(self):
790776
ctx = _base_ctx()
791777
ctx["access_token"] = "fake-jwt"
792778
_expect_validation_error(ctx, "Access Token")
779+
780+
781+
# ═══════════════════════════════════════════════════════════════════════════
782+
# Bulkcopy Entra ID: validator enforces clean token-only input
783+
# ═══════════════════════════════════════════════════════════════════════════
784+
785+
class TestBulkcopyTokenValidation:
786+
"""Documents the contract between cursor.py and py-core for bulkcopy auth.
787+
788+
When Python acquires a token via azure-identity, it MUST strip the
789+
authentication keyword, user_name, and password before passing the dict
790+
to PyCoreConnection. validate_auth enforces this — access_token must
791+
arrive alone (except server/TLS params).
792+
793+
These tests verify the validator correctly rejects stale fields,
794+
ensuring the Python layer cleans up properly.
795+
"""
796+
797+
def test_token_plus_ad_default_rejected(self):
798+
"""cursor.py must strip authentication before calling py-core."""
799+
ctx = _bare_ctx()
800+
ctx["authentication"] = "ActiveDirectoryDefault"
801+
ctx["access_token"] = "fake-jwt-default"
802+
_expect_validation_error(ctx, "Access Token")
803+
804+
def test_token_plus_login_hint_rejected(self):
805+
"""cursor.py must strip user_name (login hint) when token is set."""
806+
ctx = _bare_ctx()
807+
ctx["access_token"] = "fake-jwt-interactive"
808+
ctx["user_name"] = "user@domain.com"
809+
_expect_validation_error(ctx, "Access Token")
810+
811+
def test_token_plus_auth_plus_uid_pwd_rejected(self):
812+
"""All stale fields must be stripped — validator catches any leak."""
813+
ctx = _bare_ctx()
814+
ctx["authentication"] = "ActiveDirectoryPassword"
815+
ctx["user_name"] = "user@domain.com"
816+
ctx["password"] = "old-password"
817+
ctx["access_token"] = "fake-jwt"
818+
_expect_validation_error(ctx, "Access Token")
819+
820+
def test_token_alone_accepted(self):
821+
"""Clean input: only access_token — passes validation."""
822+
ctx = _bare_ctx()
823+
ctx["access_token"] = "fake-jwt"
824+
_expect_no_validation_error(ctx)

0 commit comments

Comments
 (0)