diff --git a/.gitignore b/.gitignore index e966622f3d04..187494b744c9 100644 --- a/.gitignore +++ b/.gitignore @@ -188,5 +188,5 @@ sdk/template/template-dpg/src/src .tshy-build-tmp # sshkey -sdk/**/sshkey -sdk/**/sshkey.pub +sdk/**/sshKey +sdk/**/sshKey.pub diff --git a/sdk/identity/identity/.gitignore b/sdk/identity/identity/.gitignore index ba21a232df7d..c594ce22f9ce 100644 --- a/sdk/identity/identity/.gitignore +++ b/sdk/identity/identity/.gitignore @@ -1,5 +1,6 @@ src/**/*.js integration/AzureFunctions/app.zip integration/AzureWebApps/.azure/ +integration/kubeconfig.yaml !assets/fake-cert.pem !assets/fake-cert-password.pem diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts b/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts index de747a680dac..b0ac3c372db1 100644 --- a/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts @@ -1,11 +1,13 @@ - import { BlobServiceClient } from "@azure/storage-blob"; import { ManagedIdentityCredential } from "@azure/identity"; import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; -export async function authenticateStorage(request: HttpRequest, context: InvocationContext): Promise { +export async function authenticateStorage( + request: HttpRequest, + context: InvocationContext, +): Promise { try { - context.log('Http function was triggered.'); + context.log("Http function was triggered."); //parse the request body await authToStorageHelper(context); @@ -20,26 +22,26 @@ export async function authenticateStorage(request: HttpRequest, context: Invocat body: error, }; } -}; +} -app.http('authenticateStorage', { - methods: ['GET', 'POST'], +app.http("authenticateStorage", { + methods: ["GET", "POST"], authLevel: "anonymous", - handler: authenticateStorage + handler: authenticateStorage, }); async function authToStorageHelper(context: InvocationContext): Promise { // This will use the system managed identity const credential1 = new ManagedIdentityCredential(); - const clientId = process.env.IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID!; + const clientId = process.env.IDENTITY_USER_DEFINED_CLIENT_ID!; const account1 = process.env.IDENTITY_STORAGE_NAME_1; const account2 = process.env.IDENTITY_STORAGE_NAME_2; - const credential2 = new ManagedIdentityCredential({ "clientId": clientId }); + const credential2 = new ManagedIdentityCredential({ clientId }); const client1 = new BlobServiceClient(`https://${account1}.blob.core.windows.net`, credential1); const client2 = new BlobServiceClient(`https://${account2}.blob.core.windows.net`, credential2); - context.log("Getting containers for storage account client: system managed identity") + context.log("Getting containers for storage account client: system managed identity"); let iter = client1.listContainers(); let i = 1; context.log("Client with system assigned identity"); @@ -49,7 +51,7 @@ async function authToStorageHelper(context: InvocationContext): Promise { containerItem = await iter.next(); } - context.log("Getting properties for storage account client: user assigned managed identity") + context.log("Getting properties for storage account client: user assigned managed identity"); iter = client2.listContainers(); context.log("Client with user assigned identity"); containerItem = await iter.next(); @@ -57,5 +59,4 @@ async function authToStorageHelper(context: InvocationContext): Promise { context.log(`Container ${i++}: ${containerItem.value.name}`); containerItem = await iter.next(); } - } diff --git a/sdk/identity/identity/integration/AzureKubernetes/Dockerfile b/sdk/identity/identity/integration/AzureKubernetes/Dockerfile index 143b832a5485..fbd781bb2648 100644 --- a/sdk/identity/identity/integration/AzureKubernetes/Dockerfile +++ b/sdk/identity/identity/integration/AzureKubernetes/Dockerfile @@ -10,11 +10,12 @@ ARG NODE_VERSION=20 ARG REGISTRY="" FROM ${REGISTRY}node:${NODE_VERSION}-alpine as repo -RUN apk --no-cache add git -RUN git clone https://github.com/azure/azure-sdk-for-js --single-branch --branch main --depth 1 /azure-sdk-for-js +WORKDIR /app -WORKDIR /azure-sdk-for-js/sdk/identity/identity/test/integration/AzureKubernetes -RUN npm install -RUN npm install -g typescript -RUN tsc -p . -CMD ["node", "index"] +COPY . . + +# Install the latest nightly build of identity +RUN npm install --no-package-lock + +# Wait for the test to `exec` into the container and run the script +CMD ["sleep", "infinity"] diff --git a/sdk/identity/identity/integration/AzureKubernetes/index.js b/sdk/identity/identity/integration/AzureKubernetes/index.js new file mode 100644 index 000000000000..479fc0c20b66 --- /dev/null +++ b/sdk/identity/identity/integration/AzureKubernetes/index.js @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const { BlobServiceClient } = require("@azure/storage-blob"); +const { ManagedIdentityCredential, WorkloadIdentityCredential } = require("@azure/identity"); + +async function main() { + const storageAccount = process.env.IDENTITY_STORAGE_NAME_2; + if (!storageAccount) { + throw new Error("Missing IDENTITY_STORAGE_NAME_2 env var"); + } + + const clientId = process.env.IDENTITY_USER_DEFINED_CLIENT_ID; + if (!clientId) { + throw new Error("Missing IDENTITY_USER_DEFINED_CLIENT_ID env var"); + } + + const blobUrl = `https://${storageAccount}.blob.core.windows.net`; + + try { + const blobServiceClient = new BlobServiceClient( + blobUrl, + new ManagedIdentityCredential({ + clientId, + }), + ); + await blobServiceClient.getProperties(); + + // The test looks for this line in the output + console.log("ManagedIdentity: Successfully authenticated with storage"); + } catch (e) { + console.error(e); + } + + try { + const blobServiceClient = new BlobServiceClient( + blobUrl, + new WorkloadIdentityCredential({ + clientId, + }), + ); + await blobServiceClient.getProperties(); + + // The test looks for this line in the output + console.log("WorkloadIdentity: Successfully authenticated with storage"); + } catch (e) { + console.error(e); + } +} + +main().then(console.log).catch(console.error); diff --git a/sdk/identity/identity/integration/AzureKubernetes/package.json b/sdk/identity/identity/integration/AzureKubernetes/package.json index ba94e773afbe..d63bac5d24fc 100644 --- a/sdk/identity/identity/integration/AzureKubernetes/package.json +++ b/sdk/identity/identity/integration/AzureKubernetes/package.json @@ -1,22 +1,13 @@ { - "name": "@azure-samples/azure-kubernetes-test", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "build": "tsc", - "start": "ts-node src/index.ts", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "@azure/identity": "^4.0.0", - "@azure/storage-blob": "^12.17.0", - "tslib": "^1.10.0", - "ts-node": "10.9.2" - }, - "devDependencies": { - "typescript": "^5.3.3" - } + "name": "@azure-samples/azure-kubernetes-test", + "version": "1.0.0", + "description": "A simple node JS script that can be used to test MSI on Kubernetes", + "main": "index.js", + "scripts": {}, + "author": "", + "license": "ISC", + "dependencies": { + "@azure/identity": "dev", + "@azure/storage-blob": "^12.17.0" } +} diff --git a/sdk/identity/identity/integration/AzureKubernetes/src/index.ts b/sdk/identity/identity/integration/AzureKubernetes/src/index.ts deleted file mode 100644 index a35f0bb35d86..000000000000 --- a/sdk/identity/identity/integration/AzureKubernetes/src/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { ManagedIdentityCredential } from "@azure/identity"; -import { BlobServiceClient } from "@azure/storage-blob"; -import * as dotenv from "dotenv"; -// Initialize the environment -dotenv.config(); - -async function main(): Promise { - let systemSuccessMessage = ""; - try{ - const account1 = process.env.IDENTITY_STORAGE_NAME_1; - const account2 = process.env.IDENTITY_STORAGE_NAME_2; - const credentialSystemAssigned = new ManagedIdentityCredential(); - const credentialUserAssigned = new ManagedIdentityCredential({clientId: process.env.IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID}) - const client1 = new BlobServiceClient(`https://${account1}.blob.core.windows.net`, credentialSystemAssigned); - const client2 = new BlobServiceClient(`https://${account2}.blob.core.windows.net`, credentialUserAssigned); - let iter = client1.listContainers(); - - let i = 1; - console.log("Client with system assigned identity"); - let containerItem = await iter.next(); - while (!containerItem.done) { - console.log(`Container ${i++}: ${containerItem.value.name}`); - containerItem = await iter.next(); - } - systemSuccessMessage = "Successfully acquired token with system-assigned ManagedIdentityCredential" - console.log("Client with user assigned identity") - iter = client2.listContainers(); - i = 1; - containerItem = await iter.next(); - while (!containerItem.done) { - console.log(`Container ${i++}: ${containerItem.value.name}`); - containerItem = await iter.next(); - } - console.log("Successfully acquired tokens with async ManagedIdentityCredential") - } - catch(e){ - console.error(`${e} \n ${systemSuccessMessage}`); - } - } - - main().catch((err) => { - console.log("error code: ", err.code); - console.log("error message: ", err.message); - console.log("error stack: ", err.stack); - }); diff --git a/sdk/identity/identity/integration/AzureKubernetes/tsconfig.json b/sdk/identity/identity/integration/AzureKubernetes/tsconfig.json deleted file mode 100644 index 48d7948cbd24..000000000000 --- a/sdk/identity/identity/integration/AzureKubernetes/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "moduleResolution": "node", - "resolveJsonModule": true, - "esModuleInterop": true, - "strict": true, - "alwaysStrict": true, - "outDir": "dist", - "rootDir": "." - } - } \ No newline at end of file diff --git a/sdk/identity/identity/integration/AzureWebApps/package.json b/sdk/identity/identity/integration/AzureWebApps/package.json index 8cce2f60ae33..56ec5c5b4d74 100644 --- a/sdk/identity/identity/integration/AzureWebApps/package.json +++ b/sdk/identity/identity/integration/AzureWebApps/package.json @@ -12,7 +12,7 @@ "author": "", "license": "ISC", "dependencies": { - "@azure/identity": "^4.0.0", + "@azure/identity": "dev", "@azure/storage-blob": "^12.17.0", "express": "^4.18.2", "tslib": "^1.10.0" diff --git a/sdk/identity/identity/integration/AzureWebApps/src/index.ts b/sdk/identity/identity/integration/AzureWebApps/src/index.ts index 235c7b4e6345..23645c9c143d 100644 --- a/sdk/identity/identity/integration/AzureWebApps/src/index.ts +++ b/sdk/identity/identity/integration/AzureWebApps/src/index.ts @@ -10,15 +10,18 @@ dotenv.config(); const app = express(); app.get("/", (req: express.Request, res: express.Response) => { - res.send("Ok") -}) + res.send("Ok"); +}); app.get("/sync", async (req: express.Request, res: express.Response) => { let systemSuccessMessage = ""; try { const account1 = process.env.IDENTITY_STORAGE_NAME_1; const credentialSystemAssigned = new ManagedIdentityCredential(); - const client1 = new BlobServiceClient(`https://${account1}.blob.core.windows.net`, credentialSystemAssigned); + const client1 = new BlobServiceClient( + `https://${account1}.blob.core.windows.net`, + credentialSystemAssigned, + ); let iter = client1.listContainers(); let i = 0; console.log("Client with system assigned identity"); @@ -29,31 +32,35 @@ app.get("/sync", async (req: express.Request, res: express.Response) => { } console.log("Client with system assigned identity"); console.log("Properties of the 1st client =", iter); - systemSuccessMessage = "Successfully acquired token with system-assigned ManagedIdentityCredential" + systemSuccessMessage = + "Successfully acquired token with system-assigned ManagedIdentityCredential"; console.log(systemSuccessMessage); - } - catch (e) { + } catch (e) { console.error(e); } try { const account2 = process.env.IDENTITY_STORAGE_NAME_2; - const credentialUserAssigned = new ManagedIdentityCredential({ clientId: process.env.IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID }) - const client2 = new BlobServiceClient(`https://${account2}.blob.core.windows.net`, credentialUserAssigned); + const credentialUserAssigned = new ManagedIdentityCredential({ + clientId: process.env.IDENTITY_USER_DEFINED_CLIENT_ID, + }); + const client2 = new BlobServiceClient( + `https://${account2}.blob.core.windows.net`, + credentialUserAssigned, + ); let iter = client2.listContainers(); let i = 0; - console.log("Client with user assigned identity") + console.log("Client with user assigned identity"); let containerItem = await iter.next(); while (!containerItem.done) { console.log(`Container ${i++}: ${containerItem.value.name}`); containerItem = await iter.next(); } - res.status(200).send("Successfully acquired tokens with async ManagedIdentityCredential") - } - catch (e) { + res.status(200).send("Successfully acquired tokens with async ManagedIdentityCredential"); + } catch (e) { console.error(e); res.status(500).send(`${e} \n ${systemSuccessMessage}`); } -}) +}); app.listen(8080, () => { console.log(`Authorization code redirect server listening on port 8080`); diff --git a/sdk/identity/identity/package.json b/sdk/identity/identity/package.json index e244d7c21828..6c610c6a07c6 100644 --- a/sdk/identity/identity/package.json +++ b/sdk/identity/identity/package.json @@ -55,7 +55,7 @@ "format": "dev-tool run vendored prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", "check-format": "dev-tool run vendored prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", "integration-test:browser": "echo skipped", - "integration-test:node": "dev-tool run test:node-ts-input -- --timeout 180000 'test/public/node/*.spec.ts' 'test/internal/node/*.spec.ts' 'test/integration/*.spec.ts'", + "integration-test:node": "dev-tool run test:node-ts-input -- --timeout 180000 'test/public/node/*.spec.ts' 'test/internal/node/*.spec.ts' 'test/integration/**/*.spec.ts'", "integration-test": "npm run integration-test:node && npm run integration-test:browser", "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", "lint": "eslint package.json api-extractor.json src test --ext .ts", diff --git a/sdk/identity/identity/test/integration/node/azureKubernetesTest.spec.ts b/sdk/identity/identity/test/integration/node/azureKubernetesTest.spec.ts new file mode 100644 index 000000000000..a352ab2557d9 --- /dev/null +++ b/sdk/identity/identity/test/integration/node/azureKubernetesTest.spec.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { execSync } from "child_process"; +import { isLiveMode } from "@azure-tools/test-recorder"; + +describe("Azure Kubernetes Integration test", function () { + let podOutput: string; + before(async function () { + if (!isLiveMode()) { + this.skip(); + } + const resourceGroup = requireEnvVar("IDENTITY_RESOURCE_GROUP"); + const aksClusterName = requireEnvVar("IDENTITY_AKS_CLUSTER_NAME"); + const subscriptionId = requireEnvVar("IDENTITY_SUBSCRIPTION_ID"); + const podName = requireEnvVar("IDENTITY_AKS_POD_NAME"); + + if (process.env.IDENTITY_CLIENT_SECRET) { + // Log in as service principal in CI + const clientId = requireEnvVar("IDENTITY_CLIENT_ID"); + const clientSecret = requireEnvVar("IDENTITY_CLIENT_SECRET"); + const tenantId = requireEnvVar("IDENTITY_TENANT_ID"); + runCommand( + "az", + `login --service-principal -u ${clientId} -p ${clientSecret} --tenant ${tenantId}`, + ); + } + + runCommand("az", `account set --subscription ${subscriptionId}`); + runCommand( + "az", + `aks get-credentials --resource-group ${resourceGroup} --name ${aksClusterName}`, + ); + const pods = runCommand("kubectl", `get pods -o jsonpath='{.items[0].metadata.name}'`); + assert.include(pods, podName); + + podOutput = runCommand("kubectl", `exec ${podName} -- node /app/index.js`); + }); + + it("can authenticate using managed identity", async function () { + if (!isLiveMode()) { + this.skip(); + } + + assert.include( + podOutput, + "ManagedIdentity: Successfully authenticated with storage", + `Expected ${podOutput} to include a ManagedIdentity success message`, + ); + }); + + it("can authenticate using workload identity", async function () { + if (!isLiveMode()) { + this.skip(); + } + + assert.include( + podOutput, + "WorkloadIdentity: Successfully authenticated with storage", + `Expected ${podOutput} to include a WorkloadIdentity success message`, + ); + }); +}); + +function runCommand(command: string, args: string = ""): string { + return execSync(`${command} ${args}`).toString().trim(); +} + +function requireEnvVar(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Required env var ${name} is not set`); + } + return value; +} diff --git a/sdk/identity/test-resources-post.ps1 b/sdk/identity/test-resources-post.ps1 index f0108f4fc09d..6835cb4bd7b6 100644 --- a/sdk/identity/test-resources-post.ps1 +++ b/sdk/identity/test-resources-post.ps1 @@ -8,14 +8,18 @@ param ( $RemainingArguments, [Parameter()] - [hashtable] $DeploymentOutputs + [hashtable] $DeploymentOutputs, + + [Parameter()] + [switch] $CI = ($null -ne $env:SYSTEM_TEAMPROJECTID) ) -# If not Linux, skip this script. -# if ($isLinux -ne "Linux") { -# Write-Host "Skipping post-deployment because not running on Linux." -# return -# } +$MIClientId = $DeploymentOutputs['IDENTITY_USER_DEFINED_CLIENT_ID'] +$MIName = $DeploymentOutputs['IDENTITY_USER_DEFINED_IDENTITY_NAME'] +$saAccountName = 'workload-identity-sa' +$podName = $DeploymentOutputs['IDENTITY_AKS_POD_NAME'] +$storageName2 = $DeploymentOutputs['IDENTITY_STORAGE_NAME_2'] +$userDefinedClientId = $DeploymentOutputs['IDENTITY_USER_DEFINED_CLIENT_ID'] $ErrorActionPreference = 'Continue' $PSNativeCommandUseErrorActionPreference = $true @@ -25,8 +29,11 @@ $workingFolder = $webappRoot; Write-Host "Working directory: $workingFolder" -az login --service-principal -u $DeploymentOutputs['IDENTITY_CLIENT_ID'] -p $DeploymentOutputs['IDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['IDENTITY_TENANT_ID'] -az account set --subscription $DeploymentOutputs['IDENTITY_SUBSCRIPTION_ID'] +if ($CI) { + Write-Host "Logging in to service principal" + az login --service-principal -u $DeploymentOutputs['IDENTITY_CLIENT_ID'] -p $DeploymentOutputs['IDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['IDENTITY_TENANT_ID'] + az account set --subscription $DeploymentOutputs['IDENTITY_SUBSCRIPTION_ID'] +} # Azure Functions app deployment Write-Host "Building the code for functions app" @@ -38,102 +45,76 @@ Write-Host "starting azure functions deployment" Compress-Archive -Path "$workingFolder/AzureFunctions/RunTest/*" -DestinationPath "$workingFolder/AzureFunctions/app.zip" -Force az functionapp deployment source config-zip -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] -n $DeploymentOutputs['IDENTITY_FUNCTION_NAME'] --src "$workingFolder/AzureFunctions/app.zip" Remove-Item -Force "$workingFolder/AzureFunctions/app.zip" - Write-Host "Deployed function app" -# $image = "$loginServer/identity-functions-test-image" -# docker build --no-cache -t $image "$workingFolder/AzureFunctions" -# docker push $image - -# az functionapp config container set -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] -n $DeploymentOutputs['IDENTITY_FUNCTION_NAME'] -i $image -r $loginServer -p $(az acr credential show -n $DeploymentOutputs['IDENTITY_ACR_NAME'] --query "passwords[0].value" -o tsv) -u $(az acr credential show -n $DeploymentOutputs['IDENTITY_ACR_NAME'] --query username -o tsv) - -# Azure Web Apps app deployment -# Push-Location "$webappRoot/AzureWebApps" -# npm install -# npm run build -# Compress-Archive -Path "$workingFolder/AzureWebApps/*" -DestinationPath "$workingFolder/AzureWebApps/app.zip" -Force -# az webapp deploy --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --name $DeploymentOutputs['IDENTITY_WEBAPP_NAME'] --src-path "$workingFolder/AzureWebApps/app.zip" --async true -# Remove-Item -Force "$workingFolder/AzureWebApps/app.zip" -# Pop-Location +Write-Host "Deplying Identity Web App" Push-Location "$webappRoot/AzureWebApps" npm install npm run build az webapp up --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --name $DeploymentOutputs['IDENTITY_WEBAPP_NAME'] --plan $DeploymentOutputs['IDENTITY_WEBAPP_PLAN'] --runtime NODE:18-lts Pop-Location - -Write-Host "Deployed webapp" -Write-Host "Sleeping for a bit to ensure logs is ready." -Start-Sleep -Seconds 300 - -# Write-Host "Sleeping for a bit to ensure container registry is ready." -# Start-Sleep -Seconds 20 -# Write-Host "trying to login to acr" -# az acr login -n $DeploymentOutputs['IDENTITY_ACR_NAME'] -# $loginServer = az acr show -n $DeploymentOutputs['IDENTITY_ACR_NAME'] --query loginServer -o tsv - -# # Azure Kubernetes Service deployment -# $image = "$loginServer/identity-aks-test-image" -# docker build --no-cache -t $image "$workingFolder/AzureKubernetes" -# docker push $image - -# Attach the ACR to the AKS cluster -# Write-Host "Attaching ACR to AKS cluster" -# az aks update -n $DeploymentOutputs['IDENTITY_AKS_CLUSTER_NAME'] -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --attach-acr $DeploymentOutputs['IDENTITY_ACR_NAME'] - -# $MIClientId = $DeploymentOutputs['IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID'] -# $MIName = $DeploymentOutputs['IDENTITY_USER_DEFINED_IDENTITY_NAME'] -# $SaAccountName = 'workload-identity-sa' -# $PodName = $DeploymentOutputs['IDENTITY_AKS_POD_NAME'] -# $storageName = $DeploymentOutputs['IDENTITY_STORAGE_NAME_2'] - -# # Get the aks cluster credentials -# Write-Host "Getting AKS credentials" -# az aks get-credentials --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --name $DeploymentOutputs['IDENTITY_AKS_CLUSTER_NAME'] - -# #Get the aks cluster OIDC issuer -# Write-Host "Getting AKS OIDC issuer" -# $AKS_OIDC_ISSUER = az aks show -n $DeploymentOutputs['IDENTITY_AKS_CLUSTER_NAME'] -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --query "oidcIssuerProfile.issuerUrl" -otsv - -# # Create the federated identity -# Write-Host "Creating federated identity" -# az identity federated-credential create --name $MIName --identity-name $MIName --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --issuer $AKS_OIDC_ISSUER --subject system:serviceaccount:default:workload-identity-sa - -# # Build the kubernetes deployment yaml -# $kubeConfig = @" -# apiVersion: v1 -# kind: ServiceAccount -# metadata: -# annotations: -# azure.workload.identity/client-id: $MIClientId -# name: $SaAccountName -# namespace: default -# --- -# apiVersion: v1 -# kind: Pod -# metadata: -# name: $PodName -# namespace: default -# labels: -# azure.workload.identity/use: "true" -# spec: -# serviceAccountName: $SaAccountName -# containers: -# - name: $PodName -# image: $image -# env: -# - name: IDENTITY_STORAGE_NAME -# value: "$StorageName" -# ports: -# - containerPort: 80 -# nodeSelector: -# kubernetes.io/os: linux -# "@ - -# Set-Content -Path "$workingFolder/kubeconfig.yaml" -Value $kubeConfig -# Write-Host "Created kubeconfig.yaml with contents:" -# Write-Host $kubeConfig - -# # Apply the config -# kubectl apply -f "$workingFolder/kubeconfig.yaml" --overwrite=true -# Write-Host "Applied kubeconfig.yaml" -# az logout +Write-Host "Deployed Identity Web App" + +Write-Host "Deploying Identity Docker image to ACR" +az acr login -n $DeploymentOutputs['IDENTITY_ACR_NAME'] +$loginServer = az acr show -n $DeploymentOutputs['IDENTITY_ACR_NAME'] --query loginServer -o tsv +$image = "$loginServer/identity-aks-test-image" +docker build --no-cache -t $image "$workingFolder/AzureKubernetes" +docker push $image +Write-Host "Deployed image to ACR" + +Write-Host "Configuring kubernetes to use our image" +az aks update -n $DeploymentOutputs['IDENTITY_AKS_CLUSTER_NAME'] -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --attach-acr $DeploymentOutputs['IDENTITY_ACR_NAME'] + +# Get the aks cluster credentials +Write-Host "Getting AKS credentials" +az aks get-credentials --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --name $DeploymentOutputs['IDENTITY_AKS_CLUSTER_NAME'] + +#Get the aks cluster OIDC issuer +Write-Host "Getting AKS OIDC issuer" +$AKS_OIDC_ISSUER = az aks show -n $DeploymentOutputs['IDENTITY_AKS_CLUSTER_NAME'] -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --query "oidcIssuerProfile.issuerUrl" -otsv + + +# Create the federated identity +Write-Host "Creating federated identity" +az identity federated-credential create --name $MIName --identity-name $MIName --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --issuer $AKS_OIDC_ISSUER --subject system:serviceaccount:default:workload-identity-sa + +# Build the kubernetes deployment yaml +$kubeConfig = @" +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + azure.workload.identity/client-id: $MIClientId + name: $saAccountName + namespace: default +--- +apiVersion: v1 +kind: Pod +metadata: + name: $podName + namespace: default + labels: + azure.workload.identity/use: "true" +spec: + serviceAccountName: $saAccountName + containers: + - name: $podName + image: $image + env: + - name: IDENTITY_STORAGE_NAME_2 + value: "$storageName2" + - name: IDENTITY_USER_DEFINED_CLIENT_ID + value: "$userDefinedClientId" + ports: + - containerPort: 80 + nodeSelector: + kubernetes.io/os: linux +"@ + +Write-Host $kubeConfig +Set-Content -Path "$workingFolder/kubeconfig.yaml" -Value $kubeConfig + +# Apply the config +kubectl apply -f "$workingFolder/kubeconfig.yaml" --overwrite=true +Write-Host "Applied kubeconfig.yaml" diff --git a/sdk/identity/test-resources-pre.ps1 b/sdk/identity/test-resources-pre.ps1 index cb1f6dc12761..a4df025a16ba 100644 --- a/sdk/identity/test-resources-pre.ps1 +++ b/sdk/identity/test-resources-pre.ps1 @@ -8,24 +8,6 @@ param ( [Parameter(ValueFromRemainingArguments = $true)] $RemainingArguments, - [Parameter()] - [string] $Location = '', - - [Parameter()] - [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')] - [string] $TestApplicationId, - - [Parameter()] - [string] $TestApplicationSecret, - - [Parameter()] - [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')] - [string] $SubscriptionId, - - [Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] $TenantId, - [Parameter()] [switch] $CI = ($null -ne $env:SYSTEM_TEAMPROJECTID) @@ -33,27 +15,20 @@ param ( Import-Module -Name $PSScriptRoot/../../eng/common/scripts/X509Certificate2 -Verbose +Remove-Item $PSScriptRoot/sshKey* -Force ssh-keygen -t rsa -b 4096 -f $PSScriptRoot/sshKey -N '' -C '' $sshKey = Get-Content $PSScriptRoot/sshKey.pub $templateFileParameters['sshPubKey'] = $sshKey -Write-Host "Sleeping for a bit to ensure service principal is ready." -Start-Sleep -s 45 - if ($CI) { # Install this specific version of the Azure CLI to avoid https://github.com/Azure/azure-cli/issues/28358. pip install azure-cli=="2.56.0" + # The owner is a service principal + $templateFileParameters['principalUserType'] = 'ServicePrincipal' + Write-Host "Sleeping for a bit to ensure service principal is ready." + Start-Sleep -s 45 } + $az_version = az version Write-Host "Azure CLI version: $az_version" - -az login --service-principal -u $TestApplicationId -p $TestApplicationSecret --tenant $TenantId -az account set --subscription $SubscriptionId -$versions = az aks get-versions -l westus -o json | ConvertFrom-Json -Write-Host "AKS versions: $($versions | ConvertTo-Json -Depth 100)" -$patchVersions = $versions.values | Where-Object { $_.isPreview -eq $null } | Select-Object -ExpandProperty patchVersions -Write-Host "AKS patch versions: $($patchVersions | ConvertTo-Json -Depth 100)" -$latestAksVersion = $patchVersions | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | Sort-Object -Descending | Select-Object -First 1 -Write-Host "Latest AKS version: $latestAksVersion" -$templateFileParameters['latestAksVersion'] = $latestAksVersion diff --git a/sdk/identity/test-resources.bicep b/sdk/identity/test-resources.bicep index 881450bd0c83..63e0a03b17f2 100644 --- a/sdk/identity/test-resources.bicep +++ b/sdk/identity/test-resources.bicep @@ -15,7 +15,7 @@ param testApplicationOid string param acrName string = 'acr${uniqueString(resourceGroup().id)}' @description('The latest AKS version available in the region.') -param latestAksVersion string +param latestAksVersion string = '1.27.7' @description('The SSH public key to use for the Linux VMs.') param sshPubKey string @@ -23,9 +23,12 @@ param sshPubKey string @description('The admin user name for the Linux VMs.') param adminUserName string = 'azureuser' +@description('The user type - ServicePrincipal in CI, User locally') +param principalUserType string = 'User' + // https://learn.microsoft.com/azure/role-based-access-control/built-in-roles -// var blobContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor var blobOwner = subscriptionResourceId('Microsoft.Authorization/roleDefinitions','b7e6dc6d-f1e8-4753-8033-0f276bb0955b') // Storage Blob Data Owner +var serviceOwner = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') var websiteContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772') // Website Contributor // Cluster parameters @@ -61,7 +64,7 @@ resource blobRoleCluster 'Microsoft.Authorization/roleAssignments@2022-04-01' = name: guid(resourceGroup().id, blobOwner, 'kubernetes') properties: { principalId: kubernetesCluster.identity.principalId - roleDefinitionId: blobOwner + roleDefinitionId: serviceOwner principalType: 'ServicePrincipal' } } @@ -71,7 +74,7 @@ resource blobRole2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(resourceGroup().id, blobOwner, userAssignedIdentity.id) properties: { principalId: userAssignedIdentity.properties.principalId - roleDefinitionId: blobOwner + roleDefinitionId: serviceOwner principalType: 'ServicePrincipal' } } @@ -82,7 +85,7 @@ resource webRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { properties: { principalId: testApplicationOid roleDefinitionId: websiteContributor - principalType: 'ServicePrincipal' + principalType: principalUserType } } @@ -92,12 +95,12 @@ resource webRole2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { properties: { principalId: testApplicationOid roleDefinitionId: websiteContributor - principalType: 'ServicePrincipal' + principalType: principalUserType } } resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = { - name: baseName + name: uniqueString(resourceGroup().id) location: location sku: { name: 'Standard_LRS' @@ -109,7 +112,7 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = { } resource storageAccount2 'Microsoft.Storage/storageAccounts@2021-08-01' = { - name: '${baseName}2' + name: '${uniqueString(resourceGroup().id)}2' location: location sku: { name: 'Standard_LRS' @@ -169,7 +172,7 @@ resource web 'Microsoft.Web/sites@2022-09-01' = { value: storageAccount2.name } { - name: 'IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID' + name: 'IDENTITY_USER_DEFINED_CLIENT_ID' value: userAssignedIdentity.properties.clientId } { @@ -210,7 +213,7 @@ resource azureFunction 'Microsoft.Web/sites@2022-09-01' = { value: storageAccount2.name } { - name: 'IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID' + name: 'IDENTITY_USER_DEFINED_CLIENT_ID' value: userAssignedIdentity.properties.clientId } { @@ -321,7 +324,7 @@ resource kubernetesCluster 'Microsoft.ContainerService/managedClusters@2023-06-0 output IDENTITY_WEBAPP_NAME string = web.name output IDENTITY_WEBAPP_PLAN string = farm.name output IDENTITY_USER_DEFINED_IDENTITY string = userAssignedIdentity.id -output IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID string = userAssignedIdentity.properties.clientId +output IDENTITY_USER_DEFINED_CLIENT_ID string = userAssignedIdentity.properties.clientId output IDENTITY_USER_DEFINED_IDENTITY_NAME string = userAssignedIdentity.name output IDENTITY_STORAGE_NAME_1 string = storageAccount.name output IDENTITY_STORAGE_NAME_2 string = storageAccount2.name