add: initial configuration

This commit is contained in:
Srđan Božović
2019-01-27 23:51:20 +01:00
parent e6ef000f7a
commit 84b6ef7c72
13 changed files with 998 additions and 0 deletions
@@ -0,0 +1,168 @@
# Function App that help automate Managed Instance related tasks
### Contents
[About this sample](#about-this-sample)<br/>
[Before you begin](#before-you-begin)<br/>
[Deploy and configure this sample](#deploy-configure-this-sample)<br/>
[Run this sample](#run-this-sample)<br/>
[Troubleshoot](#troubleshoot)<br/>
[Disclaimers](#disclaimers)<br/>
[Related links](#related-links)<br/>
<a name=about-this-sample></a>
## 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
<a name=before-you-begin></a>
## 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
<a name=deploy-configure-this-sample></a>
## 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 = '<function-app-object-id>'
# 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.
<a name=run-this-sample></a>
## 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 = "<subscription-id>"
$resourceGroupName = "<managed-instance-resource-group>"
$managedInstanceName = "<managed-instance-name>"
$functionAppName = "<function-app-name>"
$code = "<function-app-key>"
$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
```
<a name=troubleshoot></a>
## 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.
<a name=disclaimers></a>
## 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.
<a name=related-links></a>
## Related Links
<!-- Links to more articles. Remember to delete "en-us" from the link path. -->
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)
@@ -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
@@ -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
@@ -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<IActionResult> 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<string> GetManagedInstanceIdAsync(HttpRequest req)
{
var parameters = await FunctionUtils.ParseRequestAsync<AssignDirectoryReadersRoleFunctionRequest>(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<ManagedInstance> GetManagedInstanceAsync(string id)
{
var azureClient = new AzureClient();
return await azureClient.GetManagedInstanceAsync(id).ConfigureAwait(false);
}
private async static Task<DirectoryRole> 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<bool> 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;
}
}
}
@@ -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<T> ParseRequestAsync<T>(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<T>(requestBody);
}
}
}
}
@@ -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<string> 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<T> GetJsonAsync<T>(string path)
{
var cts = new CancellationTokenSource();
return GetJsonAsync<T>(path, cts.Token);
}
public async Task<T> GetJsonAsync<T>(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<T>(responseContent);
}
else
{
if(response == null)
throw new Exception($"Call to '{path}' failed.");
var message = response.ReasonPhrase;
throw new Exception($"[{message}]: '{path}'.");
}
}
public Task<TResult> PostJsonAsync<TResult, T>(string path, T value)
{
var cts = new CancellationTokenSource();
return PostJsonAsync<TResult, T>(path, value, cts.Token);
}
public async Task<TResult> PostJsonAsync<TResult, T>(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<TResult>(responseContent);
}
else
{
if (response == null)
throw new Exception($"Call to '{path}' failed.");
var message = response.ReasonPhrase;
throw new Exception($"[{message}]: '{path}'.");
}
}
public Task<bool> PostAsync<T>(string path, T value)
{
var cts = new CancellationTokenSource();
return PostAsync<T>(path, value, cts.Token);
}
public async Task<bool> PostAsync<T>(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}'.");
}
}
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.0.3" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.24" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>
@@ -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<ManagedInstance> GetManagedInstanceAsync(string id)
{
var restApiClient = new RestApiClient("https://management.azure.com", "2015-05-01-preview");
return await restApiClient.GetJsonAsync<ManagedInstance>(id).ConfigureAwait(false);
}
public async Task<DirectoryRoles> GetAzureADRolesAsync(string tenantId)
{
var restApiClient = new RestApiClient("https://graph.windows.net", "1.6");
return await restApiClient.GetJsonAsync<DirectoryRoles>($"/{tenantId}/directoryRoles").ConfigureAwait(false);
}
public async Task<DirectoryRoleTemplates> GetAzureADRoleTemplatesAsync(string tenantId)
{
var restApiClient = new RestApiClient("https://graph.windows.net", "1.6");
return await restApiClient.GetJsonAsync<DirectoryRoleTemplates>($"/{tenantId}/directoryRoleTemplates").ConfigureAwait(false);
}
public async Task<DirectoryUsers> GetAzureADDirectoryRoleMembersAsync(string tenantId, string roleObjectId)
{
var restApiClient = new RestApiClient("https://graph.windows.net", "1.6");
return await restApiClient.GetJsonAsync<DirectoryUsers>($"/{tenantId}/directoryRoles/{roleObjectId}/members").ConfigureAwait(false);
}
private class EnableAzureADRoleFunctionArguments
{
public string RoleTemplateId { get; set; }
}
public async Task<DirectoryRole> EnableAzureADRoleAsync(string tenantId, string roleTemplateId)
{
var restApiClient = new RestApiClient("https://graph.windows.net", "1.6");
return await restApiClient.PostJsonAsync<DirectoryRole, EnableAzureADRoleFunctionArguments>($"/{tenantId}/directoryRoles", new EnableAzureADRoleFunctionArguments { RoleTemplateId = roleTemplateId }).ConfigureAwait(false);
}
private class AddAzureADDirectoryRoleMembersArguments
{
public string Url { get; set; }
}
public async Task<bool> 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);
}
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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<object> 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<object> ProvisionedPlans { get; set; }
public List<object> ProvisioningErrors { get; set; }
public List<object> ProxyAddresses { get; set; }
public DateTime? RefreshTokensValidFromDateTime { get; set; }
public object ShowInAddressList { get; set; }
public List<object> 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<object> 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<object> AddIns { get; set; }
public List<string> 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<object> 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<object> Oauth2Permissions { get; set; }
public List<object> PasswordCredentials { get; set; }
public object PreferredTokenSigningKeyThumbprint { get; set; }
public object PublisherName { get; set; }
public List<object> ReplyUrls { get; set; }
public object SamlMetadataUrl { get; set; }
public List<string> ServicePrincipalNames { get; set; }
public string ServicePrincipalType { get; set; }
public object SignInAudience { get; set; }
public List<object> Tags { get; set; }
public object TokenEncryptionKeyId { get; set; }
}
public class DirectoryUsers
{
public DirectoryUser[] Value { get; set; }
}
}
@@ -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; }
}
}