Skip to content

feat(mssql): support Entra ID access token authentication (mssql-tds) - #993

Open
Vahid-b wants to merge 4 commits into
sfu-db:mainfrom
Vahid-b:feat/mssql-entra-access-token
Open

Vahid-b wants to merge 4 commits into
sfu-db:mainfrom
Vahid-b:feat/mssql-entra-access-token

Conversation

@Vahid-b

@Vahid-b Vahid-b commented Oct 2, 2026 •

Copy link
Copy Markdown

Summary

Adds Microsoft Entra ID (Azure AD) access-token authentication for SQL Server through a typed, programmatic options object, as agreed in #569 and refined in review: the token never goes in the connection string, and only the mssql-tds driver supports it.

import connectorx as cx
from azure.identity import DefaultAzureCredential

token = DefaultAzureCredential().get_token("https://database.windows.net/.default").token
options = cx.MsSqlOptions(access_token=token)
conn = "mssql://myserver.database.windows.net:1433/mydb?encrypt=true"  # no username/password
df = cx.read_sql(conn, "SELECT * FROM t", source_options=options)

Problem

ConnectorX could only authenticate to SQL Server with SQL logins or integrated auth, so Azure SQL databases that require Entra ID could not be read. Users fell back to pyodbc with SQL_COPT_SS_ACCESS_TOKEN.

Fix

  • Database-neutral extension point (Rust): connectorx::source_options::SourceOptions, a #[non_exhaustive] enum: Default | MsSql(MsSqlOptions).
    • It travels through additive entry points: get_arrow_with_options, new_record_batch_iter_with_options, partition_with_options, get_col_range_with_options and MsSQLSource::new_with_options. The existing functions delegate with SourceOptions::default().
    • The shared layer only routes options. check_source_type rejects options that don't match the connection's database.
  • MSSQL backend owns its options: MsSqlOptions has private fields, a with_access_token builder and a redacted Debug.
    • validate() rejects empty tokens.
    • In mssql-tds, validate_access_token_auth rejects a token combined with URL credentials or trusted_connection=true; only then does it set TdsAuthenticationMethod::AccessToken.
    • The options reach every connection the read opens: pooled reads, streaming, metadata (get_meta) and partition-range discovery.
  • Tiberius: not extended. A token fails with Entra ID access token support is not implemented for Tiberius, on the partition-range path too, instead of silently using URL credentials.
  • Python:
    • cx.MsSqlOptions(access_token=...) is a frozen pyclass with a keyword-only argument; its repr redacts the token and an empty token raises ValueError.
    • It is passed as source_options= to read_sql (all return types), read_sql_pandas, get_meta and partition_sql.
    • Any other value raises TypeError; federated queries reject source_options.
  • Token refresh is the caller's job: the token is only used to open connections during one call (documented).

Compatibility

  • No changes to existing APIs: SourceConn (same layout as main), get_arrow, new_record_batch_iter, partition, get_col_range and MsSQLSource::new keep their signatures and behaviour.
  • Python additions are keyword-only source_options=None arguments and a new MsSqlOptions class.
  • No new dependencies or feature flags, and no Cargo.toml or lockfile changes.

Scope

  • Not included: built-in credential flows (ActiveDirectoryDefault, managed identity, service principal, …) via mssql-tds's EntraIdTokenFactory. They could become additional MsSqlOptions fields later.
  • Standalone partitions: the public standalone tds_impl::MsSQLSourcePartition::new does not take options. Partitions created by MsSQLSource share the source's authenticated pool.
  • Pre-existing, not changed: with mssql-tds, any login failure (a wrong password, or an expired/invalid token) is retried by the bb8 pool until its 30s timeout. It then surfaces as Timed out waiting for an MsSQL connection, hiding the server's Login failed message. Worth a separate issue.
  • No live Entra ID test in CI, since that needs a real Azure tenant. See Validation for the local end-to-end probe.

Validation

Run in WSL Ubuntu 24.04, which mirrors CI.

  • cargo test --lib --features <combo>,dst_arrow,src_sqlite -- options access_token source_router configuration_tests partition:

    • src_mssql_tds: 35 passed
    • src_mssql_tiberius: 24 passed
    • src_mssql_tiberius,src_mssql_tds: 36 passed
  • just test-python -k "token or options": 18 passed. These cover:

    • pandas/arrow/arrow_stream/polars, the partition-range path, get_meta and partition_sql;
    • Tiberius rejection with and without partition_on, and the conflict checks;
    • empty tokens, repr redaction, keyword-only construction, TypeError for untyped options, non-MSSQL sources and federated queries.

    No database is needed: each case fails before any network I/O.

  • End-to-end probe against the SQL Server 2025 testcontainer, with a random token and no URL credentials: the server performs FedAuth and replies Login failed for user '', with no fallback to SQL login.

  • Full local suites:

    • cx-check static (fmt, cargo clippy --features all, just test-feature-gate, cargo doc): OK. Clippy stays at the 146-warning baseline.
      • The four new *_with_options functions carry #[allow(clippy::result_large_err)], the same lint the existing entry points trigger because of ConnectorXOutError's size.
    • cx-check rust (just test-ci): OK, including the MSSQL suites for both drivers.
    • cx-check python (mssql-tds, then tiberius, then the runtime switch): OK, 249 passed / 29 skipped / 3 xfailed.
  • Pre-existing failures: partition::tests fails without src_sqlite on main as well.

