Skip to main content

Azure DevOps

Proxy for the Azure DevOps Services REST API via the managed gateway.

What this connector does​

Gives agents gated access to an Azure DevOps organization for:

  • Listing projects.
  • Listing Git repositories in a project.
  • Querying and reading work items.

All operations are gated by PBAC policy. Every call flows: agent → gateway → /introspect (with this connector's resource_type) → OPA decision → Azure DevOps API → response.

The connector supports two upstream-auth modes:

  • entra_refresh (default) — PolicyArc uses the refresh token captured at the user's Microsoft sign-in to mint a fresh, correctly-audienced token per Microsoft service at call time. One sign-in works across any combination of Microsoft connectors — Graph (Mail, OneDrive, Teams) and Azure DevOps together. The minted token is cached per (user, resource) and re-minted on expiry. Azure DevOps enforces its own organization membership and permissions, and its audit trail shows the real person making each call.

  • idp_passthrough — forwards the user's Microsoft sign-in token directly to Azure DevOps, with no exchange. That sign-in token carries a single audience, so passthrough works for only one Microsoft resource — either Azure DevOps or Graph (Mail/OneDrive/Teams), not both; don't mix passthrough across Azure DevOps and Graph. Choose this only for strict tenants that disable refresh-token issuance (no offline_access), or that don't want a long-lived refresh token warehoused by PolicyArc. Otherwise prefer entra_refresh.

Prerequisites​

entra_refresh mode (default)​

  • Entra app registration — a confidential app used for login. It needs:
    • a client secret (stored in PolicyArc's secret store),
    • the federation redirect URI (/oauth2/callback),
    • a delegated Azure DevOps API permission (resource 499b84ac-1321-427f-aa17-267ca6975798, user_impersonation), with admin consent granted,
    • offline_access in the login scopes so a refresh token is captured.
  • PolicyArc Entra IdP registered with provider key microsoft, configured with the app's client_id/client_secret and the tenant authorize/token endpoints (https://login.microsoftonline.com/<tenant>/oauth2/v2.0/{authorize,token}). Login scopes must include offline_access so the gateway captures a refresh token at sign-in. See the Entra ID IdP guide.
  • Azure DevOps organization connected to the same Entra tenant (Org settings → Microsoft Entra), and the user must be a member of the org with permission for the operations you want to gate.
  • PBAC_CREDENTIAL_KEY (or the legacy PBAC_GATEWAY_CREDENTIAL_KEY) set on the AS (encrypts the cached tokens).

idp_passthrough mode (no token exchange)​

Passthrough forwards the user's sign-in token directly, so there is no token exchange:

  • Entra app registration — a confidential app used only as a login IdP. It needs a client secret, the /oauth2/callback redirect URI, and the delegated Azure DevOps permission (resource 499b84ac-1321-427f-aa17-267ca6975798, user_impersonation) with admin consent granted.
  • PolicyArc Entra IdP (provider key microsoft) — login scopes must be openid profile email 499b84ac-1321-427f-aa17-267ca6975798/user_impersonation, so the captured token is audienced for Azure DevOps. offline_access is optional in passthrough mode — add it only to have the forwarded token refreshed silently on expiry; omit it for tenants that disable refresh-token issuance (users re-authenticate when the token lapses). Installing the connector auto-merges user_impersonation into the IdP scope list (via IdpScopeMerger); do not also add …/.default — Entra rejects .default combined with a resource-specific scope at sign-in (AADSTS70011).
  • Azure DevOps organization connected to the same Entra tenant and the user a member of the org (same as entra_refresh).
  • PBAC_CREDENTIAL_KEY (or the legacy PBAC_GATEWAY_CREDENTIAL_KEY) set on the AS (encrypts the captured login token).

Install​

The install wizard has an Authentication picker (entra_refresh / idp_passthrough). The default is entra_refresh; choose idp_passthrough only if your deployment uses a single Azure-DevOps-audienced token for every connector and you have no Graph connectors configured.

1. Register the Entra IdP​

Register the Entra IdP in PolicyArc (provider key microsoft) using the app's client_id/client_secret and tenant endpoints as described in Prerequisites. Confirm a user can log in to PolicyArc through it before installing the connector.

2. Install the connector​

From the Admin Portal:

  1. APIs & Connectors → + Connect a resource → Azure DevOps
  2. Fill in the wizard. It has no IdP field. The connector uses the IdP whose provider key is microsoft, the one registered in step 1.
    • Authentication — select entra_refresh (default) or idp_passthrough.
    • Azure DevOps organization URL — https://dev.azure.com/<your-org>.
  3. Select Connect.

The admin UI reads setup_fields directly from this connector's manifest — same fields shown below in the Setup fields reference.

3. Verify​

Log in to PolicyArc through the Entra IdP first (so the gateway has a captured refresh token), then request a token with an Azure DevOps scope and call through the gateway:

curl "https://<your-as>/gateway/azure-devops/_apis/projects?api-version=7.1" \
-H "Authorization: Bearer <token>"

A successful 200 with the project list confirms the full chain end-to-end:

  • entra_refresh — refresh token captured → per-resource token minted → introspect allowed → gateway proxied → Azure DevOps responded.
  • idp_passthrough — no exchange: the captured Azure-DevOps-audienced login token is forwarded verbatim → introspect allowed → gateway proxied → Azure DevOps responded. (If the captured token is audienced for anything else, ADO returns 401; re-check the IdP login scopes per Prerequisites.)

What can go wrong​

SymptomCauseFix
401 with "error": "idp_link_required"The user has no stored microsoft token, or in idp_passthrough mode the stored token lacks a scope the connector now requiresOpen the link_url from the response body and sign in through the Entra IdP, so the gateway can capture a refresh token.
502 "Upstream auth failed: token resolution requires an authenticated subject"The token is a client_credentials token, which carries no userUse a user token from an authorization-code login.
502 "admin consent / API permission missing"The Entra app lacks admin-consented delegated Azure DevOps permissionGrant admin consent for the user_impersonation permission on resource 499b84ac-… in the app registration.
401 with "error": "idp_link_required" after working beforeThe refresh token is expired or revoked. If no link can be built the gateway answers 502 with "refresh token expired or revoked; user must re-authenticate" insteadOpen the link_url and sign in through the Entra IdP; ensure offline_access is in the login scopes.
401/203 from Azure DevOpsThe org is not connected to the same Entra tenant, or the user isn't a memberConnect the org to the tenant (Org settings → Microsoft Entra) and add the user.
404 at the gatewayWrong pathGateway path is /gateway/<resource-name>/<ADO REST path> — ADO paths start with /_apis/... or /{project}/_apis/....
ADO returns an HTML/redirect page instead of JSONMissing api-version query parameterAppend ?api-version=7.1 (or the version your API requires) to ADO REST calls.
403 from the gatewayAgent lacks the required scopeRequest a token with azuredevops:read.

Scopes​

ScopeMeaningRoutes
azuredevops:readRead-only operationslist projects, list repositories, query/get work items

Policy authors can scope a rule with input.context.rs_context.resource_type == "urn:connector:identos:azure-devops". Per-project rules compare input.context.rs_context.resource_id with the bare project path value, such as Classified, not with azuredevops://.... The gateway passes the first path parameter the route's resource_id_template names (ConnectorV1Adapter.java).

Setup fields​

FieldRequiredDefaultPurpose
base_urlyes—Azure DevOps organization URL, e.g. https://dev.azure.com/your-org.

The Entra IdP (provider key microsoft) supplies the client credentials and token endpoint. The connector finds it by that provider key; the install form has no field for it.

MCP tools​

When the managed gateway's MCP aggregator is enabled, this connector exposes the following tools to agents:

ToolScopeDescription
list_projectsazuredevops:readList projects in the organization.
list_repositoriesazuredevops:readList Git repositories in a project.
list_work_itemsazuredevops:readQuery work items in a project via WIQL.
get_work_itemazuredevops:readGet a single work item by ID.

Each tool's input schema is embedded in the manifest and forwarded verbatim to MCP clients.

Example policy​

Restrict a sensitive project to members of one group, where the project and group names are tenant-specific. The group comes from the groups claim of the user's IdP login, which the AS captures onto input.subject.groups (the default pbac.identity.claim-capture.allowed-claims includes groups) and replays at every gateway call:

package pbac.connectors.identos.azure_devops

# The tenant-specific part is DATA, not a rule. Operators set it at
# data["pbac.operator.connectors.identos.azure-devops"], and the key has to be
# declared in this connector's `operator_data_schema` or ConnectorLinter will
# reject the manifest. Index it directly: object.get(data, ...) passes the whole
# `data` document, which makes this package depend on itself
# (rego_recursion_error). With no operator config `_op` is undefined and the
# rule below contributes nothing.
_op := data["pbac.operator.connectors.identos.azure-devops"]

deny contains msg if {
input.resource.type == "urn:connector:identos:azure-devops"
classified := _op.classified_project
classified != ""
input.context.rs_context.tool_arguments.project == classified
# object.get over the whole path: a client_credentials token has no
# subject at all, and must be refused rather than skipped.
not _op.classified_group in object.get(input, ["subject", "groups"], [])
msg := sprintf("azure-devops: %v is restricted to the classified group", [classified])
}

On a local instance, with classified_project set to Classified and classified_group to classified-team, a list_work_items call for Classified returned 403 for a user whose groups were ["sales"] and for a client_credentials token, and reached the upstream for a user in classified-team. An earlier version of this example read input.context.actor.groups; actor is set only by token exchange, so on the gateway path that rule never fired.

Five details that are easy to get wrong, each of which fails silently rather than erroring:

  • The rule goes in this connector's package, pbac.connectors.identos.azure_devops. The aggregator iterates data.pbac.connectors (vendor, then package) and nothing else. A rule authored under pbac.operator.* is never evaluated — no error, the deny just never fires. The operator namespace is for data.
  • Read tool arguments from input.context.rs_context.tool_arguments. input.resource.<param> and input.action.method are not populated on the gateway path. Ten Google connector rules gated on them, never fired, and were removed in 0.43.0.
  • Read the user from input.subject. input.context.actor exists only on a token-exchange token.
  • Entra sends group object ids, not names. The groups claim holds each group's object id, and Entra sends it only when the app registration is configured to emit group claims (Microsoft Entra documentation, "Configure group claims for applications"). Set classified_group to the group's object id. Without the claim, input.subject.groups is absent and the example denies every user, members included.
  • Declare any operator key in operator_data_schema. This connector ships none today, so the example above needs classified_project and classified_group added before it will lint.

Manifest reference​

  • ID: identos.azure-devops
  • Version: 1.0.0
  • Resource type: urn:connector:identos:azure-devops
  • Capabilities: mcp

Supported auth modes​

TypeDetails
entra_refreshrequires IdP microsoft
idp_passthroughrequires IdP microsoft

Setup fields​

IDLabelDefaultSecret?Notes
upstream_auth.typeAuthenticationentra_refreshnoAuthentication mode. entra_refresh (default) mints a separate per-resource token for each Microsoft service from your one sign-in, so a single login works across any mix of Microsoft connectors — Graph (Mail/OneDrive/Teams) and Azure DevOps together. idp_passthrough instead forwards your Microsoft sign-in token as-is, and that token is valid for only one audience: passthrough therefore works for either Graph (Mail/OneDrive/Teams) or Azure DevOps, not both. Use entra_refresh if you need more than one.
base_urlAzure DevOps organization URL—noplaceholder: https://dev.azure.com/your-org

Scopes​

Scope
azuredevops:read

Routes​

MethodPatternScopeResource template
GET/_apis/projectsazuredevops:read—
GET/{project}/_apis/git/repositoriesazuredevops:readazuredevops://{{project}}/repositories
POST/{project}/_apis/wit/wiqlazuredevops:readazuredevops://{{project}}/workitems
GET/{project}/_apis/wit/workitems/{id}azuredevops:readazuredevops://{{project}}/workitems/{{id}}

MCP tools​

NameScopeDescription
list_projectsazuredevops:readList Azure DevOps projects in the organization.
list_repositoriesazuredevops:readList Git repositories in a project.
list_work_itemsazuredevops:readQuery work items in a project via WIQL (Work Item Query Language).
get_work_itemazuredevops:readGet a single work item by ID.