Skip to content

feat(azure): anonymous access to public Azure DevOps projects#165

Merged
MementoRC merged 2 commits into
developmentfrom
feat/azure-public-anonymous-access
May 8, 2026
Merged

feat(azure): anonymous access to public Azure DevOps projects#165
MementoRC merged 2 commits into
developmentfrom
feat/azure-public-anonymous-access

Conversation

@MementoRC
Copy link
Copy Markdown
Owner

Summary

Makes the three remaining read-only Azure DevOps tools work without a PAT against public projects (e.g. conda-forge/feedstock-builds), and removes azure_list_builds which required write-level credentials that are not available in this context.


What was removed and why

azure_list_builds has been removed entirely (function, Pydantic model, registry entry, tests, README entry).

  • It required an AZURE_DEVOPS_TOKEN with queue/trigger permissions — a PAT scope that anonymous callers cannot obtain for public projects.
  • It was already dead code: no caller in the codebase used it, no integration test covered it.
  • README tool count updated: 51 tools / 4 Azure → 50 tools / 3 Azure.

What was patched and why

The remaining three tools (azure_get_build_status, azure_get_build_logs, azure_get_failing_jobs) were returning 401 Unauthorized even against fully public projects.

Root cause

Azure DevOps rejects malformed BasicAuth credentials even on endpoints that support anonymous access. The old code constructed aiohttp.BasicAuth('', token) — when token was an empty string or unset env var, this sent a header like Authorization: Basic Og== (base64 of :), which Azure treats as a bad credential and rejects with 401 instead of falling through to anonymous handling.

Fix

client.py

  • token is now Optional[str]; defaults to None when AZURE_DEVOPS_TOKEN is unset or empty.
  • New _auth() helper returns aiohttp.BasicAuth only when a non-empty token is provided; otherwise returns None.
  • GET requests pass auth=self._auth() — when None, the auth kwarg is omitted from the request (not passed as None, which is not guaranteed to suppress the header in all aiohttp versions).
  • POST/PATCH still raise ValueError on missing token (write ops always require auth).
  • AZURE_DEVOPS_ORG now defaults to "conda-forge" so the tools work out-of-the-box for the primary use case.

api.py

  • azure_client_context() ValueError now only fires when the client cannot be constructed at all (org missing), not on missing token.

Empirical evidence for anonymous endpoint access

Curl tests against conda-forge/feedstock-builds, build 1517588, with no Authorization header:

Endpoint HTTP status
Build metadata (/_apis/build/builds/{id}) 200
Timeline (/_apis/build/builds/{id}/timeline) 200
Log list (/_apis/build/builds/{id}/logs) 200
Raw log (/_apis/build/builds/{id}/logs/{logId}) 200

Sending Authorization: Basic Og== (empty token) on the same endpoints → 401.


New tests

TestAzureClientAnonymousMode (7 test cases):

  • Anonymous GET succeeds when AZURE_DEVOPS_TOKEN is unset
  • Empty-string token is coerced to None (no auth header)
  • POST/PATCH raise ValueError without a token
  • Default org is conda-forge
  • Auth header is present and correct when a valid token is supplied

Verification status

  • Lint (ruff check): green
  • Format (ruff format): green
  • Type check (mypy): green
  • Pytest: deferred to CIpixi/pixi-bash MCP unavailable in this session; all tests are expected to pass based on the implementation.

Reviewer checklist

  • client._auth() correctly returns None (not aiohttp.BasicAuth('', '')) when token is absent/empty
  • No auth=None is passed to aiohttp requests in the anonymous path (kwarg omitted entirely)
  • POST/PATCH still raise ValueError without a token
  • azure_list_builds is fully removed: function, model, __init__.py export, registry_azure.py registration, tests, README row
  • Default org conda-forge is sensible and documented
  • CI passes (pytest, lint, typecheck)

Drops `azure_list_builds` (required a PAT we cannot obtain) and makes
the remaining three read-only tools — azure_get_build_status,
azure_get_build_logs, azure_get_failing_jobs — work anonymously
against public Azure DevOps projects such as conda-forge/feedstock-builds.

Empirical curl tests against build 1517588 confirmed all four endpoints
used by these tools (build metadata, timeline, log list, raw log) return
200 with no Authorization header for public projects. Sending a stale or
empty BasicAuth was the source of prior 401s — Azure rejects malformed
credentials even on otherwise-anonymous endpoints.

Changes:
- client.py: token now Optional[str]; _auth() helper returns None when
  token is unset/empty so the auth kwarg is omitted from the request
  entirely (passing auth=None is not guaranteed to suppress the header
  in all aiohttp versions). POST/PATCH still raise ValueError on
  missing token. AZURE_DEVOPS_ORG defaults to "conda-forge".
