Skip to content

feat(clients): add AuthStrategy pattern for extensible client auth#1285

Open
AtMrun wants to merge 11 commits intochore/remove-deprecated-v2-modulesfrom
feat/auth-strategy-pattern
Open

feat(clients): add AuthStrategy pattern for extensible client auth#1285
AtMrun wants to merge 11 commits intochore/remove-deprecated-v2-modulesfrom
feat/auth-strategy-pattern

Conversation

@AtMrun
Copy link
Copy Markdown
Collaborator

@AtMrun AtMrun commented Apr 13, 2026

Summary

  • Introduces an AuthStrategy protocol (clients/auth.py) that maps typed Credential instances to client connection parameters (URL params, connect_args, query params, HTTP headers)
  • Implements 8 built-in strategies: BasicAuthStrategy, KeypairAuthStrategy, OAuthAuthStrategy, ApiKeyAuthStrategy, BearerTokenAuthStrategy, IamUserAuthStrategy, IamRoleAuthStrategy, ServicePrincipalAuthStrategy
  • Flattens client hierarchy: ClientInterface ABC → concrete Client base class with AUTH_STRATEGIES (typed as ClassVar) and _resolve_strategy / _build_url helpers
  • Renames for clarity: BaseSQLClientSQLClient, AsyncBaseSQLClientAsyncSQLClient, BaseClientHTTPClient, base.pyhttp.py
  • Adds load_with_credential(credential, **connection_params) on both SQLClient and AsyncSQLClient as the strategy-based alternative to load(dict)
  • Migrates AzureClient to AUTH_STRATEGIES, collapses azure/ package → single azure.py
  • Adds ServicePrincipalCredential to the credential type registry

Hardening (latest commit)

  • Removes mutable class-level credentials / resolved_credentials dicts — assigned only in __init__
  • AUTH_STRATEGIES annotated as ClassVar to prevent accidental runtime mutation
  • All assert isinstance(...) in strategies replaced with raise TypeError(...) so checks survive Python -O

What a multi-auth client looks like

class SnowflakeClient(SQLClient):
    DB_CONFIG = DatabaseConfig(
        template="snowflake://{username}@{account}/{database}",
        required=["username", "account"],
    )
    AUTH_STRATEGIES = {
        BasicCredential: BasicAuthStrategy(),
        CertificateCredential: KeypairAuthStrategy(),
        OAuthClientCredential: OAuthAuthStrategy(
            url_query_params={"authenticator": "oauth"},
        ),
    }

Three auth methods. Zero branching logic.

Test plan

  • Unit tests for individual auth strategies (test_auth_strategies.py)
  • Integration tests for Client base class (test_authenticated_client.py)
  • Integration tests for load_with_credential on both sync and async clients (test_sql_auth_strategies.py)
  • Legacy path tests: load(dict) still works unchanged
  • All 129 client unit tests pass
  • Pre-commit (ruff, ruff-format, pyright, isort) passes

🤖 Generated with Claude Code

Introduces a protocol-based auth strategy system that bridges typed
credentials to client connection parameters, replacing the hardcoded
match statement in BaseSQLClient.get_auth_token().

- AuthStrategy protocol with build_url_params, build_connect_args,
  build_url_query_params, and build_headers methods
- Built-in strategies: Basic, Keypair, OAuth, ApiKey, BearerToken, IAM
- BaseSQLClient.AUTH_STRATEGIES class var + load_with_credential() method
- AsyncBaseSQLClient.load_with_credential() async counterpart
- Full backward compat: empty AUTH_STRATEGIES falls back to legacy path
@snykgituser
Copy link
Copy Markdown

snykgituser commented Apr 13, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

AtMrun added 10 commits April 13, 2026 10:59
Introduces AuthenticatedClient between ClientInterface and the
protocol-specific clients (BaseSQLClient, BaseClient). Shared
auth logic (_resolve_strategy, _build_url, add_url_params) now
lives in one place and is inherited by all client types.

