diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md index b9f373311..e6d28f75a 100644 --- a/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/challenges/challenge-01.md @@ -2,9 +2,9 @@ **[Home](../Readme.md)** - [Next Challenge](challenge-02.md) -> **⚠️ Important — before you start.** Log into GitHub using the **hackathon-provided GitHub account** (not your personal or corporate account), then **fork the MicroHack repo** with that hackathon account set as the **owner** of the fork. All later challenges — commits, pull requests, Actions runs, and OIDC federation — assume you're working from that fork under the hackathon account. +> **⚠️ Important — before you start.** Log into GitHub using the **hackathon-provided GitHub account** (labuser-0000_ms000000 — not your personal or corporate account), then **fork the MicroHack repo** in the MngEnvMCAP000000 organization with MngEnvMCAP000000 account as the **owner** of the fork. Remember to append "-labuser-0000" to the fork name to avoid conflicts. All later challenges — commits, pull requests, Actions runs, and OIDC federation — assume you're working from that fork under the hackathon account. > -> When you create your Codespace, the repo offers **many** dev container configurations (one per MicroHack track). Be sure to select **"Azure / AI / Agentic SDLC"** from the configuration picker — picking the wrong one (or the default) leaves you without the tools this track needs. +> When you create your Codespace, the repo offers **many** dev container configurations (one per MicroHack track). Do **not** create a Codespace with default settings. Select **"New with options..."**, then be sure to select **"Azure / AI / Agentic SDLC"** from the configuration picker — picking the wrong one (or the default) leaves you without the tools this track needs. ## Goal @@ -17,7 +17,7 @@ The Octocat team is behind schedule and adopting Agentic Development practices. * **Get the code** — Open your team's copy of this MicroHack in **GitHub Codespaces** (recommended), a local **VS Code Dev Container** (`.devcontainer/`), or locally. Confirm you're on the intended working branch. * **Verify runtimes** — Confirm the runtimes for your chosen API track are available (TypeScript, Python, .NET, or Java) plus Node.js for the frontend. In Codespaces / the Dev Container these are preinstalled. * **Enable GitHub Copilot** — Sign in and confirm your Copilot entitlement is active in your IDE; install the GitHub Copilot / Copilot Chat extensions. -* **Explore the repo** — Review the structure: `src/` (the app, with `src/docs/` architecture guidance), `infra/` + `scripts/` (Azure deployment), `assets/` (Challenge 3 backlog & WorkIQ inputs), and `.github/` (Copilot custom instructions & agents). Identify where the frontend, APIs, and database live. Read `src/README.md` for the code structure and build commands. +* **Explore the repo** — Review the structure: `src/` (the app, with `src/docs/` architecture guidance), `infra/` + `scripts/` (Azure setup and deployment), `assets/` (Challenge 3 backlog & WorkIQ inputs), and `.github/` (Copilot custom instructions & agents). Identify where the frontend, APIs, and database live. Read `src/README.md` for the code structure and build commands. * **Run the app locally** — Start the frontend and one API to confirm the application builds and runs before you begin changing it. ## Success criteria diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/setup-oidc.sh b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/setup-oidc.sh new file mode 100644 index 000000000..f599e3578 --- /dev/null +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/scripts/setup-oidc.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Configure GitHub Actions -> Azure federated (OIDC) authentication using a +# USER-ASSIGNED MANAGED IDENTITY. +# +# Advantages over the app-registration approach: +# - The identity is an Azure resource, so it lives/dies with the resource group +# and is managed via ARM RBAC rather than Entra directory permissions. +# - No Application Administrator / directory write privileges required. +# - It can additionally be attached to Azure resources (ACR pull, Key Vault, ...). +# +# Creates the identity, grants it Contributor on the target resource group, adds +# federated credentials for this repo, and pushes the ids into GitHub Actions +# secrets/variables. +# +# Prereqs: az login, and (optionally) gh auth login with a token that can write +# Actions secrets. If the gh write fails the values are printed for manual entry. +# +# Usage: +# ./scripts/setup-oidc.sh [-g ] [-l ] [-r ] [-b ] [-e ] [-n ] + +set -euo pipefail + +RESOURCE_GROUP="" +LOCATION="" +REPO="" +BRANCH="main" +GH_ENVIRONMENT="" +IDENTITY_NAME="" +ROLE="Contributor" + +while getopts "g:l:r:b:e:n:R:h" opt; do + case "$opt" in + g) RESOURCE_GROUP="$OPTARG" ;; + l) LOCATION="$OPTARG" ;; + r) REPO="$OPTARG" ;; + b) BRANCH="$OPTARG" ;; + e) GH_ENVIRONMENT="$OPTARG" ;; + n) IDENTITY_NAME="$OPTARG" ;; + R) ROLE="$OPTARG" ;; + h) sed -n '2,19p' "$0"; exit 0 ;; + *) exit 1 ;; + esac +done + +command -v az >/dev/null || { echo "az CLI not found" >&2; exit 1; } +command -v gh >/dev/null || { echo "gh CLI not found" >&2; exit 1; } + +az account show >/dev/null 2>&1 || { echo "Run 'az login' first." >&2; exit 1; } + +if [[ -z "$REPO" ]]; then + REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) +fi + +PLAIN_REPO_SUBJECT_PREFIX="repo:$REPO" +EMU_REPO_SUBJECT_PREFIX="" +if repo_subject_parts=$(gh api "repos/$REPO" --jq '[.owner.login, .owner.id, .name, .id] | @tsv' 2>/dev/null); then + read -r REPO_OWNER REPO_OWNER_ID REPO_NAME REPO_ID <<<"$repo_subject_parts" + if [[ -n "$REPO_OWNER" && -n "$REPO_OWNER_ID" && -n "$REPO_NAME" && -n "$REPO_ID" ]]; then + EMU_REPO_SUBJECT_PREFIX="repo:${REPO_OWNER}@${REPO_OWNER_ID}/${REPO_NAME}@${REPO_ID}" + fi +else + echo "! Could not read GitHub repository ids; skipping Enterprise Managed User OIDC subjects." +fi + +# Managed identity names allow only alphanumerics, hyphens and underscores. +[[ -n "$IDENTITY_NAME" ]] || IDENTITY_NAME="id-gh-oidc-$(echo "${REPO//\//-}" | tr -cd '[:alnum:]-_')" + +SUBSCRIPTION_ID=$(az account show --query id -o tsv) +TENANT_ID=$(az account show --query tenantId -o tsv) + +echo "Repo: $REPO" +echo "Subscription: $SUBSCRIPTION_ID" +echo "Tenant: $TENANT_ID" +echo "Resource group: $RESOURCE_GROUP ($LOCATION)" +echo "Identity: $IDENTITY_NAME (user-assigned managed identity)" +echo "Role: $ROLE" +echo + +# --- 1. Resource group ------------------------------------------------------- +az group create --name "$RESOURCE_GROUP" --location "$LOCATION" -o none +echo "✓ resource group ready" + +# --- 2. User-assigned managed identity --------------------------------------- +if az identity show --name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1; then + echo "✓ reusing existing managed identity" +else + az identity create \ + --name "$IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --location "$LOCATION" \ + -o none + echo "✓ created managed identity" +fi + +read -r CLIENT_ID PRINCIPAL_ID IDENTITY_RESOURCE_ID <<<"$(az identity show \ + --name "$IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --query "[clientId, principalId, id]" -o tsv | paste -s)" + +# --- 3. Role assignment (scoped to the resource group) ----------------------- +SCOPE="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP" +# --assignee-object-id + principal type skips the Graph lookup, which also avoids +# a race against replication of a freshly created identity. +az role assignment create \ + --assignee-object-id "$PRINCIPAL_ID" \ + --assignee-principal-type ServicePrincipal \ + --role "$ROLE" \ + --scope "$SCOPE" \ + -o none 2>/dev/null || true +echo "✓ $ROLE granted on $SCOPE" + +# --- 4. Federated credentials ------------------------------------------------ +add_federated_credential() { + local name="$1" subject="$2" + if [[ "$(az identity federated-credential list \ + --identity-name "$IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --query "[?subject=='$subject'] | length(@)" -o tsv)" != "0" ]]; then + echo " · federated credential already exists: $subject" + return + fi + az identity federated-credential create \ + --name "$name" \ + --identity-name "$IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --issuer "https://token.actions.githubusercontent.com" \ + --subject "$subject" \ + --audiences "api://AzureADTokenExchange" \ + -o none + echo " · added federated credential: $subject" +} + +add_federated_credential "gh-branch-$BRANCH" "$PLAIN_REPO_SUBJECT_PREFIX:ref:refs/heads/$BRANCH" +add_federated_credential "gh-pull-request" "$PLAIN_REPO_SUBJECT_PREFIX:pull_request" +if [[ -n "$GH_ENVIRONMENT" ]]; then + add_federated_credential "gh-env-$GH_ENVIRONMENT" "$PLAIN_REPO_SUBJECT_PREFIX:environment:$GH_ENVIRONMENT" +fi +if [[ -n "$EMU_REPO_SUBJECT_PREFIX" ]]; then + add_federated_credential "gh-emu-branch-$BRANCH" "$EMU_REPO_SUBJECT_PREFIX:ref:refs/heads/$BRANCH" + add_federated_credential "gh-emu-pull-request" "$EMU_REPO_SUBJECT_PREFIX:pull_request" + if [[ -n "$GH_ENVIRONMENT" ]]; then + add_federated_credential "gh-emu-env-$GH_ENVIRONMENT" "$EMU_REPO_SUBJECT_PREFIX:environment:$GH_ENVIRONMENT" + fi +fi +echo "✓ federated credentials configured" + +# --- 5. GitHub secrets + variables ------------------------------------------ +# The default Codespaces GITHUB_TOKEN cannot write Actions secrets (403), so fall +# back to printing the values instead of failing after the Azure side is done. +gh_write_ok=1 +push_secret() { gh secret set "$1" --repo "$REPO" --body "$2" 2>/dev/null || gh_write_ok=0; } +push_variable() { gh variable set "$1" --repo "$REPO" --body "$2" 2>/dev/null || gh_write_ok=0; } + +push_secret AZURE_CLIENT_ID "$CLIENT_ID" +push_secret AZURE_TENANT_ID "$TENANT_ID" +push_secret AZURE_SUBSCRIPTION_ID "$SUBSCRIPTION_ID" +push_variable AZURE_RESOURCE_GROUP "$RESOURCE_GROUP" +push_variable AZURE_LOCATION "$LOCATION" +push_variable AZURE_IDENTITY_RESOURCE_ID "$IDENTITY_RESOURCE_ID" + +echo +if [[ "$gh_write_ok" == "1" ]]; then + echo "✓ GitHub secrets and variables set" + echo "Done. AZURE_CLIENT_ID=$CLIENT_ID" + echo "Verify with: gh workflow run deploy.yml --repo $REPO" +else + cat < +then reference \$AZURE_IDENTITY_RESOURCE_ID as the container app's userAssignedIdentity. +EOF diff --git a/03-Azure/01-04-AI/01_Agentic_SDLC/walkthrough/challenge-05/solution-05.md b/03-Azure/01-04-AI/01_Agentic_SDLC/walkthrough/challenge-05/solution-05.md index c22f7e2d7..f84186488 100644 --- a/03-Azure/01-04-AI/01_Agentic_SDLC/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-04-AI/01_Agentic_SDLC/walkthrough/challenge-05/solution-05.md @@ -32,7 +32,7 @@ Deploy and verify ~10 min ### Task 3: Configure secure auth -- 🔑 Use **OIDC federated credentials** for Azure auth, not long-lived secrets. The **most common wall** is the federated-credential subject (`repo:/:ref:refs/heads/main` or an environment subject) or a missing role. If auth fails in the pipeline, check this first and confirm the identity has Contributor on your scope. +- 🔑 Use **OIDC federated credentials** (with User-Assigned Managed Identity) for Azure auth, not long-lived secrets. The **most common wall** is the federated-credential subject (`repo:/:ref:refs/heads/main` or an environment subject) or a missing role. If auth fails in the pipeline, check this first and confirm the identity has Contributor on your scope. Note that [`setup-oidc.sh`](../../scripts/setup-oidc.sh) is bundled in `scripts/` to help with this configuration. ### Task 4: Deploy and verify