- api.py: azure_client_context() ValueError now only fires when the
  client cannot be constructed at all (org issue), not on missing token.
- Removed azure_list_builds entirely (function, model, registration,
  tests, README entry). README tool count: 51/4 -> 50/3.
- Added TestAzureClientAnonymousMode (7 tests) covering anonymous
  request paths, write-op guards, default org, empty-token coercion.

Lint/format/typecheck: green.
Pytest: deferred to CI (pixi-bash MCP unavailable in this session).
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Claude finished @MementoRC's task —— View job


PR Code Review

I've completed a thorough review of this PR. Here's my analysis:

Review Checklist:

  • Analyze authentication changes in client.py
  • Review api.py modifications
  • Examine azure_list_builds removal
  • Check test coverage and quality
  • Evaluate security implications
  • Review code quality and best practices
  • Provide comprehensive feedback

Overall Assessment: LGTM with Minor Observations

This is a well-executed PR that solves a real problem with anonymous access to public Azure DevOps projects. The implementation is clean, well-tested, and follows good practices.

Code Quality & Best Practices

Excellent:

  • Clean separation of authentication logic in client.py:_auth() method
  • Proper error handling and user-friendly error messages
  • Comprehensive docstrings and type hints throughout
  • Good use of context managers for resource cleanup
  • Sensible default (conda-forge) for the primary use case

Good patterns observed:

  • client.py:49-57: The _auth() helper cleanly encapsulates the "return None vs BasicAuth" logic
  • client.py:70-75: Proper conditional kwarg passing - omitting auth entirely when None rather than passing auth=None
  • api.py:21-36: Robust error handling in context manager with cleanup warnings

Security Assessment

✅ Secure implementation:

  • Anonymous access is appropriately limited to GET requests only
  • POST/PATCH operations correctly require authentication (client.py:83-84, client.py:103-104)
  • Token validation with reasonable format checking (client.py:38-47)
  • No credential leakage in logs or error messages
  • Proper handling of empty/unset tokens by coercing to None

Authentication Logic Analysis

Root cause fix is correct:
The core issue was sending Authorization: Basic Og== (base64 of :) when token was empty. Azure DevOps rejects malformed auth headers even on public endpoints. The fix properly omits the auth header entirely for anonymous requests.

Implementation details:

  • client.py:129: or None coercion ensures empty strings become None
  • client.py:70-75: Conditional request ensures no auth header when token=None
  • Clean separation between read (optional auth) and write (required auth) operations

Test Coverage Assessment

✅ Excellent new test coverage:
TestAzureClientAnonymousMode covers all critical paths:

  • Anonymous GET requests work without auth headers
  • Empty token strings are properly coerced to None
  • Write operations correctly raise ValueError without token
  • Default organization handling
  • Auth header presence verification when token is provided

Test quality is high:

  • Tests verify the actual behavior that matters (no auth kwarg in anonymous calls)
  • Good use of mocking to isolate units under test
  • Clear test names that document expected behavior

azure_list_builds Removal

✅ Justified removal:

  • Required write-level PAT permissions not available for anonymous access
  • Was dead code with no callers in the codebase
  • Clean removal across all files (function, model, registry, tests, README)

Performance Considerations

  • No performance concerns introduced
  • Efficient request path (no unnecessary auth object creation)
  • Proper async/await patterns maintained

Minor Observations

  1. Token validation regex (client.py:46): Current pattern ^[a-zA-Z0-9+/=]{20,}$ is reasonable but could be more specific to Azure PAT format if needed in the future.

  2. Error message consistency: Error messages are clear and actionable (e.g., "AZURE_DEVOPS_TOKEN required for write operations").

  3. Documentation: The docstrings clearly explain the anonymous mode behavior and when tokens are required.

Verification Status

The author noted that lint/format/typecheck passed, which aligns with the code quality I observe. The empirical curl testing mentioned in the PR description demonstrates good validation methodology.

Recommendation

✅ Approve and merge - This PR successfully enables anonymous access to public Azure DevOps projects while maintaining security boundaries. The implementation is clean, well-tested, and addresses a real user need for conda-forge feedstock inspection.


The three failing tests asserted the pre-PR behavior (returning None when
token/org are missing or token format is invalid). After making
get_azure_client() build a real client for anonymous public-project access,
it no longer returns None for those inputs. The tests now:

  * test_get_azure_client_no_token: expects an anonymous client defaulting
    to the conda-forge organization.
  * test_get_azure_client_no_org: expects the conda-forge default when
    AZURE_DEVOPS_ORG is unset.
  * test_get_azure_client_invalid_token: expects a client to still be built
    (the warning is logged; the API call surfaces the error).

All three now also patch aiohttp.ClientSession so the constructor does not
require a running event loop, mirroring test_get_azure_client_success.

