From f9892bb7d46823a3d94b3c1cdfac62e61b3f0c10 Mon Sep 17 00:00:00 2001 From: KarishmaGhiya Date: Wed, 13 Mar 2024 19:44:52 -0700 Subject: [PATCH] [Identity] Managed Identity test automation: Azure Functions and Webapps (#28554) --- .gitignore | 4 + eng/.docsettings.yml | 1 + sdk/identity/identity/.gitignore | 2 + .../AzureFunctions/RunTest/function.json | 17 + .../AzureFunctions/RunTest/host.json | 15 + .../RunTest/local.settings.json | 7 + .../AzureFunctions/RunTest/package.json | 29 ++ .../authenticateToStorageFunction.ts | 61 ++++ .../AzureFunctions/RunTest/tsconfig.json | 14 + .../integration/AzureKubernetes/Dockerfile | 20 ++ .../integration/AzureKubernetes/package.json | 22 ++ .../integration/AzureKubernetes/src/index.ts | 48 +++ .../integration/AzureKubernetes/tsconfig.json | 13 + .../integration/AzureWebApps/package.json | 27 ++ .../integration/AzureWebApps/src/index.ts | 60 ++++ .../integration/AzureWebApps/tsconfig.json | 13 + sdk/identity/identity/package.json | 2 +- sdk/identity/identity/test-resources.bicep | 1 - .../integration/azureFunctionsTest.spec.ts | 39 ++ .../test/integration/azureWebAppsTest.spec.ts | 34 ++ sdk/identity/identity/tsconfig.json | 2 +- sdk/identity/test-resources-post.ps1 | 139 ++++++++ sdk/identity/test-resources-pre.ps1 | 59 ++++ sdk/identity/test-resources.bicep | 332 ++++++++++++++++++ 24 files changed, 958 insertions(+), 3 deletions(-) create mode 100644 sdk/identity/identity/integration/AzureFunctions/RunTest/function.json create mode 100644 sdk/identity/identity/integration/AzureFunctions/RunTest/host.json create mode 100644 sdk/identity/identity/integration/AzureFunctions/RunTest/local.settings.json create mode 100644 sdk/identity/identity/integration/AzureFunctions/RunTest/package.json create mode 100644 sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts create mode 100644 sdk/identity/identity/integration/AzureFunctions/RunTest/tsconfig.json create mode 100644 sdk/identity/identity/integration/AzureKubernetes/Dockerfile create mode 100644 sdk/identity/identity/integration/AzureKubernetes/package.json create mode 100644 sdk/identity/identity/integration/AzureKubernetes/src/index.ts create mode 100644 sdk/identity/identity/integration/AzureKubernetes/tsconfig.json create mode 100644 sdk/identity/identity/integration/AzureWebApps/package.json create mode 100644 sdk/identity/identity/integration/AzureWebApps/src/index.ts create mode 100644 sdk/identity/identity/integration/AzureWebApps/tsconfig.json delete mode 100644 sdk/identity/identity/test-resources.bicep create mode 100644 sdk/identity/identity/test/integration/azureFunctionsTest.spec.ts create mode 100644 sdk/identity/identity/test/integration/azureWebAppsTest.spec.ts create mode 100644 sdk/identity/test-resources-post.ps1 create mode 100644 sdk/identity/test-resources-pre.ps1 create mode 100644 sdk/identity/test-resources.bicep diff --git a/.gitignore b/.gitignore index cadfa4fd91e..e966622f3d0 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,7 @@ sdk/template/template-dpg/src/src # tshy .tshy-build-tmp + +# sshkey +sdk/**/sshkey +sdk/**/sshkey.pub diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml index 689081ab526..befbcdb9c23 100644 --- a/eng/.docsettings.yml +++ b/eng/.docsettings.yml @@ -23,6 +23,7 @@ omitted_paths: - sdk/storage/storage-datalake/README.md - sdk/storage/storage-internal-avro/* - sdk/test-utils/*/README.md + - sdk/identity/identity/integration/* language: js root_check_enabled: True diff --git a/sdk/identity/identity/.gitignore b/sdk/identity/identity/.gitignore index 3d6981104f4..ba21a232df7 100644 --- a/sdk/identity/identity/.gitignore +++ b/sdk/identity/identity/.gitignore @@ -1,3 +1,5 @@ src/**/*.js +integration/AzureFunctions/app.zip +integration/AzureWebApps/.azure/ !assets/fake-cert.pem !assets/fake-cert-password.pem diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/function.json b/sdk/identity/identity/integration/AzureFunctions/RunTest/function.json new file mode 100644 index 00000000000..f79d5fe8bf3 --- /dev/null +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/function.json @@ -0,0 +1,17 @@ +{ + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "authLevel": "anonymous", + "methods": ["get"], + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "./dist/authenticateToStorageFunction.js" +} diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/host.json b/sdk/identity/identity/integration/AzureFunctions/RunTest/host.json new file mode 100644 index 00000000000..9abc15037e0 --- /dev/null +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.0.0, 5.0.0)" + } + } \ No newline at end of file diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/local.settings.json b/sdk/identity/identity/integration/AzureFunctions/RunTest/local.settings.json new file mode 100644 index 00000000000..8cba42ef274 --- /dev/null +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node" + }, + "ConnectionStrings": {} + } \ No newline at end of file diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/package.json b/sdk/identity/identity/integration/AzureFunctions/RunTest/package.json new file mode 100644 index 00000000000..0242646db4f --- /dev/null +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/package.json @@ -0,0 +1,29 @@ +{ + "name": "@azure-samples/azure-function-test", + "version": "1.0.0", + "description": "", + "main": "dist/authenticateToStorageFunction.js", + "scripts": { + "build": "tsc", + "build:production": "npm run prestart && npm prune --production", + "clean": "rimraf --glob dist dist-*", + "prestart": "npm run build:production && func extensions install", + "start:host": "func start --typescript", + "start": "npm-run-all --parallel start:host watch", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@azure/identity": "^4.0.0", + "@azure/storage-blob": "^12.17.0", + "@azure/functions": "^4.1.0", + "applicationinsights": "^2.9.2", + "tslib": "^1.10.0" + }, + "devDependencies": { + "npm-run-all": "^4.1.5", + "typescript": "^5.3.3", + "rimraf": "^5.0.5" + } +} diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts b/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts new file mode 100644 index 00000000000..de747a680da --- /dev/null +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/src/functions/authenticateToStorageFunction.ts @@ -0,0 +1,61 @@ + +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 { + try { + context.log('Http function was triggered.'); + //parse the request body + await authToStorageHelper(context); + + return { + // status: 200, /* Defaults to 200 */ + body: "Successfully authenticated with storage", + }; + } catch (error: any) { + context.log(error); + return { + status: 400, + body: error, + }; + } +}; + +app.http('authenticateStorage', { + methods: ['GET', 'POST'], + authLevel: "anonymous", + 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 account1 = process.env.IDENTITY_STORAGE_NAME_1; + const account2 = process.env.IDENTITY_STORAGE_NAME_2; + + const credential2 = new ManagedIdentityCredential({ "clientId": 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") + let iter = client1.listContainers(); + let i = 1; + context.log("Client with system assigned identity"); + let containerItem = await iter.next(); + while (!containerItem.done) { + context.log(`Container ${i++}: ${containerItem.value.name}`); + containerItem = await iter.next(); + } + + 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(); + while (!containerItem.done) { + context.log(`Container ${i++}: ${containerItem.value.name}`); + containerItem = await iter.next(); + } + +} diff --git a/sdk/identity/identity/integration/AzureFunctions/RunTest/tsconfig.json b/sdk/identity/identity/integration/AzureFunctions/RunTest/tsconfig.json new file mode 100644 index 00000000000..62ac3de554d --- /dev/null +++ b/sdk/identity/identity/integration/AzureFunctions/RunTest/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "strict": true, + "alwaysStrict": true, + "outDir": "dist", + }, + "include": ["src"] + } + \ No newline at end of file diff --git a/sdk/identity/identity/integration/AzureKubernetes/Dockerfile b/sdk/identity/identity/integration/AzureKubernetes/Dockerfile new file mode 100644 index 00000000000..143b832a548 --- /dev/null +++ b/sdk/identity/identity/integration/AzureKubernetes/Dockerfile @@ -0,0 +1,20 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +ARG NODE_VERSION=20 + +# docker can't tell when the repo has changed and will therefore cache this layer +# internal users should provide MCR registry to build via 'docker build . --build-arg REGISTRY="mcr.microsoft.com/mirror/docker/library/"' +# public OSS users should simply leave this argument blank or ignore its presence entirely +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 /azure-sdk-for-js/sdk/identity/identity/test/integration/AzureKubernetes +RUN npm install +RUN npm install -g typescript +RUN tsc -p . +CMD ["node", "index"] diff --git a/sdk/identity/identity/integration/AzureKubernetes/package.json b/sdk/identity/identity/integration/AzureKubernetes/package.json new file mode 100644 index 00000000000..ba94e773afb --- /dev/null +++ b/sdk/identity/identity/integration/AzureKubernetes/package.json @@ -0,0 +1,22 @@ +{ + "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" + } + } diff --git a/sdk/identity/identity/integration/AzureKubernetes/src/index.ts b/sdk/identity/identity/integration/AzureKubernetes/src/index.ts new file mode 100644 index 00000000000..a35f0bb35d8 --- /dev/null +++ b/sdk/identity/identity/integration/AzureKubernetes/src/index.ts @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 00000000000..48d7948cbd2 --- /dev/null +++ b/sdk/identity/identity/integration/AzureKubernetes/tsconfig.json @@ -0,0 +1,13 @@ +{ + "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 new file mode 100644 index 00000000000..8cce2f60ae3 --- /dev/null +++ b/sdk/identity/identity/integration/AzureWebApps/package.json @@ -0,0 +1,27 @@ +{ + "name": "@azure-samples/azure-web-apps-test", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "clean": "rimraf --glob dist dist-*", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@azure/identity": "^4.0.0", + "@azure/storage-blob": "^12.17.0", + "express": "^4.18.2", + "tslib": "^1.10.0" + }, + "devDependencies": { + "npm-run-all": "^4.1.5", + "typescript": "^5.3.3", + "@types/express": "^4.17.21", + "dotenv": "16.4.4", + "rimraf": "^5.0.5" + } +} diff --git a/sdk/identity/identity/integration/AzureWebApps/src/index.ts b/sdk/identity/identity/integration/AzureWebApps/src/index.ts new file mode 100644 index 00000000000..235c7b4e634 --- /dev/null +++ b/sdk/identity/identity/integration/AzureWebApps/src/index.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import express from "express"; +import { ManagedIdentityCredential } from "@azure/identity"; +import { BlobServiceClient } from "@azure/storage-blob"; +import dotenv from "dotenv"; +// Initialize the environment +dotenv.config(); +const app = express(); + +app.get("/", (req: express.Request, res: express.Response) => { + 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); + let iter = client1.listContainers(); + let i = 0; + 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(); + } + console.log("Client with system assigned identity"); + console.log("Properties of the 1st client =", iter); + systemSuccessMessage = "Successfully acquired token with system-assigned ManagedIdentityCredential" + console.log(systemSuccessMessage); + } + 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); + let iter = client2.listContainers(); + let i = 0; + 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) { + 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/integration/AzureWebApps/tsconfig.json b/sdk/identity/identity/integration/AzureWebApps/tsconfig.json new file mode 100644 index 00000000000..d6dc7035956 --- /dev/null +++ b/sdk/identity/identity/integration/AzureWebApps/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "strict": true, + "alwaysStrict": true, + "outDir": "dist", + }, + "include": ["src"] + } \ No newline at end of file diff --git a/sdk/identity/identity/package.json b/sdk/identity/identity/package.json index de3eac1d411..e244d7c2182 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-js-input -- --timeout 180000 'dist-esm/test/public/node/*.spec.js' 'dist-esm/test/internal/node/*.spec.js'", + "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-resources.bicep b/sdk/identity/identity/test-resources.bicep deleted file mode 100644 index b3490d3b50a..00000000000 --- a/sdk/identity/identity/test-resources.bicep +++ /dev/null @@ -1 +0,0 @@ -param baseName string diff --git a/sdk/identity/identity/test/integration/azureFunctionsTest.spec.ts b/sdk/identity/identity/test/integration/azureFunctionsTest.spec.ts new file mode 100644 index 00000000000..a989f0a973d --- /dev/null +++ b/sdk/identity/identity/test/integration/azureFunctionsTest.spec.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ServiceClient } from "@azure/core-client"; +import { createPipelineRequest } from "@azure/core-rest-pipeline"; +import { assert } from "chai"; +import { Context } from "mocha"; +import { isLiveMode } from "@azure-tools/test-recorder"; + +describe("AzureFunctions Integration test", function () { + it("test the Azure Functions endpoint where the sync MI credential is used.", async function (this: Context) { + if (!isLiveMode()) { + this.skip(); + } + const baseUri = baseUrl(); + const client = new ServiceClient({ baseUri: baseUri }); + const pipelineRequest = createPipelineRequest({ + url: baseUri, + method: "GET", + }); + const response = await client.sendRequest(pipelineRequest); + console.log(response.bodyAsText); + assert.equal(response.status, 200, `Expected status 200. Received ${response.status}`); + assert.equal( + response.bodyAsText, + "Successfully authenticated with storage", + `Expected message: "Successfully authenticated with storage". Received message: ${response.bodyAsText}`, + ); + }); +}); + +function baseUrl(): string { + const functionName = process.env.IDENTITY_FUNCTION_NAME; + if (!functionName) { + console.log("IDENTITY_FUNCTION_NAME is not set"); + throw new Error("IDENTITY_FUNCTION_NAME is not set"); + } + return `https://${functionName}.azurewebsites.net/api/authenticateStorage`; +} diff --git a/sdk/identity/identity/test/integration/azureWebAppsTest.spec.ts b/sdk/identity/identity/test/integration/azureWebAppsTest.spec.ts new file mode 100644 index 00000000000..ce77e9ddd09 --- /dev/null +++ b/sdk/identity/identity/test/integration/azureWebAppsTest.spec.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ServiceClient } from "@azure/core-client"; +import { createPipelineRequest } from "@azure/core-rest-pipeline"; +import { assert } from "chai"; +import { Context } from "mocha"; +import { isLiveMode } from "@azure-tools/test-recorder"; + +describe("AzureWebApps Integration test", function () { + it("test the Azure Web Apps endpoint where the MI credential is used.", async function (this: Context) { + if (!isLiveMode()) { + this.skip(); + } + const baseUri = baseUrl(); + const client = new ServiceClient({ baseUri: baseUri }); + const pipelineRequest = createPipelineRequest({ + url: baseUri, + method: "GET", + }); + const response = await client.sendRequest(pipelineRequest); + console.log(response.bodyAsText); + assert.equal(response.status, 200, `Expected status 200. Received ${response.status}`); + }); +}); + +function baseUrl(): string { + const webAppName = process.env.IDENTITY_WEBAPP_NAME; + if (!webAppName) { + console.log("IDENTITY_WEBAPP_NAME is not set"); + throw new Error("IDENTITY_WEBAPP_NAME is not set"); + } + return `https://${webAppName}.azurewebsites.net/sync`; +} diff --git a/sdk/identity/identity/tsconfig.json b/sdk/identity/identity/tsconfig.json index dc4de9e400d..ff9ab311ff4 100644 --- a/sdk/identity/identity/tsconfig.json +++ b/sdk/identity/identity/tsconfig.json @@ -10,5 +10,5 @@ } }, "include": ["src/**/*", "test/**/*", "samples-dev/**/*.ts"], - "exclude": ["test/manual*/**/*", "node_modules"] + "exclude": ["test/manual*/**/*", "integration/**", "node_modules"] } diff --git a/sdk/identity/test-resources-post.ps1 b/sdk/identity/test-resources-post.ps1 new file mode 100644 index 00000000000..f0108f4fc09 --- /dev/null +++ b/sdk/identity/test-resources-post.ps1 @@ -0,0 +1,139 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# IMPORTANT: Do not invoke this file directly. Please instead run eng/New-TestResources.ps1 from the repository root. + +param ( + [Parameter(ValueFromRemainingArguments = $true)] + $RemainingArguments, + + [Parameter()] + [hashtable] $DeploymentOutputs +) + +# If not Linux, skip this script. +# if ($isLinux -ne "Linux") { +# Write-Host "Skipping post-deployment because not running on Linux." +# return +# } + +$ErrorActionPreference = 'Continue' +$PSNativeCommandUseErrorActionPreference = $true + +$webappRoot = "$PSScriptRoot/identity/integration" | Resolve-Path +$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'] + +# Azure Functions app deployment +Write-Host "Building the code for functions app" +Push-Location "$webappRoot/AzureFunctions/RunTest" +npm install +npm run build +Pop-Location +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 + +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 diff --git a/sdk/identity/test-resources-pre.ps1 b/sdk/identity/test-resources-pre.ps1 new file mode 100644 index 00000000000..cb1f6dc1276 --- /dev/null +++ b/sdk/identity/test-resources-pre.ps1 @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# IMPORTANT: Do not invoke this file directly. Please instead run eng/New-TestResources.ps1 from the repository root. +[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] +param ( + # Captures any arguments from eng/New-TestResources.ps1 not declared here (no parameter errors). + [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) + +) + +Import-Module -Name $PSScriptRoot/../../eng/common/scripts/X509Certificate2 -Verbose + +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" +} +$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 new file mode 100644 index 00000000000..881450bd0c8 --- /dev/null +++ b/sdk/identity/test-resources.bicep @@ -0,0 +1,332 @@ +@minLength(6) +@maxLength(23) +@description('The base resource name.') +param baseName string = resourceGroup().name + +@description('The location of the resource. By default, this is the same as the resource group.') +param location string = resourceGroup().location + +@description('The client OID to grant access to test resources.') +param testApplicationOid string + +@minLength(5) +@maxLength(50) +@description('Provide a globally unique name of the Azure Container Registry') +param acrName string = 'acr${uniqueString(resourceGroup().id)}' + +@description('The latest AKS version available in the region.') +param latestAksVersion string + +@description('The SSH public key to use for the Linux VMs.') +param sshPubKey string + +@description('The admin user name for the Linux VMs.') +param adminUserName string = 'azureuser' + +// 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 websiteContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772') // Website Contributor + +// Cluster parameters +var kubernetesVersion = latestAksVersion + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: baseName + location: location +} + +resource blobRoleWeb 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(resourceGroup().id, blobOwner) + properties: { + principalId: web.identity.principalId + roleDefinitionId: blobOwner + principalType: 'ServicePrincipal' + } +} + +resource blobRoleFunc 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(resourceGroup().id, blobOwner, 'azureFunction') + properties: { + principalId: azureFunction.identity.principalId + roleDefinitionId: blobOwner + principalType: 'ServicePrincipal' + } +} + +resource blobRoleCluster 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(resourceGroup().id, blobOwner, 'kubernetes') + properties: { + principalId: kubernetesCluster.identity.principalId + roleDefinitionId: blobOwner + principalType: 'ServicePrincipal' + } +} + +resource blobRole2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount2 + name: guid(resourceGroup().id, blobOwner, userAssignedIdentity.id) + properties: { + principalId: userAssignedIdentity.properties.principalId + roleDefinitionId: blobOwner + principalType: 'ServicePrincipal' + } +} + +resource webRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: web + name: guid(resourceGroup().id, websiteContributor, 'web') + properties: { + principalId: testApplicationOid + roleDefinitionId: websiteContributor + principalType: 'ServicePrincipal' + } +} + +resource webRole2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: azureFunction + name: guid(resourceGroup().id, websiteContributor, 'azureFunction') + properties: { + principalId: testApplicationOid + roleDefinitionId: websiteContributor + principalType: 'ServicePrincipal' + } +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: baseName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Hot' + } +} + +resource storageAccount2 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: '${baseName}2' + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Hot' + } +} + +resource farm 'Microsoft.Web/serverfarms@2021-03-01' = { + name: '${baseName}_farm' + location: location + sku: { + name: 'B1' + tier: 'Basic' + size: 'B1' + family: 'B' + capacity: 1 + } + properties: { + reserved: true + } + kind: 'app,linux' +} + +resource web 'Microsoft.Web/sites@2022-09-01' = { + name: '${baseName}webapp' + location: location + kind: 'app' + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}' : { } + } + } + properties: { + enabled: true + serverFarmId: farm.id + httpsOnly: true + keyVaultReferenceIdentity: 'SystemAssigned' + siteConfig: { + linuxFxVersion: 'NODE|18-lts' + http20Enabled: true + minTlsVersion: '1.2' + appSettings: [ + { + name: 'AZURE_REGIONAL_AUTHORITY_NAME' + value: 'eastus' + } + { + name: 'IDENTITY_STORAGE_NAME_1' + value: storageAccount.name + } + { + name: 'IDENTITY_STORAGE_NAME_2' + value: storageAccount2.name + } + { + name: 'IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID' + value: userAssignedIdentity.properties.clientId + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + ] + } + } +} + +resource azureFunction 'Microsoft.Web/sites@2022-09-01' = { + name: '${baseName}func' + location: location + kind: 'functionapp' + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}' : { } + } + } + properties: { + enabled: true + serverFarmId: farm.id + httpsOnly: true + keyVaultReferenceIdentity: 'SystemAssigned' + siteConfig: { + alwaysOn: true + http20Enabled: true + minTlsVersion: '1.2' + appSettings: [ + { + name: 'IDENTITY_STORAGE_NAME_1' + value: storageAccount.name + } + { + name: 'IDENTITY_STORAGE_NAME_2' + value: storageAccount2.name + } + { + name: 'IDENTITY_USER_DEFINED_IDENTITY_CLIENT_ID' + value: userAssignedIdentity.properties.clientId + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + { + name: 'WEBSITE_CONTENTSHARE' + value: toLower('${baseName}-func') + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' + } + { + name: 'DOCKER_CUSTOM_IMAGE_NAME' + value: 'mcr.microsoft.com/azure-functions/node:4-node18-appservice-stage3' + } + ] + } + } +} + +resource publishPolicyWeb 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01' = { + kind: 'app' + parent: web + name: 'scm' + properties: { + allow: true + } +} + +resource publishPolicyFunction 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01' = { + kind: 'functionapp' + parent: azureFunction + name: 'scm' + properties: { + allow: true + } +} + +resource acrResource 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { + name: acrName + location: location + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: true + } +} + +resource kubernetesCluster 'Microsoft.ContainerService/managedClusters@2023-06-01' = { + name: baseName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + kubernetesVersion: kubernetesVersion + enableRBAC: true + dnsPrefix: 'identitytest' + agentPoolProfiles: [ + { + name: 'agentpool' + count: 1 + vmSize: 'Standard_D2s_v3' + osDiskSizeGB: 128 + osDiskType: 'Managed' + kubeletDiskType: 'OS' + type: 'VirtualMachineScaleSets' + enableAutoScaling: false + orchestratorVersion: kubernetesVersion + mode: 'System' + osType: 'Linux' + osSKU: 'Ubuntu' + } + ] + linuxProfile: { + adminUsername: adminUserName + ssh: { + publicKeys: [ + { + keyData: sshPubKey + } + ] + } + } + oidcIssuerProfile: { + enabled: true + } + securityProfile: { + workloadIdentity: { + enabled: true + } + } + } +} + +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_IDENTITY_NAME string = userAssignedIdentity.name +output IDENTITY_STORAGE_NAME_1 string = storageAccount.name +output IDENTITY_STORAGE_NAME_2 string = storageAccount2.name +output IDENTITY_FUNCTION_NAME string = azureFunction.name +output IDENTITY_AKS_CLUSTER_NAME string = kubernetesCluster.name +output IDENTITY_AKS_POD_NAME string = 'javascript-test-app' +output IDENTITY_ACR_NAME string = acrResource.name +output IDENTITY_ACR_LOGIN_SERVER string = acrResource.properties.loginServer