mirror of
https://github.com/Microsoft/sql-server-samples.git
synced 2025-12-08 14:58:54 +00:00
Merge pull request #515 from srdan-bozovic-msft/master
Function App that helps automate Managed Instance related tasks
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# Function App that helps 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 required permissions 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 or higher
|
||||
2. Azure PowerShell 5.4.2 or higher
|
||||
3. Visual Studio 2017
|
||||
|
||||
**Azure prerequisites:**
|
||||
|
||||
Person who does the setup needs to have following rights:
|
||||
|
||||
1. Azure AD `Privileged Role Administrator` role
|
||||
2. Permissions to add `Readers` permission for Function App principal on any of the following scopes: Managed Instance, Resource group, Subscription. Associated scope depends on deployment and security policies.
|
||||
|
||||
<a name=deploy-configure-this-sample></a>
|
||||
|
||||
## Deploy and configure this sample
|
||||
|
||||
Steps below show how to deploy pre-build package. Alternatively you could deploy Function App using Visual Studio and source code provided with this sample.
|
||||
|
||||
1. Create Function App by 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 Function App by following [Grant access](https://docs.microsoft.com/en-us/azure/role-based-access-control/quickstart-assign-role-user-portal#grant-access). For easier selection, choose `Function App` in `Assign access to` dropbox. It might take up to an hour for this permission grant to become effective.
|
||||
5. Add system-assigned identity by 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) and note generated `Object ID`.
|
||||
6. Run PowerShell below to provide Function App required Azure AD permissions.
|
||||
|
||||
```powershell
|
||||
|
||||
Connect-AzureAD
|
||||
|
||||
$managedInstanceAutomationObjectId = '<function-app-object-id>'
|
||||
|
||||
# Get Azure AD role "Privileged Role Administrator" 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}
|
||||
}
|
||||
|
||||
# Check if service principal is already member of "Privileged Role Administrator" role
|
||||
$allRoleMembers = Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId
|
||||
$selectedRoleMember = $allRoleMembers | where{$_.ObjectId -match $managedInstanceAutomationObjectId}
|
||||
|
||||
if ($selectedRoleMember -eq $null)
|
||||
{
|
||||
# Add principal to "Privileged Role Administrator" 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` to get user name and password. If you are using PowerShell to upload package, put user name and password under single quotes as with double quotes character `$` has special meaning.
|
||||
|
||||
<a name=run-this-sample></a>
|
||||
|
||||
## Run this sample
|
||||
|
||||
Function App exposes functionality through REST API. Sample below shows how you could invoke 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 doesn't 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 at step 5. in [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 at step 4. in [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 at step 4. in [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 at step 6. in [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)
|
||||
+25
@@ -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
|
||||
+264
@@ -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
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -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}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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>
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -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; }
|
||||
}
|
||||
}
|
||||
+23
@@ -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; }
|
||||
}
|
||||
}
|
||||
+86
@@ -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; }
|
||||
}
|
||||
}
|
||||
+46
@@ -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; }
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#Make zip package in this folder available
|
||||
!*.zip
|
||||
Reference in New Issue
Block a user