Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/artifacts-helper/NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
This installs [Azure Artifacts Credential Provider](https://github.com/microsoft/artifacts-credprovider)
and optionally configures shims which shadow `dotnet`, `nuget`, `npm`, `yarn`, `rush`, and `pnpm`.
These dynamically sets an authentication token for pulling artifacts from a feed before running the command.
and optionally configures shims which shadow `dotnet`, `nuget`, `npm`, `yarn`, `rush`, `pnpm`, and `az`.
These dynamically set an authentication token for pulling artifacts from a feed before running the command.

The `az` shim specifically intercepts `az account get-access-token` requests and uses the `azure-auth-helper`
to acquire tokens via the ado-codespaces-auth VS Code extension. This enables `DefaultAzureCredential`'s
`AzureCliCredential` to work in Codespaces without requiring `az login`.

For `npm`, `yarn`, `rush`, and `pnpm` this requires that your `~/.npmrc` file is configured to use the ${ARTIFACTS_ACCESSTOKEN}
environment variable for the `authToken`. A helper script has been added that you can use to write your `~/.npmrc`
Expand Down
7 changes: 6 additions & 1 deletion src/artifacts-helper/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Azure Artifacts Credential Helper",
"id": "artifacts-helper",
"version": "3.0.2",
"version": "3.0.3",
"description": "Configures Codespace to authenticate with Azure Artifact feeds",
"options": {
"nugetURIPrefixes": {
Expand Down Expand Up @@ -49,6 +49,11 @@
"default": true,
"description": "Create alias for pnpm"
},
"azAlias": {
"type": "boolean",
"default": true,
"description": "Create alias for az (Azure CLI)"
},
"shimDirectory": {
"type": "string",
"default": "/usr/local/share/codespace-shims",
Expand Down
4 changes: 4 additions & 0 deletions src/artifacts-helper/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ALIAS_YARN="${YARNALIAS:-"true"}"
ALIAS_NPX="${NPXALIAS:-"true"}"
ALIAS_RUSH="${RUSHALIAS:-"true"}"
ALIAS_PNPM="${PNPMALIAS:-"true"}"
ALIAS_AZ="${AZALIAS:-"true"}"
INSTALL_PIP_HELPER="${PYTHON:-"false"}"
SHIM_DIRECTORY="${SHIMDIRECTORY:-"/usr/local/share/codespace-shims/"}"

Expand Down Expand Up @@ -39,6 +40,9 @@ if [ "${ALIAS_PNPM}" = "true" ]; then
ALIASES_ARR+=('pnpm')
ALIASES_ARR+=('pnpx')
fi
if [ "${ALIAS_AZ}" = "true" ]; then
ALIASES_ARR+=('az')
fi

# Source /etc/os-release to get OS info
. /etc/os-release
Expand Down
109 changes: 109 additions & 0 deletions src/artifacts-helper/scripts/az
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/bin/bash
# Azure CLI shim for GitHub Codespaces
# Intercepts 'az account get-access-token' requests and uses azure-auth-helper
# to acquire tokens via the ado-codespaces-auth VS Code extension.

# If ACTIONS_ID_TOKEN_REQUEST_URL is set, we are in GitHub Actions - skip interception
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ]; then
source "$(dirname "$0")"/resolve-shim.sh
AZ_EXE="$(resolve_shim)"
exec "${AZ_EXE}" "$@"
fi

source "$(dirname "$0")"/resolve-shim.sh

# Well-known resource type mappings (az account get-access-token --resource-type)
declare -A RESOURCE_TYPE_MAP=(
["arm"]="https://management.azure.com"
["aad-graph"]="https://graph.windows.net"
["ms-graph"]="https://graph.microsoft.com"
["batch"]="https://batch.core.windows.net"
["data-lake"]="https://datalake.azure.net"
["media"]="https://rest.media.azure.net"
["oss-rdbms"]="https://ossrdbms-aad.database.windows.net"
)

# Check if this is a get-access-token request that we should intercept
if [[ "$1" == "account" && "$2" == "get-access-token" ]]; then
resource=""
scope=""
resource_type=""
prev=""

for arg in "${@:3}"; do
case "$arg" in
--resource=*) resource="${arg#--resource=}" ;;
--scope=*) scope="${arg#--scope=}" ;;
--resource-type=*) resource_type="${arg#--resource-type=}" ;;
*)
case "$prev" in
--resource) resource="$arg" ;;
--scope) scope="$arg" ;;
--resource-type) resource_type="$arg" ;;
esac
;;
esac
prev="$arg"
done

# Resolve resource-type to resource URL if specified
if [[ -n "$resource_type" && -z "$resource" ]]; then
resource="${RESOURCE_TYPE_MAP[$resource_type]}"
fi

# Determine the scope to request
request_scope=""
if [[ -n "$scope" ]]; then
request_scope="$scope"
elif [[ -n "$resource" ]]; then
if [[ "$resource" == *"/.default" ]]; then
request_scope="$resource"
else
request_scope="${resource}/.default"
fi
fi

# If we have a scope and azure-auth-helper exists, use it
if [[ -n "$request_scope" && -f "${HOME}/azure-auth-helper" ]]; then
token=$("${HOME}/azure-auth-helper" get-access-token "$request_scope" 2>/dev/null)
if [[ $? -eq 0 && -n "$token" ]]; then
# Escape token for safe JSON embedding (handle backslashes and quotes)
escaped_token="${token//\\/\\\\}"
escaped_token="${escaped_token//\"/\\\"}"

