From 84b6ef7c72d64c04bbf9af4bb381dc913efe6448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sr=C4=91an=20Bo=C5=BEovi=C4=87?= Date: Sun, 27 Jan 2019 23:51:20 +0100 Subject: [PATCH] add: initial configuration --- .../automation-functions/README.md | 168 +++++++++++ .../src/ManagedInstanceAutomation.sln | 25 ++ .../src/ManagedInstanceAutomation/.gitignore | 264 ++++++++++++++++++ .../AssignDirectoryReadersRoleFunction.cs | 101 +++++++ .../Core/FunctionUtils.cs | 28 ++ .../Core/RestApiClient.cs | 158 +++++++++++ .../ManagedInstanceAutomation.csproj | 19 ++ .../Shared/AzureClient.cs | 58 ++++ .../Shared/DirectoryRoleTemplates.cs | 20 ++ .../Shared/DirectoryRoles.cs | 23 ++ .../Shared/DirectoryUsers.cs | 86 ++++++ .../Shared/ManagedInstance.cs | 46 +++ .../src/ManagedInstanceAutomation/host.json | 2 + 13 files changed, 998 insertions(+) create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/README.md create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation.sln create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/.gitignore create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/AssignDirectoryReadersRoleFunction.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/FunctionUtils.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/RestApiClient.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/ManagedInstanceAutomation.csproj create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/AzureClient.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoleTemplates.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoles.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryUsers.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/ManagedInstance.cs create mode 100644 samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/host.json diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/README.md b/samples/manage/azure-sql-db-managed-instance/automation-functions/README.md new file mode 100644 index 00000000..6baf96c0 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/README.md @@ -0,0 +1,168 @@ +# Function App that help automate Managed Instance related tasks + +### Contents + +[About this sample](#about-this-sample)
+[Before you begin](#before-you-begin)
+[Deploy and configure this sample](#deploy-configure-this-sample)
+[Run this sample](#run-this-sample)
+[Troubleshoot](#troubleshoot)
+[Disclaimers](#disclaimers)
+[Related links](#related-links)
+ + + +## About this sample + +- **Applies to:** Azure SQL Database +- **Key features:** Managed Instance +- **Workload:** n/a +- **Programming Language:** C#, PowerShell +- **Authors:** Srdan Bozovic +- **Update history:** n/a + +This sample shows one approach to Managed Instance management automation using Function App and system-assigned identity. + +With system-assigned identity, Function App could be assigned permissions to invoke proper actions in a safe way. Instead of granting excessive permissions to users admins could grant permission to Function App that exposes very restrained set of functionalities through API. Code running on Function App doesn't have any secrets configured or hardcoded. + +Currently available functions: +- Assign Azure AD Directory Readers permissions to Managed Instance principal + + + +## Before you begin + +To run this sample, you need the following prerequisites. + +**Software prerequisites:** + +1. PowerShell 5.1 +2. Azure PowerShell 5.4.2 or higher +3. Visual Studio 2017 + +**Azure prerequisites:** + +1. Azure AD Privileged Role Administrator role +2. Permissions to add principal with Readers permission on any of the following levels Managed Instance, Resource group, Subscription + + + +## Deploy and configure this sample + +Steps below show how to deploy pre-build package, alternatively you could do that using Visual Studio and source code provided with this sample. + +1. Create Function App following [Create your first function in the Azure portal](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function) quickstart +2. Download [package](./zip-deploy/ManagedInstanceAutomationDemo.zip?raw=true) +3. Publish package using [Azure CLI](https://docs.microsoft.com/en-us/azure/azure-functions/deployment-zip-push#cli), with [cURL](https://docs.microsoft.com/en-us/azure/azure-functions/deployment-zip-push#with-curl) or with [PowerShell](https://docs.microsoft.com/en-us/azure/azure-functions/deployment-zip-push#with-powershell) +4. Grant access to Azure Function following [Grant access](https://docs.microsoft.com/en-us/azure/role-based-access-control/quickstart-assign-role-user-portal#grant-access). Choose `Function App` as option on `Assign access to prompt` +5. Add system-assigned identity and note generated Object ID following [Adding a system-assigned identity](https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?toc=%2fazure%2fazure-functions%2ftoc.json#adding-a-system-assigned-identity) +6. Run PowerShell below to provide Function App required Azure AD permissions. + +```powershell + +Connect-AzureAD + +$managedInstanceAutomationObjectId = '' + +# Get Azure AD role "Directory Users" and create if it doesn't exist +$roleName = "Privileged Role Administrator" +$role = Get-AzureADDirectoryRole | Where-Object {$_.displayName -eq $roleName} +if ($role -eq $null) { + # Instantiate an instance of the role template + $roleTemplate = Get-AzureADDirectoryRoleTemplate | Where-Object {$_.displayName -eq $roleName} + Enable-AzureADDirectoryRole -RoleTemplateId $roleTemplate.ObjectId + $role = Get-AzureADDirectoryRole | Where-Object {$_.displayName -eq $roleName} +} +$role + +# Check if service principal is already member of readers role +$allDirReaders = Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId +$selDirReader = $allDirReaders | where{$_.ObjectId -match $managedInstanceAutomationObjectId} + +if ($selDirReader -eq $null) +{ + # Add principal to privileged role admins role + Write-Output "Adding service principal to 'Privileged Role Administrator' role..." + Add-AzureADDirectoryRoleMember -ObjectId $role.ObjectId -RefObjectId $managedInstanceAutomationObjectId + Write-Output "Service principal added to 'Privileged Role Administrator' role'." +} +else +{ + Write-Output "Service principal is already member of 'Privileged Role Administrator' role'." +} + +``` + +### Note + +In Step 3. use `Get publish profile` do download file with user name and password. If using PowerShell to upload package you will need to escape $ character in password. + + + +## Run this sample + +Function App exposes functionality through REST API. Sample below shows how you could call function using PowerShell. [Here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function#test-the-function) you can find how to get Function App key. + +This [article](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/azure-function?view=azdevops) shows how you can invoke Function App from Azure Pipeline | TFS 2018 | TFS 2017. + +```powershell + +$subscriptionId = "" +$resourceGroupName = "" +$managedInstanceName = "" + +$functionAppName = "" +$code = "" + +$managedInstanceId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Sql/managedInstances/$managedInstanceName" +$apiUrl="https://$functionAppName.azurewebsites.net/api/AssignDirectoryReadersRoleFunction?code=$code" +$body = @{id=$managedInstanceId} | ConvertTo-Json + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Invoke-RestMethod $apiUrl -Method POST -ContentType "application/json" -Body $body + +``` + + + +## Troubleshoot + +If Function App is not configured or run properly you will get HTTP 400 error with error message in plain text. + +Below is list of errors with actions to resolve them. + +### Managed Service Identity (MSI) is not assigned. + +Add system-asigned identity as described in step 5 of [deploy and configure this sample](#deploy-configure-this-sample) section. + +### [Forbidden]: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Sql/managedInstances/{name}'. + +Function App doesn't have permissions to read Managed Instance properties. Add permission as described in step 4 of [deploy and configure this sample](#deploy-configure-this-sample) section. + +### [Not Found]: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Sql/managedInstances/{name}'. + +Function App doesn't have permissions to read Managed Instance properties. Add permission as described in step 4 of [deploy and configure this sample](#deploy-configure-this-sample) section. + +#### [MSI Not Assigned]: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Sql/managedInstances/{name}'. + +Managed Instance you want to enable for Azure AD authentication doesn't have it's own system-asigned identity (this is different from first error in this section where Function App doesn't have identity assigned). + +### [Forbidden]: '/{tenantId}/directoryRoles'. + +Function App doesn't have permissions to read Azure AD. Add permission as described in step 6 of [deploy and configure this sample](#deploy-configure-this-sample) section. + + + +## Disclaimers +The scripts and this guide are copyright Microsoft Corporations and are provided as samples. They are not part of any Azure service and are not covered by any SLA or other Azure-related agreements. They are provided as-is with no warranties express or implied. Microsoft takes no responsibility for the use of the scripts or the accuracy of this document. Familiarize yourself with the scripts before using them. + + + +## Related Links + + +For more information, see these articles: + +- [Azure SQL Database Managed Instance](https://docs.microsoft.com/en-us/azure/sql-database/sql-database-managed-instance-index) +- [Configure and manage Azure Active Directory authentication with SQL](https://docs.microsoft.com/en-us/azure/sql-database/sql-database-aad-authentication-configure) +- [How to use managed identities for App Service and Azure Functions](https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity) \ No newline at end of file diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation.sln b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation.sln new file mode 100644 index 00000000..00aeadeb --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2047 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedInstanceAutomation", "ManagedInstanceAutomation\ManagedInstanceAutomation.csproj", "{786D6608-054E-4D58-991E-E6CD2E288D6C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {786D6608-054E-4D58-991E-E6CD2E288D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {786D6608-054E-4D58-991E-E6CD2E288D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {786D6608-054E-4D58-991E-E6CD2E288D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {786D6608-054E-4D58-991E-E6CD2E288D6C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E9DED34-768D-4B2D-B494-777D5A5832B7} + EndGlobalSection +EndGlobal diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/.gitignore b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/.gitignore new file mode 100644 index 00000000..ff5b00c5 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/AssignDirectoryReadersRoleFunction.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/AssignDirectoryReadersRoleFunction.cs new file mode 100644 index 00000000..c3829180 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/AssignDirectoryReadersRoleFunction.cs @@ -0,0 +1,101 @@ + +using System.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Host; +using Newtonsoft.Json; +using Microsoft.Azure.Services.AppAuthentication; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using System; +using ManagedInstanceAutomation.Core; +using ManagedInstanceAutomation.Shared; +using System.Linq; + +namespace ManagedInstanceAutomation +{ + public static class AssignDirectoryReadersRoleFunction + { + [FunctionName("AssignDirectoryReadersRoleFunction")] + public static async Task Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, ILogger log) + { + try + { + var id = await GetManagedInstanceIdAsync(req); + + var managedInstance = await GetManagedInstanceAsync(id).ConfigureAwait(false); + + var tenantId = managedInstance?.Identity?.TenantId; + var principalId = managedInstance?.Identity?.PrincipalId; + + if (string.IsNullOrEmpty(tenantId)) + throw new Exception($"[MSI Not Assigned]: '{managedInstance.Id}'"); + + var directoryReadersRole = await GetAzureADDirectoryRoleAsync(tenantId, "Directory Readers"); + + return (await AddMemberToAzureADRole(tenantId, directoryReadersRole.ObjectId, principalId).ConfigureAwait(false)) ? + (IActionResult)new NoContentResult() : (IActionResult)new BadRequestResult(); + } + catch(Exception xcp) + { + return new BadRequestObjectResult(xcp.Message); + } + } + + private class AssignDirectoryReadersRoleFunctionRequest + { + public string Id { get; set; } + } + + private async static Task GetManagedInstanceIdAsync(HttpRequest req) + { + var parameters = await FunctionUtils.ParseRequestAsync(req); + + if (string.IsNullOrEmpty(parameters.Id)) + throw new Exception(@"Please pass Managed Instance 'id' in the request body."); + + return parameters.Id; + } + + private async static Task GetManagedInstanceAsync(string id) + { + var azureClient = new AzureClient(); + + return await azureClient.GetManagedInstanceAsync(id).ConfigureAwait(false); + } + + private async static Task GetAzureADDirectoryRoleAsync(string tenantId, string displayName) + { + var azureClient = new AzureClient(); + + var roles = await azureClient.GetAzureADRolesAsync(tenantId).ConfigureAwait(false); + + var directoryReadersRole = roles.Value.FirstOrDefault(r => r.DisplayName == displayName); + + if (directoryReadersRole == null) + { + var roleTemplates = await azureClient.GetAzureADRoleTemplatesAsync(tenantId).ConfigureAwait(false); + + var directoryReaderRoleTemplate = roleTemplates.Value.FirstOrDefault(r => r.displayName == displayName); + + directoryReadersRole = await azureClient.EnableAzureADRoleAsync(tenantId, directoryReaderRoleTemplate.objectId).ConfigureAwait(false); + } + return directoryReadersRole; + } + + private async static Task AddMemberToAzureADRole(string tenantId, string roleId, string memberId) + { + var azureClient = new AzureClient(); + var users = await azureClient.GetAzureADDirectoryRoleMembersAsync(tenantId, roleId).ConfigureAwait(false); + + if (!users.Value.Any(u => u.ObjectId == memberId)) + { + return await azureClient.AddAzureADDirectoryRoleMembersAsync(tenantId, roleId, memberId).ConfigureAwait(false); + } + + return true; + } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/FunctionUtils.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/FunctionUtils.cs new file mode 100644 index 00000000..6c260ce7 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/FunctionUtils.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace ManagedInstanceAutomation.Core +{ + static class FunctionUtils + { + public async static Task ParseRequestAsync(HttpRequest req) + { + if (req == null) + throw new ArgumentNullException("req"); + + using (var rdr = new StreamReader(req.Body)) + { + var requestBody = await rdr.ReadToEndAsync().ConfigureAwait(false); + if(string.IsNullOrEmpty(requestBody)) + throw new Exception("Request body should not be empty."); + + return JsonConvert.DeserializeObject(requestBody); + } + } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/RestApiClient.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/RestApiClient.cs new file mode 100644 index 00000000..fcf844f5 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Core/RestApiClient.cs @@ -0,0 +1,158 @@ +using Microsoft.Azure.Services.AppAuthentication; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ManagedInstanceAutomation.Core +{ + public class RestApiClient + { + private string ResourceType { get; set; } + private string ApiVersion { get; set; } + + public RestApiClient(string resourceType, string apiVersion) + { + ResourceType = resourceType; + ApiVersion = apiVersion; + } + + private async Task GetAccessTokenAsync() + { + try + { + var provider = new AzureServiceTokenProvider(); + return await provider.GetAccessTokenAsync(ResourceType).ConfigureAwait(false); + } + catch + { + throw new Exception("Managed Service Identity (MSI) is not assigned."); + } + } + + private string GetUrl(string path) + { + return $"{ResourceType.TrimEnd('/')}/{path.TrimStart('/')}?api-version={ApiVersion}"; + } + + public Task GetJsonAsync(string path) + { + var cts = new CancellationTokenSource(); + return GetJsonAsync(path, cts.Token); + } + + public async Task GetJsonAsync(string path, CancellationToken cancellationToken) + { + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.AutomaticDecompression = System.Net.DecompressionMethods.GZip; + + var client = new HttpClient(httpClientHandler); + + var accessToken = await GetAccessTokenAsync(); + + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}"); + + var response = await client.GetAsync(GetUrl(path), cancellationToken).ConfigureAwait(false); + if (response != null && + (response.IsSuccessStatusCode)) + { + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonConvert.DeserializeObject(responseContent); + } + else + { + if(response == null) + throw new Exception($"Call to '{path}' failed."); + + var message = response.ReasonPhrase; + throw new Exception($"[{message}]: '{path}'."); + } + } + public Task PostJsonAsync(string path, T value) + { + var cts = new CancellationTokenSource(); + return PostJsonAsync(path, value, cts.Token); + } + + public async Task PostJsonAsync(string path, T value, CancellationToken cancellationToken) + { + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.AutomaticDecompression = System.Net.DecompressionMethods.GZip; + + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + + var client = new HttpClient(httpClientHandler); + + var accessToken = await GetAccessTokenAsync(); + + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}"); + + var json = JsonConvert.SerializeObject(value, serializerSettings); + + var content = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var response = await client.PostAsync(GetUrl(path), content, cancellationToken).ConfigureAwait(false); + if (response != null && + (response.IsSuccessStatusCode)) + { + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonConvert.DeserializeObject(responseContent); + } + else + { + if (response == null) + throw new Exception($"Call to '{path}' failed."); + + var message = response.ReasonPhrase; + throw new Exception($"[{message}]: '{path}'."); + } + } + + public Task PostAsync(string path, T value) + { + var cts = new CancellationTokenSource(); + return PostAsync(path, value, cts.Token); + } + + public async Task PostAsync(string path, T value, CancellationToken cancellationToken) + { + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.AutomaticDecompression = System.Net.DecompressionMethods.GZip; + + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + + var client = new HttpClient(httpClientHandler); + + var accessToken = await GetAccessTokenAsync(); + + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}"); + + var json = JsonConvert.SerializeObject(value, serializerSettings); + + var content = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var response = await client.PostAsync(GetUrl(path), content, cancellationToken).ConfigureAwait(false); + if (response != null && + (response.IsSuccessStatusCode)) + { + return true; + } + else + { + if (response == null) + throw new Exception($"Call to '{path}' failed."); + + var message = response.ReasonPhrase; + throw new Exception($"[{message}]: '{path}'."); + } + } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/ManagedInstanceAutomation.csproj b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/ManagedInstanceAutomation.csproj new file mode 100644 index 00000000..f1cede93 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/ManagedInstanceAutomation.csproj @@ -0,0 +1,19 @@ + + + netstandard2.0 + v2 + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + \ No newline at end of file diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/AzureClient.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/AzureClient.cs new file mode 100644 index 00000000..1aa814ee --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/AzureClient.cs @@ -0,0 +1,58 @@ +using ManagedInstanceAutomation.Core; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace ManagedInstanceAutomation.Shared +{ + public class AzureClient + { + public async Task GetManagedInstanceAsync(string id) + { + var restApiClient = new RestApiClient("https://management.azure.com", "2015-05-01-preview"); + return await restApiClient.GetJsonAsync(id).ConfigureAwait(false); + } + + public async Task GetAzureADRolesAsync(string tenantId) + { + var restApiClient = new RestApiClient("https://graph.windows.net", "1.6"); + return await restApiClient.GetJsonAsync($"/{tenantId}/directoryRoles").ConfigureAwait(false); + } + + public async Task GetAzureADRoleTemplatesAsync(string tenantId) + { + var restApiClient = new RestApiClient("https://graph.windows.net", "1.6"); + return await restApiClient.GetJsonAsync($"/{tenantId}/directoryRoleTemplates").ConfigureAwait(false); + } + + public async Task GetAzureADDirectoryRoleMembersAsync(string tenantId, string roleObjectId) + { + var restApiClient = new RestApiClient("https://graph.windows.net", "1.6"); + return await restApiClient.GetJsonAsync($"/{tenantId}/directoryRoles/{roleObjectId}/members").ConfigureAwait(false); + } + + private class EnableAzureADRoleFunctionArguments + { + public string RoleTemplateId { get; set; } + } + + public async Task EnableAzureADRoleAsync(string tenantId, string roleTemplateId) + { + var restApiClient = new RestApiClient("https://graph.windows.net", "1.6"); + return await restApiClient.PostJsonAsync($"/{tenantId}/directoryRoles", new EnableAzureADRoleFunctionArguments { RoleTemplateId = roleTemplateId }).ConfigureAwait(false); + } + + private class AddAzureADDirectoryRoleMembersArguments + { + public string Url { get; set; } + } + + public async Task AddAzureADDirectoryRoleMembersAsync(string tenantId, string roleObjectId, string memberId) + { + var restApiClient = new RestApiClient("https://graph.windows.net", "1.6"); + var url = $"https://graph.windows.net/{tenantId}/directoryObjects/{memberId}"; + return await restApiClient.PostAsync($"/{tenantId}/directoryRoles/{roleObjectId}/$links/members", new AddAzureADDirectoryRoleMembersArguments { Url = url }).ConfigureAwait(false); + } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoleTemplates.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoleTemplates.cs new file mode 100644 index 00000000..04f4d07c --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoleTemplates.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ManagedInstanceAutomation.Shared +{ + public class DirectoryRoleTemplate + { + public string objectType { get; set; } + public string objectId { get; set; } + public object deletionTimestamp { get; set; } + public string description { get; set; } + public string displayName { get; set; } + } + + public class DirectoryRoleTemplates + { + public DirectoryRoleTemplate[] Value { get; set; } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoles.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoles.cs new file mode 100644 index 00000000..0c1c959d --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryRoles.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ManagedInstanceAutomation.Shared +{ + public class DirectoryRole + { + public string ObjectType { get; set; } + public string ObjectId { get; set; } + public object DeletionTimestamp { get; set; } + public string Description { get; set; } + public string DisplayName { get; set; } + public bool IsSystem { get; set; } + public bool RoleDisabled { get; set; } + public string RoleTemplateId { get; set; } + } + + public class DirectoryRoles + { + public DirectoryRole[] Value { get; set; } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryUsers.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryUsers.cs new file mode 100644 index 00000000..f5c1b009 --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/DirectoryUsers.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ManagedInstanceAutomation.Shared +{ + public class DirectoryUser + { + public string ObjectType { get; set; } + public string ObjectId { get; set; } + public object DeletionTimestamp { get; set; } + public bool AccountEnabled { get; set; } + public object AgeGroup { get; set; } + public object City { get; set; } + public object CompanyName { get; set; } + public object ConsentProvidedForMinor { get; set; } + public object Country { get; set; } + public object CreatedDateTime { get; set; } + public object CreationType { get; set; } + public object Department { get; set; } + public bool? DirSyncEnabled { get; set; } + public string DisplayName { get; set; } + public object EmployeeId { get; set; } + public object FacsimileTelephoneNumber { get; set; } + public string GivenName { get; set; } + public string ImmutableId { get; set; } + public object IsCompromised { get; set; } + public object JobTitle { get; set; } + public DateTime? LastDirSyncTime { get; set; } + public object LegalAgeGroupClassification { get; set; } + public object Mail { get; set; } + public string MailNickname { get; set; } + public object Mobile { get; set; } + public string OnPremisesDistinguishedName { get; set; } + public string OnPremisesSecurityIdentifier { get; set; } + public List OtherMails { get; set; } + public object PasswordPolicies { get; set; } + public object PhysicalDeliveryOfficeName { get; set; } + public object PostalCode { get; set; } + public object PreferredLanguage { get; set; } + public List ProvisionedPlans { get; set; } + public List ProvisioningErrors { get; set; } + public List ProxyAddresses { get; set; } + public DateTime? RefreshTokensValidFromDateTime { get; set; } + public object ShowInAddressList { get; set; } + public List SignInNames { get; set; } + public object SipProxyAddress { get; set; } + public object State { get; set; } + public object StreetAddress { get; set; } + public string Surname { get; set; } + public object TelephoneNumber { get; set; } + public string UsageLocation { get; set; } + public List UserIdentities { get; set; } + public string UserPrincipalName { get; set; } + public object UserState { get; set; } + public object UserStateChangedOn { get; set; } + public string UserType { get; set; } + public List AddIns { get; set; } + public List AlternativeNames { get; set; } + public object AppDisplayName { get; set; } + public string AppId { get; set; } + public object AppOwnerTenantId { get; set; } + public bool? AppRoleAssignmentRequired { get; set; } + public List AppRoles { get; set; } + public object ErrorUrl { get; set; } + public object Homepage { get; set; } + public object InformationalUrls { get; set; } + public object LogoutUrl { get; set; } + public List Oauth2Permissions { get; set; } + public List PasswordCredentials { get; set; } + public object PreferredTokenSigningKeyThumbprint { get; set; } + public object PublisherName { get; set; } + public List ReplyUrls { get; set; } + public object SamlMetadataUrl { get; set; } + public List ServicePrincipalNames { get; set; } + public string ServicePrincipalType { get; set; } + public object SignInAudience { get; set; } + public List Tags { get; set; } + public object TokenEncryptionKeyId { get; set; } + } + + public class DirectoryUsers + { + public DirectoryUser[] Value { get; set; } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/ManagedInstance.cs b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/ManagedInstance.cs new file mode 100644 index 00000000..052d0a7d --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/Shared/ManagedInstance.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ManagedInstanceAutomation.Shared +{ + public class Identity + { + public string PrincipalId { get; set; } + public string Type { get; set; } + public string TenantId { get; set; } + } + + public class Sku + { + public string Name { get; set; } + public string Tier { get; set; } + public string Family { get; set; } + public int Capacity { get; set; } + } + + public class Properties + { + public string FullyQualifiedDomainName { get; set; } + public string AdministratorLogin { get; set; } + public string SubnetId { get; set; } + public string State { get; set; } + public string LicenseType { get; set; } + public int VCores { get; set; } + public int StorageSizeInGB { get; set; } + public string Collation { get; set; } + public string DnsZone { get; set; } + public bool PublicDataEndpointEnabled { get; set; } + } + + public class ManagedInstance + { + public Identity Identity { get; set; } + public Sku Sku { get; set; } + public Properties Properties { get; set; } + public string Location { get; set; } + public string Id { get; set; } + public string Name { get; set; } + public string Type { get; set; } + } +} diff --git a/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/host.json b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/host.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/samples/manage/azure-sql-db-managed-instance/automation-functions/src/ManagedInstanceAutomation/host.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file