- BaseSQLClient and BaseClient both extend AuthenticatedClient
- AUTH_STRATEGIES, credentials, strategy resolution moved to parent
- BaseSQLClient.add_connection_params kept as backward-compat alias
- BaseClient gains auth strategy support for free (needed by Phase 4)
- load_with_credential() simplified to use shared helpers
get_iam_user_token, get_iam_role_token, get_auth_token, and
get_sqlalchemy_connection_string are now deprecated in favor of
AUTH_STRATEGIES + load_with_credential(). They remain for backward
compat until existing connectors migrate to typed credentials.
Removes get_iam_user_token, get_iam_role_token, get_auth_token, and
get_sqlalchemy_connection_string — all replaced by AUTH_STRATEGIES +
load_with_credential(). The load(dict) path now builds connection
strings inline without the legacy dispatch chain.

Deleted ~170 lines of code. The logic lives in auth strategies
(IamUserAuthStrategy, IamRoleAuthStrategy, BasicAuthStrategy, etc.)
and AuthenticatedClient._build_url().
…entInterface

Replaces the 3-level hierarchy (ClientInterface → AuthenticatedClient
→ BaseSQLClient/BaseClient) with a flat one:

  BaseClient (root)
      ├── BaseSQLClient     — SQLAlchemy
      ├── BaseHTTPClient    — httpx (renamed from BaseClient)
      └── AzureClient       — Azure SDK

- ClientInterface ABC deleted (was just load + close stubs)
- AuthenticatedClient merged into BaseClient in clients/__init__.py
- Current BaseClient renamed to BaseHTTPClient in clients/base.py
- AzureClient now extends BaseClient (gains AUTH_STRATEGIES for free)
- All 93 connectors will extend one of these three
…PClient)

- BaseClient → Client (root, in clients/client.py)
- BaseSQLClient → SQLClient
- AsyncBaseSQLClient → AsyncSQLClient
- BaseHTTPClient → HTTPClient

Backward-compat aliases preserved:
- clients/__init__.py: BaseClient = Client, ClientInterface = Client
- clients/sql.py: BaseSQLClient = SQLClient, AsyncBaseSQLClient = AsyncSQLClient
- clients/base.py: BaseHTTPClient = HTTPClient, BaseClient = Client
File now matches its contents — HTTPClient lives in http.py.

  clients/
      __init__.py   ← re-exports Client + backward-compat aliases
      client.py     ← Client (root)
      http.py       ← HTTPClient (httpx transport)
      sql.py        ← SQLClient (SQLAlchemy transport)
      auth.py       ← AuthStrategy protocol
      auth_strategies/
      azure/        ← AzureClient
…zure/ → azure.py

- AzureClient now uses AUTH_STRATEGIES with ServicePrincipalAuthStrategy
- Deleted AzureAuthProvider (~240 lines) — replaced by 37-line strategy
- Added ServicePrincipalCredential to typed credentials + registry
- Collapsed azure/ folder (3 files) into single azure.py
- load(dict) legacy path parses raw dict into ServicePrincipalCredential
  then delegates to load_with_credential()

Net: -319 lines. All auth now goes through the strategy pattern.
Remove BaseClient, ClientInterface, BaseSQLClient, AsyncBaseSQLClient,
BaseHTTPClient aliases. Clean names only: Client, SQLClient,
AsyncSQLClient, HTTPClient.
…RATEGIES

- SQLClient renamed from BaseSQLClient
- PostgresClient uses AUTH_STRATEGIES with BasicAuthStrategy
- Shows the pattern connector authors will follow
…ar, assert→TypeError

- Remove mutable class-level `credentials` dict from Client; assign only in __init__
- Remove stale `resolved_credentials` class-level dict from SQLClient
- Fix mutable default arg `credentials={}` → `credentials=None` in SQLClient.__init__
- Annotate AUTH_STRATEGIES as ClassVar to signal it must not be mutated at runtime
- Replace all `assert isinstance(...)` in auth strategies with proper
  `if not isinstance(...): raise TypeError(...)` so type checks survive `-O`
@Aryamanz29
Copy link
Copy Markdown
Member

v3 relevance check: ✅ Active feature — AuthStrategy pattern. Duplicate #1284 was closed. @AtMrun is this still the active branch?

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.

3 participants