Skip to content

Commit 32490e8

Browse files
committed
feat(security): secure-files pipeline download via ViewSecrets elevation
- Add secure-files download for pipeline runs, auto-elevating ViewSecrets permission and reverting after download - Correct Security API token (project UUID) and accessControlLists wrapper - Support preview api-version and broaden no_scope detection - Add permanent grant/revoke for ViewSecrets as escape hatch - Remove secure-files download subcommand (MSA platform gap) - Add pipelines secure-files tests and security tests - Ignore docs/superpowers planning/spec docs
1 parent cd16465 commit 32490e8

15 files changed

Lines changed: 1255 additions & 335 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ node_modules/
4343
# Node.js test artifacts (root only)
4444
/package.json
4545
/package-lock.json
46+
.pi-subagents/
47+
48+
# Superpowers planning/spec docs (not committed)
49+
docs/superpowers/

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [0.5.0] - 2026-07-22
99

10+
### Unreleased
11+
12+
- **`ado pipelines secure-files`** — list, show, upload, ~~download~~, and delete
13+
Secure Files in the Pipeline Library (`/_apis/distributedtask/securefiles`).
14+
Useful for shipping certs, kubeconfigs, and signing keys to pipeline agents
15+
via the `DownloadSecureFile@1` task. **The `download` subcommand was
16+
removed in this branch** — see "Removed" below.
17+
18+
### Removed
19+
20+
- **`ado pipelines secure-files download` subcommand (this branch only).**
21+
Microsoft's secure-files API does not issue a `downloadTicket` field in
22+
the metadata response to bearer tokens for personal Microsoft accounts
23+
(e.g. `outlook.com` / `hotmail.com` / `live.com`), even with the
24+
`vso.securefiles_read` PAT scope and the Library/ViewSecrets permission
25+
explicitly granted (verified live with `allow:63 = View|Administer|
26+
Create|ViewSecrets|Use|Owner`). The web UI works because it uses
27+
session-cookie auth with a different code path. Direct REST does not.
28+
Re-introduce this subcommand only when either (a) Microsoft fixes the
29+
platform gap, or (b) the caller authenticates with a work/school
30+
Entra ID (AAD) identity rather than a personal Microsoft account.
31+
The other secure-files subcommands (`list`, `show`, `upload`, `delete`)
32+
are unaffected and continue to work for all account types. See
33+
`ado skills read ado-cli/references/pipelines.md` for examples.
34+
35+
36+
1037
### Added
1138

1239
- **`ado prs reviewers list --search QUERY`** — fuzzy-filter PR reviewers

lib/ado_cli/cli.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ defmodule AdoCli.CLI do
9797
areas: Areas,
9898
connections: Connections,
9999
extensions: Extensions,
100+
security: Security,
100101
iterations: Iterations,
101102
imports: Imports,
102103
pipelines: Pipelines,

lib/ado_cli/cli/helpers.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,30 @@ defmodule AdoCli.CLI.Helpers do
1313
import CliMate.CLI
1414
alias AdoCli.CLI.Output
1515

16+
@doc """
17+
Surfaces an API error to the user and halts with a non-zero exit code.
18+
19+
Shared by `AdoCli.CLI.Pipelines`, `AdoCli.CLI.WorkItems`, and
20+
`AdoCli.CLI.PullRequests`. Previously duplicated in each module.
21+
"""
22+
def bail(reason, parsed) do
23+
handle_api_result({:error, reason}, parsed, nil)
24+
end
25+
26+
@doc """
27+
Extracts the human-readable `message` field from an Azure DevOps
28+
error body. Returns `nil` if the body doesn't have one.
29+
30+
iex> extract_error_message(%{"message" => "Forbidden"})
31+
"Forbidden"
32+
iex> extract_error_message(%{"other" => "stuff"})
33+
nil
34+
iex> extract_error_message("not a map")
35+
nil
36+
"""
37+
def extract_error_message(%{"message" => m}) when is_binary(m), do: m
38+
def extract_error_message(_), do: nil
39+
1640
@doc """
1741
Handles a `{:ok, data} | {:error, reason}` result from the API client.
1842

0 commit comments

Comments
 (0)