feat(integrations): Azure DevOps browse endpoints (PR 5/N)#7629
feat(integrations): Azure DevOps browse endpoints (PR 5/N)#7629asaphko wants to merge 9 commits into
Conversation
…gration
Fifth plan in the stacked-PRs rollout. Covers the Azure DevOps browse
endpoints: extends the REST client with list_repositories,
list_pull_requests, and list_work_items (WIQL + workitemsbatch); adds
four ListAPIView subclasses under /api/v1/projects/{pk}/azure-devops/;
splits the serializers module into a subpackage with new browse
query-param serializers.
Spec deviations captured: ADO doesn't support PR title search; WIQL
single-quote escaping by doubling; workitemsbatch is POST (the spec
said GET). Browse URL prefix matches GitLab's "/{pk}/{vendor}/"
pattern to avoid routing conflict with the CRUD viewset's {pk}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new types covering the resource shapes the browse endpoints will return: AdoRepository, AdoPullRequest, AdoPullRequestsPage, AdoWorkItem, AdoWorkItemsPage. Same page-shape (results + continuation_token) as AdoProjectsPage — ADO's REST API uses continuation tokens uniformly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists git repositories within an ADO project. Generalises _ado_request to accept paths that already contain "_apis/" (project-scoped endpoints) alongside the existing bare-path shape. defaultBranch is optional on the wire (ADO omits it for empty repos) so we default to "" when absent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists pull requests in an ADO project, filterable by state (active / completed / abandoned / all). ADO's REST API doesn't expose a text search for PRs, so this function takes state + paging only — title search is a work-item-only capability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements work-item search via WIQL + workitemsbatch:
1. POST /{project}/_apis/wit/wiql with a parameterised query for
state, type, and title CONTAINS — returns matched IDs.
2. POST /_apis/wit/workitemsbatch with the page-sized slice of IDs
— returns rows with id/title/state/type/url.
Pagination is offset-based on the WIQL ID list (continuation_token is
the next offset as a stringified integer; None on the final page).
Single-quote escaping in CONTAINS uses WIQL's double-quote convention.
Column names are hard-coded; only user-supplied values are escaped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four DRF serializers covering paging + per-resource filter shapes: - AdoBrowseQueryParamsSerializer: paging only (projects browse). - AdoRepositoriesQueryParamsSerializer: + ado_project_id. - AdoPullRequestsQueryParamsSerializer: + state (choice-validated). - AdoWorkItemsQueryParamsSerializer: + search_text, state, work_item_type. The existing AzureDevOpsConfigurationSerializer moves to serializers/__init__.py so the `from integrations.azure_devops.serializers import X` path keeps working for PR 2's viewset and tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four ListAPIView subclasses sharing a _AdoListView base for permission handling, config lookup, error mapping (400 no config / 502 auth / 503 unreachable), and continuation-token paging. The next commit wires their URLs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four nested paths under /api/v1/projects/{project_pk}/azure-devops/:
projects, repositories, pull-requests, work-items. Path prefix is
"azure-devops/" (not "integrations/azure-devops/") to mirror the
GitLab precedent and avoid routing conflict with the CRUD viewset's
{pk} capture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r 502 path Three correctness fixes surfaced by the final whole-PR review: 1. _AdoListView.list now catches AzureDevOpsNotFoundError and returns 404 with a structured detail. Previously, a bad ado_project_id produced a 500 via Django's default exception handler. 2. AdoWorkItemsQueryParamsSerializer overrides continuation_token to an IntegerField(min_value=0). The work-items client interprets the token as an integer offset into the WIQL ID list; negative values used to leak the wrong slice, and non-integer strings used to raise uncaught ValueErrors. The view stringifies the validated int back before passing to list_work_items. 3. New tests cover the 502 auth-error branch and the 404 not-found branch on the browse views, plus the two new serializer rejections. Also updates the spec to match the implementation: - Browse URL prefix /azure-devops/ (not /integrations/azure-devops/) - list_pull_requests has no search_text (ADO doesn't support PR title search) - workitemsbatch is POST (the spec said GET) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
There was a problem hiding this comment.
Code Review
This pull request introduces browse endpoints for the Azure DevOps (ADO) integration, allowing users to list projects, repositories, pull requests, and work items. It extends the ADO client with new functions, implements a WIQL-based work item search with batch hydration, adds query parameter serializers, and wires up the corresponding views and URLs. A security review of the changes identified a potential path traversal vulnerability in the ado_project_id query parameter, which is interpolated directly into API request paths. It is recommended to add a regex validator to restrict slashes and backslashes in the project ID.
| from rest_framework import serializers | ||
|
|
||
| _PR_STATE_CHOICES = ("active", "completed", "abandoned", "all") | ||
|
|
||
|
|
||
| class AdoBrowseQueryParamsSerializer(serializers.Serializer[None]): | ||
| top = serializers.IntegerField(default=100, min_value=1, max_value=200) | ||
| continuation_token = serializers.CharField(required=False, allow_blank=True) | ||
|
|
||
|
|
||
| class AdoRepositoriesQueryParamsSerializer(AdoBrowseQueryParamsSerializer): | ||
| ado_project_id = serializers.CharField() |
There was a problem hiding this comment.
Security Vulnerability: Path Traversal in ado_project_id
The ado_project_id query parameter is interpolated directly into the Azure DevOps API request paths (e.g., f"{ado_project_id}/_apis/git/repositories"). Since there is no validation on ado_project_id, an attacker with access to the browse endpoints could pass path traversal sequences (such as ../../) to manipulate the request URL. This would allow them to perform unauthorized API requests against other organizations or projects using the stored Personal Access Token (PAT).
Remediation:
Add a RegexValidator to ado_project_id in AdoRepositoriesQueryParamsSerializer to ensure it does not contain slashes (/) or backslashes (\\), which are forbidden in valid Azure DevOps project IDs and names anyway.
| from rest_framework import serializers | |
| _PR_STATE_CHOICES = ("active", "completed", "abandoned", "all") | |
| class AdoBrowseQueryParamsSerializer(serializers.Serializer[None]): | |
| top = serializers.IntegerField(default=100, min_value=1, max_value=200) | |
| continuation_token = serializers.CharField(required=False, allow_blank=True) | |
| class AdoRepositoriesQueryParamsSerializer(AdoBrowseQueryParamsSerializer): | |
| ado_project_id = serializers.CharField() | |
| from django.core.validators import RegexValidator | |
| from rest_framework import serializers | |
| _PR_STATE_CHOICES = ("active", "completed", "abandoned", "all") | |
| class AdoBrowseQueryParamsSerializer(serializers.Serializer[None]): | |
| top = serializers.IntegerField(default=100, min_value=1, max_value=200) | |
| continuation_token = serializers.CharField(required=False, allow_blank=True) | |
| class AdoRepositoriesQueryParamsSerializer(AdoBrowseQueryParamsSerializer): | |
| ado_project_id = serializers.CharField( | |
| validators=[ | |
| RegexValidator( | |
| regex=r"^[^/\\\\]+$", | |
| message="Invalid Azure DevOps project ID or name.", | |
| ) | |
| ] | |
| ) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/azure-devops-04-tagging #7629 +/- ##
==============================================================
Coverage 98.54% 98.54%
==============================================================
Files 1469 1472 +3
Lines 55614 55992 +378
==============================================================
+ Hits 54803 55179 +376
- Misses 811 813 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
PR 5 of the stacked Azure DevOps integration rollout. Extends the REST client with three new functions and exposes four paginated browse endpoints under
/api/v1/projects/{flagsmith_project_id}/azure-devops/.list_repositories,list_pull_requests,list_work_items(WIQL + workitemsbatch orchestration)._ado_requestgeneralised to handle both bare paths and project-scoped_apis/-bearing paths.projects,repositories,pull-requests,work-items. Continuation-token pagination matches ADO's native shape. Shared_AdoListViewbase handles permissions, config lookup, and error mapping (400 no config / 404 unknown resource / 502 auth failure / 503 unreachable).serializers/browse.py(inheritance chain rooted atAdoBrowseQueryParamsSerializer). The existingAzureDevOpsConfigurationSerializermoves toserializers/__init__.pyso all PR 2 imports keep working.Stack
Plan:
docs/superpowers/plans/2026-05-28-azure-devops-05-browse.md.Spec deviations captured during implementation
GET /_apis/git/pullrequestsexposes onlysearchCriteria.status— no text search. The PR-browse endpoint takesstate=only. Work-item search via WIQLCONTAINSstill works.workitemsbatchisPOST, notGET. The spec said GET; ADO's REST docs require POST. Implementing as POST./{project_pk}/azure-devops/..., not/{project_pk}/integrations/azure-devops/...to avoid routing conflict with the CRUD viewset's{pk}capture. Matches GitLab's/{project_pk}/gitlab/projects/precedent.{results, next, previous}withprevious=Noneand nocount. ADO continuation tokens don't reverse-paginate or expose totals. ThenextURL containscontinuation_token=<token>.Spec doc updated in commit
bed431a51to reflect these.Notable implementation details
_escape_wiql_string.continuation_tokenvalidation:AdoWorkItemsQueryParamsSerializeroverrides the field toIntegerField(min_value=0)so negative offsets / non-integer strings are rejected at serializer-validation time (400) rather than crashing the view (500).Caught + fixed during final review
AzureDevOpsNotFoundErrorin the browse view → now maps to 404 with a structured detail. New test added.continuation_token=-1previously leaked the wrong slice → now rejected by the serializer with 400. Two new serializer tests.Out of scope
metrics.py).Test plan
make lintcleanmake typecheckcleanmake test opts='-n0 tests/unit/integrations/azure_devops/'— 173 passed (PR 4 baseline 127 + 46 new)make test opts='tests/unit/integrations/gitlab tests/unit/integrations/github tests/unit/features/test_unit_feature_external_resources_views.py tests/unit/features/test_migrations.py'— passing (adjacent-integration regression guard)make django-make-migrations opts='--check --dry-run'— no drift (PR 5 introduces no schema changes)🤖 Generated with Claude Code