# Calculate expiry timestamps (conservative 1 hour estimate)
# expires_on = POSIX timestamp, expiresOn = local datetime
if date --version >/dev/null 2>&1; then
# GNU date (Linux)
expires_on=$(date -d "+1 hour" "+%s")
expires_on_datetime=$(date -d "+1 hour" "+%Y-%m-%d %H:%M:%S.000000")
else
# BSD date (macOS)
expires_on=$(date -v+1H "+%s")
expires_on_datetime=$(date -v+1H "+%Y-%m-%d %H:%M:%S.000000")
fi

# Return in az CLI JSON format (matching real az CLI output)
cat <<EOF
{
"accessToken": "${escaped_token}",
"expiresOn": "${expires_on_datetime}",
"expires_on": ${expires_on},
"subscription": "",
"tenant": "",
"tokenType": "Bearer"
}
EOF
exit 0
fi
fi
fi

# Fall through to real az CLI for all other commands
AZ_EXE="$(resolve_shim)"
if [[ -n "$AZ_EXE" ]]; then
exec "${AZ_EXE}" "$@"
else
echo "Error: Azure CLI not found in PATH" >&2
exit 1
fi
9 changes: 9 additions & 0 deletions test/artifacts-helper/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,14 @@
"nugetAlias": true
}
}
},
"test_az_shim": {
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"ghcr.io/devcontainers/features/azure-cli:1": {},
"artifacts-helper": {
"azAlias": true
}
}
}
}
81 changes: 81 additions & 0 deletions test/artifacts-helper/test_az_shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash

# Import test library bundled with the devcontainer CLI
source dev-container-features-test-lib || exit 1

# Test that az shim is installed
check "az shim exists" test -f /usr/local/share/codespace-shims/az
check "az shim is executable" test -x /usr/local/share/codespace-shims/az

# Test that az shim sources resolve-shim.sh
check "az shim sources resolve-shim.sh" grep -q 'source.*resolve-shim.sh' /usr/local/share/codespace-shims/az

# Test GitHub Actions environment detection
check "az shim has GitHub Actions detection" grep -q 'ACTIONS_ID_TOKEN_REQUEST_URL' /usr/local/share/codespace-shims/az

# Test that az shim intercepts get-access-token command
check "az shim intercepts get-access-token" grep -q 'get-access-token' /usr/local/share/codespace-shims/az

# Test argument parsing handles both formats (--resource value and --resource=value)
check "az shim handles equals format args" grep -q '\-\-resource=\*' /usr/local/share/codespace-shims/az
check "az shim handles space-separated args" grep -q '\-\-resource)' /usr/local/share/codespace-shims/az

# Test that az shim falls back to real az CLI for other commands
TEST_HOME=$(mktemp -d)
check "az shim falls back for non-token commands" bash -c '
export HOME='"$TEST_HOME"'
# az --version should pass through to real az CLI (if installed)
# or fail gracefully if az is not installed
output=$(/usr/local/share/codespace-shims/az --version 2>&1) || true
# Check that it either shows az version or "not found" error - both are valid
echo "$output" | grep -qE "(azure-cli|Azure CLI|not found)" && echo "SUCCESS" || echo "FAILED"
' | grep -q "SUCCESS"

# Test that az shim handles missing azure-auth-helper gracefully
check "az shim handles missing azure-auth-helper" bash -c '
export HOME='"$TEST_HOME"'
# Remove azure-auth-helper if it exists
rm -f "${HOME}/azure-auth-helper"
# Call az account get-access-token - should fall through to real az
# (which will fail, but shim should not crash)
/usr/local/share/codespace-shims/az account get-access-token --resource https://management.azure.com 2>&1 || true
# If we get here, the shim handled it gracefully
echo "completed"
' | grep -q "completed"

# Test that az shim returns proper JSON format when azure-auth-helper exists
check "az shim returns valid JSON format" bash -c '
export HOME='"$TEST_HOME"'
# Create a mock azure-auth-helper that returns a test token
cat > "${HOME}/azure-auth-helper" << '\''HELPER'\''
#!/bin/bash
echo "test-token-12345"
HELPER
chmod +x "${HOME}/azure-auth-helper"

# Call the shim and verify JSON output
output=$(/usr/local/share/codespace-shims/az account get-access-token --resource https://management.azure.com 2>&1)

# Check that output contains expected JSON fields
echo "$output" | grep -q "accessToken" && \
echo "$output" | grep -q "tokenType" && \
echo "$output" | grep -q "Bearer" && \
echo "SUCCESS" || echo "FAILED"
' | grep -q "SUCCESS"

# Test GitHub Actions bypass (simulate by setting the env var)
check "az shim bypasses interception in GitHub Actions" bash -c '
export HOME='"$TEST_HOME"'
export ACTIONS_ID_TOKEN_REQUEST_URL="https://example.com/token"
# In Actions mode, shim should skip interception and call real az directly
# (will fail if az not installed, but should not attempt token interception)
output=$(/usr/local/share/codespace-shims/az account get-access-token --resource https://management.azure.com 2>&1) || true
# Should NOT contain our mock token (which means bypass worked)
echo "$output" | grep -qv "test-token-12345" && echo "SUCCESS" || echo "FAILED"
' | grep -q "SUCCESS"

# Cleanup
rm -rf "$TEST_HOME"

# Report results
reportResults
Loading