diff --git a/sdk/resourcemanager/azure-resourcemanager/README.md b/sdk/resourcemanager/azure-resourcemanager/README.md index 0a3c61d15527..dd2047889fc3 100644 --- a/sdk/resourcemanager/azure-resourcemanager/README.md +++ b/sdk/resourcemanager/azure-resourcemanager/README.md @@ -60,6 +60,8 @@ See [API design][design] for general introduction on design and key concepts on See [Samples][sample] for code snippets and samples. +See [runnable samples][samples_readme] for self-contained programs demonstrating common management scenarios. + ## Troubleshooting ## Next steps @@ -82,4 +84,5 @@ For details on contributing to this repository, see the [contributing guide](htt [azure_core_http_netty]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/core/azure-core-http-netty [authenticate]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/docs/AUTH.md [sample]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/docs/SAMPLE.md +[samples_readme]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/README.md [design]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/docs/DESIGN.md diff --git a/sdk/resourcemanager/azure-resourcemanager/assets.json b/sdk/resourcemanager/azure-resourcemanager/assets.json index 1bbbbd7e1d74..fbd4daca8a8f 100644 --- a/sdk/resourcemanager/azure-resourcemanager/assets.json +++ b/sdk/resourcemanager/azure-resourcemanager/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/resourcemanager/azure-resourcemanager", - "Tag": "java/resourcemanager/azure-resourcemanager_ac929eccb7" + "Tag": "java/resourcemanager/azure-resourcemanager_b40f5da6f5" } diff --git a/sdk/resourcemanager/azure-resourcemanager/pom.xml b/sdk/resourcemanager/azure-resourcemanager/pom.xml index e7068bec7ff7..09cf3028f301 100644 --- a/sdk/resourcemanager/azure-resourcemanager/pom.xml +++ b/sdk/resourcemanager/azure-resourcemanager/pom.xml @@ -315,6 +315,29 @@ + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.1 + + + + add-samples-resources + generate-test-resources + + add-test-resource + + + + + src/samples/resources + + + + + + + org.jacoco jacoco-maven-plugin diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/README.md b/sdk/resourcemanager/azure-resourcemanager/src/samples/README.md new file mode 100644 index 000000000000..3978a197a115 --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/README.md @@ -0,0 +1,94 @@ +--- +page_type: sample +languages: + - java +products: + - azure +urlFragment: azure-resourcemanager-samples +--- + +# Azure Resource Manager samples for Java + +This contains Java samples for managing Azure resources with the Azure Resource Manager client library. Each sample is +self-contained, focuses on a specific scenario, and exposes a `main()` entrypoint so it can be run directly. + +## Getting started + +Getting started is explained in detail [here][RESOURCEMANAGER_README_GETTING_STARTED]. +Please refer to it to add the dependency and configure the authentication environment variables. + +For general documentation on how to use the management libraries, please [visit here][MGMT_SDK_LINK]. +For additional code snippets grouped by service, see [SAMPLE.md][RESOURCEMANAGER_SAMPLE_DOC]. + +## Sample details + +### App Service + +- [Connect a web app to a SQL database (passwordless)][sample_connect_web_app_to_sql_database] +- [Connect a web app to a storage account (passwordless)][sample_connect_web_app_to_storage_account] +- [Deploy an image from Azure Container Registry to a Linux web app][sample_deploy_image_from_acr_to_linux_web_app] +- [Manage web app deployment slots][sample_manage_web_app_slots] +- [Manage a web app with a custom domain][sample_manage_web_app_with_custom_domain] +- [Scale a web app across multiple regions with a Traffic Manager][sample_scale_web_app_with_traffic_manager] + +### Authorization (Microsoft Entra ID / RBAC) + +- [Manage users, groups and roles][sample_manage_users_groups_and_roles] +- [Manage service principals][sample_manage_service_principal] + +### Compute + +- [Create a virtual machine from a custom image][sample_create_vm_using_custom_image_from_vm] +- [Create a virtual machine from a specialized managed disk (snapshot)][sample_create_vm_using_specialized_disk_from_snapshot] +- [Create multiple virtual machines in parallel in the same network][sample_manage_vms_in_parallel] + +### Container Registry + +- [Manage container registries][sample_manage_container_registry] + +### Container Service + +- [Manage an Azure Kubernetes Service (AKS) cluster][sample_manage_kubernetes_cluster] + +### SQL Database + +- [Create and manage a SQL database][sample_manage_sql_database] +- [Manage SQL databases across multiple regions][sample_manage_sql_databases_across_regions] + +## Next steps + +Start using the Azure Resource Manager Java SDK in your solutions. Our SDK details can be found at [SDK README][RESOURCEMANAGER_SDK_README]. + +For more information about other Azure Management SDKs, refer [here][MGMT_SDK_LINK]. + +## Contributing + +For details on contributing to this repository, see the [contributing guide][RESOURCEMANAGER_README_CONTRIBUTING]. + +1. Fork it +1. Create your feature branch (`git checkout -b my-new-feature`) +1. Commit your changes (`git commit -am 'Add some feature'`) +1. Push to the branch (`git push origin my-new-feature`) +1. Create new Pull Request + + +[MGMT_SDK_LINK]: https://aka.ms/azsdk/java/mgmt +[RESOURCEMANAGER_SDK_README]: https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/resourcemanager/azure-resourcemanager +[RESOURCEMANAGER_README_GETTING_STARTED]: https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/resourcemanager/azure-resourcemanager#getting-started +[RESOURCEMANAGER_README_CONTRIBUTING]: https://github.com/Azure/azure-sdk-for-java/blob/main/CONTRIBUTING.md +[RESOURCEMANAGER_SAMPLE_DOC]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/docs/SAMPLE.md +[sample_connect_web_app_to_sql_database]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToSqlDatabase.java +[sample_connect_web_app_to_storage_account]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToStorageAccount.java +[sample_deploy_image_from_acr_to_linux_web_app]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/DeployImageFromAcrToLinuxWebApp.java +[sample_manage_web_app_slots]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppSlots.java +[sample_manage_web_app_with_custom_domain]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppWithCustomDomain.java +[sample_scale_web_app_with_traffic_manager]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ScaleWebAppWithTrafficManager.java +[sample_manage_users_groups_and_roles]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageUsersGroupsAndRoles.java +[sample_manage_service_principal]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageServicePrincipal.java +[sample_create_vm_using_custom_image_from_vm]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/compute/samples/CreateVirtualMachineUsingCustomImageFromVM.java +[sample_create_vm_using_specialized_disk_from_snapshot]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/compute/samples/CreateVirtualMachineUsingSpecializedDiskFromSnapshot.java +[sample_manage_vms_in_parallel]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/compute/samples/ManageVirtualMachinesInParallel.java +[sample_manage_container_registry]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/containerregistry/samples/ManageContainerRegistry.java +[sample_manage_kubernetes_cluster]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/containerservice/samples/ManageKubernetesCluster.java +[sample_manage_sql_database]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/sql/samples/ManageSqlDatabase.java +[sample_manage_sql_databases_across_regions]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/sql/samples/ManageSqlDatabasesAcrossRegions.java diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToSqlDatabase.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToSqlDatabase.java index ac9e33f2797c..97d48ab6fdf7 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToSqlDatabase.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToSqlDatabase.java @@ -45,6 +45,7 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { try { // Create a web app with a system-assigned managed identity; that identity is used to reach SQL. + // HTTPS-only is enforced; minimum TLS 1.2 and FTPS-only are already the App Service defaults. WebApp app = azureResourceManager.webApps() .define(appName) .withRegion(Region.US_WEST) @@ -52,6 +53,7 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { .withNewWindowsPlan(PricingTier.STANDARD_S1) .withJavaVersion(JavaVersion.JAVA_11) .withWebContainer(WebContainer.TOMCAT_9_0_NEWEST) + .withHttpsOnly(true) .withSystemAssignedManagedServiceIdentity() .create(); diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToStorageAccount.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToStorageAccount.java index 0e453696b623..f3c727ef94ba 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToStorageAccount.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ConnectWebAppToStorageAccount.java @@ -4,25 +4,41 @@ package com.azure.resourcemanager.appservice.samples; import com.azure.core.credential.TokenCredential; +import com.azure.core.exception.HttpResponseException; import com.azure.core.http.policy.HttpLogDetailLevel; import com.azure.core.management.Region; import com.azure.core.management.profile.AzureProfile; import com.azure.core.models.AzureCloud; import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.resourcemanager.AzureResourceManager; -import com.azure.resourcemanager.appservice.models.ConnectionStringType; -import com.azure.resourcemanager.appservice.models.JavaVersion; import com.azure.resourcemanager.appservice.models.PricingTier; +import com.azure.resourcemanager.appservice.models.RuntimeStack; import com.azure.resourcemanager.appservice.models.WebApp; -import com.azure.resourcemanager.appservice.models.WebContainer; +import com.azure.resourcemanager.authorization.models.BuiltInRole; +import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; import com.azure.resourcemanager.samples.SampleUtils; import com.azure.resourcemanager.storage.models.StorageAccount; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.time.Duration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + /** - * Azure App Service sample for connecting a web app to a storage account. - * - Create a storage account - * - Create a web app - * - Add the storage account connection string to the web app settings + * Azure App Service sample for connecting a web app to a storage account without secrets (passwordless). + * - Create a storage account with shared-key access disabled + * - Create a Node.js web app with a system-assigned managed identity + * - Grant that identity the Storage Blob Data Contributor role on the account + * - Expose only the blob endpoint (no account key or connection string) to the web app + * - Deploy a small Node.js app that reads/writes a blob using that managed identity + *