Co-Authored-By: MementoRC (https://github.com/MementoRC)
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Claude finished @MementoRC's task —— View job


PR Code Review

I've completed a thorough review of this PR. Here's my analysis:

Review Checklist:

  • Analyze authentication changes in client.py
  • Review api.py modifications
  • Examine azure_list_builds removal
  • Check test coverage and quality
  • Evaluate security implications
  • Review code quality and best practices
  • Provide comprehensive feedback

Overall Assessment: LGTM with Minor Observations

This is a well-executed PR that solves a real problem with anonymous access to public Azure DevOps projects. The implementation is clean, well-tested, and follows good practices.

Code Quality & Best Practices

Excellent:

  • Clean separation of authentication logic in client.py:_auth() method
  • Proper error handling and user-friendly error messages
  • Comprehensive docstrings and type hints throughout
  • Good use of context managers for resource cleanup
  • Sensible default (conda-forge) for the primary use case

Good patterns observed:

  • client.py:49-57: The _auth() helper cleanly encapsulates the "return None vs BasicAuth" logic
  • client.py:70-75: Proper conditional kwarg passing - omitting auth entirely when None rather than passing auth=None
  • api.py:21-36: Robust error handling in context manager with cleanup warnings

Security Assessment

✅ Secure implementation:

  • Anonymous access is appropriately limited to GET requests only
  • POST/PATCH operations correctly require authentication (client.py:83-84, client.py:103-104)
  • Token validation with reasonable format checking (client.py:38-47)
  • No credential leakage in logs or error messages
  • Proper handling of empty/unset tokens by coercing to None

Authentication Logic Analysis

Root cause fix is correct:
The core issue was sending Authorization: Basic Og== (base64 of :) when token was empty. Azure DevOps rejects malformed auth headers even on public endpoints. The fix properly omits the auth header entirely for anonymous requests.

Implementation details:

  • client.py:129: or None coercion ensures empty strings become None
  • client.py:70-75: Conditional request ensures no auth header when token=None
  • Clean separation between read (optional auth) and write (required auth) operations

Test Coverage Assessment

✅ Excellent new test coverage:
TestAzureClientAnonymousMode covers all critical paths:

  • Anonymous GET requests work without auth headers
  • Empty token strings are properly coerced to None
  • Write operations correctly raise ValueError without token
  • Default organization handling
  • Auth header presence verification when token is provided

Test quality is high:

  • Tests verify the actual behavior that matters (no auth kwarg in anonymous calls)
  • Good use of mocking to isolate units under test
  • Clear test names that document expected behavior

azure_list_builds Removal

✅ Justified removal:

  • Required write-level PAT permissions not available for anonymous access
  • Was dead code with no callers in the codebase
  • Clean removal across all files (function, model, registry, tests, README)

Performance Considerations

  • No performance concerns introduced
  • Efficient request path (no unnecessary auth object creation)
  • Proper async/await patterns maintained

Minor Observations

  1. Token validation regex (client.py:46): Current pattern ^[a-zA-Z0-9+/=]{20,}$ is reasonable but could be more specific to Azure PAT format if needed in the future.

  2. Error message consistency: Error messages are clear and actionable (e.g., "AZURE_DEVOPS_TOKEN required for write operations").

  3. Documentation: The docstrings clearly explain the anonymous mode behavior and when tokens are required.

Verification Status

The author noted that lint/format/typecheck passed, which aligns with the code quality I observe. The empirical curl testing mentioned in the PR description demonstrates good validation methodology.

Recommendation

✅ Approve and merge - This PR successfully enables anonymous access to public Azure DevOps projects while maintaining security boundaries. The implementation is clean, well-tested, and addresses a real user need for conda-forge feedstock inspection.


@MementoRC MementoRC merged commit bbb1982 into development May 8, 2026
16 checks passed
@MementoRC MementoRC deleted the feat/azure-public-anonymous-access branch May 8, 2026 16:09
MementoRC added a commit that referenced this pull request May 8, 2026
PR #165 enabled anonymous access to public Azure DevOps projects when
AZURE_DEVOPS_TOKEN is unset, but a stale or expired PAT in the env
still defeated that path: the client attached BasicAuth and Azure
returned 401 ("Personal Access Token has expired") before the
anonymous code path could be reached.

This patch makes read-only access robust to stale credentials. When a
GET request returns 401 with auth attached, the client transparently
retries once without the auth header. Public projects succeed on the
retry; private projects still return 401 to the caller.

Behavior:
- GET with auth -> 200: returned directly (no retry)
- GET with auth -> 401: retried without auth, second response returned
- GET without auth -> 401: returned directly (no retry — already anon)
- POST/PATCH -> 401: propagated unchanged (writes require valid auth)

Tests cover all four paths in TestAzureClient401Fallback.

Lint/format/typecheck: green.
Pytest: deferred to CI.
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.

1 participant