Docs: docs/databases/mssql.md (section "Microsoft Entra ID access token") and docs/api.md.

Closes #569

This PR was prepared with AI assistance (GitHub Copilot) and reviewed/validated locally.

@Vahid-b Vahid-b left a comment •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Summary and verdict: One blocking compatibility issue remains: the change threads access-token auth correctly, but it makes a public Rust struct harder to construct from downstream code. No other correctness issue stood out in the reviewed paths.

Verification: reviewed head 7b522cbba3c9540ff9c63af0a3ef8d3573dff8ed, merge-base 5e688e0cbda61fbdf3597735533e81a72b7d0b90, with gh pr view 993 --repo sfu-db/connector-x --json ..., gh api repos/sfu-db/connector-x/pulls/993/reviews, gh pr checks 993 --repo sfu-db/connector-x (no checks reported), git diff --check $(git merge-base origin/main HEAD)..HEAD (clean), and started ~/.local/bin/cx-check static / ~/.local/bin/cx-check python (both still running when the report was finalized, then stopped to end the run). Not verified: completed cx-check static and cx-check python output, and live Entra ID auth against SQL Server.

Findings

Blocking

  • [R2/Blocking] connectorx/src/source_router.rs:25-32 — SourceConn — Cited. Adding the private access_token field changes the layout of a public struct, so downstream Rust code that still constructs SourceConn { ty, conn, proto } will stop compiling. Keep the public struct layout stable in this release, or move auth state behind a separate wrapper / non-public type that does not alter SourceConn.

Suggestion

  • None.

Nit

  • None.

Severity count

Round Blocking Suggestion Nit Total
R1 0 0 0 0
R2 1 0 0 1
Total 1 0 0 1

@Vahid-b

Vahid-b commented Oct 2, 2026

Copy link
Copy Markdown
Author

Re: the SourceConn struct-layout finding. I checked it and I'm keeping the field; it's now a documented compatibility note rather than a blocker.

What's true: adding any field (private or public) to SourceConn breaks downstream code that builds it with a struct literal, SourceConn { ty, conn, proto }.