+ * The deployed app authenticates to Blob storage with {@code DefaultAzureCredential}, which picks up the web app's + * managed identity. Its source is under {@code src/samples/resources/appservice/mi-blob-verify}. */ public final class ConnectWebAppToStorageAccount { @@ -39,30 +55,57 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { final String containerName = SampleUtils.randomResourceName(azureResourceManager, "jcontainer", 20); try { - // Create a storage account for the web app to use. + // Create a storage account with shared-key access disabled, so data-plane access requires + // Microsoft Entra credentials (passwordless) rather than an account key. StorageAccount storageAccount = azureResourceManager.storageAccounts() .define(storageName) .withRegion(Region.JAPAN_EAST) .withNewResourceGroup(rgName) + .disableSharedKeyAccess() .create(); - String accountKey = storageAccount.getKeys().get(0).value(); - String connectionString = String.format("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s", - storageAccount.name(), accountKey); - - // Create a web app and store the storage connection string in its settings. + // Create a Node.js web app with a system-assigned managed identity and expose only the blob endpoint. + // HTTPS-only is enforced here; minimum TLS 1.2 and FTPS-only are already the App Service defaults. WebApp app = azureResourceManager.webApps() .define(appName) .withRegion(Region.JAPAN_EAST) .withExistingResourceGroup(rgName) - .withNewWindowsPlan(PricingTier.STANDARD_S1) - .withJavaVersion(JavaVersion.JAVA_11) - .withWebContainer(WebContainer.TOMCAT_9_0_NEWEST) - .withConnectionString("storage.connectionString", connectionString, ConnectionStringType.CUSTOM) - .withAppSetting("storage.containerName", containerName) + .withNewLinuxPlan(PricingTier.STANDARD_S1) + .withBuiltInImage(RuntimeStack.NODEJS_22_LTS) + .withHttpsOnly(true) + .withSystemAssignedManagedServiceIdentity() + .withAppSetting("STORAGE_BLOB_ENDPOINT", storageAccount.endPoints().primary().blob()) + .withAppSetting("STORAGE_CONTAINER_NAME", containerName) + .create(); + + // Grant the web app's managed identity data-plane access to blobs (least-privilege, passwordless). + azureResourceManager.accessManagement() + .roleAssignments() + .define(SampleUtils.randomUuid(azureResourceManager)) + .forObjectId(app.systemAssignedManagedServiceIdentityPrincipalId()) + .withBuiltInRole(BuiltInRole.STORAGE_BLOB_DATA_CONTRIBUTOR) + .withResourceScope(storageAccount) .create(); + // Deploy the small Node.js app that touches a blob using the web app's managed identity. + // A freshly created web app may briefly have an initial deployment in progress, so retry on HTTP 409. + File appPackage = nodeAppPackage(); + for (int i = 0;; i++) { + try { + app.zipDeploy(appPackage); + break; + } catch (HttpResponseException e) { + if (i >= 5 || e.getResponse() == null || e.getResponse().getStatusCode() != 409) { + throw e; + } + ResourceManagerUtils.sleep(Duration.ofSeconds(30)); + } + } + System.out.println("Connected web app " + app.name() + " to storage account " + storageAccount.name()); + // To verify manually, browse to https://.azurewebsites.net/ once role assignment has propagated; + // the app returns {"ok":true,...} when it can read/write a blob using its managed identity. + System.out.println("Verify passwordless access at: https://" + app.defaultHostname() + "/"); return true; } finally { azureResourceManager.resourceGroups().beginDeleteByName(rgName); @@ -92,6 +135,41 @@ public static void main(String[] args) { } } + /** + * Builds a zip package (server.js + package.json) for the small Node.js verification app from the sample + * resources. The verifier has no npm dependencies, so App Service deploys it without a build step. + * + * @return the zip package file + */ + private static File nodeAppPackage() { + String[] files = { "server.js", "package.json" }; + try { + File zipFile = File.createTempFile("mi-blob-verify", ".zip"); + zipFile.deleteOnExit(); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + byte[] buffer = new byte[4096]; + for (String file : files) { + zos.putNextEntry(new ZipEntry(file)); + try (InputStream is = ConnectWebAppToStorageAccount.class + .getResourceAsStream("/appservice/mi-blob-verify/" + file)) { + if (is == null) { + throw new FileNotFoundException( + "Sample resource not found on classpath: /appservice/mi-blob-verify/" + file); + } + int read; + while ((read = is.read(buffer)) > 0) { + zos.write(buffer, 0, read); + } + } + zos.closeEntry(); + } + } + return zipFile; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + private ConnectWebAppToStorageAccount() { } } diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/DeployImageFromAcrToLinuxWebApp.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/DeployImageFromAcrToLinuxWebApp.java index 2018a559c3d1..f1fce8e70535 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/DeployImageFromAcrToLinuxWebApp.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/DeployImageFromAcrToLinuxWebApp.java @@ -52,6 +52,7 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { String privateImage = azureRegistry.loginServerUrl() + "/samples/tomcat:latest"; // Create a Linux web app that pulls its image from the private registry. + // HTTPS-only is enforced; minimum TLS 1.2 and FTPS-only are already the App Service defaults. WebApp app = azureResourceManager.webApps() .define(appName) .withRegion(region) @@ -60,6 +61,7 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { .withPrivateRegistryImage(privateImage, "https://" + azureRegistry.loginServerUrl()) .withCredentials(acrCredentials.username(), acrCredentials.accessKeys().get(AccessKeyType.PRIMARY)) .withAppSetting("PORT", "8080") + .withHttpsOnly(true) .create(); System.out.println("Deployed image " + privateImage + " to web app " + app.defaultHostname()); diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppSlots.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppSlots.java index 42961bfd4bea..18caac9aaaac 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppSlots.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppSlots.java @@ -39,6 +39,7 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { try { // Create a web app running Tomcat on a Windows plan. + // HTTPS-only is enforced; minimum TLS 1.2 and FTPS-only are already the App Service defaults. WebApp app = azureResourceManager.webApps() .define(appName) .withRegion(Region.US_EAST) @@ -46,6 +47,7 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { .withNewWindowsPlan(PricingTier.STANDARD_S1) .withJavaVersion(JavaVersion.JAVA_11) .withWebContainer(WebContainer.TOMCAT_9_0_NEWEST) + .withHttpsOnly(true) .create(); // Create a staging deployment slot that inherits the production configuration. diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppWithCustomDomain.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppWithCustomDomain.java index d3a67aac24a6..bfe32f0a2650 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppWithCustomDomain.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ManageWebAppWithCustomDomain.java @@ -39,11 +39,13 @@ public static boolean runSample(AzureResourceManager azureResourceManager) { try { // Create a web app with a new app service plan. + // HTTPS-only is enforced; minimum TLS 1.2 and FTPS-only are already the App Service defaults. WebApp app = azureResourceManager.webApps() .define(appName) .withRegion(Region.US_WEST) .withNewResourceGroup(rgName) .withNewWindowsPlan(PricingTier.STANDARD_S1) + .withHttpsOnly(true) .create(); // Purchase an Azure-managed domain. diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ScaleWebAppWithTrafficManager.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ScaleWebAppWithTrafficManager.java index 743a01e4a1e8..542287237d4e 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ScaleWebAppWithTrafficManager.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/appservice/samples/ScaleWebAppWithTrafficManager.java @@ -97,6 +97,7 @@ private static WebApp createWebApp(AzureResourceManager azureResourceManager, St .withExistingLinuxPlan(plan) .withExistingResourceGroup(rgName) .withBuiltInImage(RuntimeStack.JAVA_17_JAVA17) + .withHttpsOnly(true) .create(); } diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageServicePrincipal.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageServicePrincipal.java new file mode 100644 index 000000000000..7d233904ef8a --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageServicePrincipal.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.authorization.samples; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.policy.HttpLogDetailLevel; +import com.azure.core.management.profile.AzureProfile; +import com.azure.core.models.AzureCloud; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.resourcemanager.AzureResourceManager; +import com.azure.resourcemanager.authorization.models.ActiveDirectoryApplication; +import com.azure.resourcemanager.authorization.models.BuiltInRole; +import com.azure.resourcemanager.authorization.models.PasswordCredential; +import com.azure.resourcemanager.authorization.models.RoleAssignment; +import com.azure.resourcemanager.authorization.models.ServicePrincipal; +import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; +import com.azure.resourcemanager.samples.SampleUtils; + +import java.time.Duration; +import java.util.function.Consumer; + +/** + * Azure service principal sample for managing service principals. + * - Create an Active Directory application + * - Create a service principal for the application and assign it the Reader role on the subscription + * - Use the service principal's credential to list the subscription's virtual machines + * - Update the application (add a reply URL) + * - Update the service principal to revoke its password credential and role + * - Delete the application (which also deletes the service principal) + */ +public final class ManageServicePrincipal { + /** + * Main function which runs the actual sample. + * + * @param azureResourceManager instance of AzureResourceManager + * @param profile the profile the sample is running in + * @return true if sample runs successfully + */ + public static boolean runSample(AzureResourceManager azureResourceManager, AzureProfile profile) { + final String appName = SampleUtils.randomResourceName(azureResourceManager, "adapp", 20); + final String passwordName = SampleUtils.randomResourceName(azureResourceManager, "password", 20); + final PasswordHolder passwordHolder = new PasswordHolder(); + ActiveDirectoryApplication application = null; + try { + // Create an Active Directory application. + application = azureResourceManager.accessManagement() + .activeDirectoryApplications() + .define(appName) + .withSignOnUrl("https://github.com/Azure/azure-sdk-for-java/" + appName) + .create(); + + // Create a service principal for the application, add a password credential, and assign the + // Reader role on the subscription (least privilege: the sample only needs to read resources below). + // The service principal must share the application's display name. + ServicePrincipal servicePrincipal = azureResourceManager.accessManagement() + .servicePrincipals() + .define(appName) + .withExistingApplication(application) + .definePasswordCredential(passwordName) + .withPasswordConsumer(passwordHolder) + .attach() + .withNewRoleInSubscription(BuiltInRole.READER, azureResourceManager.subscriptionId()) + .create(); + + // Wait for the service principal and role assignment to propagate. + ResourceManagerUtils.sleep(Duration.ofSeconds(15)); + + // Use the service principal's credential to access the subscription and list its virtual machines. + TokenCredential spCredential + = new ClientSecretCredentialBuilder().tenantId(azureResourceManager.tenantId()) + .clientId(servicePrincipal.applicationId()) + .clientSecret(passwordHolder.password) + .authorityHost(profile.getEnvironment().getActiveDirectoryEndpoint()) + .build(); + long vmCount = AzureResourceManager.authenticate(spCredential, profile) + .withDefaultSubscription() + .virtualMachines() + .list() + .stream() + .count(); + System.out.println("Service principal listed " + vmCount + " virtual machine(s) in the subscription."); + + // Update the application: add a reply URL. + application.update().withReplyUrl("https://github.com/Azure/azure-sdk-for-java").apply(); + + // Update the service principal: revoke its password credential and role assignment. + ServicePrincipal.Update spUpdate = servicePrincipal.update().withoutCredential(passwordName); + for (RoleAssignment roleAssignment : servicePrincipal.roleAssignments()) { + spUpdate.withoutRole(roleAssignment); + } + spUpdate.apply(); + + System.out.println("Revoked the service principal's password credential and role assignment."); + return true; + } finally { + if (application != null) { + // Deleting the application also deletes its service principal. + azureResourceManager.accessManagement().activeDirectoryApplications().deleteById(application.id()); + } + } + } + + private static final class PasswordHolder implements Consumer { + private String password; + + @Override + public void accept(PasswordCredential passwordCredential) { + this.password = passwordCredential.value(); + } + } + + /** + * Main entry point. + * + * @param args the parameters + */ + public static void main(String[] args) { + try { + final AzureProfile profile = new AzureProfile(AzureCloud.AZURE_PUBLIC_CLOUD); + final TokenCredential credential = new DefaultAzureCredentialBuilder() + .authorityHost(profile.getEnvironment().getActiveDirectoryEndpoint()) + .build(); + + AzureResourceManager azureResourceManager = AzureResourceManager.configure() + .withLogLevel(HttpLogDetailLevel.BASIC) + .authenticate(credential, profile) + .withDefaultSubscription(); + + runSample(azureResourceManager, profile); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private ManageServicePrincipal() { + } +} diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageUsersGroupsAndRoles.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageUsersGroupsAndRoles.java new file mode 100644 index 000000000000..ce598448f2f1 --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/ManageUsersGroupsAndRoles.java @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.authorization.samples; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.policy.HttpLogDetailLevel; +import com.azure.core.management.profile.AzureProfile; +import com.azure.core.models.AzureCloud; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.resourcemanager.AzureResourceManager; +import com.azure.resourcemanager.authorization.models.ActiveDirectoryApplication; +import com.azure.resourcemanager.authorization.models.ActiveDirectoryGroup; +import com.azure.resourcemanager.authorization.models.ActiveDirectoryUser; +import com.azure.resourcemanager.authorization.models.BuiltInRole; +import com.azure.resourcemanager.authorization.models.RoleAssignment; +import com.azure.resourcemanager.authorization.models.RoleDefinition; +import com.azure.resourcemanager.authorization.models.ServicePrincipal; +import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; +import com.azure.resourcemanager.samples.SampleUtils; + +import java.time.Duration; + +/** + * Azure Users, Groups and Roles sample. + * - Create a user + * - Assign a role to the user, then revoke it + * - Get a role definition by scope and role name + * - Create a service principal and assign a role to it + * - Create two Active Directory groups + * - Add the user, the service principal and the first group as members of the second group + */ +public final class ManageUsersGroupsAndRoles { + /** + * Main function which runs the actual sample. + * + * @param azureResourceManager instance of AzureResourceManager + * @return true if sample runs successfully + */ + public static boolean runSample(AzureResourceManager azureResourceManager) { + final String subscriptionId = azureResourceManager.subscriptionId(); + final String userEmail = SampleUtils.randomResourceName(azureResourceManager, "test", 15); + final String userName = userEmail.replace("test", "Test "); + final String spName = SampleUtils.randomResourceName(azureResourceManager, "sp", 15); + final String raName1 = SampleUtils.randomUuid(azureResourceManager); + final String raName2 = SampleUtils.randomUuid(azureResourceManager); + final String groupEmail1 = SampleUtils.randomResourceName(azureResourceManager, "group1", 15); + final String groupEmail2 = SampleUtils.randomResourceName(azureResourceManager, "group2", 15); + final String groupName1 = groupEmail1.replace("group1", "Group "); + final String groupName2 = groupEmail2.replace("group2", "Group "); + ActiveDirectoryUser user = null; + ServicePrincipal sp = null; + RoleAssignment spRoleAssignment = null; + ActiveDirectoryGroup group1 = null; + ActiveDirectoryGroup group2 = null; + try { + // Create an AD user. + user = azureResourceManager.accessManagement() + .activeDirectoryUsers() + .define(userName) + .withEmailAlias(userEmail) + .withPassword(SampleUtils.password()) + .create(); + + // Assign the Reader role to the user, then revoke it. + RoleAssignment roleAssignment1 = azureResourceManager.accessManagement() + .roleAssignments() + .define(raName1) + .forUser(user) + .withBuiltInRole(BuiltInRole.READER) + .withSubscriptionScope(subscriptionId) + .create(); + azureResourceManager.accessManagement().roleAssignments().deleteById(roleAssignment1.id()); + + // Get a role definition by scope and role name. + RoleDefinition roleDefinition = azureResourceManager.accessManagement() + .roleDefinitions() + .getByScopeAndRoleName("subscriptions/" + subscriptionId, "Contributor"); + System.out.println("Contributor role definition id: " + roleDefinition.id()); + + // Create a service principal and assign the Contributor role to it. + sp = azureResourceManager.accessManagement() + .servicePrincipals() + .define(spName) + .withNewApplication() + .create(); + // Wait for the service principal to propagate before assigning a role. + ResourceManagerUtils.sleep(Duration.ofSeconds(15)); + + spRoleAssignment = azureResourceManager.accessManagement() + .roleAssignments() + .define(raName2) + .forServicePrincipal(sp) + .withBuiltInRole(BuiltInRole.CONTRIBUTOR) + .withSubscriptionScope(subscriptionId) + .create(); + + // Create two Active Directory groups. + group1 = azureResourceManager.accessManagement() + .activeDirectoryGroups() + .define(groupName1) + .withEmailAlias(groupEmail1) + .create(); + group2 = azureResourceManager.accessManagement() + .activeDirectoryGroups() + .define(groupName2) + .withEmailAlias(groupEmail2) + .create(); + + // Add the user, the service principal and the first group as members of the second group. + group2.update().withMember(user).withMember(sp).withMember(group1).apply(); + + System.out.println("Added user, service principal and group " + groupName1 + " to group " + groupName2); + return true; + } finally { + if (group2 != null) { + azureResourceManager.accessManagement().activeDirectoryGroups().deleteById(group2.id()); + } + if (group1 != null) { + azureResourceManager.accessManagement().activeDirectoryGroups().deleteById(group1.id()); + } + if (spRoleAssignment != null) { + azureResourceManager.accessManagement().roleAssignments().deleteById(spRoleAssignment.id()); + } + if (sp != null) { + // withNewApplication() created an AD application named after the service principal; delete that + // application (which also deletes the service principal) so nothing is left orphaned. Guard the + // lookup because getByName(...) can return null under eventual consistency or a name collision. + ActiveDirectoryApplication application = azureResourceManager.accessManagement() + .activeDirectoryApplications() + .getByName(spName); + if (application != null) { + azureResourceManager.accessManagement().activeDirectoryApplications().deleteById(application.id()); + } + } + if (user != null) { + azureResourceManager.accessManagement().activeDirectoryUsers().deleteById(user.id()); + } + } + } + + /** + * Main entry point. + * + * @param args the parameters + */ + public static void main(String[] args) { + try { + final AzureProfile profile = new AzureProfile(AzureCloud.AZURE_PUBLIC_CLOUD); + final TokenCredential credential = new DefaultAzureCredentialBuilder() + .authorityHost(profile.getEnvironment().getActiveDirectoryEndpoint()) + .build(); + + AzureResourceManager azureResourceManager = AzureResourceManager.configure() + .withLogLevel(HttpLogDetailLevel.BASIC) + .authenticate(credential, profile) + .withDefaultSubscription(); + + runSample(azureResourceManager); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private ManageUsersGroupsAndRoles() { + } +} diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/package-info.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/package-info.java new file mode 100644 index 000000000000..26c505b175e6 --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/authorization/samples/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Microsoft Entra ID (Azure Active Directory) samples. + */ +package com.azure.resourcemanager.authorization.samples; diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/samples/SampleUtils.java b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/samples/SampleUtils.java index 57211a96ddb9..ced67cad174b 100644 --- a/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/samples/SampleUtils.java +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/java/com/azure/resourcemanager/samples/SampleUtils.java @@ -14,6 +14,7 @@ import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPublicKey; import java.util.Base64; +import java.util.UUID; /** * Minimal set of helpers shared by the resource manager samples. @@ -45,6 +46,16 @@ public static String randomUuid(AzureResourceManager azure) { return azure.resourceGroups().manager().internalContext().randomUuid(); } + /** + * Generates a strong password that satisfies Microsoft Entra ID complexity requirements. + * + * @return a strong password + */ + public static String password() { + return "P@0" + Base64.getUrlEncoder().withoutPadding().encodeToString(UUID.randomUUID().toString().getBytes( + StandardCharsets.US_ASCII)).substring(0, 12); + } + /** * Generates an SSH public key in OpenSSH format. *

diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/resources/appservice/mi-blob-verify/package.json b/sdk/resourcemanager/azure-resourcemanager/src/samples/resources/appservice/mi-blob-verify/package.json new file mode 100644 index 000000000000..c9677d98257b --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/resources/appservice/mi-blob-verify/package.json @@ -0,0 +1,8 @@ +{ + "name": "mi-blob-verify", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + } +} diff --git a/sdk/resourcemanager/azure-resourcemanager/src/samples/resources/appservice/mi-blob-verify/server.js b/sdk/resourcemanager/azure-resourcemanager/src/samples/resources/appservice/mi-blob-verify/server.js new file mode 100644 index 000000000000..57fd182ccb9b --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/samples/resources/appservice/mi-blob-verify/server.js @@ -0,0 +1,76 @@ +const http = require('http'); +const https = require('https'); + +// The web app reaches Blob storage with its managed identity (no account key or connection string). +// This verifier has no npm dependencies so it deploys without a build step: it fetches a token from the +// App Service managed-identity endpoint and calls the Blob REST API directly. +const endpoint = (process.env.STORAGE_BLOB_ENDPOINT || '').replace(/\/$/, ''); +const container = process.env.STORAGE_CONTAINER_NAME || 'verify'; +const apiVersion = '2021-08-06'; + +function request(method, url, headers, body) { + return new Promise((resolve, reject) => { + const req = https.request(url, { method, headers }, res => { + const chunks = []; + res.on('data', d => chunks.push(d)); + res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() })); + }); + req.on('error', reject); + if (body) { + req.write(body); + } + req.end(); + }); +} + +function getToken() { + // App Service injects IDENTITY_ENDPOINT / IDENTITY_HEADER for the managed identity. + const url = process.env.IDENTITY_ENDPOINT + + '?resource=https://storage.azure.com/&api-version=2019-08-01'; + return new Promise((resolve, reject) => { + http.get(url, { headers: { 'X-IDENTITY-HEADER': process.env.IDENTITY_HEADER } }, res => { + const chunks = []; + res.on('data', d => chunks.push(d)); + res.on('end', () => { + try { + resolve(JSON.parse(Buffer.concat(chunks).toString()).access_token); + } catch (e) { + reject(e); + } + }); + }).on('error', reject); + }); +} + +async function verify() { + const token = await getToken(); + const auth = { 'Authorization': 'Bearer ' + token, 'x-ms-version': apiVersion, 'x-ms-date': new Date().toUTCString() }; + const content = 'hello-from-managed-identity'; + + // Create the container (ignore 409 Conflict if it already exists). + const create = await request('PUT', endpoint + '/' + container + '?restype=container', auth); + if (create.status !== 201 && create.status !== 409) { + throw new Error('create container failed: ' + create.status + ' ' + create.body); + } + + // Upload and read back a blob using only the managed identity. + const blobUrl = endpoint + '/' + container + '/hello.txt'; + const put = await request('PUT', blobUrl, + Object.assign({ 'x-ms-blob-type': 'BlockBlob', 'Content-Length': Buffer.byteLength(content) }, auth), content); + if (put.status !== 201) { + throw new Error('upload failed: ' + put.status + ' ' + put.body); + } + const get = await request('GET', blobUrl, auth); + return { ok: get.body === content, endpoint, container, read: get.body }; +} + +const server = http.createServer(async (req, res) => { + res.setHeader('content-type', 'application/json'); + try { + res.end(JSON.stringify(await verify())); + } catch (e) { + res.statusCode = 500; + res.end(JSON.stringify({ ok: false, error: e.message })); + } +}); +server.listen(process.env.PORT || 8080, () => console.log('listening')); diff --git a/sdk/resourcemanager/azure-resourcemanager/src/test/java/com/azure/resourcemanager/samples/GraphRbacSampleTests.java b/sdk/resourcemanager/azure-resourcemanager/src/test/java/com/azure/resourcemanager/samples/GraphRbacSampleTests.java new file mode 100644 index 000000000000..02d7573de846 --- /dev/null +++ b/sdk/resourcemanager/azure-resourcemanager/src/test/java/com/azure/resourcemanager/samples/GraphRbacSampleTests.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.samples; + +import com.azure.core.http.HttpPipeline; +import com.azure.core.management.profile.AzureProfile; +import com.azure.core.test.annotation.LiveOnly; +import com.azure.resourcemanager.authorization.samples.ManageServicePrincipal; +import com.azure.resourcemanager.authorization.samples.ManageUsersGroupsAndRoles; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Microsoft Entra ID (Graph/RBAC) samples run live-only: they create users, groups, applications and service + * principals whose secrets (passwords, client secrets, tokens) must never be persisted in a recording, and the + * service-principal re-authentication cannot be played back through the test proxy. + */ +public class GraphRbacSampleTests extends SamplesTestBase { + + private AzureProfile profile; + + @Test + @LiveOnly + public void testManageUsersGroupsAndRoles() { + Assertions.assertTrue(ManageUsersGroupsAndRoles.runSample(azureResourceManager)); + } + + @Test + @LiveOnly + public void testManageServicePrincipal() { + Assertions.assertTrue(ManageServicePrincipal.runSample(azureResourceManager, profile)); + } + + @Override + protected void initializeClients(HttpPipeline httpPipeline, AzureProfile profile) { + super.initializeClients(httpPipeline, profile); + this.profile = profile; + } +}