Create new key pair for app cert in KeyVault (#24)
- fix comments and warnings - create a new key pair in keyvault instead of in the service - start with deployment script, still lacks a few security settings
This commit is contained in:
Родитель
5ba205f1cc
Коммит
42890acd3a
|
@ -17,7 +17,7 @@ charset = utf-8
|
|||
indent_size = 2
|
||||
|
||||
# project files
|
||||
[*.{sh]
|
||||
[*.{sh}]
|
||||
end_of_line = lf
|
||||
|
||||
[*.{cs}]
|
||||
|
@ -32,7 +32,7 @@ trim_trailing_whitespace = false
|
|||
|
||||
[*.{cs}]
|
||||
# Organize usings
|
||||
dotnet_sort_system_directives_first = false
|
||||
dotnet_sort_system_directives_first = true:warning
|
||||
|
||||
# this. preferences
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
<Project>
|
||||
<Import Project="project.props" />
|
||||
<Import Project="common.props" />
|
||||
<Import Project="version.props" />
|
||||
</Project>
|
||||
|
|
|
@ -22,19 +22,19 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
[Authorize]
|
||||
public class ApplicationController : Controller
|
||||
{
|
||||
private IOpcVault opcVault;
|
||||
private readonly OpcVaultApiOptions opcVaultOptions;
|
||||
private readonly AzureADOptions azureADOptions;
|
||||
private readonly ITokenCacheService tokenCacheService;
|
||||
private IOpcVault _opcVault;
|
||||
private readonly OpcVaultApiOptions _opcVaultOptions;
|
||||
private readonly AzureADOptions _azureADOptions;
|
||||
private readonly ITokenCacheService _tokenCacheService;
|
||||
|
||||
public ApplicationController(
|
||||
OpcVaultApiOptions opcVaultOptions,
|
||||
AzureADOptions azureADOptions,
|
||||
ITokenCacheService tokenCacheService)
|
||||
{
|
||||
this.opcVaultOptions = opcVaultOptions;
|
||||
this.azureADOptions = azureADOptions;
|
||||
this.tokenCacheService = tokenCacheService;
|
||||
_opcVaultOptions = opcVaultOptions;
|
||||
_azureADOptions = azureADOptions;
|
||||
_tokenCacheService = tokenCacheService;
|
||||
}
|
||||
|
||||
|
||||
|
@ -46,7 +46,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
var applicationsTrimmed = new List<ApplicationRecordTrimmedApiModel>();
|
||||
do
|
||||
{
|
||||
var applications = await opcVault.QueryApplicationsPageAsync(applicationQuery);
|
||||
var applications = await _opcVault.QueryApplicationsPageAsync(applicationQuery);
|
||||
foreach (var app in applications.Applications)
|
||||
{
|
||||
applicationsTrimmed.Add(new ApplicationRecordTrimmedApiModel(app));
|
||||
|
@ -64,7 +64,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
return new BadRequestResult();
|
||||
}
|
||||
AuthorizeClient();
|
||||
var application = await opcVault.GetApplicationAsync(id);
|
||||
var application = await _opcVault.GetApplicationAsync(id);
|
||||
if (application == null)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
|
@ -79,7 +79,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
public async Task<ActionResult> UnregisterConfirmedAsync([Bind("Id")] string id)
|
||||
{
|
||||
AuthorizeClient();
|
||||
await opcVault.UnregisterApplicationAsync(id);
|
||||
await _opcVault.UnregisterApplicationAsync(id);
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
public async Task<ActionResult> DetailsAsync(string id)
|
||||
{
|
||||
AuthorizeClient();
|
||||
var application = await opcVault.GetApplicationAsync(id);
|
||||
var application = await _opcVault.GetApplicationAsync(id);
|
||||
if (application == null)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
|
@ -97,11 +97,11 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
|
||||
private void AuthorizeClient()
|
||||
{
|
||||
if (opcVault == null)
|
||||
if (_opcVault == null)
|
||||
{
|
||||
ServiceClientCredentials serviceClientCredentials =
|
||||
new OpcVaultLoginCredentials(opcVaultOptions, azureADOptions, tokenCacheService, User);
|
||||
opcVault = new OpcVault(new Uri(opcVaultOptions.BaseAddress), serviceClientCredentials);
|
||||
new OpcVaultLoginCredentials(_opcVaultOptions, _azureADOptions, _tokenCacheService, User);
|
||||
_opcVault = new OpcVault(new Uri(_opcVaultOptions.BaseAddress), serviceClientCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See License.txt in the project root for
|
||||
// license information.
|
||||
//
|
||||
|
@ -21,18 +21,18 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
public class DownloadController : Controller
|
||||
{
|
||||
protected IOpcVault opcVault;
|
||||
private readonly OpcVaultApiOptions opcVaultOptions;
|
||||
private readonly AzureADOptions azureADOptions;
|
||||
private readonly ITokenCacheService tokenCacheService;
|
||||
private readonly OpcVaultApiOptions _opcVaultOptions;
|
||||
private readonly AzureADOptions _azureADOptions;
|
||||
private readonly ITokenCacheService _tokenCacheService;
|
||||
|
||||
public DownloadController(
|
||||
OpcVaultApiOptions opcVaultOptions,
|
||||
AzureADOptions azureADOptions,
|
||||
ITokenCacheService tokenCacheService)
|
||||
{
|
||||
this.opcVaultOptions = opcVaultOptions;
|
||||
this.azureADOptions = azureADOptions;
|
||||
this.tokenCacheService = tokenCacheService;
|
||||
_opcVaultOptions = opcVaultOptions;
|
||||
_azureADOptions = azureADOptions;
|
||||
_tokenCacheService = tokenCacheService;
|
||||
}
|
||||
|
||||
[ActionName("Details")]
|
||||
|
@ -298,8 +298,8 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.Controllers
|
|||
if (opcVault == null)
|
||||
{
|
||||
ServiceClientCredentials serviceClientCredentials =
|
||||
new OpcVaultLoginCredentials(opcVaultOptions, azureADOptions, tokenCacheService, User);
|
||||
opcVault = new OpcVault(new Uri(opcVaultOptions.BaseAddress), serviceClientCredentials);
|
||||
new OpcVaultLoginCredentials(_opcVaultOptions, _azureADOptions, _tokenCacheService, User);
|
||||
opcVault = new OpcVault(new Uri(_opcVaultOptions.BaseAddress), serviceClientCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,16 +5,16 @@ EXPOSE 44342
|
|||
|
||||
FROM microsoft/dotnet:2.1-sdk AS build
|
||||
WORKDIR /src
|
||||
COPY opc-gds-app/Microsoft.Azure.IIoT.OpcUa.Services.Gds.App.csproj Gds.App/
|
||||
RUN dotnet restore Gds.App/Microsoft.Azure.IIoT.OpcUa.Services.Gds.App.csproj
|
||||
COPY Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.csproj Vault.App/
|
||||
RUN dotnet restore Vault.App/Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.csproj
|
||||
COPY . .
|
||||
WORKDIR /src/Gds.App
|
||||
RUN dotnet build Microsoft.Azure.IIoT.OpcUa.Services.Gds.App.csproj -c Release -o /app
|
||||
WORKDIR /src/Vault.App
|
||||
RUN dotnet build Microsoft.Azure.IIoT.OpcUa.Services.Gds.Vault.csproj -c Release -o /app
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish Microsoft.Azure.IIoT.OpcUa.Services.Gds.App.csproj -c Release -o /app
|
||||
RUN dotnet publish Microsoft.Azure.IIoT.OpcUa.Services.Gds.Vault.csproj -c Release -o /app
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app .
|
||||
ENTRYPOINT ["dotnet", "Microsoft.Azure.IIoT.OpcUa.Services.Gds.App.dll"]
|
||||
ENTRYPOINT ["dotnet", "Microsoft.Azure.IIoT.OpcUa.Services.Vault.App.dll"]
|
||||
|
|
|
@ -10,11 +10,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
.travis.yml = .travis.yml
|
||||
common.props = common.props
|
||||
CONTRIBUTING.md = CONTRIBUTING.md
|
||||
DEVELOPMENT.md = DEVELOPMENT.md
|
||||
Dockerfile = Dockerfile
|
||||
Dockerfile.Windows = Dockerfile.Windows
|
||||
src\Dockerfile = src\Dockerfile
|
||||
app\Dockerfile = app\Dockerfile
|
||||
src\Dockerfile.Windows = src\Dockerfile.Windows
|
||||
LICENSE = LICENSE
|
||||
project.props = project.props
|
||||
README.md = README.md
|
||||
version.props = version.props
|
||||
EndProjectSection
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<Product>Microsoft Azure Industrial IoT OPC UA Vault Service</Product>
|
||||
<PackageLicenseUrl>$(RepositoryUrl)/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<Authors>Microsoft</Authors>
|
||||
<Company>Microsoft</Company>
|
||||
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
|
||||
<PackageIconUrl>http://go.microsoft.com/fwlink/?LinkID=288890</PackageIconUrl>
|
||||
<PackageReleaseNotes>$(RepositoryUrl)/releases</PackageReleaseNotes>
|
||||
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<HighEntropyVA>true</HighEntropyVA>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- prevent building packages and releasing to nuget until ready/public -->
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -0,0 +1,3 @@
|
|||
.user
|
||||
.app
|
||||
.env
|
|
@ -0,0 +1,134 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Deploys to Azure
|
||||
|
||||
.DESCRIPTION
|
||||
Deploys an Azure Resource Manager template of choice
|
||||
|
||||
.PARAMETER resourceGroupName
|
||||
The resource group where the template will be deployed.
|
||||
|
||||
.PARAMETER aadConfig
|
||||
The AAD configuration the template will be configured with.
|
||||
|
||||
.PARAMETER interactive
|
||||
Whether to run in interactive mode
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$True)] [string] $resourceGroupName,
|
||||
$aadConfig = $null,
|
||||
$interactive = $true
|
||||
)
|
||||
|
||||
#******************************************************************************
|
||||
# Generate a random password
|
||||
#******************************************************************************
|
||||
Function CreateRandomPassword() {
|
||||
param(
|
||||
$length = 15
|
||||
)
|
||||
$punc = 46..46
|
||||
$digits = 48..57
|
||||
$lcLetters = 65..90
|
||||
$ucLetters = 97..122
|
||||
|
||||
$password = `
|
||||
[char](Get-Random -Count 1 -InputObject ($lcLetters)) + `
|
||||
[char](Get-Random -Count 1 -InputObject ($ucLetters)) + `
|
||||
[char](Get-Random -Count 1 -InputObject ($digits)) + `
|
||||
[char](Get-Random -Count 1 -InputObject ($punc))
|
||||
$password += get-random -Count ($length -4) `
|
||||
-InputObject ($punc + $digits + $lcLetters + $ucLetters) |`
|
||||
% -begin { $aa = $null } -process {$aa += [char]$_} -end {$aa}
|
||||
|
||||
return $password
|
||||
}
|
||||
|
||||
#******************************************************************************
|
||||
# Script body
|
||||
#******************************************************************************
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path $script:MyInvocation.MyCommand.Path
|
||||
|
||||
# Register RPs
|
||||
Register-AzureRmResourceProvider -ProviderNamespace "microsoft.web" | Out-Null
|
||||
#Register-AzureRmResourceProvider -ProviderNamespace "microsoft.compute" | Out-Null
|
||||
|
||||
# Set admin password
|
||||
$adminPassword = CreateRandomPassword
|
||||
$templateParameters = @{
|
||||
# adminPassword = $adminPassword
|
||||
}
|
||||
|
||||
try {
|
||||
# Try set branch name as current branch
|
||||
$output = cmd /c "git rev-parse --abbrev-ref HEAD" 2`>`&1
|
||||
$branchName = ("{0}" -f $output);
|
||||
Write-Host "VM deployment will use configuration from '$branchName' branch."
|
||||
# $templateParameters.Add("branchName", $branchName)
|
||||
}
|
||||
catch {
|
||||
# $templateParameters.Add("branchName", "master")
|
||||
}
|
||||
|
||||
# Configure auth
|
||||
if ($aadConfig) {
|
||||
if (![string]::IsNullOrEmpty($aadConfig.Audience)) {
|
||||
# $templateParameters.Add("authAudience", $aadConfig.Audience)
|
||||
}
|
||||
if (![string]::IsNullOrEmpty($aadConfig.ClientId)) {
|
||||
# $templateParameters.Add("aadClientId", $aadConfig.ClientId)
|
||||
}
|
||||
if (![string]::IsNullOrEmpty($aadConfig.TenantId)) {
|
||||
# $templateParameters.Add("aadTenantId", $aadConfig.TenantId)
|
||||
}
|
||||
if (![string]::IsNullOrEmpty($aadConfig.Instance)) {
|
||||
# $templateParameters.Add("aadInstance", $aadConfig.Instance)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Set website name
|
||||
if ($interactive) {
|
||||
$webAppName = Read-Host "Please specify a website name"
|
||||
if (![string]::IsNullOrEmpty($webAppName)) {
|
||||
$templateParameters.Add("webAppName", $webAppName)
|
||||
}
|
||||
}
|
||||
|
||||
# Start the deployment
|
||||
$templateFilePath = Join-Path $ScriptDir "template.json"
|
||||
Write-Host "Starting deployment..."
|
||||
$deployment = New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName `
|
||||
-TemplateFile $templateFilePath -TemplateParameterObject $templateParameters
|
||||
|
||||
$webAppPortalUrl = $deployment.Outputs["webAppPortalUrl"].Value
|
||||
$webAppServiceUrl = $deployment.Outputs["webAppServiceUrl"].Value
|
||||
#$adminUser = $deployment.Outputs["adminUsername"].Value
|
||||
|
||||
if ($aadConfig -and $aadConfig.ClientObjectId) {
|
||||
#
|
||||
# Update client application to add reply urls required permissions.
|
||||
#
|
||||
Write-Host "Adding ReplyUrls:"
|
||||
$replyUrls = New-Object System.Collections.Generic.List[System.String]
|
||||
$replyUrls.Add($webAppPortalUrl)
|
||||
$replyUrls.Add($webAppPortalUrl + "/oauth2-redirect.html")
|
||||
Write-Host $webAppPortalUrl + "/oauth2-redirect.html"
|
||||
# still connected
|
||||
Set-AzureADApplication -ObjectId $aadConfig.ClientObjectId -ReplyUrls $replyUrls
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host "To access the web portal go to:"
|
||||
Write-Host $webAppPortalUrl
|
||||
Write-Host
|
||||
Write-Host "To access the web service go to:"
|
||||
Write-Host $webAppServiceUrl
|
||||
Write-Host
|
||||
#Write-Host "Use the following User and Password to log onto your VM:"
|
||||
#Write-Host
|
||||
#Write-Host $adminUser
|
||||
#Write-Host $adminPassword
|
||||
#Write-Host
|
|
@ -0,0 +1,131 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"webAppName": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Base name of the resource such as web app name and app service plan "
|
||||
},
|
||||
"minLength": 2
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"tenantId": "[subscription().tenantId]",
|
||||
"randomSuffix": "[take(uniqueString(subscription().subscriptionId, resourceGroup().id, parameters('webAppName')), 5)]",
|
||||
"applocation": "[resourceGroup().location]",
|
||||
"keyVaultName": "[concat('vault-', variables('randomSuffix'))]",
|
||||
"vaultSku": "Premium",
|
||||
"enabledForDeployment": true,
|
||||
"enabledForTemplateDeployment": false,
|
||||
"enableVaultForVolumeEncryption": false,
|
||||
"webAppSku": "S1",
|
||||
"webAppPortalName": "[concat(parameters('webAppName'), '-app')]",
|
||||
"webAppServiceName": "[concat(parameters('webAppName'), '-service')]",
|
||||
"appServicePlanName": "[concat('AppServicePlan-', parameters('webAppName'))]",
|
||||
"documentDBName": "[concat('cosmosdb-', variables('randomSuffix'))]",
|
||||
"documentDBApiVersion": "2016-03-19",
|
||||
"documentDBResourceId": "[resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDBName'))]",
|
||||
"apiType": "SQL",
|
||||
"offerType": "Standard"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2017-08-01",
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"kind": "app",
|
||||
"name": "[variables('appServicePlanName')]",
|
||||
"location": "[variables('applocation')]",
|
||||
"comments": "This app service plan is used for the web app and slots.",
|
||||
"properties": {},
|
||||
"dependsOn": [],
|
||||
"sku": {
|
||||
"name": "[variables('webAppSku')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comments": "Azure CosmosDb",
|
||||
"apiVersion": "[variables('documentDBApiVersion')]",
|
||||
"type": "Microsoft.DocumentDb/databaseAccounts",
|
||||
"kind": "GlobalDocumentDB",
|
||||
"name": "[variables('documentDBName')]",
|
||||
"location": "[variables('applocation')]",
|
||||
"properties": {
|
||||
"name": "[variables('documentDBName')]",
|
||||
"databaseAccountOfferType": "standard",
|
||||
"consistencyPolicy": {
|
||||
"defaultConsistencyLevel": "Session",
|
||||
"maxStalenessPrefix": 10,
|
||||
"maxIntervalInSeconds": 5
|
||||
}
|
||||
},
|
||||
"dependsOn": []
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.KeyVault/vaults",
|
||||
"name": "[variables('keyVaultName')]",
|
||||
"apiVersion": "2015-06-01",
|
||||
"location": "[variables('applocation')]",
|
||||
"tags": {
|
||||
"displayName": "KeyVault"
|
||||
},
|
||||
"properties": {
|
||||
"enabledForDeployment": "[variables('enabledForDeployment')]",
|
||||
"enabledForTemplateDeployment": "[variables('enabledForTemplateDeployment')]",
|
||||
"enabledForVolumeEncryption": "[variables('enableVaultForVolumeEncryption')]",
|
||||
"tenantId": "[variables('tenantId')]",
|
||||
"sku": {
|
||||
"name": "[variables('vaultSku')]",
|
||||
"family": "A"
|
||||
},
|
||||
"accessPolicies": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2016-08-01",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"kind": "app",
|
||||
"name": "[variables('webAppPortalName')]",
|
||||
"location": "[variables('applocation')]",
|
||||
"comments": "This is the web app, also the default 'nameless' slot.",
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"apiVersion": "2016-08-01",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"kind": "app",
|
||||
"name": "[variables('webAppServiceName')]",
|
||||
"location": "[variables('applocation')]",
|
||||
"comments": "This is the web app service, also the default 'nameless' slot.",
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"webAppPortalUrl": {
|
||||
"type": "string",
|
||||
"value": "[concat('https://', reference(concat('Microsoft.Web/sites/', variables('webAppPortalName'))).hostNames[0])]"
|
||||
},
|
||||
"webAppServiceUrl": {
|
||||
"type": "string",
|
||||
"value": "[concat('https://', reference(concat('Microsoft.Web/sites/', variables('webAppServiceName'))).hostNames[0])]"
|
||||
},
|
||||
"resourceGroup": {
|
||||
"type": "string",
|
||||
"value": "[resourceGroup().name]"
|
||||
},
|
||||
"tenantId": {
|
||||
"type": "string",
|
||||
"value": "[variables('tenantId')]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,770 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Deploys Industrial IoT services to Azure
|
||||
|
||||
.DESCRIPTION
|
||||
Deploys the Industrial IoT services dependencies and optionally micro services and UI to Azure.
|
||||
|
||||
.PARAMETER type
|
||||
The type of deployment (cloud, vm, local)
|
||||
|
||||
.PARAMETER resourceGroupName
|
||||
Can be the name of an existing or a new resource group.
|
||||
|
||||
.PARAMETER subscriptionId
|
||||
Optional, the subscription id where resources will be deployed.
|
||||
|
||||
.PARAMETER subscriptionName
|
||||
Or alternatively the subscription name.
|
||||
|
||||
.PARAMETER resourceGroupLocation
|
||||
Optional, a resource group location. If specified, will try to create a new resource group in this location.
|
||||
|
||||
.PARAMETER withAuthentication
|
||||
Whether to enable authentication - defaults to $true.
|
||||
|
||||
.PARAMETER tenantId
|
||||
AD tenant to use.
|
||||
|
||||
.PARAMETER credentials
|
||||
To support non interactive usage of script. (TODO)
|
||||
#>
|
||||
|
||||
param(
|
||||
[string] $type = "cloud",
|
||||
[string] $resourceGroupName,
|
||||
[string] $resourceGroupLocation,
|
||||
[string] $subscriptionName,
|
||||
[string] $subscriptionId,
|
||||
[string] $accountName,
|
||||
$credentials,
|
||||
[string] $tenantId,
|
||||
[bool] $withAuthentication = $true,
|
||||
[ValidateSet("AzureCloud")] [string] $environmentName = "AzureCloud"
|
||||
)
|
||||
|
||||
$script:optionIndex = 0
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Validate environment names
|
||||
#*******************************************************************************************************
|
||||
Function SelectEnvironment() {
|
||||
switch ($script:environmentName) {
|
||||
"AzureCloud" {
|
||||
if ((Get-AzureRMEnvironment AzureCloud) -eq $null) {
|
||||
Add-AzureRMEnvironment -Name AzureCloud -EnableAdfsAuthentication $False `
|
||||
-ActiveDirectoryServiceEndpointResourceId https://management.core.windows.net/ `
|
||||
-GalleryUrl https://gallery.azure.com/ `
|
||||
-ServiceManagementUrl https://management.core.windows.net/ `
|
||||
-SqlDatabaseDnsSuffix .database.windows.net `
|
||||
-StorageEndpointSuffix core.windows.net `
|
||||
-ActiveDirectoryAuthority https://login.microsoftonline.com/ `
|
||||
-GraphUrl https://graph.windows.net/ `
|
||||
-trafficManagerDnsSuffix trafficmanager.net `
|
||||
-AzureKeyVaultDnsSuffix vault.azure.net `
|
||||
-AzureKeyVaultServiceEndpointResourceId https://vault.azure.net `
|
||||
-ResourceManagerUrl https://management.azure.com/ `
|
||||
-ManagementPortalUrl http://go.microsoft.com/fwlink/?LinkId=254433
|
||||
}
|
||||
$script:locations = @("West US", "North Europe", "West Europe")
|
||||
}
|
||||
default {
|
||||
throw ("'{0}' is not a supported Azure Cloud environment" -f $script:environmentName)
|
||||
}
|
||||
}
|
||||
$script:environment = Get-AzureRmEnvironment $script:environmentName
|
||||
$script:environmentName = $script:environment.Name
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Called in case no account is configured to let user choose the account.
|
||||
#*******************************************************************************************************
|
||||
Function SelectAccount() {
|
||||
if (![string]::IsNullOrEmpty($script:accountName)) {
|
||||
# Validate Azure account
|
||||
$account = Get-AzureAccount -Name $script:accountName
|
||||
if ($account -eq $null) {
|
||||
Write-Error ("Specified account {0} does not exist" -f $script:accountName)
|
||||
}
|
||||
else {
|
||||
if ([string]::IsNullOrEmpty($account.Subscriptions) -or `
|
||||
(Get-AzureSubscription -SubscriptionId `
|
||||
($account.Subscriptions -replace '(?:\r\n)',',').split(",")[0]) -eq $null) {
|
||||
Write-Warning ("No subscriptions in account {0}." -f $script:accountName)
|
||||
$account = $null
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($account -eq $null) {
|
||||
$accounts = Get-AzureAccount
|
||||
if ($accounts -eq $null) {
|
||||
$account = Add-AzureAccount -Environment $script:environmentName
|
||||
}
|
||||
else {
|
||||
Write-Host "Select Azure account to use"
|
||||
$script:optionIndex = 1
|
||||
Write-Host
|
||||
Write-Host ("Available accounts in Azure environment '{0}':" -f $script:environmentName)
|
||||
Write-Host
|
||||
Write-Host (($accounts | `
|
||||
Format-Table @{ `
|
||||
Name = 'Option'; `
|
||||
Expression = { `
|
||||
$script:optionIndex; $script:optionIndex+=1 `
|
||||
}; `
|
||||
Alignment = 'right' `
|
||||
}, Id, Subscriptions -AutoSize) | Out-String).Trim()
|
||||
Write-Host (("{0}" -f $script:optionIndex).PadLeft(6) + " Use another account")
|
||||
Write-Host
|
||||
$account = $null
|
||||
while ($account -eq $null) {
|
||||
try {
|
||||
[int]$script:optionIndex = Read-Host "Select an option"
|
||||
}
|
||||
catch {
|
||||
Write-Host "Must be a number"
|
||||
continue
|
||||
}
|
||||
if ($script:optionIndex -eq $accounts.length + 1) {
|
||||
$account = Add-AzureAccount -Environment $script:environmentName
|
||||
break
|
||||
}
|
||||
if ($script:optionIndex -lt 1 -or $script:optionIndex -gt $accounts.length) {
|
||||
continue
|
||||
}
|
||||
$account = $accounts[$script:optionIndex - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrEmpty($account.Id)) {
|
||||
throw ("There was no account selected. Please check and try again.")
|
||||
}
|
||||
$script:accountName = $account.Id
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Perform login - uses profile file if exists
|
||||
#*******************************************************************************************************
|
||||
Function Login() {
|
||||
$rmProfileLoaded = $False
|
||||
$profileFile = Join-Path $script:ScriptDir ".user"
|
||||
if ([string]::IsNullOrEmpty($script:accountName)) {
|
||||
# Try to use saved profile if one is available
|
||||
if (Test-Path "$profileFile") {
|
||||
Write-Output ("Use saved profile from '{0}'" -f $profileFile)
|
||||
$rmProfile = Import-AzureRmContext -Path "$profileFile"
|
||||
$rmProfileLoaded = ($rmProfile -ne $null) `
|
||||
-and ($rmProfile.Context -ne $null) `
|
||||
-and ((Get-AzureRmSubscription) -ne $null)
|
||||
}
|
||||
if ($rmProfileLoaded) {
|
||||
$script:accountName = $rmProfile.Context.Account.Id
|
||||
}
|
||||
}
|
||||
if (!$rmProfileLoaded) {
|
||||
# select account
|
||||
Write-Host "Logging in..."
|
||||
SelectAccount
|
||||
try {
|
||||
Login-AzureRmAccount -EnvironmentName $script:environmentName -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
throw "The login to the Azure account was not successful."
|
||||
}
|
||||
$reply = Read-Host -Prompt "Save user profile in $profileFile? [y/n]"
|
||||
if ($reply -match "[yY]") {
|
||||
Save-AzureRmContext -Path "$profileFile"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Select the subscription identifier
|
||||
#*******************************************************************************************************
|
||||
Function SelectSubscription() {
|
||||
$subscriptions = Get-AzureRMSubscription
|
||||
if ($script:subscriptionName -ne $null -and $script:subscriptionName -ne "") {
|
||||
$subscriptionId = Get-AzureRmSubscription -SubscriptionName $script:subscriptionName
|
||||
}
|
||||
else {
|
||||
$subscriptionId = $script:subscriptionId
|
||||
}
|
||||
|
||||
if (![string]::IsNullOrEmpty($subscriptionId)) {
|
||||
if (!$subscriptions.Id.Contains($subscriptionId)) {
|
||||
Write-Error ("Invalid subscription id {0}" -f $subscriptionId)
|
||||
$subscriptionId = ""
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrEmpty($subscriptionId)) {
|
||||
if ($subscriptions.Count -eq 1) {
|
||||
$subscriptionId = $subscriptions[0].Id
|
||||
}
|
||||
else {
|
||||
Write-Output "Select an Azure subscription to use... "
|
||||
$script:optionIndex = 1
|
||||
Write-Host
|
||||
Write-Host ("Available subscriptions for account '{0}'" -f $script:accountName)
|
||||
Write-Host
|
||||
Write-Host ($subscriptions | Format-Table @{ `
|
||||
Name='Option'; `
|
||||
Expression={ `
|
||||
$script:optionIndex;$script:optionIndex+=1 `
|
||||
}; `
|
||||
Alignment='right' `
|
||||
},Name, Id -AutoSize | Out-String).Trim()
|
||||
Write-Host
|
||||
while (!$subscriptions.Id.Contains($subscriptionId)) {
|
||||
try {
|
||||
[int]$script:optionIndex = Read-Host "Select an option"
|
||||
}
|
||||
catch {
|
||||
Write-Host "Must be a number!"
|
||||
continue
|
||||
}
|
||||
if ($script:optionIndex -lt 1 -or $script:optionIndex -gt $subscriptions.length) {
|
||||
continue
|
||||
}
|
||||
$subscriptionId = $subscriptions[$script:optionIndex - 1].Id
|
||||
}
|
||||
}
|
||||
}
|
||||
$script:subscriptionId = $subscriptionId
|
||||
Write-Host "Azure subscriptionId '$subscriptionId' selected."
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Called if no Azure location is configured for the deployment to let the user choose a location.
|
||||
#*******************************************************************************************************
|
||||
Function SelectLocation() {
|
||||
$locations = @()
|
||||
$index = 1
|
||||
foreach ($location in $script:locations) {
|
||||
$newLocation = New-Object System.Object
|
||||
$newLocation | Add-Member -MemberType NoteProperty -Name "Option" -Value $index
|
||||
$newLocation | Add-Member -MemberType NoteProperty -Name "Location" -Value $location
|
||||
$locations += $newLocation
|
||||
$index += 1
|
||||
}
|
||||
Write-Host
|
||||
Write-Host ("Please choose a location in Azure environment '{0}':" -f $script:environmentName)
|
||||
$script:optionIndex = 1
|
||||
Write-Host ($locations | Format-Table @{ `
|
||||
Name='Option'; `
|
||||
Expression={ `
|
||||
$script:optionIndex;$script:optionIndex+=1 `
|
||||
}; `
|
||||
Alignment='right' `
|
||||
}, @{ `
|
||||
Name="Location"; `
|
||||
Expression={ `
|
||||
$_.Location `
|
||||
} `
|
||||
} -AutoSize | Out-String).Trim()
|
||||
Write-Host
|
||||
$location = ""
|
||||
while ($location -eq "" -or !(ValidateLocation $location)) {
|
||||
try {
|
||||
[int]$script:optionIndex = Read-Host "Select an option"
|
||||
}
|
||||
catch {
|
||||
Write-Host "Must be a number"
|
||||
continue
|
||||
}
|
||||
if ($script:optionIndex -lt 1 -or $script:optionIndex -ge $index) {
|
||||
continue
|
||||
}
|
||||
$location = $script:locations[$script:optionIndex - 1]
|
||||
}
|
||||
Write-Verbose "Azure location '$location' selected."
|
||||
$script:resourceGroupLocation = $location
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Validate a location
|
||||
#*******************************************************************************************************
|
||||
Function ValidateLocation() {
|
||||
Param (
|
||||
[string] $locationToValidate
|
||||
)
|
||||
if (![string]::IsNullOrEmpty($locationToValidate)) {
|
||||
$locationToValidate = $locationToValidate.Replace(' ', '').ToLowerInvariant()
|
||||
foreach ($location in $script:locations) {
|
||||
if ($location.Replace(' ', '').ToLowerInvariant() -eq $locationToValidate) {
|
||||
return $True
|
||||
}
|
||||
}
|
||||
Write-Warning "Location '$locationToValidate' is not available."
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Acquire bearer token for user
|
||||
#*******************************************************************************************************
|
||||
Function AcquireToken() {
|
||||
Param (
|
||||
[string] $tenant,
|
||||
[string] $authUri,
|
||||
[string] $resourceUri,
|
||||
[Parameter(Mandatory=$false)] [string] $user = $null,
|
||||
[Parameter(Mandatory=$false)] [string] $prompt = "Auto"
|
||||
)
|
||||
$psAadClientId = "1950a258-227b-4e31-a9cf-717495945fc2"
|
||||
[Uri]$aadRedirectUri = "urn:ietf:wg:oauth:2.0:oob"
|
||||
$authority = "{0}{1}" -f $authUri, $tenant
|
||||
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" `
|
||||
-ArgumentList $authority,$true
|
||||
$userId = [Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier]::AnyUser
|
||||
if (![string]::IsNullOrEmpty($user)) {
|
||||
$userId = new-object Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier `
|
||||
-ArgumentList $user, "OptionalDisplayableId"
|
||||
}
|
||||
$authResult = $authContext.AcquireToken($resourceUri, $psAadClientId, `
|
||||
$aadRedirectUri, $prompt, $userId)
|
||||
return $authResult
|
||||
}
|
||||
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Select azure ad tenant available to user
|
||||
#*******************************************************************************************************
|
||||
Function SelectAzureADTenantId() {
|
||||
$tenants = Get-AzureRmTenant
|
||||
if ($tenants.Count -eq 0) {
|
||||
throw ("No Active Directory domains found for '{0}'" -f $script:accountName)
|
||||
}
|
||||
if ($tenants.Count -eq 1) {
|
||||
$tenantId = $tenants[0].Id
|
||||
}
|
||||
else {
|
||||
# List Active directories associated with account
|
||||
$directories = @()
|
||||
$index = 1
|
||||
[int]$selectedIndex = -1
|
||||
foreach ($tenantObj in $tenants) {
|
||||
$tenant = $tenantObj.Id
|
||||
$uri = "{0}{1}/me?api-version=1.6" -f $script:environment.GraphUrl, $tenant
|
||||
$token = AcquireToken $tenant $script:environment.ActiveDirectoryAuthority `
|
||||
$script:environment.GraphUrl $script:accountName "Auto"
|
||||
$result = Invoke-RestMethod -Method "GET" -Uri $uri -Headers @{ `
|
||||
"Authorization"=$($token.CreateAuthorizationHeader()); `
|
||||
"Content-Type"="application/json" `
|
||||
}
|
||||
$directory = New-Object System.Object
|
||||
$directory | Add-Member -MemberType NoteProperty `
|
||||
-Name "Option" -Value $index
|
||||
$directory | Add-Member -MemberType NoteProperty `
|
||||
-Name "Directory Name" -Value ($result.userPrincipalName.Split('@')[1])
|
||||
$directory | Add-Member -MemberType NoteProperty `
|
||||
-Name "Tenant Id" -Value $tenant
|
||||
$directories += $directory
|
||||
$index += 1
|
||||
}
|
||||
if ($selectedIndex -eq -1) {
|
||||
Write-Host
|
||||
Write-Host "Select an Active Directory Tenant to use..."
|
||||
Write-Host "Available:"
|
||||
Write-Host
|
||||
Write-Host ($directories | Out-String) -NoNewline
|
||||
while ($selectedIndex -lt 1 -or $selectedIndex -ge $index) {
|
||||
try {
|
||||
[int]$selectedIndex = Read-Host "Select an option"
|
||||
}
|
||||
catch {
|
||||
Write-Host "Must be a number"
|
||||
}
|
||||
}
|
||||
}
|
||||
$tenantId = $tenants[$selectedIndex - 1].Id
|
||||
}
|
||||
return $tenantId
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Login to Azure AD (interactive if credentials are not already provided.
|
||||
#*******************************************************************************************************
|
||||
Function ConnectToAzureADTenant() {
|
||||
if ($script:interactive) {
|
||||
# Interactive
|
||||
if (!$script:tenantId) {
|
||||
if (!$script:withAuthentication) {
|
||||
$reply = Read-Host -Prompt "Enable authentication? [y/n]"
|
||||
if ( $reply -notmatch "[yY]" ) {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
$script:tenantId = SelectAzureADTenantId
|
||||
}
|
||||
}
|
||||
if (!$script:credentials) {
|
||||
if (!$script:tenantId) {
|
||||
throw "No tenant selected for AAD connect."
|
||||
}
|
||||
else {
|
||||
# Make sure we get token from token cache instead of interactive logon
|
||||
$graphAuth = AcquireToken $script:tenantId $script:environment.ActiveDirectoryAuthority `
|
||||
$script:environment.GraphUrl $script:accountName "Auto"
|
||||
$user = Invoke-RestMethod -Method "GET" `
|
||||
-Uri ("{0}{1}/me?api-version=1.6" -f $script:environment.GraphUrl, $script:tenantId) `
|
||||
-Headers @{ `
|
||||
"Authorization"=$($graphAuth.CreateAuthorizationHeader()); `
|
||||
"Content-Type"="application/json" `
|
||||
}
|
||||
return Connect-AzureAD -MsAccessToken $graphAuth.AccessToken -TenantId $script:tenantId `
|
||||
-ErrorAction Stop -AadAccessToken $graphAuth.AccessToken -AccountId $user.userPrincipalName
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$script:tenantId) {
|
||||
# use home tenant
|
||||
return Connect-AzureAD -Credential $script:credential `
|
||||
-ErrorAction Stop
|
||||
}
|
||||
else {
|
||||
return Connect-AzureAD -Credential $script:credential -TenantId $script:tenantId `
|
||||
-ErrorAction Stop
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure
|
||||
#*******************************************************************************************************
|
||||
Function AddResourcePermission() {
|
||||
Param (
|
||||
$requiredAccess,
|
||||
$exposedPermissions,
|
||||
[string]$requiredAccesses, `
|
||||
[string]$permissionType
|
||||
)
|
||||
foreach($permission in $requiredAccesses.Trim().Split("|")) {
|
||||
foreach($exposedPermission in $exposedPermissions) {
|
||||
if ($exposedPermission.Value -eq $permission) {
|
||||
$resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess
|
||||
# Scope = Delegated permissions | Role = Application permissions
|
||||
$resourceAccess.Type = $permissionType
|
||||
# Read directory data
|
||||
$resourceAccess.Id = $exposedPermission.Id
|
||||
$requiredAccess.ResourceAccess.Add($resourceAccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Example: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read"
|
||||
#*******************************************************************************************************
|
||||
Function GetRequiredPermissions() {
|
||||
Param(
|
||||
[string] $applicationDisplayName,
|
||||
[string] $requiredDelegatedPermissions,
|
||||
[string] $requiredApplicationPermissions,
|
||||
$servicePrincipal
|
||||
)
|
||||
|
||||
# If we are passed the service principal we use it directly, otherwise we find it from
|
||||
# the display name (which might not be unique)
|
||||
if ($servicePrincipal) {
|
||||
$sp = $servicePrincipal
|
||||
}
|
||||
else {
|
||||
$sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'"
|
||||
}
|
||||
|
||||
$appid = $sp.AppId
|
||||
$requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess
|
||||
$requiredAccess.ResourceAppId = $appid
|
||||
$requiredAccess.ResourceAccess =
|
||||
New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess]
|
||||
|
||||
if ($requiredDelegatedPermissions) {
|
||||
AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions `
|
||||
-requiredAccesses $requiredDelegatedPermissions -permissionType "Scope"
|
||||
}
|
||||
if ($requiredApplicationPermissions) {
|
||||
AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles `
|
||||
-requiredAccesses $requiredApplicationPermissions -permissionType "Role"
|
||||
}
|
||||
return $requiredAccess
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Create an application role of given name and description
|
||||
#*******************************************************************************************************
|
||||
Function CreateAppRole() {
|
||||
param(
|
||||
$current,
|
||||
[string] $name,
|
||||
[string] $description,
|
||||
[string] $value
|
||||
)
|
||||
$appRole = $current | Where-Object { $_.Value -eq $value }
|
||||
if (!$appRole) {
|
||||
$appRole = New-Object Microsoft.Open.AzureAD.Model.AppRole
|
||||
$appRole.AllowedMemberTypes = New-Object System.Collections.Generic.List[string]
|
||||
$appRole.AllowedMemberTypes.Add("User");
|
||||
$appRole.Id = New-Guid
|
||||
$appRole.IsEnabled = $true
|
||||
$appRole.Value = $value;
|
||||
}
|
||||
$appRole.DisplayName = $name
|
||||
$appRole.Description = $description
|
||||
return $appRole
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Get configuration object for service and client applications
|
||||
#*******************************************************************************************************
|
||||
Function GetAzureADApplicationConfig() {
|
||||
$serviceDisplayName = $script:resourceGroupName + "-service"
|
||||
$clientDisplayName = $script:resourceGroupName + "-client"
|
||||
$moduleDisplayName = $script:resourceGroupName + "-module"
|
||||
try {
|
||||
$creds = ConnectToAzureADTenant
|
||||
if (!$creds) {
|
||||
return $null
|
||||
}
|
||||
$script:tenantId = $creds.Tenant.Id
|
||||
if (!$script:tenantId) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$tenant = Get-AzureADTenantDetail
|
||||
$tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name
|
||||
Write-Host "Selected Tenant '$tenantName' as authority."
|
||||
|
||||
$serviceAadApplication=Get-AzureADApplication `
|
||||
-Filter "identifierUris/any(uri:uri eq 'https://$tenantName/$serviceDisplayName')"
|
||||
if (!$serviceAadApplication) {
|
||||
$serviceAadApplication = New-AzureADApplication -DisplayName $serviceDisplayName `
|
||||
-PublicClient $False -HomePage "https://localhost" `
|
||||
-IdentifierUris "https://$tenantName/$serviceDisplayName"
|
||||
Write-Host "Created new AAD service application."+$serviceDisplayName
|
||||
}
|
||||
$serviceServicePrincipal=Get-AzureADServicePrincipal `
|
||||
-Filter "AppId eq '$($serviceAadApplication.AppId)'"
|
||||
if (!$serviceServicePrincipal) {
|
||||
$serviceServicePrincipal = New-AzureADServicePrincipal `
|
||||
-AppId $serviceAadApplication.AppId `
|
||||
-Tags {WindowsAzureActiveDirectoryIntegratedApp}
|
||||
}
|
||||
|
||||
$clientAadApplication=Get-AzureADApplication `
|
||||
-Filter "DisplayName eq '$clientDisplayName'"
|
||||
if (!$clientAadApplication) {
|
||||
$clientAadApplication = New-AzureADApplication -DisplayName $clientDisplayName `
|
||||
-PublicClient $True
|
||||
Write-Host "Created new AAD client application."+$clientDisplayName
|
||||
}
|
||||
|
||||
$moduleAadApplication=Get-AzureADApplication `
|
||||
-Filter "DisplayName eq '$moduleDisplayName'"
|
||||
if (!$moduleAadApplication) {
|
||||
$moduleAadApplication = New-AzureADApplication -DisplayName $moduleDisplayName `
|
||||
-PublicClient $True
|
||||
Write-Host "Created new AAD Module application. "+$moduleDisplayName
|
||||
}
|
||||
|
||||
# Find client principal
|
||||
$clientServicePrincipal=Get-AzureADServicePrincipal `
|
||||
-Filter "AppId eq '$($clientAadApplication.AppId)'"
|
||||
if (!$clientServicePrincipal) {
|
||||
$clientServicePrincipal = New-AzureADServicePrincipal `
|
||||
-AppId $clientAadApplication.AppId `
|
||||
-Tags {WindowsAzureActiveDirectoryIntegratedApp}
|
||||
}
|
||||
|
||||
#
|
||||
# Try to add current user as app owner
|
||||
#
|
||||
try {
|
||||
$user = Get-AzureADUser -ObjectId $creds.Account.Id -ErrorAction Stop
|
||||
# TODO: Check whether already owner...
|
||||
|
||||
Add-AzureADApplicationOwner -ObjectId $serviceAadApplication.ObjectId `
|
||||
-RefObjectId $user.ObjectId
|
||||
Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId `
|
||||
-RefObjectId $user.ObjectId
|
||||
Add-AzureADApplicationOwner -ObjectId $moduleAadApplication.ObjectId `
|
||||
-RefObjectId $user.ObjectId
|
||||
Write-Host "'$($user.UserPrincipalName)' added as owner for applications."
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Adding $($creds.Account.Id) as owner failed."
|
||||
}
|
||||
|
||||
#
|
||||
# Update service application to add roles, known applications and required permissions
|
||||
#
|
||||
$approverRole = CreateAppRole -current $serviceAadApplication.AppRoles -name "Approver" `
|
||||
-value "Sign" -description "Approvers have the ability to issue certificates."
|
||||
$writerRole = CreateAppRole -current $serviceAadApplication.AppRoles -name "Writer" `
|
||||
-value "Write" -description "Writers Have the ability to change entities."
|
||||
$adminRole = CreateAppRole -current $serviceAadApplication.AppRoles -name "Administrator" `
|
||||
-value "Admin" -description "Admins can access advanced features."
|
||||
$appRoles = New-Object `
|
||||
System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.AppRole]
|
||||
$appRoles.Add($writerRole)
|
||||
$appRoles.Add($approverRole)
|
||||
$appRoles.Add($adminRole)
|
||||
$knownApplications = New-Object System.Collections.Generic.List[System.String]
|
||||
$knownApplications.Add($clientAadApplication.AppId)
|
||||
$requiredResourcesAccess = `
|
||||
New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]
|
||||
$requiredPermissions = GetRequiredPermissions -applicationDisplayName "Azure Key Vault" `
|
||||
-requiredDelegatedPermissions "user_impersonation"
|
||||
$requiredResourcesAccess.Add($requiredPermissions)
|
||||
$requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" `
|
||||
-requiredDelegatedPermissions "User.Read"
|
||||
$requiredResourcesAccess.Add($requiredPermissions)
|
||||
Set-AzureADApplication -ObjectId $serviceAadApplication.ObjectId `
|
||||
-RequiredResourceAccess $requiredResourcesAccess `
|
||||
-KnownClientApplications $knownApplications -AppRoles $appRoles
|
||||
|
||||
#
|
||||
# Update client application to add reply urls required permissions.
|
||||
#
|
||||
$replyUrls = New-Object System.Collections.Generic.List[System.String]
|
||||
$replyUrls.Add("urn:ietf:wg:oauth:2.0:oob")
|
||||
$requiredResourcesAccess = `
|
||||
New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]
|
||||
$requiredPermissions = GetRequiredPermissions -applicationDisplayName $serviceDisplayName `
|
||||
-requiredDelegatedPermissions "user_impersonation" # "Directory.Read.All|User.Read"
|
||||
$requiredResourcesAccess.Add($requiredPermissions)
|
||||
$requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" `
|
||||
-requiredDelegatedPermissions "User.Read"
|
||||
$requiredResourcesAccess.Add($requiredPermissions)
|
||||
Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId `
|
||||
-RequiredResourceAccess $requiredResourcesAccess -ReplyUrls $replyUrls `
|
||||
-Oauth2AllowImplicitFlow $True -Oauth2AllowUrlPathMatching $True
|
||||
|
||||
#
|
||||
# Update module application to add reply urls required permissions.
|
||||
#
|
||||
$replyUrls = New-Object System.Collections.Generic.List[System.String]
|
||||
$replyUrls.Add("urn:ietf:wg:oauth:2.0:oob")
|
||||
$requiredResourcesAccess = `
|
||||
New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]
|
||||
$requiredPermissions = GetRequiredPermissions -applicationDisplayName $serviceDisplayName `
|
||||
-requiredDelegatedPermissions "user_impersonation" # "Directory.Read.All|User.Read"
|
||||
$requiredResourcesAccess.Add($requiredPermissions)
|
||||
$requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" `
|
||||
-requiredDelegatedPermissions "User.Read"
|
||||
$requiredResourcesAccess.Add($requiredPermissions)
|
||||
Set-AzureADApplication -ObjectId $moduleAadApplication.ObjectId `
|
||||
-RequiredResourceAccess $requiredResourcesAccess -ReplyUrls $replyUrls `
|
||||
-Oauth2AllowImplicitFlow $True -Oauth2AllowUrlPathMatching $True
|
||||
|
||||
return [pscustomobject] @{
|
||||
TenantId = $tenantId
|
||||
Instance = $script:environment.ActiveDirectoryAuthority
|
||||
Audience = $serviceAadApplication.IdentifierUris[0].ToString()
|
||||
AppId = $serviceAadApplication.AppId
|
||||
AppObjectId = $serviceAadApplication.ObjectId
|
||||
ClientId = $clientAadApplication.AppId
|
||||
ClientObjectId = $clientAadApplication.ObjectId
|
||||
ModuleId = $moduleAadApplication.AppId
|
||||
ModuleObjectId = $moduleAadApplication.ObjectId
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$ex = $_.Exception
|
||||
|
||||
Write-Host
|
||||
Write-Host "An error occurred: $($ex.Message)"
|
||||
Write-Host
|
||||
Write-Host "Ensure you have installed the AzureAD cmdlets:"
|
||||
Write-Host "1) Run Powershell as an administrator"
|
||||
Write-Host "2) in the PowerShell window, type: Install-Module AzureAD"
|
||||
Write-Host
|
||||
|
||||
$reply = Read-Host -Prompt "Continue without authentication? [y/n]"
|
||||
if ($reply -match "[yY]") {
|
||||
return $null
|
||||
}
|
||||
throw $ex
|
||||
}
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Get or create new resource group
|
||||
#*******************************************************************************************************
|
||||
Function GetOrCreateResourceGroup() {
|
||||
|
||||
# Registering default resource providers
|
||||
Register-AzureRmResourceProvider -ProviderNamespace "microsoft.devices" | Out-Null
|
||||
Register-AzureRmResourceProvider -ProviderNamespace "microsoft.documentdb" | Out-Null
|
||||
Register-AzureRmResourceProvider -ProviderNamespace "microsoft.eventhub" | Out-Null
|
||||
Register-AzureRmResourceProvider -ProviderNamespace "microsoft.storage" | Out-Null
|
||||
|
||||
while ([string]::IsNullOrEmpty($script:resourceGroupName)) {
|
||||
Write-Host
|
||||
$script:resourceGroupName = Read-Host "Please provide a name for the resource group"
|
||||
}
|
||||
|
||||
# Create or check for existing resource group
|
||||
Select-AzureRmSubscription -SubscriptionId $script:subscriptionId -Force | Out-Host
|
||||
$resourceGroup = Get-AzureRmResourceGroup -Name $script:resourceGroupName `
|
||||
-ErrorAction SilentlyContinue
|
||||
if(!$resourceGroup) {
|
||||
Write-Host "Resource group '$script:resourceGroupName' does not exist."
|
||||
if(!(ValidateLocation $script:resourceGroupLocation)) {
|
||||
SelectLocation
|
||||
}
|
||||
New-AzureRmResourceGroup -Name $script:resourceGroupName `
|
||||
-Location $script:resourceGroupLocation | Out-Host
|
||||
return $True
|
||||
}
|
||||
else{
|
||||
Write-Host "Using existing resource group '$script:resourceGroupName'..."
|
||||
return $False
|
||||
}
|
||||
}
|
||||
|
||||
#*******************************************************************************************************
|
||||
# Script body
|
||||
#*******************************************************************************************************
|
||||
$ErrorActionPreference = "Stop"
|
||||
$script:ScriptDir = Split-Path $script:MyInvocation.MyCommand.Path
|
||||
|
||||
$deploymentScript = Join-Path $script:ScriptDir $script:type
|
||||
$deploymentScript = Join-Path $deploymentScript "run.ps1"
|
||||
if(![System.IO.File]::Exists($deploymentScript)) {
|
||||
throw "Invalid deployment type '$type' specified."
|
||||
}
|
||||
|
||||
$script:interactive = $($script:credential -eq $null)
|
||||
$script:subscriptionId = $null
|
||||
|
||||
SelectEnvironment
|
||||
Login
|
||||
SelectSubscription
|
||||
|
||||
$deleteOnErrorPrompt = GetOrCreateResourceGroup
|
||||
$aadConfig = GetAzureADApplicationConfig
|
||||
|
||||
try {
|
||||
Write-Host "Almost done..."
|
||||
& ($deploymentScript) -resourceGroupName $script:resourceGroupName `
|
||||
-interactive $script:interactive -aadConfig $aadConfig
|
||||
Write-Host "Deployment succeeded."
|
||||
}
|
||||
catch {
|
||||
Write-Host "Deployment failed."
|
||||
$ex = $_.Exception
|
||||
if ($deleteOnErrorPrompt) {
|
||||
$reply = Read-Host -Prompt "Delete resource group? [y/n]"
|
||||
if ($reply -match "[yY]") {
|
||||
try {
|
||||
Remove-AzureRmResourceGroup -Name $script:resourceGroupName -Force
|
||||
}
|
||||
catch {
|
||||
Write-Host $_.Exception.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
throw $ex
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using Microsoft.Azure.IIoT.OpcUa.Api.Vault;
|
||||
using Microsoft.Azure.IIoT.OpcUa.Api.Vault.Models;
|
||||
using Opc.Ua.Gds.Server.OpcVault;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Azure.IIoT.OpcUa.Api.Vault;
|
||||
using Microsoft.Azure.IIoT.OpcUa.Api.Vault.Models;
|
||||
using Opc.Ua.Gds.Server.OpcVault;
|
||||
|
||||
namespace Opc.Ua.Gds.Server.Database.OpcVault
|
||||
{
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<Product>Azure Industrial IoT OPC Vault Services</Product>
|
||||
<RepositoryUrl>https://github.com/Azure/azure-iiot-opc-vault-service</RepositoryUrl>
|
||||
<PackageTags>Industrial;Manufacturing;OPC;OPCUA;Azure;IoT;IIoT;Certificate;OpcVault;KeyVault;.NET</PackageTags>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -1,4 +1,4 @@
|
|||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
@ -24,23 +24,23 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
{
|
||||
internal sealed class CosmosDBApplicationsDatabase : IApplicationsDatabase
|
||||
{
|
||||
const int DefaultRecordsPerQuery = 10;
|
||||
const int _defaultRecordsPerQuery = 10;
|
||||
private readonly ILogger _log;
|
||||
private readonly string Endpoint;
|
||||
private readonly SecureString AuthKeyOrResourceToken;
|
||||
private readonly ILifetimeScope Scope = null;
|
||||
private readonly string _endpoint;
|
||||
private readonly SecureString _authKeyOrResourceToken;
|
||||
private readonly ILifetimeScope _scope = null;
|
||||
|
||||
public CosmosDBApplicationsDatabase(
|
||||
ILifetimeScope scope,
|
||||
IServicesConfig config,
|
||||
ILogger logger)
|
||||
{
|
||||
this.Scope = scope;
|
||||
this.Endpoint = config.CosmosDBEndpoint;
|
||||
this.AuthKeyOrResourceToken = new SecureString();
|
||||
_scope = scope;
|
||||
_endpoint = config.CosmosDBEndpoint;
|
||||
_authKeyOrResourceToken = new SecureString();
|
||||
foreach (char ch in config.CosmosDBToken)
|
||||
{
|
||||
this.AuthKeyOrResourceToken.AppendChar(ch);
|
||||
_authKeyOrResourceToken.AppendChar(ch);
|
||||
}
|
||||
_log = logger;
|
||||
_log.Debug("Creating new instance of `CosmosDBApplicationsDatabase` service " + config.CosmosDBEndpoint, () => { });
|
||||
|
@ -59,7 +59,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
application.ID = await GetMaxAppIDAsync();
|
||||
application.CreateTime = application.UpdateTime = DateTime.UtcNow;
|
||||
application.ApplicationId = Guid.NewGuid();
|
||||
var result = await Applications.CreateAsync(application);
|
||||
var result = await _applications.CreateAsync(application);
|
||||
applicationId = new Guid(result.Id);
|
||||
|
||||
return applicationId.ToString();
|
||||
|
@ -93,7 +93,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
{
|
||||
retryUpdate = false;
|
||||
|
||||
var record = await Applications.GetAsync(applicationId);
|
||||
var record = await _applications.GetAsync(applicationId);
|
||||
if (record == null)
|
||||
{
|
||||
throw new ArgumentException("A record with the specified application id does not exist.", nameof(id));
|
||||
|
@ -114,7 +114,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
record.DiscoveryUrls = application.DiscoveryUrls;
|
||||
try
|
||||
{
|
||||
await Applications.UpdateAsync(applicationId, record, record.ETag);
|
||||
await _applications.UpdateAsync(applicationId, record, record.ETag);
|
||||
}
|
||||
catch (DocumentClientException dce)
|
||||
{
|
||||
|
@ -139,13 +139,13 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
|
||||
List<byte[]> certificates = new List<byte[]>();
|
||||
|
||||
var application = await Applications.GetAsync(appId);
|
||||
var application = await _applications.GetAsync(appId);
|
||||
if (application == null)
|
||||
{
|
||||
throw new ResourceNotFoundException("A record with the specified application id does not exist.");
|
||||
}
|
||||
|
||||
ICertificateRequest certificateRequestsService = Scope.Resolve<ICertificateRequest>();
|
||||
ICertificateRequest certificateRequestsService = _scope.Resolve<ICertificateRequest>();
|
||||
// mark all requests as deleted
|
||||
ReadRequestResultModel[] certificateRequests;
|
||||
string nextPageLink = null;
|
||||
|
@ -158,7 +158,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
}
|
||||
} while (nextPageLink != null);
|
||||
|
||||
await Applications.DeleteAsync(appId);
|
||||
await _applications.DeleteAsync(appId);
|
||||
}
|
||||
|
||||
public async Task<Application> GetApplicationAsync(string id)
|
||||
|
@ -169,7 +169,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
}
|
||||
|
||||
Guid appId = new Guid(id);
|
||||
return await Applications.GetAsync(appId);
|
||||
return await _applications.GetAsync(appId);
|
||||
}
|
||||
|
||||
public async Task<Application[]> FindApplicationAsync(string applicationUri)
|
||||
|
@ -179,7 +179,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
throw new ArgumentException("The applicationUri must be provided", nameof(applicationUri));
|
||||
}
|
||||
|
||||
var results = await Applications.GetAsync(ii => ii.ApplicationUri == applicationUri);
|
||||
var results = await _applications.GetAsync(ii => ii.ApplicationUri == applicationUri);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
|
@ -217,10 +217,10 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
bool lastQuery = false;
|
||||
do
|
||||
{
|
||||
uint queryRecords = complexQuery ? DefaultRecordsPerQuery : maxRecordsToReturn;
|
||||
uint queryRecords = complexQuery ? _defaultRecordsPerQuery : maxRecordsToReturn;
|
||||
string query = CreateServerQuery(startingRecordId, queryRecords);
|
||||
nextRecordId = startingRecordId + 1;
|
||||
var applications = await Applications.GetAsync(query);
|
||||
var applications = await _applications.GetAsync(query);
|
||||
lastQuery = queryRecords == 0 || applications.Count() < queryRecords || applications.Count() == 0;
|
||||
|
||||
foreach (var application in applications)
|
||||
|
@ -315,13 +315,13 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
|
||||
if (maxRecordsToReturn < 0)
|
||||
{
|
||||
maxRecordsToReturn = DefaultRecordsPerQuery;
|
||||
maxRecordsToReturn = _defaultRecordsPerQuery;
|
||||
}
|
||||
string query = CreateServerQuery(0, 0);
|
||||
do
|
||||
{
|
||||
IEnumerable<Application> applications;
|
||||
(nextPageLink, applications) = await Applications.GetPageAsync(query, nextPageLink, maxRecordsToReturn - records.Count);
|
||||
(nextPageLink, applications) = await _applications.GetPageAsync(query, nextPageLink, maxRecordsToReturn - records.Count);
|
||||
|
||||
foreach (var application in applications)
|
||||
{
|
||||
|
@ -389,8 +389,8 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
#region Private Members
|
||||
private void Initialize()
|
||||
{
|
||||
db = new DocumentDBRepository(Endpoint, AuthKeyOrResourceToken);
|
||||
Applications = new DocumentDBCollection<Application>(db);
|
||||
_db = new DocumentDBRepository(_endpoint, _authKeyOrResourceToken);
|
||||
_applications = new DocumentDBCollection<Application>(_db);
|
||||
}
|
||||
|
||||
private string CreateServerQuery(uint startingRecordId, uint maxRecordsToQuery)
|
||||
|
@ -527,15 +527,14 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault
|
|||
private async Task<int> GetMaxAppIDAsync()
|
||||
{
|
||||
// find new ID for QueryServers
|
||||
var maxAppIDEnum = await Applications.GetAsync("SELECT TOP 1 * FROM Applications a ORDER BY a.ID DESC");
|
||||
var maxAppIDEnum = await _applications.GetAsync("SELECT TOP 1 * FROM Applications a ORDER BY a.ID DESC");
|
||||
var maxAppID = maxAppIDEnum.SingleOrDefault();
|
||||
return (maxAppID != null) ? maxAppID.ID + 1 : 1;
|
||||
}
|
||||
#endregion
|
||||
#region Private Fields
|
||||
private DateTime queryCounterResetTime = DateTime.UtcNow;
|
||||
private DocumentDBRepository db;
|
||||
private IDocumentDBCollection<Application> Applications;
|
||||
private DocumentDBRepository _db;
|
||||
private IDocumentDBCollection<Application> _applications;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,12 +21,23 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.CosmosDB
|
|||
/// <inheritdoc/>
|
||||
public DocumentCollection Collection { get; private set; }
|
||||
private readonly IDocumentDBRepository db;
|
||||
private readonly string CollectionId = typeof(T).Name;
|
||||
private readonly string CollectionId;
|
||||
private const int RequestLevelLowest = 400;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DocumentDBCollection(IDocumentDBRepository db)
|
||||
public DocumentDBCollection(IDocumentDBRepository db) : this(db, typeof(T).Name)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DocumentDBCollection(IDocumentDBRepository db, string collectionId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(collectionId))
|
||||
throw new ArgumentNullException("collectionId must be set");
|
||||
if (db == null)
|
||||
throw new ArgumentNullException(nameof(db));
|
||||
|
||||
this.CollectionId = collectionId;
|
||||
this.db = db;
|
||||
CreateCollectionIfNotExistsAsync().Wait();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
FROM microsoft/dotnet:2.1-aspnetcore-runtime-nanoserver-sac2016 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
|
||||
FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 AS build
|
||||
WORKDIR /src
|
||||
COPY src/Microsoft.Azure.IIoT.OpcUa.Services.Vault.csproj src/
|
||||
COPY NuGet.Config NuGet.Config
|
||||
RUN dotnet restore --configfile NuGet.Config -nowarn:msb3202,nu1503 src/Microsoft.Azure.IIoT.OpcUa.Services.Vault.csproj
|
||||
COPY . .
|
||||
WORKDIR /src/src
|
||||
RUN dotnet build Microsoft.Azure.IIoT.OpcUa.Services.Vault.csproj -c Release -o /app
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish Microsoft.Azure.IIoT.OpcUa.Services.Vault.csproj -c Release -o /app
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app .
|
||||
ENTRYPOINT ["dotnet", "Microsoft.Azure.IIoT.OpcUa.Services.Vault.dll"]
|
|
@ -1,4 +1,4 @@
|
|||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
@ -301,11 +301,11 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revokes a certificate collection.
|
||||
/// Finds for each the matching CA cert version and updates Crl.
|
||||
/// </summary>
|
||||
|
||||
public async Task<X509Certificate2Collection> RevokeCertificatesAsync(
|
||||
X509Certificate2Collection certificates)
|
||||
{
|
||||
|
@ -354,6 +354,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
Crl = await _keyVaultServiceClient.LoadCACrl(Configuration.Id, Certificate);
|
||||
return remainingCertificates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new key pair with certificate offline and signs it with KeyVault.
|
||||
/// </summary>
|
||||
|
@ -363,6 +364,52 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
string[] domainNames,
|
||||
string privateKeyFormat,
|
||||
string privateKeyPassword)
|
||||
{
|
||||
await LoadPublicAssets().ConfigureAwait(false);
|
||||
|
||||
DateTime notBefore = TrimmedNotBeforeDate();
|
||||
DateTime notAfter = notBefore.AddMonths(Configuration.DefaultCertificateLifetime);
|
||||
// create new cert in HSM storage
|
||||
using (var signedCertWithPrivateKey = await _keyVaultServiceClient.CreateSignedKeyPairAsync(
|
||||
Configuration.Id,
|
||||
Certificate,
|
||||
application.ApplicationUri,
|
||||
application.ApplicationNames.Count > 0 ? application.ApplicationNames[0].Text : "ApplicationName",
|
||||
subjectName,
|
||||
domainNames,
|
||||
notBefore,
|
||||
notAfter,
|
||||
Configuration.DefaultCertificateKeySize,
|
||||
Configuration.DefaultCertificateHashSize,
|
||||
new KeyVaultSignatureGenerator(_keyVaultServiceClient, _caCertKeyIdentifier, Certificate)
|
||||
).ConfigureAwait(false))
|
||||
{
|
||||
byte[] privateKey;
|
||||
if (privateKeyFormat == "PFX")
|
||||
{
|
||||
privateKey = signedCertWithPrivateKey.Export(X509ContentType.Pfx, privateKeyPassword);
|
||||
}
|
||||
else if (privateKeyFormat == "PEM")
|
||||
{
|
||||
privateKey = CertificateFactory.ExportPrivateKeyAsPEM(signedCertWithPrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Invalid private key format");
|
||||
}
|
||||
return new X509Certificate2KeyPair(new X509Certificate2(signedCertWithPrivateKey.RawData), privateKeyFormat, privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new key pair with certificate offline and signs it with KeyVault.
|
||||
/// </summary>
|
||||
public async Task<X509Certificate2KeyPair> NewKeyPairRequestOfflineAsync(
|
||||
ApplicationRecordDataType application,
|
||||
string subjectName,
|
||||
string[] domainNames,
|
||||
string privateKeyFormat,
|
||||
string privateKeyPassword)
|
||||
{
|
||||
DateTime notBefore = DateTime.UtcNow.AddDays(-1);
|
||||
// create self signed
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IIoT.Diagnostics;
|
||||
|
@ -466,6 +467,113 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new signed application certificate in group id.
|
||||
/// </summary>
|
||||
public async Task<X509Certificate2> CreateSignedKeyPairAsync(
|
||||
string caCertId,
|
||||
X509Certificate2 issuerCert,
|
||||
string applicationUri,
|
||||
string applicationName,
|
||||
string subjectName,
|
||||
string[] domainNames,
|
||||
DateTime notBefore,
|
||||
DateTime notAfter,
|
||||
int keySize,
|
||||
int hashSize,
|
||||
KeyVaultSignatureGenerator generator,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
CertificateOperation createResult = null;
|
||||
var certName = KeyStoreName(caCertId, Guid.NewGuid().ToString());
|
||||
try
|
||||
{
|
||||
// policy unknown issuer, new key, exportable
|
||||
var policyUnknownNewExportable = CreateCertificatePolicy(subjectName, keySize, false, false, true);
|
||||
var attributes = CreateCertificateAttributes(notBefore, notAfter);
|
||||
|
||||
// create the CSR
|
||||
createResult = await _keyVaultClient.CreateCertificateAsync(
|
||||
_vaultBaseUrl,
|
||||
certName,
|
||||
policyUnknownNewExportable,
|
||||
attributes,
|
||||
null,
|
||||
ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (createResult.Csr == null)
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Failed to read CSR from CreateCertificate.");
|
||||
}
|
||||
|
||||
// decode the CSR and verify consistency
|
||||
var pkcs10CertificationRequest = new Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest(createResult.Csr);
|
||||
var info = pkcs10CertificationRequest.GetCertificationRequestInfo();
|
||||
if (createResult.Csr == null ||
|
||||
pkcs10CertificationRequest == null ||
|
||||
!pkcs10CertificationRequest.Verify())
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Invalid CSR.");
|
||||
}
|
||||
|
||||
// create the self signed app cert
|
||||
var publicKey = KeyVaultCertFactory.GetRSAPublicKey(info.SubjectPublicKeyInfo);
|
||||
var signedcert = await KeyVaultCertFactory.CreateSignedCertificate(
|
||||
applicationUri,
|
||||
applicationName,
|
||||
subjectName,
|
||||
domainNames,
|
||||
(ushort)keySize,
|
||||
notBefore,
|
||||
notAfter,
|
||||
(ushort)hashSize,
|
||||
issuerCert,
|
||||
publicKey,
|
||||
generator,
|
||||
true);
|
||||
|
||||
// merge signed cert with keystore
|
||||
var mergeResult = await _keyVaultClient.MergeCertificateAsync(
|
||||
_vaultBaseUrl,
|
||||
certName,
|
||||
new X509Certificate2Collection(signedcert)
|
||||
);
|
||||
|
||||
X509Certificate2 keyPair = null;
|
||||
var secret = await _keyVaultClient.GetSecretAsync(mergeResult.SecretIdentifier.Identifier, ct);
|
||||
if (secret.ContentType == CertificateContentType.Pfx)
|
||||
{
|
||||
var certBlob = Convert.FromBase64String(secret.Value);
|
||||
keyPair = CertificateFactory.CreateCertificateFromPKCS12(certBlob, string.Empty);
|
||||
}
|
||||
else if (secret.ContentType == CertificateContentType.Pem)
|
||||
{
|
||||
Encoding encoder = Encoding.UTF8;
|
||||
var privateKey = encoder.GetBytes(secret.Value.ToCharArray());
|
||||
keyPair = CertificateFactory.CreateCertificateWithPEMPrivateKey(signedcert, privateKey, string.Empty);
|
||||
}
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadInternalError, "Failed to create new key pair certificate");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
var deletedCertBundle = await _keyVaultClient.DeleteCertificateAsync(_vaultBaseUrl, certName, ct);
|
||||
await _keyVaultClient.PurgeDeletedCertificateAsync(_vaultBaseUrl, certName, ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// intentionally fall through, purge may fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports a new CRL for group id.
|
||||
/// </summary>
|
||||
|
@ -752,7 +860,8 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
string subject,
|
||||
int keySize,
|
||||
bool selfSigned,
|
||||
bool reuseKey = false)
|
||||
bool reuseKey = false,
|
||||
bool exportable = false)
|
||||
{
|
||||
|
||||
var policy = new CertificatePolicy
|
||||
|
@ -763,9 +872,9 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
},
|
||||
KeyProperties = new KeyProperties
|
||||
{
|
||||
Exportable = false,
|
||||
Exportable = exportable,
|
||||
KeySize = keySize,
|
||||
KeyType = _keyStoreHSM ? "RSA-HSM" : "RSA",
|
||||
KeyType = (_keyStoreHSM && !exportable) ? "RSA-HSM" : "RSA",
|
||||
ReuseKey = reuseKey
|
||||
},
|
||||
SecretProperties = new SecretProperties
|
||||
|
@ -780,9 +889,13 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
return policy;
|
||||
}
|
||||
|
||||
private string CrlSecretName(string name, X509Certificate2 certificate)
|
||||
private string KeyStoreName(string id, string requestId)
|
||||
{
|
||||
return name + "Crl" + certificate.Thumbprint;
|
||||
return id + "Keys" + requestId;
|
||||
}
|
||||
private string CrlSecretName(string id, X509Certificate2 certificate)
|
||||
{
|
||||
return id + "Crl" + certificate.Thumbprint;
|
||||
}
|
||||
|
||||
private async Task<X509CRL> LoadCrlSecret(string secretIdentifier, CancellationToken ct = default(CancellationToken))
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
public class KeyVaultCertFactory
|
||||
{
|
||||
const int SerialNumberLength = 20;
|
||||
const int DefaultKeySize = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a KeyVault signed certificate.
|
||||
|
@ -131,7 +132,6 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var issuerSubjectName = issuerCAKeyCert != null ? issuerCAKeyCert.SubjectName : subjectDN;
|
||||
X509Certificate2 signedCert = request.Create(
|
||||
issuerSubjectName,
|
||||
|
@ -355,9 +355,9 @@ namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.KeyVault
|
|||
ref ushort keySize)
|
||||
{
|
||||
// enforce recommended keysize unless lower value is enforced.
|
||||
if (keySize < 1024)
|
||||
if (keySize < 2048)
|
||||
{
|
||||
keySize = CertificateFactory.defaultKeySize;
|
||||
keySize = DefaultKeySize;
|
||||
}
|
||||
|
||||
if (keySize % 1024 != 0)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
@ -7,17 +7,19 @@
|
|||
|
||||
namespace Microsoft.Azure.IIoT.OpcUa.Services.Vault.v1.Controllers
|
||||
{
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.IIoT.Diagnostics;
|
||||
using Microsoft.Azure.IIoT.OpcUa.Services.Vault.v1.Auth;
|
||||
using Microsoft.Azure.IIoT.OpcUa.Services.Vault.v1.Filters;
|
||||
using Microsoft.Azure.IIoT.OpcUa.Services.Vault.v1.Models;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Route(VersionInfo.PATH + "/[controller]"), TypeFilter(typeof(ExceptionsFilterAttribute))]
|
||||
[Produces("application/json")]
|
||||
|
||||
[Authorize(Policy = Policies.CanRead)]
|
||||
public sealed class StatusController : Controller
|
||||
{
|
||||
private readonly ILogger log;
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -2,11 +2,5 @@
|
|||
<PropertyGroup>
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<VersionSuffix>preview-$([System.DateTime]::Now.ToString("yyyyMMdd"))</VersionSuffix>
|
||||
<Copyright>Copyright © 2018 Microsoft Corp. All rights reserved.</Copyright>
|
||||
<Company>Microsoft</Company>
|
||||
<PackageLicenseUrl>https://github.com/Azure/azure-iiot-opc-vault-service/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<Product>Microsoft Azure Industrial IoT OPC UA Vault Service</Product>
|
||||
<!-- prevent building packages and releasing to nuget until ready/public -->
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
Загрузка…
Ссылка в новой задаче