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:
Martin Regen 2018-12-21 11:38:50 +01:00 коммит произвёл GitHub
Родитель 5ba205f1cc
Коммит 42890acd3a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
24 изменённых файлов: 3137 добавлений и 86 удалений

Просмотреть файл

@ -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

17
common.props Normal file
Просмотреть файл

@ -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>

3
deploy/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
.user
.app
.env

134
deploy/cloud/run.ps1 Normal file
Просмотреть файл

@ -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

131
deploy/cloud/template.json Normal file
Просмотреть файл

@ -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')]"
}
}
}

770
deploy/deploy.ps1 Normal file
Просмотреть файл

@ -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
{

7
project.props Normal file
Просмотреть файл

@ -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;

1798
thirdpartynotices.txt Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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>