Why it isn't blocking:

  • Constructors unchanged: SourceConn::new, SourceConn::try_from and parse_source work exactly as before, and every in-repo caller (Python, C++, tests) uses them.
  • No known downstream literal use: a GitHub code search for connectorx::source_router / SourceConn { outside this repo found no external crate building it with a literal. Downstream users such as RyanGreenup/database_exporter and antivirak/redshift-iam-rs call SourceConn::try_from and then get_arrow.
  • The token has to travel with the connection: get_arrow, new_record_batch_iter, partition and get_col_range all open MSSQL connections from a &SourceConn. Leaving the struct unchanged would mean parallel public variants of each of those functions with a token parameter, which adds more API surface than it protects. The field is private so it can only be set through the validating set_access_token (MSSQL-only, non-empty), and it is redacted from Debug.
  • 0.x release: the crate is still 0.x.

The PR description's Compatibility section lists this. Happy to change approach if maintainers would rather not touch the struct.

@Vahid-b
Vahid-b marked this pull request as ready for review October 2, 2026 22:46
@Vahid-b

Vahid-b commented Oct 2, 2026

Copy link
Copy Markdown
Author

@saurabh500 this is ready for review. It follows your guidance on #569: programmatic \�ccess_token\ only (no JWT in the connection string), and mssql-tds only (Tiberius rejects tokens with a clear error). Could you also approve the CI workflow run for this fork PR? Thanks!

Comment thread connectorx/src/sources/mssql/tiberius_impl.rs Outdated
Comment thread connectorx-python/connectorx/tests/test_mssql.py Outdated
Comment thread connectorx/src/sources/mssql/tds_impl.rs
Comment thread connectorx/src/source_router.rs Outdated
@saurabh500

Copy link
Copy Markdown
Collaborator

@Vahid-b thanks for the PR. I have recommended some API changes after reading the code.
There are parts which should be kept database neutral, and access_token is a sql server specific option.
Let me know what you think.

Vahid-b added a commit to Vahid-b/connector-x that referenced this pull request Oct 3, 2026
…ions

Address review feedback on sfu-db#993:

- Keep `SourceConn` and the existing entry points unchanged. Backend-specific
  configuration now travels through a typed `SourceOptions` enum
  (`Default` | `MsSql(MsSqlOptions)`) and additive entry points:
  `get_arrow_with_options`, `new_record_batch_iter_with_options`,
  `partition_with_options`, `get_col_range_with_options` and
  `MsSQLSource::new_with_options`. The shared layer only routes options and
  rejects ones that don't match the source type.
- `MsSqlOptions` lives in the MSSQL backend, which owns token validation:
  non-empty token, conflicting URL credentials / trusted_connection
  (extracted into `validate_access_token_auth`), and driver support.
- Python: `cx.MsSqlOptions(access_token=...)` passed as `source_options=` to
  `read_sql`, `read_sql_pandas`, `get_meta` and `partition_sql`; its repr
  redacts the token, and other values raise TypeError.
- Tiberius error now reads "Entra ID access token support is not
  implemented for Tiberius".
- Tests use randomly generated, non-JWT-shaped placeholder tokens.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Vahid-b
Vahid-b force-pushed the feat/mssql-entra-access-token branch from e4f3921 to 81f86cb Compare October 8, 2026 21:26
Vahid-b added a commit to Vahid-b/connector-x that referenced this pull request Oct 8, 2026
…ions

Address review feedback on sfu-db#993:

- Keep `SourceConn` and the existing entry points unchanged. Backend-specific
  configuration now travels through a typed `SourceOptions` enum
  (`Default` | `MsSql(MsSqlOptions)`) and additive entry points:
  `get_arrow_with_options`, `new_record_batch_iter_with_options`,
  `partition_with_options`, `get_col_range_with_options` and
  `MsSQLSource::new_with_options`. The shared layer only routes options and
  rejects ones that don't match the source type.
- `MsSqlOptions` lives in the MSSQL backend, which owns token validation:
  non-empty token, conflicting URL credentials / trusted_connection
  (extracted into `validate_access_token_auth`), and driver support.
- Python: `cx.MsSqlOptions(access_token=...)` passed as `source_options=` to
  `read_sql`, `read_sql_pandas`, `get_meta` and `partition_sql`; its repr
  redacts the token, and other values raise TypeError.
- Tiberius error now reads "Entra ID access token support is not
  implemented for Tiberius".
- Tests use randomly generated, non-JWT-shaped placeholder tokens.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Vahid-b

Vahid-b commented Oct 8, 2026

Copy link
Copy Markdown
Author

@saurabh500 your review comments are addressed and all four threads are resolved. Could you take another look?

  • SourceConn and the existing entry points are unchanged. The token now travels as typed options: cx.read_sql(conn, query, source_options=cx.MsSqlOptions(access_token=token)) in Python, and SourceOptions::MsSql(MsSqlOptions) through additive *_with_options functions in Rust. MSSQL-specific validation lives in the MSSQL backend.
  • The branch is rebased onto current main (including docs: enable missing_docs and document the source/destination traits (#976) #986's missing_docs). It adds no new warnings, and the full local Rust and Python suites pass for both drivers.
  • CI hasn't run yet: GitHub is holding the workflow for this fork PR (action_required). Could you approve it?

(Also correcting my earlier comment, where formatting got garbled: the original argument was access_token. It has since been replaced by source_options as described above.)

Vahid-b and others added 4 commits October 9, 2026 07:11
Add a programmatic `access_token` argument to `read_sql`, `read_sql_pandas`,
`get_meta` and `partition_sql` that authenticates SQL Server connections with
a Microsoft Entra ID (Azure AD) access token. The token is never read from the
connection string.

- SourceConn carries the token in a private field (redacted from Debug) and
  only accepts it for mssql:// sources.
- mssql-tds: uses TdsAuthenticationMethod::AccessToken for the main, metadata
  and partition-range connections; combining a token with URL credentials or
  trusted_connection=true is an error.
- Tiberius: rejects tokens with a clear error (no new Tiberius support).
- Rust: MsSQLSource::new_with_access_token; MsSQLSource::new is unchanged.

Closes sfu-db#569

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ions

Address review feedback on sfu-db#993:

- Keep `SourceConn` and the existing entry points unchanged. Backend-specific
  configuration now travels through a typed `SourceOptions` enum
  (`Default` | `MsSql(MsSqlOptions)`) and additive entry points:
  `get_arrow_with_options`, `new_record_batch_iter_with_options`,
  `partition_with_options`, `get_col_range_with_options` and
  `MsSQLSource::new_with_options`. The shared layer only routes options and
  rejects ones that don't match the source type.
- `MsSqlOptions` lives in the MSSQL backend, which owns token validation:
  non-empty token, conflicting URL credentials / trusted_connection
  (extracted into `validate_access_token_auth`), and driver support.
- Python: `cx.MsSqlOptions(access_token=...)` passed as `source_options=` to
  `read_sql`, `read_sql_pandas`, `get_meta` and `partition_sql`; its repr
  redacts the token, and other values raise TypeError.
- Tiberius error now reads "Entra ID access token support is not
  implemented for Tiberius".
- Tests use randomly generated, non-JWT-shaped placeholder tokens.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keeps the PR at zero new missing_docs warnings after rebasing onto sfu-db#986.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…tions arg

After rebasing onto sfu-db#992, its new unit tests call build_client_context
with the old one-argument signature.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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 for MS SQL Server Token Authentication.

2 participants