diff --git a/.github/workflows/admin-service-api-deploy.yml b/.github/workflows/admin-service-api-deploy.yml new file mode 100644 index 00000000..9503893c --- /dev/null +++ b/.github/workflows/admin-service-api-deploy.yml @@ -0,0 +1,67 @@ +--- +name: ASDK Administration Service API - Deploy to Azure Web Services + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + AZURE_WEBAPP_NAME: 'admin-api-asdk-test-fd4k' # set this to your application's name + AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root + DOTNET_VERSION: 7.x.x + PROJECT_DIR: ./src/Saas.Admin/Saas.Admin.Service + PROJECT_PATH: ./src/Saas.Admin/Saas.Admin.Service/Saas.Admin.Service.csproj + OUTPUT_PATH: ./publish/admin-api + BUILD_CONFIGURATION: Release # setting the configuration manager build configuration value for our workflow. + + APP_NAME: admin-api +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + #azure login + - uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + # checkout the repo specifying the branch name in 'ref:' + - uses: actions/checkout@v3 + with: + ref: main #IMPORTANT we're checking out and deploying the 'main' branch here + token: ${{ secrets.GITHUB_TOKEN }} + + # Setup .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + # Run dotnet build and publish + - name: dotnet build and publish + run: | + dotnet restore ${{ env.PROJECT_DIR }} + + dotnet build ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} + + dotnet publish ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} \ + --output '${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }}' + + # Deploy to Azure Web apps + - name: Run Azure webapp deploy action using publish profile credentials + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name + package: ${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }} + # slot-name: 'PermissionsApi-Staging' + + # Azure logout + - name: logout + run: | + az logout diff --git a/.github/workflows/permissions-api-deploy.yml b/.github/workflows/permissions-api-deploy.yml index 045b8809..37d8f76c 100644 --- a/.github/workflows/permissions-api-deploy.yml +++ b/.github/workflows/permissions-api-deploy.yml @@ -9,13 +9,15 @@ permissions: contents: read env: - AZURE_WEBAPP_NAME: 'api-permission-asdk-test-b3yf' # set this to your application's name + AZURE_WEBAPP_NAME: 'api-permission-asdk-test-fd4k' # set this to your application's name AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root DOTNET_VERSION: 7.x.x PROJECT_DIR: ./src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1 PROJECT_PATH: ./src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.csproj - OUTPUT_PATH: ./publish + OUTPUT_PATH: ./publish/api-permission + BUILD_CONFIGURATION: Release # setting the configuration manager build configuration value for our workflow. + APP_NAME: permissions-api jobs: build-and-deploy: runs-on: ubuntu-latest @@ -30,7 +32,7 @@ jobs: # checkout the repo specifying the branch name in 'ref:' - uses: actions/checkout@v3 with: - ref: main #IMPORTAT we're checking out and deploying the 'main' branch here + ref: main #IMPORTANT we're checking out and deploying the 'main' branch here token: ${{ secrets.GITHUB_TOKEN }} # Setup .NET Core SDK @@ -45,10 +47,10 @@ jobs: dotnet restore ${{ env.PROJECT_DIR }} dotnet build ${{ env.PROJECT_PATH }} \ - --configuration Release + --configuration ${{ env.BUILD_CONFIGURATION }} dotnet publish ${{ env.PROJECT_PATH }} \ - --configuration Release \ + --configuration ${{ env.BUILD_CONFIGURATION }} \ --output '${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }}' # Deploy to Azure Web apps diff --git a/.github/workflows/signup-administration-deploy.yml b/.github/workflows/signup-administration-deploy.yml new file mode 100644 index 00000000..c61776f5 --- /dev/null +++ b/.github/workflows/signup-administration-deploy.yml @@ -0,0 +1,67 @@ +--- +name: ASDK Sign-up Administration Web App - Deploy to Azure Web Services + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + AZURE_WEBAPP_NAME: 'signupadmin-app-asdk-test-fd4k' # set this to your application's name + AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root + DOTNET_VERSION: 7.x.x + PROJECT_DIR: ./src/Saas.SignupAdministration/Saas.SignupAdministration.Web + PROJECT_PATH: ./src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Saas.SignupAdministration.Web.csproj + OUTPUT_PATH: ./publish/signupadmin + BUILD_CONFIGURATION: Release # setting the configuration manager build configuration value for our workflow. + + APP_NAME: signupadmin-app +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + #azure login + - uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + # checkout the repo specifying the branch name in 'ref:' + - uses: actions/checkout@v3 + with: + ref: main #IMPORTANT we're checking out and deploying the 'main' branch here + token: ${{ secrets.GITHUB_TOKEN }} + + # Setup .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + # Run dotnet build and publish + - name: dotnet build and publish + run: | + dotnet restore ${{ env.PROJECT_DIR }} + + dotnet build ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} + + dotnet publish ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} \ + --output '${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }}' + + # Deploy to Azure Web apps + - name: Run Azure webapp deploy action using publish profile credentials + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name + package: ${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }} + # slot-name: 'PermissionsApi-Staging' + + # Azure logout + - name: logout + run: | + az logout diff --git a/AzureSaaSDevKit.sln b/AzureSaaSDevKit.sln deleted file mode 100644 index a23d15c2..00000000 --- a/AzureSaaSDevKit.sln +++ /dev/null @@ -1,78 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32014.148 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "src\TestUtilities\TestUtilities.csproj", "{02F6CFF4-70ED-4C9D-AF21-4EE3AD668AA4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.SignupAdministration.Web", "src\Saas.SignupAdministration\Saas.SignupAdministration.Web\Saas.SignupAdministration.Web.csproj", "{71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}" -EndProject -Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "Saas.SignupAdministration.Web.Deployment", "src\Saas.SignupAdministration\Saas.SignupAdministration.Web.Deployment\Saas.SignupAdministration.Web.Deployment.deployproj", "{6A0836ED-483E-4F7E-8248-AC00FB52E778}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Admin.Service.Tests", "src\Saas.Admin\Saas.Admin.Service.Tests\Saas.Admin.Service.Tests.csproj", "{7AAD364B-DEFB-4C66-AEBA-D0A10DBDE45B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Admin.Service", "src\Saas.Admin\Saas.Admin.Service\Saas.Admin.Service.csproj", "{B75AFCC1-A92D-4122-9FB2-6C478C67132F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7037F896-6D8A-49AD-964C-6A0FA9AE09DC}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Application.Web", "src\Saas.Application\Saas.Application.Web\Saas.Application.Web.csproj", "{29BBEAD7-1043-41EC-A89D-766E1402B701}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.AspNetCore.Authorization", "src\Saas.Authorization\Saas.AspNetCore.Authorization\Saas.AspNetCore.Authorization.csproj", "{6B9F75FC-4739-4CA1-9347-2F4092A794B1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Permissions.Service", "src\Saas.Identity\Saas.Permissions\Saas.Permissions.Service\Saas.Permissions.Service.csproj", "{E8F9B31E-E2E7-45F7-AE9B-68409630C82E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.AspNetCore.Authorization.Tests", "src\Saas.Authorization\Saas.AspNetCore.Authorization.Tests\Saas.AspNetCore.Authorization.Tests.csproj", "{AEDE788C-35EF-4C03-AA2F-1D1D787001FA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {02F6CFF4-70ED-4C9D-AF21-4EE3AD668AA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {02F6CFF4-70ED-4C9D-AF21-4EE3AD668AA4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {02F6CFF4-70ED-4C9D-AF21-4EE3AD668AA4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {02F6CFF4-70ED-4C9D-AF21-4EE3AD668AA4}.Release|Any CPU.Build.0 = Release|Any CPU - {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Release|Any CPU.Build.0 = Release|Any CPU - {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Release|Any CPU.Build.0 = Release|Any CPU - {7AAD364B-DEFB-4C66-AEBA-D0A10DBDE45B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7AAD364B-DEFB-4C66-AEBA-D0A10DBDE45B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7AAD364B-DEFB-4C66-AEBA-D0A10DBDE45B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7AAD364B-DEFB-4C66-AEBA-D0A10DBDE45B}.Release|Any CPU.Build.0 = Release|Any CPU - {B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Release|Any CPU.Build.0 = Release|Any CPU - {29BBEAD7-1043-41EC-A89D-766E1402B701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29BBEAD7-1043-41EC-A89D-766E1402B701}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29BBEAD7-1043-41EC-A89D-766E1402B701}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29BBEAD7-1043-41EC-A89D-766E1402B701}.Release|Any CPU.Build.0 = Release|Any CPU - {6B9F75FC-4739-4CA1-9347-2F4092A794B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B9F75FC-4739-4CA1-9347-2F4092A794B1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B9F75FC-4739-4CA1-9347-2F4092A794B1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B9F75FC-4739-4CA1-9347-2F4092A794B1}.Release|Any CPU.Build.0 = Release|Any CPU - {E8F9B31E-E2E7-45F7-AE9B-68409630C82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8F9B31E-E2E7-45F7-AE9B-68409630C82E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8F9B31E-E2E7-45F7-AE9B-68409630C82E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8F9B31E-E2E7-45F7-AE9B-68409630C82E}.Release|Any CPU.Build.0 = Release|Any CPU - {AEDE788C-35EF-4C03-AA2F-1D1D787001FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEDE788C-35EF-4C03-AA2F-1D1D787001FA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEDE788C-35EF-4C03-AA2F-1D1D787001FA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEDE788C-35EF-4C03-AA2F-1D1D787001FA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FD825DD0-F9E6-41F6-AA7B-33A4ECC441F2} - EndGlobalSection -EndGlobal diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bda900b5..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: "3.8" - -services: - asdk-admin: - image: asdk-admin - container_name: asdk-admin - build: - context: ./src/ - dockerfile: ./Saas.Admin/Saas.Admin.Service/Dockerfile - expose: - - "80" - ports: - - "8080:80" - asdk-web: - image: asdk-web - container_name: asdk-web - build: - context: ./src/ - dockerfile: ./Saas.Application/Saas.Application.Web/Dockerfile - expose: - - "80" - ports: - - "8081:80" - asdk-signup: - image: asdk-signup - container_name: asdk-signup - build: - context: ./src/ - dockerfile: ./Saas.SignupAdministration/Saas.SignupAdministration.Web/Dockerfile - expose: - - "80" - ports: - - "8082:80" - - asdk-permissions: - image: asdk-permissions - container_name: asdk-permissions - build: - context: ./src/ - dockerfile: ./Saas.Identity/Saas.Permissions/Saas.Permissions.Service/Dockerfile - expose: - - "80" - ports: - - "8083:80" - asdk-identity-setup: - image: asdk-identity-setup - container_name: asdk-identity-setup - build: - context: ./src/Saas.Identity - dockerfile: ./Saas.IdentityProvider/scripts/Dockerfile - diff --git a/src/Saas.Admin/Saas.Admin.Service.Deployment/Deploy-AzureResourceGroup.ps1 b/src/Saas.Admin/Saas.Admin.Service.Deployment/Deploy-AzureResourceGroup.ps1 deleted file mode 100644 index bd5bde54..00000000 --- a/src/Saas.Admin/Saas.Admin.Service.Deployment/Deploy-AzureResourceGroup.ps1 +++ /dev/null @@ -1,120 +0,0 @@ -#Requires -Version 3.0 - -Param( - [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation, - [string] $ResourceGroupName = 'Saas.Admin', - [switch] $UploadArtifacts, - [string] $StorageAccountName, - [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts', - [string] $TemplateFile = 'azuredeploy.json', - [string] $TemplateParametersFile = 'azuredeploy.parameters.json', - [string] $ArtifactStagingDirectory = '.', - [string] $DSCSourceFolder = 'DSC', - [switch] $ValidateOnly -) - -try { - [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0') -} catch { } - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -function Format-ValidationOutput { - param ($ValidationOutput, [int] $Depth = 0) - Set-StrictMode -Off - return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) }) -} - -$OptionalParameters = New-Object -TypeName Hashtable -$TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile)) -$TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile)) - -if ($UploadArtifacts) { - # Convert relative paths to absolute paths if needed - $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory)) - $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder)) - - # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present - $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json - if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) { - $JsonParameters = $JsonParameters.parameters - } - $ArtifactsLocationName = '_artifactsLocation' - $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken' - $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore - $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore - - # Create DSC configuration archive - if (Test-Path $DSCSourceFolder) { - $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName}) - foreach ($DSCSourceFilePath in $DSCSourceFilePaths) { - $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip' - Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose - } - } - - # Create a storage account name if none was provided - if ($StorageAccountName -eq '') { - $StorageAccountName = 'stage' + ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19) - } - - $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}) - - # Create the storage account if it doesn't already exist - if ($StorageAccount -eq $null) { - $StorageResourceGroupName = 'ARM_Deploy_Staging' - New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force - $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation" - } - - # Generate the value for artifacts location if it is not provided in the parameter file - if ($OptionalParameters[$ArtifactsLocationName] -eq $null) { - $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName + '/' - } - - # Copy files from the local storage staging location to the storage account container - New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1 - - $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName} - foreach ($SourcePath in $ArtifactFilePaths) { - Set-AzureStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) ` - -Container $StorageContainerName -Context $StorageAccount.Context -Force - } - - # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file - if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) { - $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force ` - (New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4)) - } -} - -# Create the resource group only when it doesn't already exist -if ((Get-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue) -eq $null) { - New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop -} - -if ($ValidateOnly) { - $ErrorMessages = Format-ValidationOutput (Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName ` - -TemplateFile $TemplateFile ` - -TemplateParameterFile $TemplateParametersFile ` - @OptionalParameters) - if ($ErrorMessages) { - Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.' - } - else { - Write-Output '', 'Template is valid.' - } -} -else { - New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) ` - -ResourceGroupName $ResourceGroupName ` - -TemplateFile $TemplateFile ` - -TemplateParameterFile $TemplateParametersFile ` - @OptionalParameters ` - -Force -Verbose ` - -ErrorVariable ErrorMessages - if ($ErrorMessages) { - Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") }) - } -} \ No newline at end of file diff --git a/src/Saas.Admin/Saas.Admin.Service.Deployment/Deployment.targets b/src/Saas.Admin/Saas.Admin.Service.Deployment/Deployment.targets deleted file mode 100644 index 0d792ec6..00000000 --- a/src/Saas.Admin/Saas.Admin.Service.Deployment/Deployment.targets +++ /dev/null @@ -1,123 +0,0 @@ - - - - Debug - AnyCPU - bin\$(Configuration)\ - false - true - false - None - obj\ - $(BaseIntermediateOutputPath)\ - $(BaseIntermediateOutputPath)$(Configuration)\ - $(IntermediateOutputPath)ProjectReferences - $(ProjectReferencesOutputPath)\ - true - - - - false - false - - - - - - - - - - - Always - - - Never - - - false - Build - - - - - - - - _GetDeploymentProjectContent; - _CalculateContentOutputRelativePaths; - _GetReferencedProjectsOutput; - _CalculateArtifactStagingDirectory; - _CopyOutputToArtifactStagingDirectory; - - - - - - - - - - - - - - - - - Configuration=$(Configuration);Platform=$(Platform) - - - - - - - $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)')) - - - - - - - $(OutDir) - $(OutputPath) - $(ArtifactStagingDirectory)\ - $(ArtifactStagingDirectory)staging\ - $(Build_StagingDirectory) - - - - - - - <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity) - <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', '')) - - - - - $(_RelativePath) - - - - - - - - - PrepareForRun - - - - - - - - - - - diff --git a/src/Saas.Admin/Saas.Admin.Service.Deployment/Saas.Admin.Service.Deployment.deployproj b/src/Saas.Admin/Saas.Admin.Service.Deployment/Saas.Admin.Service.Deployment.deployproj deleted file mode 100644 index c7a7aba4..00000000 --- a/src/Saas.Admin/Saas.Admin.Service.Deployment/Saas.Admin.Service.Deployment.deployproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Debug - AnyCPU - - - Release - AnyCPU - - - - d52c84e3-a85c-4bde-9550-35e6c075381c - - - - - - - - - - - - - - - False - - - - - \ No newline at end of file diff --git a/src/Saas.Admin/Saas.Admin.Service.Deployment/azuredeploy.json b/src/Saas.Admin/Saas.Admin.Service.Deployment/azuredeploy.json deleted file mode 100644 index 320f1342..00000000 --- a/src/Saas.Admin/Saas.Admin.Service.Deployment/azuredeploy.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "SaasProviderName": { - "type": "string", - "defaultValue": "contoso" - }, - "SaasEnvironment": { - "type": "string", - "defaultValue": "dev", - "allowedValues": [ - "dev", - "staging", - "test", - "prod" - ] - }, - "SaasLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for the Cosmos DB account." - } - }, - "SaasInstanceNumber": { - "type": "string", - "defaultValue": "001" - }, - "CosmosDbEndpoint": { - "type": "string" - }, - "CosmosDbAccountKey": { - "type": "string", - "metadata": { - "description": "The account key output of CosmosDB" - } - }, - "CosmosDbAccountName": { - "type": "string", - "defaultValue": "[concat('cosmos-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]", - "metadata": { - "description": "Cosmos DB account name" - } - }, - "CosmosDbDatabaseName": { - "type": "string", - "defaultValue": "azuresaas", - "metadata": { - "description": "The name for the Core (SQL) database" - } - }, - "CosmosDbConnectionString": { - "type": "string" - }, - "IdentityDbConnectionString": { - "type": "string" - }, - "CatalogDbConnectionString": { - "type": "string" - } - }, - "variables": { - "appServicePlanName": "[concat('app-', parameters('SaasProviderName'), '-admin-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]", - "adminWebAppName": "[concat('app-', parameters('SaasProviderName'), '-admin-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]" - }, - "resources": [ - { - "name": "[variables('appServicePlanName')]", - "type": "Microsoft.Web/serverfarms", - "location": "[resourceGroup().location]", - "apiVersion": "2015-08-01", - "sku": { - "name": "F1" - }, - "dependsOn": [], - "tags": { - "displayName": "SaaS Provider App Service Plan" - }, - "properties": { - "name": "[variables('appServicePlanName')]", - "numberOfWorkers": 1 - }, - "resources": [ - { - "name": "[variables('adminWebAppName')]", - "type": "Microsoft.Web/sites", - "location": "[resourceGroup().location]", - "apiVersion": "2015-08-01", - "dependsOn": [ - "[concat('Microsoft.Web/serverFarms/', variables('appServicePlanName'))]" - ], - "tags": { - "displayName": "SaaS Administration Web App" - }, - "properties": { - "name": "[variables('adminWebAppName')]", - "serverFarmId": "[resourceId(resourceGroup().name, 'Microsoft.Web/serverFarms', variables('appServicePlanName'))]", - "siteConfig": { - "netFrameworkVersion": "v6.0", - "appSettings": [ - { - "name": "ASPNETCORE_ENVIRONMENT", - "value": "Production" - }, - { - "name": "AppSettings:CosmosDb:Account", - "value": "[parameters('CosmosDbEndpoint')]" - }, - { - "name": "AppSettings:CosmosDb:Key", - "value": "[parameters('CosmosDbAccountKey')]" - }, - { - "name": "AppSettings:CosmosDb:DatabaseName", - "value": "[parameters('CosmosDbDatabaseName')]" - }, - { - "name": "AppSettings:CosmosDb:ContainerName", - "value": "OnboardingFlow" - } - ], - "connectionStrings": [ - { - "name": "CosmosDb", - "connectionString": "[parameters('CosmosDbConnectionString')]", - "type": "Custom" - }, - { - "name": "IdentityDbConnection", - "connectionString": "[parameters('IdentityDbConnectionString')]", - "type": "SQLAzure" - }, - { - "name": "CatalogDbConnection", - "connectionString": "[parameters('CatalogDbConnectionString')]", - "type": "SQLAzure" - } - ] - } - }, - "resources": [ - { - "name": "MSDeploy", - "type": "extensions", - "location": "[resourceGroup().location]", - "apiVersion": "2015-08-01", - "dependsOn": [ "[resourceId('Microsoft.Web/sites', variables('adminWebAppName'))]" ], - "tags": { "displayName": "Deploy" }, - "properties": { - "packageUri": "https://stsaasdev001.blob.core.windows.net/artifacts/saas-admin/Saas.Admin.Service.zip?sv=2020-04-08&st=2021-06-07T19%3A23%3A20Z&se=2022-06-08T19%3A23%3A00Z&sr=c&sp=rl&sig=kNf0qwTfaCJg02xYeUHlfmHOJvI1bGU1HftjUJ5hl5o%3D" - } - } - ] - } - ] - } - ], - "outputs": { - } - } \ No newline at end of file diff --git a/src/Saas.Admin/Saas.Admin.Service.Deployment/azuredeploy.parameters.json b/src/Saas.Admin/Saas.Admin.Service.Deployment/azuredeploy.parameters.json deleted file mode 100644 index 18b8c0e4..00000000 --- a/src/Saas.Admin/Saas.Admin.Service.Deployment/azuredeploy.parameters.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - } - } \ No newline at end of file diff --git a/src/Saas.Admin/Saas.Admin.Service.Tests/Controllers/NewTenantRequestTests.cs b/src/Saas.Admin/Saas.Admin.Service.Tests/Controllers/NewTenantRequestTests.cs index fd780ac0..5a1f3da3 100644 --- a/src/Saas.Admin/Saas.Admin.Service.Tests/Controllers/NewTenantRequestTests.cs +++ b/src/Saas.Admin/Saas.Admin.Service.Tests/Controllers/NewTenantRequestTests.cs @@ -1,15 +1,12 @@ -namespace Saas.Admin.Service.Tests +namespace Saas.Admin.Service.Tests; + +public class NewTenantRequestTests { - using Xunit; - - public class NewTenantRequestTests + [Theory, AutoDataNSubstitute] + public void All_Values_Are_Copied_To_Tenant(NewTenantRequest tenantRequest) { - [Theory, AutoDataNSubstitute] - public void All_Values_Are_Copied_To_Tenant(NewTenantRequest tenantRequest) - { - Tenant tenant = tenantRequest.ToTenant(); + Tenant tenant = tenantRequest.ToTenant(); - AssertAdditions.AllPropertiesAreEqual(tenant, tenantRequest, nameof(tenant.ConcurrencyToken), nameof(tenant.CreatedTime), nameof(tenant.Id)); - } + AssertAdditions.AllPropertiesAreEqual(tenant, tenantRequest, nameof(tenant.ConcurrencyToken), nameof(tenant.CreatedTime), nameof(tenant.Id)); } -} \ No newline at end of file +} diff --git a/src/Saas.Admin/Saas.Admin.Service.Tests/Saas.Admin.Service.Tests.csproj b/src/Saas.Admin/Saas.Admin.Service.Tests/Saas.Admin.Service.Tests.csproj index 59cc1f07..e362d6b5 100644 --- a/src/Saas.Admin/Saas.Admin.Service.Tests/Saas.Admin.Service.Tests.csproj +++ b/src/Saas.Admin/Saas.Admin.Service.Tests/Saas.Admin.Service.Tests.csproj @@ -1,21 +1,21 @@ - net6.0 + net7.0 enable false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Saas.Admin/Saas.Admin.Service.Tests/Services/TenantServiceTests.cs b/src/Saas.Admin/Saas.Admin.Service.Tests/Services/TenantServiceTests.cs index 6a664599..51c14fe7 100644 --- a/src/Saas.Admin/Saas.Admin.Service.Tests/Services/TenantServiceTests.cs +++ b/src/Saas.Admin/Saas.Admin.Service.Tests/Services/TenantServiceTests.cs @@ -1,109 +1,98 @@ -namespace Saas.Admin.Service.Tests +using System.Collections.Generic; +using System.Linq; +using AutoFixture.Xunit2; +using Microsoft.EntityFrameworkCore; +using Saas.Admin.Service.Exceptions; +using Saas.Admin.Service.Services; + +namespace Saas.Admin.Service.Tests; + +public class TenantServiceTests { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using AutoFixture.Xunit2; - - using Microsoft.EntityFrameworkCore; - - using Saas.Admin.Service.Data; - using Saas.Admin.Service.Exceptions; - using Saas.Admin.Service.Services; - - using TestUtilities; - - using Xunit; - - public class TenantServiceTests + [Theory, AutoDataNSubstitute] + public async Task Will_Not_Return_Null_When_No_Tenants(TenantService tenantService) { - - [Theory, AutoDataNSubstitute] - public async Task Will_Not_Return_Null_When_No_Tenants(TenantService tenantService) - { - IEnumerable? result = await tenantService.GetAllTenantsAsync(); - Assert.NotNull(result); - Assert.Empty(result.ToList()); - } - - [Theory, AutoDataNSubstitute] - public async Task Will_throw_if_tenenent_Not_Found([Frozen] TenantsContext tenantsContext, TenantService tenantService, Guid tenantId) - { - Assert.Null(await tenantsContext.Tenants.FindAsync(tenantId)); - - await Assert.ThrowsAsync(() => tenantService.GetTenantAsync(tenantId)); - } - - [Theory, AutoDataNSubstitute] - public async Task Will_throw_if_tenenent_Not_Found2([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] tenants) - { - await tenantsContext.Tenants.AddRangeAsync(tenants); - - Guid id = tenants[^1].Id; - TenantDTO? tenant = await tenantService.GetTenantAsync(id); - - AssertAdditions.AllPropertiesAreEqual(tenant, tenants[^1], nameof(tenant.CreatedTime), nameof(tenant.Version)); - await Assert.ThrowsAsync(() => tenantService.GetTenantAsync(Guid.NewGuid())); - } - - [Theory, AutoDataNSubstitute] - public async Task can_find_tenent([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] tenants) - { - await tenantsContext.Tenants.AddRangeAsync(tenants); - - Guid id = tenants[^1].Id; - TenantDTO? tenant = await tenantService.GetTenantAsync(id); - - AssertAdditions.AllPropertiesAreEqual(tenant, tenants[^1], nameof(tenant.CreatedTime), nameof(tenant.Version)); - } - - [Theory, AutoDataNSubstitute] - public async Task Can_update_tenant([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] originalTenants, TenantDTO updatedDto) - { - await tenantsContext.Tenants.AddRangeAsync(originalTenants); - await tenantsContext.SaveChangesAsync(); - - Tenant tenant = await tenantsContext.Tenants.FirstAsync(); - DateTime originalCreated = tenant.CreatedTime!.Value; - - updatedDto.Id = tenant.Id; - updatedDto.Version = null; - - TenantDTO? updatedTenant = await tenantService.UpdateTenantAsync(updatedDto); - - AssertAdditions.AllPropertiesAreEqual(updatedDto, updatedTenant, nameof(updatedTenant.Version), nameof(updatedTenant.CreatedTime)); - Assert.Equal(originalCreated, updatedTenant.CreatedTime); - } - - [Theory, AutoDataNSubstitute] - public async Task Can_delete_tenant([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] originalTenants) - { - await tenantsContext.Tenants.AddRangeAsync(originalTenants); - await tenantsContext.SaveChangesAsync(); - - Guid idToDelete = originalTenants[0].Id; - - Assert.NotNull(await tenantService.GetTenantAsync(idToDelete)); - - await tenantService.DeleteTenantAsync(idToDelete); - - await Assert.ThrowsAsync(() => tenantService.GetTenantAsync(idToDelete)); - } - - - [Theory, AutoDataNSubstitute] - public async Task Check_For_Existing_Route([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] originalTenants, string notInDBRoute) - { - await tenantsContext.Tenants.AddRangeAsync(originalTenants); - await tenantsContext.SaveChangesAsync(); - - bool exists = await tenantService.CheckPathExists(originalTenants[0].Route); - Assert.True(exists); - - bool notExists = await tenantService.CheckPathExists(notInDBRoute); - Assert.False(notExists); - } + IEnumerable? result = await tenantService.GetAllTenantsAsync(); + Assert.NotNull(result); + Assert.Empty(result.ToList()); } -} \ No newline at end of file + + [Theory, AutoDataNSubstitute] + public async Task Will_throw_if_tenenent_Not_Found([Frozen] TenantsContext tenantsContext, TenantService tenantService, Guid tenantId) + { + Assert.Null(await tenantsContext.Tenants.FindAsync(tenantId)); + + await Assert.ThrowsAsync(() => tenantService.GetTenantAsync(tenantId)); + } + + [Theory, AutoDataNSubstitute] + public async Task Will_throw_if_tenenent_Not_Found2([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] tenants) + { + await tenantsContext.Tenants.AddRangeAsync(tenants); + + Guid id = tenants[^1].Id; + TenantDTO? tenant = await tenantService.GetTenantAsync(id); + + AssertAdditions.AllPropertiesAreEqual(tenant, tenants[^1], nameof(tenant.CreatedTime), nameof(tenant.Version)); + await Assert.ThrowsAsync(() => tenantService.GetTenantAsync(Guid.NewGuid())); + } + + [Theory, AutoDataNSubstitute] + public async Task can_find_tenent([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] tenants) + { + await tenantsContext.Tenants.AddRangeAsync(tenants); + + Guid id = tenants[^1].Id; + TenantDTO? tenant = await tenantService.GetTenantAsync(id); + + AssertAdditions.AllPropertiesAreEqual(tenant, tenants[^1], nameof(tenant.CreatedTime), nameof(tenant.Version)); + } + + [Theory, AutoDataNSubstitute] + public async Task Can_update_tenant([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] originalTenants, TenantDTO updatedDto) + { + await tenantsContext.Tenants.AddRangeAsync(originalTenants); + await tenantsContext.SaveChangesAsync(); + + Tenant tenant = await tenantsContext.Tenants.FirstAsync(); + DateTime originalCreated = tenant.CreatedTime!.Value; + + updatedDto.Id = tenant.Id; + updatedDto.Version = null; + + TenantDTO? updatedTenant = await tenantService.UpdateTenantAsync(updatedDto); + + AssertAdditions.AllPropertiesAreEqual(updatedDto, updatedTenant, nameof(updatedTenant.Version), nameof(updatedTenant.CreatedTime)); + Assert.Equal(originalCreated, updatedTenant.CreatedTime); + } + + [Theory, AutoDataNSubstitute] + public async Task Can_delete_tenant([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] originalTenants) + { + await tenantsContext.Tenants.AddRangeAsync(originalTenants); + await tenantsContext.SaveChangesAsync(); + + Guid idToDelete = originalTenants[0].Id; + + Assert.NotNull(await tenantService.GetTenantAsync(idToDelete)); + + await tenantService.DeleteTenantAsync(idToDelete); + + await Assert.ThrowsAsync(() => tenantService.GetTenantAsync(idToDelete)); + } + + + [Theory, AutoDataNSubstitute] + public async Task Check_For_Existing_Route([Frozen] TenantsContext tenantsContext, TenantService tenantService, Tenant[] originalTenants, string notInDBRoute) + { + await tenantsContext.Tenants.AddRangeAsync(originalTenants); + await tenantsContext.SaveChangesAsync(); + + bool exists = await tenantService.CheckPathExists(originalTenants[0].Route); + Assert.True(exists); + + bool notExists = await tenantService.CheckPathExists(notInDBRoute); + Assert.False(notExists); + } +} diff --git a/src/Saas.Admin/Saas.Admin.Service/Controllers/TenantDTO.cs b/src/Saas.Admin/Saas.Admin.Service/Controllers/TenantDTO.cs index 740cbc88..0a0b466a 100644 --- a/src/Saas.Admin/Saas.Admin.Service/Controllers/TenantDTO.cs +++ b/src/Saas.Admin/Saas.Admin.Service/Controllers/TenantDTO.cs @@ -26,7 +26,9 @@ public class TenantDTO ProductTierId = tenant.ProductTierId; CategoryId = tenant.CategoryId; - Version = tenant.ConcurrencyToken != null ? Convert.ToBase64String(tenant.ConcurrencyToken) : null; + Version = tenant.ConcurrencyToken is not null + ? Convert.ToBase64String(tenant.ConcurrencyToken) + : null; } public Tenant ToTenant() diff --git a/src/Saas.Admin/Saas.Admin.Service/Program.cs b/src/Saas.Admin/Saas.Admin.Service/Program.cs index bf881844..c10b784e 100644 --- a/src/Saas.Admin/Saas.Admin.Service/Program.cs +++ b/src/Saas.Admin/Saas.Admin.Service/Program.cs @@ -1,37 +1,78 @@ -using System.Security.Cryptography.X509Certificates; - using Azure.Identity; - +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.Extensions.Options; using Saas.Admin.Service; using Saas.Admin.Service.Data; -using Saas.Admin.Service.Utilities; using Saas.AspNetCore.Authorization.AuthHandlers; using Saas.AspNetCore.Authorization.ClaimTransformers; +using Saas.Shared.Options; +using Saas.Swagger; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddApplicationInsightsTelemetry(); -X509Certificate2 permissionsApiCertificate; +string projectName = Assembly.GetCallingAssembly().GetName().Name + ?? throw new NullReferenceException("Project name cannot be null."); -if (builder.Environment.IsProduction()) +var logger = LoggerFactory.Create(config => config.AddConsole()).CreateLogger(projectName); + +logger.LogInformation("001"); + +/* IMPORTANT + In the configuration pattern used here, we're seeking to minimize the use of appsettings.json, + as well as eliminate the need for storing local secrets. + + Instead we're utilizing the Azure App Configuration service for storing settings and the Azure Key Vault to store secrets. + Azure App Configuration still hold references to the secret, but not the secret themselves. + + This approach is more secure and allows us to have a single source of truth + for all settings and secrets. + + The settings and secrets are provisioned by the deployment script made available for deploying this service. + Please see the readme for the project for details. + + For local development, please see the ASDK Permission Service readme.md for more + on how to set up and run this service in a local development environment - i.e., a local dev machine. +*/ + +if (builder.Environment.IsDevelopment()) { - // Get Secrets From Azure Key Vault if in production. If not in production, secrets are automatically loaded in from the .NET secrets manager - // https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0 - builder.Configuration.AddAzureKeyVault( - new Uri(builder.Configuration["KeyVault:Url"]), - new DefaultAzureCredential(), - new CustomPrefixKeyVaultSecretManager("admin")); - + InitializeDevEnvironment(); +} +else +{ + InitializeProdEnvironment(); } +builder.Services.Configure( + builder.Configuration.GetRequiredSection(AzureB2CAdminApiOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(ClaimToRoleTransformerOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(AzureB2CPermissionsApiOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(PermissionsApiOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(SqlOptions.SectionName)); + +// Using Entity Framework for accessing permission data stored in the Permissions Db. builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("TenantsContext"))); +{ + var sqlConnectionString = builder.Configuration.GetRequiredSection(SqlOptions.SectionName) + .Get()?.SQLConnectionString + ?? throw new NullReferenceException("SQL Connection string cannot be null."); +}); // Add authentication for incoming requests -builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration, "AzureAdB2C"); +builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration, AzureB2CAdminApiOptions.SectionName); +builder.Services.AddTransient(); - -builder.Services.AddClaimToRoleTransformer(builder.Configuration, "ClaimToRoleTransformer"); builder.Services.AddRouteBasedRoleHandler("tenantId"); builder.Services.AddRouteBasedRoleHandler("userId"); @@ -39,7 +80,6 @@ builder.Services.AddRouteBasedRoleHandler("userId"); builder.Services.AddAuthorization(options => { - options.AddPolicy(AppConstants.Policies.Authenticated, policyBuilder => { policyBuilder.RequireAuthenticatedUser(); @@ -93,26 +133,25 @@ builder.Services.AddAuthorization(options => }); - builder.Services.AddControllers(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient() - .ConfigureHttpClient(options => + .ConfigureHttpClient((serviceProvider, client) => { - options.BaseAddress = new Uri(builder.Configuration["PermissionsApi:BaseUrl"]); - options.DefaultRequestHeaders.Add("x-api-key", builder.Configuration["PermissionsApi:ApiKey"]); - }); + using var scope = serviceProvider.CreateScope(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - string? xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); -}); + var baseUrl = scope.ServiceProvider.GetRequiredService>().Value.BaseUrl + ?? throw new NullReferenceException("Permissions Base Url cannot be null"); + + var apiKey = scope.ServiceProvider.GetRequiredService>().Value.ApiKey + ?? throw new NullReferenceException("Permissions Base Api Key cannot be null"); + + client.BaseAddress = new Uri(baseUrl); + client.DefaultRequestHeaders.Add("x-api-key", apiKey); + }); var app = builder.Build(); @@ -134,4 +173,78 @@ app.UseAuthorization(); app.MapControllers(); -app.Run(); \ No newline at end of file +app.Run(); + + +/*--------------- + local methods +----------------*/ + +void InitializeDevEnvironment() +{ + // IMPORTANT + // The current version. + // Must correspond exactly to the version string of our deployment as specificed in the deployment config.json. + var version = "ver0.8.0"; + + logger.LogInformation("Version: {version}", version); + logger.LogInformation($"Is Development."); + + // For local development, use the Secret Manager feature of .NET to store a connection string + // and likewise for storing a secret for the permission-api app. + // https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows + + var appConfigurationconnectionString = builder.Configuration.GetConnectionString("AppConfig") + ?? throw new NullReferenceException("App config missing."); + + // Use the connection string to access Azure App Configuration to get access to app settings stored there. + // To gain access to Azure Key Vault use 'Azure Cli: az login' to log into Azure. + // This login on will also now provide valid access tokens to the local development environment. + // For more details and the option to chain and combine multiple credential options with `ChainedTokenCredential` + // please see: https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#define-a-custom-authentication-flow-with-chainedtokencredential + + AzureCliCredential credential = new(); + + builder.Configuration.AddAzureAppConfiguration(options => + options.Connect(appConfigurationconnectionString) + .ConfigureKeyVault(kv => kv.SetCredential(new ChainedTokenCredential(credential))) + .Select(KeyFilter.Any, version)); // <-- Important: since we're using labels in our Azure App Configuration store + + // Configuring Swagger. + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + + // Enabling to option for add the 'x-api-key' header to swagger UI. + builder.Services.AddSwaggerGen(option => + { + option.SwaggerDoc("v1", new() { Title = "Admin API", Version = "v1.1" }); + option.OperationFilter(); + }); +} + +void InitializeProdEnvironment() +{ + // For procution environment, we'll configured Managed Identities for managing access Azure App Services + // and Key Vault. The Azure App Services endpoint is stored in an environment variable for the web app. + + var version = builder.Configuration.GetRequiredSection("Version")?.Value + ?? throw new NullReferenceException("The Version value cannot be found. Has the 'Version' environment variable been set correctly for the Web App?"); + + logger.LogInformation("Version: {version}", version); + logger.LogInformation($"Is Production."); + + var appConfigurationEndpoint = builder.Configuration.GetRequiredSection("AppConfiguration:Endpoint")?.Value + ?? throw new NullReferenceException("The Azure App Configuration Endpoint cannot be found. Has the endpoint environment variable been set correctly for the Web App?"); + + // Get the ClientId of the UserAssignedIdentity + // If we don't set this ClientID in the ManagedIdentityCredential constructor, it doesn't know it should use the user assigned managed id. + var managedIdentityClientId = builder.Configuration.GetRequiredSection("UserAssignedManagedIdentityClientId")?.Value + ?? throw new NullReferenceException("The Environment Variable 'UserAssignedManagedIdentityClientId' cannot be null. Check the App Service Configuration."); + + ManagedIdentityCredential userAssignedManagedCredentials = new(managedIdentityClientId); + + builder.Configuration.AddAzureAppConfiguration(options => + options.Connect(new Uri(appConfigurationEndpoint), userAssignedManagedCredentials) + .ConfigureKeyVault(kv => kv.SetCredential(userAssignedManagedCredentials)) + .Select(KeyFilter.Any, version)); // <-- Important since we're using labels in our Azure App Configuration store +} \ No newline at end of file diff --git a/src/Saas.Admin/Saas.Admin.Service/Saas.Admin.Service.csproj b/src/Saas.Admin/Saas.Admin.Service/Saas.Admin.Service.csproj index b020bc0d..1c3e2402 100644 --- a/src/Saas.Admin/Saas.Admin.Service/Saas.Admin.Service.csproj +++ b/src/Saas.Admin/Saas.Admin.Service/Saas.Admin.Service.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 enable enable aspnet-Saas.Admin.Service-5358E0C3-EA51-44EA-B381-CA2F9D9710D3 @@ -11,20 +11,22 @@ - - + + - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + @@ -32,6 +34,7 @@ - + + diff --git a/src/Saas.Admin/Saas.Admin.Service/appsettings.json b/src/Saas.Admin/Saas.Admin.Service/appsettings.json index 4c0a8dc3..d398ffdc 100644 --- a/src/Saas.Admin/Saas.Admin.Service/appsettings.json +++ b/src/Saas.Admin/Saas.Admin.Service/appsettings.json @@ -1,8 +1,4 @@ -{ - "AzureAdB2C": { - "SignedOutCallbackPath": "/signout/B2C_1A_SIGNUP_SIGNIN", - "SignUpSignInPolicyId": "B2C_1A_SIGNUP_SIGNIN" - }, +{ "Logging": { "LogLevel": { "Default": "Information", @@ -13,9 +9,9 @@ "Url": "", "PermissionsApiCertName": "devenvcert" }, - "PermissionsApi": { - "BaseUrl": "https://localhost:7023" - }, + //"PermissionsApi": { + // "BaseUrl": "https://localhost:7023" + //}, "ClaimToRoleTransformer": { "SourceClaimType": "permissions", //Name of the claim custom roles are in "RoleClaimtype": "MyCustomRoles", //Type of the claim to use in the new Identity (works along side of built in) diff --git a/src/Saas.Admin/Saas.Admin.sln b/src/Saas.Admin/Saas.Admin.sln index af280bb9..4aa5331b 100644 --- a/src/Saas.Admin/Saas.Admin.sln +++ b/src/Saas.Admin/Saas.Admin.sln @@ -1,22 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saas.Admin.Service", "Saas.Admin.Service\Saas.Admin.Service.csproj", "{A6134452-BA8E-4C84-8879-C8EEF82ED5C9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Admin.Service", "Saas.Admin.Service\Saas.Admin.Service.csproj", "{A6134452-BA8E-4C84-8879-C8EEF82ED5C9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saas.Admin.Service.Tests", "Saas.Admin.Service.Tests\Saas.Admin.Service.Tests.csproj", "{5974515A-43DF-43E7-A1E6-FC97E47E480F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Admin.Service.Tests", "Saas.Admin.Service.Tests\Saas.Admin.Service.Tests.csproj", "{5974515A-43DF-43E7-A1E6-FC97E47E480F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtilities", "..\TestUtilities\TestUtilities.csproj", "{5A220537-38DA-495B-8CCF-723FA65310F1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\TestUtilities\TestUtilities.csproj", "{5A220537-38DA-495B-8CCF-723FA65310F1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.AspNetCore.Authorization", "..\Saas.Lib\Saas.Authorization\Saas.AspNetCore.Authorization\Saas.AspNetCore.Authorization.csproj", "{9B4C4680-3BBA-419B-AF79-CB80E1422B1A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Shared", "..\Saas.Lib\Saas.Shared\Saas.Shared.csproj", "{E584B588-FBB6-4D30-9EA2-6B6E531F82EC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A6134452-BA8E-4C84-8879-C8EEF82ED5C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6134452-BA8E-4C84-8879-C8EEF82ED5C9}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -30,5 +31,19 @@ Global {5A220537-38DA-495B-8CCF-723FA65310F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A220537-38DA-495B-8CCF-723FA65310F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A220537-38DA-495B-8CCF-723FA65310F1}.Release|Any CPU.Build.0 = Release|Any CPU + {9B4C4680-3BBA-419B-AF79-CB80E1422B1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B4C4680-3BBA-419B-AF79-CB80E1422B1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B4C4680-3BBA-419B-AF79-CB80E1422B1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B4C4680-3BBA-419B-AF79-CB80E1422B1A}.Release|Any CPU.Build.0 = Release|Any CPU + {E584B588-FBB6-4D30-9EA2-6B6E531F82EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E584B588-FBB6-4D30-9EA2-6B6E531F82EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E584B588-FBB6-4D30-9EA2-6B6E531F82EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E584B588-FBB6-4D30-9EA2-6B6E531F82EC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {503B7AA4-0DAC-4A3D-95CE-B1D2DCAE55B0} EndGlobalSection EndGlobal diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/.actrc b/src/Saas.Admin/deployment/act/.actrc similarity index 100% rename from src/Saas.Identity/Saas.Permissions/deployment/act/.actrc rename to src/Saas.Admin/deployment/act/.actrc diff --git a/src/Saas.Admin/deployment/act/.gitignore b/src/Saas.Admin/deployment/act/.gitignore new file mode 100644 index 00000000..9f2eaf0d --- /dev/null +++ b/src/Saas.Admin/deployment/act/.gitignore @@ -0,0 +1,5 @@ +# nektos/act +.secret +.secrets +secret +secrets \ No newline at end of file diff --git a/src/Saas.Admin/deployment/act/clean.sh b/src/Saas.Admin/deployment/act/clean.sh new file mode 100644 index 00000000..5fdbc67a --- /dev/null +++ b/src/Saas.Admin/deployment/act/clean.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# repo base +repo_base="$( git rev-parse --show-toplevel )" +REPO_BASE="${repo_base}" + +host_deployment_dir="${repo_base}/src/Saas.Admin/deployment" +container_deployment_dir="/asdk/src/Saas.Admin/deployment" + +# running the './act/script/clean-credentials' script using our ASDK deployment script container - i.e., not the act container +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${REPO_BASE}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${HOME}/asdk/act/.secret":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE=/asdk/src/Saas.Admin/deployment" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash /asdk/src/Saas.Lib/Deployment.Script.Modules/clean-credentials.sh + +./setup.sh -s \ No newline at end of file diff --git a/src/Saas.Admin/deployment/act/deploy.sh b/src/Saas.Admin/deployment/act/deploy.sh new file mode 100644 index 00000000..4e4a5bc8 --- /dev/null +++ b/src/Saas.Admin/deployment/act/deploy.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -u -e -o pipefail + +# shellcheck disable=SC1091 +{ + source "./../constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" +} + +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +host_act_secrets_dir="${HOME}/asdk/act/.secret" +host_deployment_dir="${repo_base}/src/Saas.Admin/deployment" +container_deployment_dir="/asdk/src/Saas.Admin/deployment" + +# running the './act/script/patch-app-name.sh' script using our ASDK deployment script container - i.e., not the act container +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${host_deployment_dir}/act/workflows/":"${container_deployment_dir}/act/workflows" \ + --volume "${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${REPO_BASE}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${host_act_secrets_dir}":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash /asdk/src/Saas.Lib/Deployment.Script.Modules/deploy-debug.sh + +# --artifact-server-path=./src/publish \ + +# run act container to run github action locally, using local workflow file and local code base. +gh act workflow_dispatch \ + --rm \ + --bind \ + --pull=false \ + --secret-file "${host_act_secrets_dir}/secret" \ + --directory "${REPO_BASE}" \ + --workflows "${ACT_LOCAL_WORKFLOW_DEBUG_FILE}" \ + --platform "ubuntu-latest=${ACT_CONTAINER_NAME}" diff --git a/src/Saas.Admin/deployment/act/readme.md b/src/Saas.Admin/deployment/act/readme.md new file mode 100644 index 00000000..5bdb19a5 --- /dev/null +++ b/src/Saas.Admin/deployment/act/readme.md @@ -0,0 +1,9 @@ +# Saving Time Running Local GitHub Actions + +GitHub actions are terrific for CI/CD automated deployment. It the *inner loop* for getting GitHub actions right can be a tedious affair - i.e., having to commit, push and run when testing and troubleshoot. + +Luckily, there a solution for this called [act](https://github.com/nektos/act). Act lets you run the a GitHub running locally in a container that mimics what is running in GitHub. You still have to commit your latest code to GitHub, as act will pull it from there when it runs. However, you don't have to commit and push every time you make a change to the GitHub action workflow. This last part can save a lot of time and avoid all this *testing*, *wip* etc. commit and pushes to you main branch. It also allows you to have a slightly different `workflow.yml` file that pulls from for instance your dev branch rather than your main branch. + + + +... bla. bla. \ No newline at end of file diff --git a/src/Saas.Admin/deployment/act/setup.sh b/src/Saas.Admin/deployment/act/setup.sh new file mode 100644 index 00000000..9485ac09 --- /dev/null +++ b/src/Saas.Admin/deployment/act/setup.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +skip_docker_build=false +force_update=false + +while getopts 'sf' flag; do + case "${flag}" in + s) skip_docker_build=true ;; + f) force_update=true ;; + *) skip_docker_build=false ;; + esac +done + +# shellcheck disable=SC1091 +source "../constants.sh" + +echo "Setting up the SaaS Admin Service API Act deployment environment." +echo "Settings execute permissions on necessary scripts files." + +sudo mkdir -p "${ACT_SECRETS_DIR}" + +sudo chmod +x ${ACT_DIR}/*.sh +sudo chmod +x ${SCRIPT_DIR}/*.sh >/dev/null 2>&1 +sudo touch ${ACT_SECRETS_FILE} +sudo chown "${USER}" ${ACT_SECRETS_FILE} +sudo touch ${ACT_SECRETS_FILE_RG} +sudo chown "${USER}" ${ACT_SECRETS_FILE_RG} + +if [ "${skip_docker_build}" = false ]; then + echo "Building the deployment container." + + if [[ "${force_update}" == false ]]; then + "${ACT_CONTAINER_DIR}"/build.sh -n "${ACT_CONTAINER_NAME}" + else + "${ACT_CONTAINER_DIR}"/build.sh -n "${ACT_CONTAINER_NAME}" -f + fi +fi + +echo "SaaS SaaS Admin Service API Act environment setup complete. You can now run the local deployment script using the command './deploy.sh'." diff --git a/src/Saas.Admin/deployment/act/workflows/admin-service-api-deploy-debug.yml b/src/Saas.Admin/deployment/act/workflows/admin-service-api-deploy-debug.yml new file mode 100644 index 00000000..30d3634a --- /dev/null +++ b/src/Saas.Admin/deployment/act/workflows/admin-service-api-deploy-debug.yml @@ -0,0 +1,84 @@ +--- +name: ASDK Administration Service API - Deploy to Azure Web Services + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + APP_NAME: admin-api + AZURE_WEBAPP_NAME: admin-api-asdk-test-fd4k # set this to your application's name + AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root + DOTNET_VERSION: 7.x.x + PROJECT_DIR: ./src/Saas.Admin/Saas.Admin.Service + PROJECT_PATH: ${{ env.PROJECT_DIR }}/Saas.Admin.Service.csproj + PUBLISH_PATH: ./publish + OUTPUT_PATH: ${{ env.PUBLISH_PATH }}/${{ env.APP_NAME }}/package + SYMBOLS_PATH: ${{ env.PUBLISH_PATH }}/symbols + BUILD_CONFIGURATION: Debug # setting the configuration manager build configuration value for our workflow. + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + +################################################################## +# this section is specifically changed for for local deployment. # +################################################################## + # Azure login + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + # checkout the _local_ repository + - name: Checkout + uses: actions/checkout@v3 +################################################################## +# end of local deployment specific section. # +################################################################## + + # Setup .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + # Run dotnet build and publish + - name: dotnet build and publish + run: | + dotnet restore ${{ env.PROJECT_DIR }} + + dotnet build ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} + + dotnet publish ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} \ + --output ${{ env.OUTPUT_PATH }} + + # Deploy to Azure Web apps + - name: Run Azure webapp deploy action using publish profile credentials + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name + package: ${{ env.OUTPUT_PATH }} + + ###################### + # *** Debug only *** # + ###################### + # Copy symbols files (*.pdb)) to local publish folder # rm -rf ${{ env.OUTPUT_PATH }}/${{ env.AZURE_WEBAPP_NAME }} + - name: copy symbols files (*.pdb)) to local publish folder + run: | + mkdir -p ${{ env.PUBLISH_PATH }}/symbols + echo "Copying symbols files to '${{ env.SYMBOLS_PATH }}'" + cp -r ${{ env.OUTPUT_PATH }}/*.pdb ${{ env.SYMBOLS_PATH }} + ###################### + # *** End *** # + ###################### + + # Azure logout + - name: logout + run: | + az logout diff --git a/src/Saas.Admin/deployment/bicep/Parameters/.gitignore b/src/Saas.Admin/deployment/bicep/Parameters/.gitignore new file mode 100644 index 00000000..39a81f30 --- /dev/null +++ b/src/Saas.Admin/deployment/bicep/Parameters/.gitignore @@ -0,0 +1,2 @@ +*-parameters.json +identity-foundation-outputs.json \ No newline at end of file diff --git a/src/Saas.Admin/deployment/bicep/Parameters/parameters-template.json b/src/Saas.Admin/deployment/bicep/Parameters/parameters-template.json new file mode 100644 index 00000000..d90c44f3 --- /dev/null +++ b/src/Saas.Admin/deployment/bicep/Parameters/parameters-template.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/src/Saas.Admin/deployment/bicep/deployAppService.bicep b/src/Saas.Admin/deployment/bicep/deployAppService.bicep new file mode 100644 index 00000000..abcbefec --- /dev/null +++ b/src/Saas.Admin/deployment/bicep/deployAppService.bicep @@ -0,0 +1,51 @@ +@description('The SaaS Signup Administration web site name.') +param adminapi string + +@description('Version') +param version string + +@description('Environment') +@allowed([ + 'Development' + 'Staging' + 'Production' +]) +param environment string + +@description('The App Service Plan ID.') +param appServicePlanName string + +@description('The Uri of the Key Vault.') +param keyVaultUri string + +@description('The location for all resources.') +param location string + +@description('Azure App Configuration User Assigned Identity Name.') +param userAssignedIdentityName string + +@description('The name of the Azure App Configuration.') +param appConfigurationName string + +@description('The name of the Log Analytics Workspace used by Application Insigths.') +param logAnalyticsWorkspaceName string + +@description('The name of Application Insights.') +param applicationInsightsName string + +module signupAdministrationWebApp './../../../Saas.Lib/Saas.Bicep.Module/appServiceModuleWithObservability.bicep' = { + name: 'AdminServiceApi' + params: { + appServiceName: adminapi + version: version + environment: environment + appServicePlanName: appServicePlanName + keyVaultUri: keyVaultUri + location: location + userAssignedIdentityName: userAssignedIdentityName + appConfigurationName: appConfigurationName + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + applicationInsightsName: applicationInsightsName + } +} + diff --git a/src/Saas.Admin/deployment/bicep/deployConfigEntries.bicep b/src/Saas.Admin/deployment/bicep/deployConfigEntries.bicep new file mode 100644 index 00000000..2cc44c4f --- /dev/null +++ b/src/Saas.Admin/deployment/bicep/deployConfigEntries.bicep @@ -0,0 +1,159 @@ +@description('Version') +param version string + +@description('The name of the key vault') +param keyVaultName string + +@description('Azure B2C Domain Name.') +param azureB2CDomain string + +@description('Azure B2C Tenant Id.') +param azureB2cTenantId string + +@description('Azure AD Instance') +param azureAdInstance string + +@description('The Azure B2C Signed Out Call Back Path.') +param signedOutCallBackPath string + +@description('The Azure B2C Sign up/in Policy Id.') +param signUpSignInPolicyId string + +@description('The Azure B2C Permissions API base Url.') +param baseUrl string + +@description('The Client Id found on registered Permissions API app page.') +param clientId string + +@description('User Identity Name') +param userAssignedIdentityName string + +@description('App Configuration Name') +param appConfigurationName string + +@description('Indicates the Authentication type for new identity') +param authenticationType string + +@description('Type of the claim to use in the new Identity, works alongside built-in') +param roleClaimType string + +@description('Name of the claim custom roles are in') +param sourceClaimType string + +@description('Application ID URI for the exposed Admin API scopes.') +param applicationIdUri string + +@description('Admin API scopes.') +param adminApiScopes string + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { + name: userAssignedIdentityName +} + +// Create object with array of objects containing the kayname and value to be stored in Azure App Configuration store. + +var azureB2CKeyName = 'AzureB2C' +var adminApiKeyName = 'AdminApi' +var claimToRoleTransformerKeyName = 'ClaimToRoleTransformer' + +var appConfigStore = { + appConfigurationName: appConfigurationName + keyVaultName: keyVaultName + userAssignedIdentityName: userAssignedIdentity.name + label: version + entries: [ + { + key: '${adminApiKeyName}:${azureB2CKeyName}:BaseUrl' + value: baseUrl + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:ClientId' + value: clientId + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:TenantId' + value: azureB2cTenantId + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:Domain' + value: azureB2CDomain + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:Instance' + value: azureAdInstance + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:Audience' + value: clientId + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:SignedOutCallbackPath' + value: signedOutCallBackPath + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:${azureB2CKeyName}:SignUpSignInPolicyId' + value: signUpSignInPolicyId + isSecret: false + contentType: 'text/plain' + } + { + key: '${claimToRoleTransformerKeyName}:AuthenticationType' + value: authenticationType + isSecret: false + contentType: 'text/plain' + } + { + key: '${claimToRoleTransformerKeyName}:RoleClaimType' + value: roleClaimType + isSecret: false + contentType: 'text/plain' + } + { + key: '${claimToRoleTransformerKeyName}:SourceClaimType' + value: sourceClaimType + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:ApplicationIdUri' + value: applicationIdUri + isSecret: false + contentType: 'text/plain' + } + { + key: '${adminApiKeyName}:Scopes' + value: ' ${string(adminApiScopes)}' // notice the space before the string, this is a necessary hack. https://github.com/Azure/bicep/issues/6167 + isSecret: false + contentType: 'application/json' + } + ] +} + +// Adding App Configuration entries +module appConfigurationSettings './../../../Saas.Lib/Saas.Bicep.Module/addConfigEntry.bicep' = [ for entry in appConfigStore.entries: { + name: replace('Entry-${entry.key}', ':', '-') + params: { + appConfigurationName: appConfigStore.appConfigurationName + userAssignedIdentityName: appConfigStore.userAssignedIdentityName + keyVaultName: keyVaultName + value: entry.value + contentType: entry.contentType + keyName: entry.key + label: appConfigStore.label + isSecret: entry.isSecret + } +}] diff --git a/src/Saas.Admin/deployment/build.sh b/src/Saas.Admin/deployment/build.sh new file mode 100644 index 00000000..69bc6a5a --- /dev/null +++ b/src/Saas.Admin/deployment/build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +force_update=false + +while getopts f flag +do + case "${flag}" in + f) force_update=true;; + *) force_update=false;; + esac +done + +repo_base="$( git rev-parse --show-toplevel )" +docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + + +# redirect to build.sh in the Deployment.Container folder +if [[ "${force_update}" == false ]]; then + "${docker_file_folder}/build.sh" +else + "${docker_file_folder}/build.sh" -f +fi diff --git a/src/Saas.Admin/deployment/constants.sh b/src/Saas.Admin/deployment/constants.sh new file mode 100644 index 00000000..c3b02a4f --- /dev/null +++ b/src/Saas.Admin/deployment/constants.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# disable unused variable warning https://www.shellcheck.net/wiki/SC2034 +# shellcheck disable=SC2034 + +# app naming +APP_NAME="admin-api" +APP_DEPLOYMENT_NAME="adminServiceApi" + +# repo base +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +# project base directory +BASE_DIR="${REPO_BASE}/src/Saas.Admin/deployment" + +# local script directory +SCRIPT_DIR="${BASE_DIR}/script" + +#local log directory +LOG_FILE_DIR="${BASE_DIR}/log" + +# act directory +ACT_DIR="${BASE_DIR}/act" + +# GitHub workflows +WORKFLOW_BASE="${REPO_BASE}/.github/workflows" +GITHUB_ACTION_WORKFLOW_FILE="${WORKFLOW_BASE}/admin-service-api-deploy.yml" +ACT_LOCAL_WORKFLOW_DEBUG_FILE="${ACT_DIR}/workflows/admin-service-api-deploy-debug.yml" + +# global script directory +SHARED_MODULE_DIR="${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules" + +# adding app service global constants +source "${SHARED_MODULE_DIR}/app-service-constants.sh" diff --git a/src/Saas.Admin/deployment/run.sh b/src/Saas.Admin/deployment/run.sh new file mode 100644 index 00000000..e22f310d --- /dev/null +++ b/src/Saas.Admin/deployment/run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source "./constants.sh" + +repo_base="$(git rev-parse --show-toplevel)" +host_act_secrets_dir="${HOME}/asdk/act/.secret" + +host_deployment_dir="${repo_base}/src/Saas.Admin/deployment" +container_deployment_dir="/asdk/src/Saas.Admin/deployment" + +# using volumes '--volume' to mount only the needed directories to the container. +# using ':ro' to make scrip directories etc. read-only. Only config and log directories are writable. +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${host_deployment_dir}/log":"${container_deployment_dir}/log" \ + --volume "${host_deployment_dir}/Bicep/Parameters":"${container_deployment_dir}"/Bicep/Parameters \ + --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${repo_base}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${repo_base}/src/Saas.Lib/Saas.Bicep.Module":/asdk/src/Saas.Lib/Saas.Bicep.Module:ro \ + --volume "${repo_base}/.github/workflows":/asdk/.github/workflows \ + --volume "${repo_base}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${host_act_secrets_dir}":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash ${container_deployment_dir}/start.sh diff --git a/src/Saas.Admin/deployment/script/map-to-config-entries-parameters.py b/src/Saas.Admin/deployment/script/map-to-config-entries-parameters.py new file mode 100644 index 00000000..87539e72 --- /dev/null +++ b/src/Saas.Admin/deployment/script/map-to-config-entries-parameters.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import json +import sys +import re + +def get_b2c_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['azureb2c'][key] + + return { + keyName: { + 'value': value + } + } + +def get_claimTransformer_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['claimToRoleTransformer'][key] + return { + keyName: { + 'value': value + } + } + +def get_deploy_b2c_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['deployment']['azureb2c'][key] + return { + keyName: { + 'value': value + } + } + +def get_app_value( + config: dict, + app_name: str, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + for app in config['appRegistrations']: + if app['name'] == app_name: + return { + keyName: { + 'value': app[key] + } + } + +def get_app_scopes( + config: dict, + app_name: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + scopes = [] + + for app in config['appRegistrations']: + if app['name'] == app_name: + for scope in app['scopes']: + scopes.append(scope['name']) + + return { + keyName: { + 'value': json.dumps(scopes) + } + } + +def get_output_value(outputs: dict, output_name: str) -> 'dict[str, dict[str, str]]': + item = outputs[output_name] + if item : return { + output_name : { + 'value': item['value'] + } + } + +def patch_paramenters_file( + app_name: str, + identity_outputs: str, + paramenter_file: str, + config_file: str) -> None: + + with open(config_file, 'r') as f: + config = json.load(f) + + with open(identity_outputs, 'r') as f: + identity_outputs = json.load(f) + + with open(paramenter_file, 'r') as f: + parameters = json.load(f) + + parameters['parameters'].update(get_output_value(identity_outputs, 'version')) + parameters['parameters'].update(get_output_value(identity_outputs, 'keyVaultName')) + + parameters['parameters'].update(get_deploy_b2c_value(config, 'domainName', 'azureB2CDomain')) + parameters['parameters'].update(get_deploy_b2c_value(config, 'tenantId', 'azureB2cTenantId')) + parameters['parameters'].update(get_deploy_b2c_value(config, 'instance', 'azureAdInstance')) + + parameters['parameters'].update(get_b2c_value(config, 'signedOutCallBackPath', 'signedOutCallBackPath')) + parameters['parameters'].update(get_b2c_value(config, 'signUpSignInPolicyId', 'signUpSignInPolicyId')) + + parameters['parameters'].update(get_app_value(config, app_name, 'appId', 'clientId')) + parameters['parameters'].update(get_app_value(config, app_name, 'baseUrl', 'baseUrl')) + + parameters['parameters'].update(get_app_value(config, app_name, 'applicationIdUri', 'applicationIdUri')) + parameters['parameters'].update(get_app_scopes(config, app_name, 'adminApiScopes')) + + parameters['parameters'].update(get_output_value(identity_outputs, 'userAssignedIdentityName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'appConfigurationName')) + + parameters['parameters'].update(get_claimTransformer_value(config, 'authenticationType', 'authenticationType')) + parameters['parameters'].update(get_claimTransformer_value(config, 'roleClaimType', 'roleClaimType')) + parameters['parameters'].update(get_claimTransformer_value(config, 'sourceClaimType', 'sourceClaimType')) + + with open(paramenter_file, 'w') as f: + f.write(json.dumps(parameters, indent=4)) + +# Main entry point for the script +if __name__ == "__main__": + app_name = sys.argv[1] + identity_outputs = sys.argv[2] + paramenter_file = sys.argv[3] + config_file = sys.argv[4] + + patch_paramenters_file(app_name, identity_outputs, paramenter_file, config_file) \ No newline at end of file diff --git a/src/Saas.Admin/deployment/setup.sh b/src/Saas.Admin/deployment/setup.sh new file mode 100644 index 00000000..6f3e5ce1 --- /dev/null +++ b/src/Saas.Admin/deployment/setup.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +source "./constants.sh" + +echo "Setting up the deployment environment." +echo "Settings execute permissions on necessary scripts files." + +( + sudo chmod +x ./*.sh + sudo chmod +x ./script/*.sh >/dev/null 2>&1 + sudo chmod +x ./script/*.py +) || + { + echo "Failed to set execute permissions on the necessary scripts." + exit 1 + } + +repo_base="$(git rev-parse --show-toplevel)" || + { + echo "Failed to get the root of the repository." + exit 1 + } + +docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + +# redirect to build.sh in the Deployment.Container folder +sudo chmod +x "${docker_file_folder}/build.sh" || + { + echo "Failed to set execute permissions on the 'build.sh' script." + exit 1 + } + +echo "Building the deployment container." +./build.sh || + { + echo "Failed to build the deployment container. Please ensure that Docker is installed and running." + exit 1 + } + +( + echo "Setting up log folder..." + mkdir -p "$LOG_FILE_DIR" + sudo chown "${USER}" "$LOG_FILE_DIR" +) || + { + echo "Failed to set up log folder." + exit 1 + } + +echo +echo "Setup complete. You can now run the deployment script using the command './run.sh'." diff --git a/src/Saas.Admin/deployment/start.sh b/src/Saas.Admin/deployment/start.sh new file mode 100644 index 00000000..c17f72f4 --- /dev/null +++ b/src/Saas.Admin/deployment/start.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +# if not running in a container +if ! [ -f /.dockerenv ]; then + echo "Running outside of a container us not supported. Please run the script using './run.sh'." + exit 0 +fi + +# shellcheck disable=SC1091 +{ + source "${ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE}/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/log-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" +} + +# set bash options to exit on unset variables and errors (exit 1) including pipefail +set -u -e -o pipefail + +if ! [[ -f $CONFIG_FILE ]]; then + echo "The ASDK Identity Foundation has not completed or 'config.json' file from it's deployment is missing. Please run the Identity Foundation deployment script first." + exit 0 +fi + +# get now date and time for backup file name +now=$(date '+%Y-%m-%d--%H-%M-%S') + +# set run time for deployment script instance +export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" + +# using the az cli settings and cache from the host machine +initialize-az-cli "$HOME/.azure" + +echo "Provisioning the SaaS Administration Service API..." | + log-output \ + --level info \ + --header "SaaS Administration Service API" + +"${SHARED_MODULE_DIR}/"deploy-app-service.sh + +"${SHARED_MODULE_DIR}/"deploy-config-entries.sh + +echo "Patching '${APP_NAME}' GitHub Action workflow file." | + log-output \ + --level info \ + --header "SaaS Administration Service API" + +"${SHARED_MODULE_DIR}/patch-github-workflow.py" \ + "${APP_NAME}" \ + "${CONFIG_FILE}" \ + "${GITHUB_ACTION_WORKFLOW_FILE}" || + echo "Failed to patch ${APP_NAME} GitHub Action workflow file" | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 + +git_repo_origin="$(git config --get remote.origin.url)" + +echo "'${APP_NAME}' is ready to be deployed. You have two options:" +echo " a) To deploy to production, use the GitHub Action: ${git_repo_origin::-4}/actions" +echo +echo " b) To deploy for live debugging in Azure; navigate to the act directory ('cd act') and run './setup.sh' and then run './deploy.sh' to deploy for remote debugging." diff --git a/src/Saas.Admin/readme.md b/src/Saas.Admin/readme.md index 5c02e854..7673181b 100644 --- a/src/Saas.Admin/readme.md +++ b/src/Saas.Admin/readme.md @@ -1,76 +1,64 @@ -# Saas.Admin.Service +# SaaS Admin Service API -The SaaS Admin Service is an API that is reponsible for tenant management operations. Within this folder, you will find 3 sections: +The SaaS Admin Service is an API that is reponsible for tenant management operations. -1. Saas.Admin.Service - The .NET Web API project containing the code for the API +This project hosts a service API which serves as a gateway to administrate the SaaS ecosystem of Tenants. -2. Saas.Admin.Service.Deployment - The bicep module for deploying the infrastructure required to host the API in Azure +## Overview -3. Saas.Admin.Service.Tests - Unit tests for the service +Within this folder you will find two subfolders: -## 1. Module Overview +- **Saas.Permissions.Service** - the C# project for the API +- **deployment** - a set of tools for deploying the API for production + - The sub-subfolder **[act](./deployment/act)** is for deploying the API for remote debugging +- Saas.Admin.Service.Tests - Unit tests for the API. -This project hosts a service api which serves as a gateway to administrate the SaaS ecosystem of Tenants. It is fully self-contained such that it includes complete copies of all necessary classes for operation. Since it contains no direct references to the other projects, it can be extracted to launch in isolation. However, keep in mind that some functionality within the API does have [dependencies](https://azure.github.io/azure-saas/components/admin-service/#dependencies) on other services. +## Dependencies + +The service depends on: + +- The **Identity Foundation** that was deployed a spart of the Identity Foundation and on the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). +- The **[SaaS Permissions Services API](./../Saas.Identity/Saas.Permissions/readme.md)**. +- The [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview). For a complete overview, please see the [SaaS.Admin.Service](https://azure.github.io/azure-saas/components/admin-service/) page in our documentation site. -## 2. How to Run Locally +## Provisioning the API -Once configured, this app presents an api service which exposes endpoints to perform CRUD operations on application tenant data. It may be run locally during development of service logic and for regenerating its included NSwag api client. (An NSwag file is included in the Admin project to generate its client.) +To work with the SaaS Admin Services API it must first be provisions to your Azure ASDK resource group. This is true even if you initially is planning to run the API in your local development environment. The provisioning ensure that configuration and settings to be correctly added to your Azure App Configuration store and readies the API for later deployment to Azure. -### i. Requirements +Provisioning is easy: -To run the web api, you must have the following installed on your machine: +1. Navigate to the sub folder `deployment`. -- [.NET 6.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -- [ASP.NET Core 6.0](https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0) -- (Recommended) [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Visual Studio Code](https://code.visualstudio.com/download) -- A connection string to a running, empty SQL Server Database. - - [Local DB](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-express-localdb?view=sql-server-ver15) (Windows Only) - See `Additional Resources` below for basic config secret - - [SQL Server Docker Container](https://hub.docker.com/_/microsoft-mssql-server) - - [SQL Server Developer Edition](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) -- A deployed [Identity Framework](https://azure.github.io/azure-saas/quick-start/) instance - - [Azure AD B2C](https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/) - created automatically with Bicep deployment +2. Run these commands: -### ii. Development Tools + ```bash + sudo chmod +x setup.sh + ./setup.sh + ./run.sh + ``` -- [NSwag](https://github.com/RicoSuter/NSwag) - An NSwag configuration file has been included to generate an appropriate client from the included Admin project. - *Consumes Clients:* - - [permissions-service-client-generator.nswag](Saas.Admin.Service/permissions-service-client-generator.nswag) - *Consumed By:* - - [Saas.SignupAdministration](../Saas.SignupAdministration) - - [Saas.Application](../Saas.Application) +Now you're ready to move on. -### iii. App Settings +## How to Run Locally -In order to run the project locally, the App Settings marked as `secret: true` must be set using the [.NET secrets manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows). When deployed to azure using the bicep deployments, these secrets are [loaded from Azure Key Vault](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#secret-storage-in-the-development-environment) instead. +Guidelines for getting up and running with SaaS Signup Administration in your local development, are identical to the guidelines found the *[Requirements](./../Saas.Identity/Saas.Permissions/readme.md#Requirements)* and the *[Configuration, settings and secrets when running locally](./../Saas.Identity/Saas.Permissions/readme.md#running-the-saas-permissions-service-api-locally)* section in the [SaaS Permissions Service readme](./../Saas.Identity/Saas.Permissions/readme.md). -Default values for non secret app settings can be found in [appsettings.json](Saas.Admin.Service/appsettings.json) +## Running the SaaS Administration Service API Locally -| AppSetting Key | Description | Secret | Default Value | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------- | -| AzureAdB2C:ClientId | The service client corresponding to the Signup Admin application | true | | -| AzureAdB2C:Domain | Domain name for the Azure AD B2C instance | true | | -| AzureAdB2C:Instance | URL for the root of the Azure AD B2C instance | true | | -| AzureAdB2C:SignedOutCallbackPath | Callback path (not full url) contacted after signout | false | /signout/B2C_1A_SIGNUP_SIGNIN | -| AzureAdB2C:SignUpSignInPolicyId | Name of signup/signin policy | false | B2C_1A_SIGNUP_SIGNIN | -| AzureAdB2C:TenantId | Identifier for the overall Azure AD B2C tenant for the overall SaaS ecosystem | true | | -| ClaimToRoleTransformer:AuthenticationType | Indicates the Authentication type for new identity | false | MyCustomRoleAuth | -| ClaimToRoleTransformer:RoleClaimtype | Type of the claim to use in the new Identity, works alongside built-in | false | MyCustomRoles | -| ClaimToRoleTransformer:SourceClaimType | Name of the claim custom roles are in | false | permissions | -| ConnectionStrings:TenantsContext | Connection String to SQL server database used to store permission data. | true | (localdb connnection string) | -| KeyVault:Url | KeyVault URL to pull secret values from in production | false | | -| Logging:LogLevel:Default | Logging level when no configured provider is matched | false | Information | -| Logging:LogLevel:Microsoft.AspNetCore | Logging level for AspNetCore logging | false | Warning | -| PermissionsApi:BaseUrl | URL for downstream [Permissions API](../Saas.Identity/Saas.Permissions/readme.md) | false | | -| PermissionsApi:ApiKey | API Key to use for authentication with the downstream [Permissions API](../Saas.Identity/Saas.Permissions/readme.md) | true | | +--- TODO BEGIN --- -### iv. Starting the App +*Add some guidelines about how to create valid JWT tokens to test the API locally etc...* -1. Insert secrets marked as required for running locally into your secrets manager (such as by using provided script). -1. Start app. Service will launch as presented Swagger API. +---TODO END --- -## 3. Additional Resources +## How to Deploy the SaaS Administration Service API to Azure -### i. LocalDB -If using the LocalDB persistance for local development, tables and data can be interacted with directly through Visual Studio. Under the `View` menu, find `SQL Server Object Explorer`. Additional documentation is available [here](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-express-localdb?view=sql-server-ver16) \ No newline at end of file +The guidelines are identity to *[How to Deploy SaaS Permissions Service API to Azure](./../Saas.Identity/Saas.Permissions/readme.md#how--to-deploy-saas-permissions-service-api-to-azure)*. + +## Debugging in Azure + +The guidelines are identity to *[Debugging in Azure](./../Saas.Identity/Saas.Permissions/readme.md#debugging-in-azure)* for the SaaS Permissions Service API. + +Happy debugging! \ No newline at end of file diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleCusomizer.cs b/src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleCusomizer.cs deleted file mode 100644 index 26507a76..00000000 --- a/src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleCusomizer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Saas.AspNetCore.Authorization.AuthHandlers -{ - public class RouteBasedRoleCusomizer : IRoleCustomizer - { - private readonly IHttpContextAccessor _httpContextAccessor; - public RouteBasedRoleCusomizer(IHttpContextAccessor httpContextAccessor, string routeName, bool includeOriginals = false) - { - _httpContextAccessor = httpContextAccessor; - IncludeOriginals = includeOriginals; - RouteName = routeName; - } - - public bool IncludeOriginals { get; internal set; } - public string RouteName { get; protected set; } - - public IEnumerable CustomizeRoles(IEnumerable allowedRoles) - { - HttpContext httpContext = _httpContextAccessor.HttpContext; - string context = httpContext.GetRouteValue(RouteName) as string; - - if (context != null && allowedRoles != null) - { - foreach (string role in allowedRoles) - { - yield return string.Format("{0}.{1}", context, role); - } - if (IncludeOriginals) - { - foreach (string role in allowedRoles) - { - yield return role; - } - } - } - } - } -} diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformer.cs b/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformer.cs deleted file mode 100644 index 0333aa27..00000000 --- a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Options; - -namespace Saas.AspNetCore.Authorization.ClaimTransformers -{ - /// - /// Transforms a custom claim in space delimited format to roles - /// The user principal will factor in the custom roles when IsInRole is called - /// - public class ClaimToRoleTransformer : IClaimsTransformation - { - private readonly string _sourceClaimType; - private readonly string _roleType; - private readonly string _authType; - - /// - /// Constructor - /// - /// Name of the space delimited claim to transform - /// Type of the individual role claims generated - /// Authentication type to set the new identity to - public ClaimToRoleTransformer(string sourceClaimType, string roleClaimType, string authType) - { - _sourceClaimType = sourceClaimType; - _roleType = roleClaimType; - _authType = authType; - } - - - public ClaimToRoleTransformer(IOptions options) - : this(options.Value.SourceClaimType, options.Value.RoleClaimType, options.Value.AuthenticationType) - { - } - - public Task TransformAsync(ClaimsPrincipal principal) - { - System.Collections.Generic.IEnumerable customClaims = principal.Claims.Where(c => _sourceClaimType.Equals(c.Type, StringComparison.OrdinalIgnoreCase)); - System.Collections.Generic.IEnumerable roleClaims = customClaims.SelectMany(c => - { - return c.Value.Split(' ').Select(s => new Claim(_roleType, s)); - }); - - if (!roleClaims.Any()) - { - return Task.FromResult(principal); - } - - - ClaimsPrincipal transformed = new ClaimsPrincipal(principal); - ClaimsIdentity rolesIdentity = new ClaimsIdentity(roleClaims, _authType, null, _roleType); - transformed.AddIdentity(rolesIdentity); - return Task.FromResult(transformed); - } - } -} diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerExtensions.cs b/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerExtensions.cs deleted file mode 100644 index a9ca53e9..00000000 --- a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Saas.AspNetCore.Authorization.ClaimTransformers -{ - public static class ClaimToRoleTransformerExtensions - { - public static IServiceCollection AddClaimToRoleTransformer(this IServiceCollection services, string sourceClaimType, - string roleClaimType = ClaimToRoleTransformerOptions.DefaultRoleClaimType, - string authenticationType = ClaimToRoleTransformerOptions.DefaultAuthenticationType) - { - services.AddOptions() - .Configure(options => - { - options.RoleClaimType = roleClaimType; - options.SourceClaimType = sourceClaimType; - options.AuthenticationType = authenticationType; - }); - - services.RegisterTransformer(); - return services; - } - - public static IServiceCollection AddClaimToRoleTransformer(this IServiceCollection services, - IConfiguration configurationSection, Action configure = null) - { - services.Configure(configurationSection); - - if (configure != null) - { - services.Configure(configure); - } - - services.RegisterTransformer(); - return services; - } - - public static IServiceCollection AddClaimToRoleTransformer(this IServiceCollection services, - IConfiguration configuration, string configSectionName, - Action configure = null) - { - if (configuration == null) - { - throw new ArgumentException("configuration"); - } - - if (string.IsNullOrEmpty(configSectionName)) - { - throw new ArgumentException("configSectionName"); - } - - IConfigurationSection section = configuration.GetSection(configSectionName); - - AddClaimToRoleTransformer(services, section, configure); - return services; - } - - private static void RegisterTransformer(this IServiceCollection services) - { - services.AddTransient(); - } - } -} diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerOptions.cs b/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerOptions.cs deleted file mode 100644 index 4bda37c1..00000000 --- a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerOptions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Saas.AspNetCore.Authorization.ClaimTransformers -{ - public class ClaimToRoleTransformerOptions - { - public const string ConfigSectionName = "ClaimToRoleTransformer"; - public const string DefaultRoleClaimType = "CustomRole"; - public const string DefaultAuthenticationType = "CustomRoleAuthentication"; - - - /// - /// Name of the space delimited claim to transform - /// - public string SourceClaimType { get; set; } = string.Empty; - - /// - /// Type of the individual role claims generated - /// - public string RoleClaimType { get; set; } = string.Empty; - - /// - /// Authentication type to set the new identity to - /// - public string AuthenticationType { get; set; } = string.Empty; - } -} diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/addConfigEntries.bicep b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/addConfigEntries.bicep index 2cd40903..677bb0b7 100644 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/addConfigEntries.bicep +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/addConfigEntries.bicep @@ -1,29 +1,41 @@ @description('Version') param version string -@description('URL for downstream admin service.') -param appSettingsAdminServiceBaseUrl string - @description('The name of the key vault') param keyVaultName string -@description('URL for downstream admin service.') -param azureB2CDomain string +// @description('URL for downstream admin service.') +// param azureB2CInstance string -@description('The B2C login endpoint in format of https://(Tenant Name).b2clogin.com.') -param azureB2CLoginEndpoint string +// @description('URL for downstream admin service.') +// param azureB2CClientId string -@description('Tenant Id found on your AD B2C dashboard.') -param azureB2CTenantId string +// @description('URL for downstream admin service.') +// param azureB2CDomain string -@description('The Client Id found on registered Permissions API app page.') -param permissionApiClientId string +// @description('The B2C login endpoint in format of https://(Tenant Name).b2clogin.com.') +// param azureB2CLoginEndpoint string -@description('Permissions API Certificate Name') -param permissionCertificateName string +// @description('Tenant Id found on your AD B2C dashboard.') +// param azureB2CTenantId string -@description('Permissions API Instance') -param permissionInstance string +// @description('The Azure B2C Signed Out Call Back Path.') +// param signedOutCallBackPath string + +// @description('The Azure B2C Sign up/in Policy Id.') +// param signUpSignInPolicyId string + +// @description('The Azure B2C Permissions API base Url.') +// param permissionsBaseUrl string + +// @description('The Client Id found on registered Permissions API app page.') +// param permissionApiClientId string + +// @description('Permissions API Certificate Name') +// param permissionCertificateName string + +// @description('Permissions API Instance') +// param permissionInstance string @description('Select an admin account name used for resource creation.') param sqlAdministratorLogin string @@ -31,8 +43,8 @@ param sqlAdministratorLogin string @description('User Identity Name') param userAssignedIdentityName string -@description('Key Vault Url') -param keyVaultUrl string +// @description('Key Vault Url') +// param keyVaultUrl string @description('App Configuration Name') param appConfigurationName string @@ -47,24 +59,17 @@ param permissionsSqlServerFQDN string @secure() param sqlAdministratorLoginPassword string +// @description('PermissionsAPI key name') +// param permissionsApiKeyName string + resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { name: userAssignedIdentityName } // Create object with array of objects containing the kayname and value to be stored in Azure App Configuration store. -var azureB2C = 'AzureB2C' -var permissionApi = 'PermissionApi' -var msGraph = 'MsGraph' -var sql = 'Sql' - -var permissionCertificates = [ - { - SourceType: keyVaultName - KeyVaultUrl: keyVaultUrl - KeyVaultCertificateName: permissionCertificateName - } -] +var msGraphKeyName = 'MsGraph' +var sqlKeyName = 'Sql' var appConfigStore = { appConfigurationName: appConfigurationName @@ -73,103 +78,31 @@ var appConfigStore = { label: version entries: [ { - key: '${azureB2C}:AdminServiceBaseUrl' - value: appSettingsAdminServiceBaseUrl - isSecret: false - contentType: 'text/plain' - } - { - key: '${azureB2C}:Domain' - value: azureB2CDomain - isSecret: false - contentType: 'text/plain' - } - { - key: '${azureB2C}:LoginEndpoint' - value: azureB2CLoginEndpoint - isSecret: false - contentType: 'text/plain' - } - { - key: '${azureB2C}:TenantId' - value: azureB2CTenantId - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:ClientId' - value: permissionApiClientId - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:TenantId' - value: azureB2CTenantId - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:Domain' - value: azureB2CDomain - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:Instance' - value: permissionInstance - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:Audience' - value: permissionApiClientId - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:CallbackPath' - value: '/signin-oidc' - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:SignedOutCallbackPath' - value: '/signout-oidc' - isSecret: false - contentType: 'text/plain' - } - { - key: '${permissionApi}:Certificates' - value: replace('${permissionCertificates}', '\'','"') // replace single quotes with double quotes in the json string - isSecret: false - contentType: 'application/json' - } - { - key: '${sql}:SQLAdministratorLoginName' + key: '${sqlKeyName}:SQLAdministratorLoginName' value: sqlAdministratorLogin isSecret: false contentType: 'text/plain' } { - key: '${sql}:SQLAdministratorLoginPassword' + key: '${sqlKeyName}:SQLAdministratorLoginPassword' value: sqlAdministratorLoginPassword isSecret: true contentType: 'text/plain' } { - key: '${sql}:SQLConnectionString' + key: '${sqlKeyName}:SQLConnectionString' value: 'Data Source=tcp:${permissionsSqlServerFQDN},1433;Initial Catalog=${permissionsSqlDatabaseName};User Id=${sqlAdministratorLogin}@${permissionsSqlServerFQDN};Password=${sqlAdministratorLoginPassword};' isSecret: true contentType: 'text/plain' } { - key: '${msGraph}:BaseUrl' + key: '${msGraphKeyName}:BaseUrl' value: 'https://graph.microsoft.com/v1.0' isSecret: false contentType: 'text/plain' } { - key: '${msGraph}:Scopes' + key: '${msGraphKeyName}:Scopes' value: 'https://graph.microsoft.com/.default' isSecret: false contentType: 'text/plain' @@ -178,8 +111,8 @@ var appConfigStore = { } // Adding App Configuration entries -module appConfigurationSettings 'addConfigEntry.bicep' = [ for entry in appConfigStore.entries: { - name: replace('AppConfigurationSettings-${entry.key}', ':', '-') +module appConfigurationSettings './../../../../../../Saas.Lib/Saas.Bicep.Module/addConfigEntry.bicep' = [ for entry in appConfigStore.entries: { + name: replace('Entry-${entry.key}', ':', '-') params: { appConfigurationName: appConfigStore.appConfigurationName userAssignedIdentityName: appConfigStore.userAssignedIdentityName diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/appPlan.bicep b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/appPlan.bicep new file mode 100644 index 00000000..6ee9be92 --- /dev/null +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/appPlan.bicep @@ -0,0 +1,221 @@ +@description('The App Service Plan ID.') +param appServicePlanName string + +@description('The location for all resources.') +param location string + +@description('Azure App Configuration User Assigned Identity Name.') +param userAssignedIdentityName string + + +@description('The name of the Log Analytics Workspace used by Application Insigths.') +param logAnalyticsWorkspaceName string + +@description('The name of the Automation Account.') +param automationAccountName string + +@description('The name of Application Insights.') +param applicationInsightsName string + +@description('App Service Plan OS') +@allowed([ + 'linux' + 'windows' +]) +param appServicePlanOS string + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { + name: userAssignedIdentityName +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: logAnalyticsWorkspaceName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + features: { + enableLogAccessUsingOnlyResourcePermissions: true + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } +} + +resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' = { + name: automationAccountName + location: location + properties: { + sku: { + name: 'Basic' + } + } +} + +var automationAccountLinkedWorkspaceName = 'Automation' + +resource automationAccountLinkedWorkspace 'Microsoft.OperationalInsights/workspaces/linkedServices@2020-08-01' = { + name: '${logAnalyticsWorkspace.name}/${automationAccountLinkedWorkspaceName}' + properties: { + resourceId: automationAccount.id + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: applicationInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + WorkspaceResourceId: logAnalyticsWorkspace.id + } +} + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: appServicePlanName + location: location + kind: appServicePlanOS + sku: { + name: 'S1' + } + properties: { + reserved: ((appServicePlanOS == 'linux') ? true : false) + } +} + +// resource appConfig 'Microsoft.AppConfiguration/configurationStores@2022-05-01' existing = { +// name: appConfigurationName +// } + +// // metadata :[ +// // { +// // name:'CURRENT_STACK' +// // value:'dotnetcode' +// // } +// // ] + +// resource permissionsApi 'Microsoft.Web/sites@2022-03-01' = { +// name: permissionsApiName +// location: location +// kind: 'app,windows' +// properties: { +// serverFarmId: appServicePlan.name +// httpsOnly: true +// // clientCertEnabled: true // https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth?tabs=bicep +// clientCertMode: 'Required' +// siteConfig: { +// ftpsState: 'FtpsOnly' +// alwaysOn: true +// http20Enabled: true +// keyVaultReferenceIdentity: userAssignedIdentity.id // Must specify this when using User Assigned Managed Identity. Read here: https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli#access-vaults-with-a-user-assigned-identity +// detailedErrorLoggingEnabled: true +// netFrameworkVersion: 'v7.0' +// // linuxFxVersion: 'DOTNETCORE|7.0' +// } +// } +// identity: { +// type: 'UserAssigned' +// userAssignedIdentities: { +// '${userAssignedIdentity.id}': {} +// } +// } +// resource appsettings 'config@2022-03-01' = { +// name: 'appsettings' +// properties: { +// Version: 'ver${version}' +// Logging__LogLevel__Default: 'Information' +// Logging__LogLevel__Microsoft__AspNetCore: 'Warning' +// KeyVault__Url: keyVaultUri +// ASPNETCORE_ENVIRONMENT: environment +// UserAssignedManagedIdentityClientId: userAssignedIdentity.properties.clientId +// AppConfiguration__Endpoint : appConfig.properties.endpoint +// APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString // https://learn.microsoft.com/en-us/azure/azure-monitor/app/migrate-from-instrumentation-keys-to-connection-strings +// ApplicationInsightsAgent_EXTENSION_VERSION: '~2' +// } +// } +// } + +// resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { +// name: 'string' +// scope: permissionsApi +// properties: { +// logs: [ +// { +// categoryGroup: 'allLogs' +// enabled: true +// retentionPolicy: { +// days: 7 +// enabled: true +// } +// } +// ] +// metrics: [ +// { +// category: 'AllMetrics' +// enabled: true +// } +// ] +// workspaceId: logAnalyticsWorkspace.id +// } +// } + +// // resource permissionsApiStagingSlot 'Microsoft.Web/sites/slots@2022-03-01' = { +// // name: 'PermissionsApi-Staging' +// // parent: permissionsApi +// // location: location +// // kind: 'app,linux' +// // properties: { +// // serverFarmId: appServicePlan.name +// // httpsOnly: true +// // siteConfig: { +// // alwaysOn: true +// // linuxFxVersion: 'DOTNETCORE|7.0' +// // http20Enabled: true +// // } +// // } +// // identity: { +// // type: 'UserAssigned' +// // userAssignedIdentities: { '${userAssignedIdentity.id}': {} } +// // } +// // resource appsettings 'config@2022-03-01' = { +// // name: 'appsettings' +// // properties: { +// // Version: version +// // Logging__LogLevel__Default: 'Information' +// // Logging__LogLevel__Microsoft__AspNetCore: 'Warning' +// // KeyVault__Url: keyVaultUri +// // ASPNETCORE_ENVIRONMENT: 'Development' +// // UserAssignedManagedIdentityClientId: userAssignedIdentity.properties.clientId +// // AppConfiguration__Endpoint : appConfig.properties.endpoint +// // } +// // } +// // resource metadata 'config@2022-03-01' = { +// // name: 'metadata' +// // properties: { +// // CURRENT_STACK: 'dotnet' +// // } +// // } +// // } + +// // Resource - Permissions Api - Deployment +// ////////////////////////////////////////////////// +// // resource permissionsApiDeployment 'Microsoft.Web/sites/extensions@2021-03-01' = { +// // parent: permissionsApi +// // name: 'MSDeploy' +// // properties: { +// // packageUri: 'https://stsaasdev001.blob.${environment().suffixes.storage}/artifacts/saas-provider/Saas.Provider.Web.zip?sv=2020-04-08&st=2021-06-07T19%3A23%3A20Z&se=2022-06-08T19%3A23%3A00Z&sr=c&sp=rl&sig=kNf0qwTfaCJg02xYeUHlfmHOJvI1bGU1HftjUJ5hl5o%3D' +// // } +// // } + +// // Outputs +// ////////////////////////////////////////////////// +// output permissionsApiHostName string = permissionsApi.properties.defaultHostName +output appServicePlanName string = appServicePlan.name diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/permissionsApi.bicep b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/permissionsApi.bicep deleted file mode 100644 index 10f63ee9..00000000 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/permissionsApi.bicep +++ /dev/null @@ -1,219 +0,0 @@ -// Parameters -////////////////////////////////////////////////// -@description('Version') -param version string - -@description('The App Service Plan ID.') -param appServicePlanName string - -@description('The Uri of the Key Vault.') -param keyVaultUri string - -@description('The location for all resources.') -param location string - -@description('The Permissions Api name.') -param permissionsApiName string - -@description('Azure App Configuration User Assigned Identity Name.') -param userAssignedIdentityName string - -@description('The name of the Azure App Configuration.') -param appConfigurationName string - -@description('The name of the Log Analytics Workspace used by Application Insigths.') -param logAnalyticsWorkspaceName string - -@description('The name of the Automation Account.') -param automationAccountName string - -@description('The name of Application Insights.') -param applicationInsightsName string - -resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { - name: userAssignedIdentityName -} - -resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { - name: logAnalyticsWorkspaceName - location: location - properties: { - sku: { - name: 'PerGB2018' - } - retentionInDays: 30 - features: { - enableLogAccessUsingOnlyResourcePermissions: true - } - } - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentity.id}': {} - } - } -} - -resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' = { - name: automationAccountName - location: location - properties: { - sku: { - name: 'Basic' - } - } -} - -var automationAccountLinkedWorkspaceName = 'Automation' - -resource automationAccountLinkedWorkspace 'Microsoft.OperationalInsights/workspaces/linkedServices@2020-08-01' = { - name: '${logAnalyticsWorkspace.name}/${automationAccountLinkedWorkspaceName}' - properties: { - resourceId: automationAccount.id - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: applicationInsightsName - location: location - kind: 'web' - properties: { - Application_Type: 'web' - publicNetworkAccessForIngestion: 'Enabled' - publicNetworkAccessForQuery: 'Enabled' - WorkspaceResourceId: logAnalyticsWorkspace.id - } -} - -resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: appServicePlanName - location: location - kind: 'windows' - sku: { - name: 'S1' - } - properties: { - reserved: false // change to true to enable request Linux rather than Windows. Go figure :) - } -} - -resource appConfig 'Microsoft.AppConfiguration/configurationStores@2022-05-01' existing = { - name: appConfigurationName -} - -resource permissionsApi 'Microsoft.Web/sites@2022-03-01' = { - name: permissionsApiName - location: location - kind: 'app,windows' - properties: { - serverFarmId: appServicePlan.name - httpsOnly: true - // clientCertEnabled: true // https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth?tabs=bicep - clientCertMode: 'Required' - siteConfig: { - ftpsState: 'FtpsOnly' - alwaysOn: true - // linuxFxVersion: 'DOTNETCORE|7.0' - http20Enabled: true - keyVaultReferenceIdentity: userAssignedIdentity.id // Must specify this when using User Assigned Managed Identity. Read here: https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli#access-vaults-with-a-user-assigned-identity - detailedErrorLoggingEnabled: true - } - } - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentity.id}': {} - } - } - resource appsettings 'config@2022-03-01' = { - name: 'appsettings' - properties: { - Version: 'ver${version}' - Logging__LogLevel__Default: 'Information' - Logging__LogLevel__Microsoft__AspNetCore: 'Warning' - KeyVault__Url: keyVaultUri - ASPNETCORE_ENVIRONMENT: 'Production' - UserAssignedManagedIdentityClientId: userAssignedIdentity.properties.clientId - AppConfiguration__Endpoint : appConfig.properties.endpoint - APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString // https://learn.microsoft.com/en-us/azure/azure-monitor/app/migrate-from-instrumentation-keys-to-connection-strings - ApplicationInsightsAgent_EXTENSION_VERSION: '~2' - } - } -} - -resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: 'string' - scope: permissionsApi - properties: { - logs: [ - { - categoryGroup: 'allLogs' - enabled: true - retentionPolicy: { - days: 7 - enabled: true - } - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - workspaceId: logAnalyticsWorkspace.id - } -} - -// resource permissionsApiStagingSlot 'Microsoft.Web/sites/slots@2022-03-01' = { -// name: 'PermissionsApi-Staging' -// parent: permissionsApi -// location: location -// kind: 'app,linux' -// properties: { -// serverFarmId: appServicePlan.name -// httpsOnly: true -// siteConfig: { -// alwaysOn: true -// linuxFxVersion: 'DOTNETCORE|7.0' -// http20Enabled: true -// } -// } -// identity: { -// type: 'UserAssigned' -// userAssignedIdentities: { '${userAssignedIdentity.id}': {} } -// } -// resource appsettings 'config@2022-03-01' = { -// name: 'appsettings' -// properties: { -// Version: version -// Logging__LogLevel__Default: 'Information' -// Logging__LogLevel__Microsoft__AspNetCore: 'Warning' -// KeyVault__Url: keyVaultUri -// ASPNETCORE_ENVIRONMENT: 'Development' -// UserAssignedManagedIdentityClientId: userAssignedIdentity.properties.clientId -// AppConfiguration__Endpoint : appConfig.properties.endpoint -// } -// } -// resource metadata 'config@2022-03-01' = { -// name: 'metadata' -// properties: { -// CURRENT_STACK: 'dotnet' -// } -// } -// } - -// Resource - Permissions Api - Deployment -////////////////////////////////////////////////// -// resource permissionsApiDeployment 'Microsoft.Web/sites/extensions@2021-03-01' = { -// parent: permissionsApi -// name: 'MSDeploy' -// properties: { -// packageUri: 'https://stsaasdev001.blob.${environment().suffixes.storage}/artifacts/saas-provider/Saas.Provider.Web.zip?sv=2020-04-08&st=2021-06-07T19%3A23%3A20Z&se=2022-06-08T19%3A23%3A00Z&sr=c&sp=rl&sig=kNf0qwTfaCJg02xYeUHlfmHOJvI1bGU1HftjUJ5hl5o%3D' -// } -// } - -// Outputs -////////////////////////////////////////////////// -output permissionsApiHostName string = permissionsApi.properties.defaultHostName -output appServicePlanName string = appServicePlan.name diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/deployIdentityFoundation.bicep b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/deployIdentityFoundation.bicep index c9f584dd..8ba256e8 100644 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/deployIdentityFoundation.bicep +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/deployIdentityFoundation.bicep @@ -1,11 +1,17 @@ @description('Version') param version string +@description('Environment') +@allowed([ + 'Development' + 'Staging' + 'Production' +]) +param environment string + @description('The ip address of the dev machine') param devMachineIp string -@description('URL for downstream admin service.') -param appSettingsAdminServiceBaseUrl string @description('postfix') param solutionPostfix string @@ -19,27 +25,6 @@ param solutionName string @description('The name of the key vault') param keyVaultName string -@description('URL for downstream admin service.') -param azureB2CDomain string - -@description('The B2C login endpoint in format of https://(Tenant Name).b2clogin.com.') -param azureB2CLoginEndpoint string - -@description('Tenant Id found on your AD B2C dashboard.') -param azureB2CTenantId string - -@description('The Client Id found on registered Permissions API app page.') -param permissionApiClientId string - -@description('Permissions API Certificate Name') -param permissionCertificateName string - -@description('Permissions API Instance') -param permissionInstance string - -@description('Permission API Name') -param permissionsApiName string - @description('Permissions API Secret key') param permissionApiKey string @@ -49,6 +34,7 @@ param sqlAdministratorLogin string @description('The location for all resources.') param location string = resourceGroup().location +var appServicePlanOS = 'windows' var appServicePlanName = 'plan-${solutionPrefix}-${solutionName}-${solutionPostfix}' var appConfigurationName = 'appconfig-${solutionPrefix}-${solutionName}-${solutionPostfix}' var permissionsSqlDatabaseName = 'sqldb-permissions-${solutionPrefix}-${solutionName}-${solutionPostfix}' @@ -85,8 +71,7 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } -// Create object w/ array of objects containing the kayname and value to be stored in Azure App Configuration store. -var permissionApi = 'PermissionApi' + module appConfigurationModule './Module/appConfigurationStore.bicep' = { name: 'AppConfigurationDeployment' @@ -106,6 +91,9 @@ module keyVaultAccessPolicyModule 'Module/keyVaultAccessRBAC.bicep' = { keyVault ] } + +// Create object w/ array of objects containing the kayname and value to be stored in Azure App Configuration store. +var permissionsApiKeyName = 'PermissionsApi' module restApiKeyModule './Module/linkToExistingKeyVaultSecret.bicep' = { name: 'PermissionApiKeyDeployment' params: { @@ -114,7 +102,7 @@ module restApiKeyModule './Module/linkToExistingKeyVaultSecret.bicep' = { appConfigurationName: appConfigurationName userAssignedIdentityName: userAssignedIdentity.name keyVaultKeyName: permissionApiKey - keyName: '${permissionApi}:apiKey' + keyName: '${permissionsApiKeyName}:ApiKey' } dependsOn: [ keyVaultAccessPolicyModule @@ -127,16 +115,13 @@ resource appConfigurationStore 'Microsoft.AppConfiguration/configurationStores@2 name: appConfigurationName } -module permissionsApiModule './Module/permissionsApi.bicep' = { +module appPlanModule './Module/appPlan.bicep' = { name: 'PermissionsApiDeployment' params: { - version: version + appServicePlanOS: appServicePlanOS appServicePlanName: appServicePlanName - keyVaultUri: keyVault.properties.vaultUri location: location - permissionsApiName: permissionsApiName userAssignedIdentityName: userAssignedIdentity.name - appConfigurationName: appConfigurationStore.name applicationInsightsName: applicationInsightsName logAnalyticsWorkspaceName: logAnalyticsWorkspaceName automationAccountName: automationAccountName @@ -153,17 +138,9 @@ module configurationEntriesModule './Module/addConfigEntries.bicep' = { name: 'ConfigurationEntriesDeployment' params: { version: version - appSettingsAdminServiceBaseUrl: appSettingsAdminServiceBaseUrl keyVaultName: keyVault.name - azureB2CDomain: azureB2CDomain - azureB2CLoginEndpoint: azureB2CLoginEndpoint - azureB2CTenantId: azureB2CTenantId - permissionApiClientId: permissionApiClientId - permissionCertificateName: permissionCertificateName - permissionInstance: permissionInstance sqlAdministratorLogin: sqlAdministratorLogin userAssignedIdentityName: userAssignedIdentity.name - keyVaultUrl: keyVault.properties.vaultUri appConfigurationName: appConfigurationStore.name permissionsSqlDatabaseName: permissionsSqlDatabaseName permissionsSqlServerFQDN: permissionsSqlModule.outputs.permissionsSqlServerFQDN @@ -174,21 +151,21 @@ module configurationEntriesModule './Module/addConfigEntries.bicep' = { keyVaultAccessPolicyModule keyVault restApiKeyModule - permissionsApiModule + appPlanModule ] } output version string = version output location string = location +output environment string = environment output appConfigurationName string = appConfigurationName output keyVaultName string = keyVault.name output keyVaultUri string = keyVault.properties.vaultUri -output appServicePlanName string = permissionsApiModule.outputs.appServicePlanName +output appServicePlanName string = appPlanModule.outputs.appServicePlanName output permissionsSqlServerName string = permissionsSqlModule.outputs.permissionsSqlServerName output userAssignedIdentityName string = userAssignedIdentity.name output userAssignedIdentityId string = userAssignedIdentity.id output permissionsSqlServerFQDN string = permissionsSqlModule.outputs.permissionsSqlServerFQDN -output permissionsApiHostName string = permissionsApiModule.outputs.permissionsApiHostName output applicationInsightsName string = applicationInsightsName output logAnalyticsWorkspaceName string = logAnalyticsWorkspaceName output automationAccountName string = automationAccountName diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/build.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/build.sh index 5cf4edc5..69bc6a5a 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/build.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/build.sh @@ -1,7 +1,22 @@ #!/usr/bin/env bash +force_update=false + +while getopts f flag +do + case "${flag}" in + f) force_update=true;; + *) force_update=false;; + esac +done + repo_base="$( git rev-parse --show-toplevel )" docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + # redirect to build.sh in the Deployment.Container folder -"${docker_file_folder}/build.sh" \ No newline at end of file +if [[ "${force_update}" == false ]]; then + "${docker_file_folder}/build.sh" +else + "${docker_file_folder}/build.sh" -f +fi diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/config/config-template.json b/src/Saas.Identity/Saas.IdentityProvider/deployment/config/config-template.json index cd72e784..36496c94 100644 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/config/config-template.json +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/config/config-template.json @@ -16,8 +16,7 @@ } }, "version": "0.8.0", - "environment": "development", - "production": false, + "environment": "Production", "git": { "branch": "main" }, @@ -42,6 +41,8 @@ } }, "azureb2c": { + "signedOutCallBackPath": "/signout/B2C_1A_SIGNUP_SIGNIN", + "signUpSignInPolicyId": "B2C_1A_SIGNUP_SIGNIN", "policyKeys": [ { "name": "TokenSigningKeyContainer", @@ -62,10 +63,16 @@ "keyUsage": "Signature" } ] - }, + }, + "claimToRoleTransformer": { + "authenticationType": "MyCustomRoleAuth", + "roleClaimType": "MyCustomRole", + "sourceClaimType": "permissions" + }, "appRegistrations": [ { "name": "admin-api", + "appServiceName": null, "certificate": false, "redirectUri": null, "applicationIdUri": null, @@ -117,6 +124,7 @@ }, { "name": "signupadmin-app", + "appServiceName": null, "certificate": true, "redirectUri": null, "appId": null, @@ -148,6 +156,7 @@ }, { "name": "saas-app", + "appServiceName": null, "certificate": true, "redirectUri": null, "appId": null, @@ -174,6 +183,8 @@ }, { "name": "permissions-api", + "baseUrl": null, + "appServiceName": null, "apiName": null, "appId": null, "objectId": null, @@ -182,7 +193,6 @@ "redirectUri": null, "permissionsApiUrl": null, "rolesApiUrl": null, - "instance": "https://login.microsoftonline.com/", "publicKeyPath": null, "certificateKeyName": null, "scopes": null, @@ -195,15 +205,8 @@ "offline_access" ], "appRoles": [ - "User.Read.All" - ] - }, - { - "grantAdminConsent": true, - "resourceId": "00000003-0000-0000-c000-000000000000", - "scopes": [ - "openid", - "offline_access" + "User.Read.All", + "Application.ReadWrite.OwnedBy" ] } ] @@ -266,6 +269,18 @@ "sqlAdminLoginName": "sqlAdmin" }, "deployment": { + "identityFoundation": { + "name": "IdentityFoundationDeployment" + }, + "adminServiceApi": { + "name": "AdminServiceAPIDeployment" + }, + "signupAdministration": { + "name": "SignupAdmininstrationDeployment" + }, + "permissionApi": { + "name": "PermissionApiDeployment" + }, "users": [], "azureCli": { "configDir": null diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/config/identity-bicep-parameters-template.json b/src/Saas.Identity/Saas.IdentityProvider/deployment/config/identity-bicep-parameters-template.json index 9fefe0df..f9ff53f1 100644 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/config/identity-bicep-parameters-template.json +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/config/identity-bicep-parameters-template.json @@ -4,11 +4,11 @@ "parameters": { "version": { "value": null - }, - "devMachineIp": { + }, + "environment": { "value": null }, - "appSettingsAdminServiceBaseUrl": { + "devMachineIp": { "value": null }, "solutionPostfix": { @@ -23,27 +23,6 @@ "keyVaultName": { "value": null }, - "azureB2CDomain": { - "value": null - }, - "azureB2CLoginEndpoint": { - "value": null - }, - "azureB2CTenantId": { - "value": null - }, - "permissionsApiName": { - "value": null - }, - "permissionApiClientId": { - "value": null - }, - "permissionInstance": { - "value": null - }, - "permissionCertificateName": { - "value": null - }, "permissionApiKey": { "value": null }, diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/readme.md b/src/Saas.Identity/Saas.IdentityProvider/deployment/readme.md index 5258e382..dc12999e 100644 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/readme.md +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/readme.md @@ -16,9 +16,11 @@ The purpose of the Azure SaaS Dev Kit is to boost your SaaS journey by providing *Answer*: Yes, and no. In fact Bicep is used when ever possible as part of the deployment script. Yet, the ASDK Identity Foundation relies on [Azure Active Directory B2C](https://learn.microsoft.com/en-us/azure/active-directory-b2c/overview) as well as defining [Azure Active Directory App Registrations](https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-how-applications-are-added). At the time of writing those resources and their configurations cannot be automated by ARM and Bicep. -## Running the Deployment Script in a Container, Using Docker (recommended) +## Running the Deployment Script -Running the deployment script utilizing a container is highly recommend. Using a container will ensure that you have all the required dependencies in their correct configurations etc. In short; when the script runs and that you are running in a controlled environment. It will also minimize the chances that some other properties of your existing environment interferes with the script or that the script inadvertently interferes with your existing environment. +Running the deployment script requires utilizing a container and Docker. + +Using a container will ensure that you have all the required dependencies in their correct configurations in a controlled environment. This will also minimize the chances that some other properties of your existing environment might with the script, or that the script inadvertently will interferes with your existing environment. ### Prerequisites @@ -43,7 +45,7 @@ No matter the operating system you're using, you will need these tools to be ins - [**Docker Desktop**](https://docs.docker.com/get-docker/). - If you have Docker already, make sure to get the latest updates before you begin. If you have Docker installed but haven't used it for a while. Reinstalling will often solve potential issues. - [Azure Command Line Interface (**az cli**)](https://learn.microsoft.com/en-us/cli/azure/what-is-azure-cli) from the terminal: [How to install the Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli). -- [GitHub’s official command line tool (**gh**)]([GitHub CLI | Take GitHub to the command line](https://cli.github.com/)). For more on installation see [here](https://github.com/cli/cli#installation). +- [GitHub’s official command line tool (**gh**)](https://cli.github.com/). For more on installation see [here](https://github.com/cli/cli#installation). - **Zip** which can be installed with the command: `sudo apt install zip` . - Note: **zip** is already installed on MacOS per default. @@ -72,7 +74,9 @@ chmod +x setup.sh # only needed the first time to set execute permissions on set This will take a few minutes to complete and you will only need to do it once. The container will be named `asdk-script-deployment`. -> Tip: If you make changes to `Dockerfile`, defining the container, you can update the container by running `./build.sh`. +> *Tip #1*: If you make changes to `Dockerfile`, defining the container, you can update the container by running `./build.sh`. +> +> Tip #2: If you want to force a rebuild of the container, please us `./build.sh -f`. This can be handy if there's a new version of az cli or GitHub cli that you want to update the container with. ### Running the deployment script using the container @@ -195,34 +199,6 @@ While running the script the second time, you will be asked to log in once, and > Info: The script will cache this login session too, so that if you need to run the script multiple times, you will not be asked to log in to your Azure AD B2C tenant again. The login session for Azure B2C is cached here: `$HOME/asdk/.cache/`. -## Running Deployment Script on Your Computer Without Docker (not recommended) - -While not recommended, you can also run the deployment script *bare-bone* on you computing without using a container. It will generally run slower. More importantly, since the run environment is not controlled, there is a higher risk for something going off the rails. That said, the script is tested for this and will work in many circumstances. - -The script have been tested on: - -- Windows 10/11 running in WSL with a Ubuntu 22.04 distro. -- Ubuntu 22.04. -- MacOS Ventura. 13.1+, including MacOS running on Apple Silicon. -- While not tested on other configurations, it will likely run recent Linux distros and versions as well as and earlier and recent versions of MacOS too. - -Make sure that you have all the tools mentioned above as well as the following installed on your machine before running the script: - -- [JQ v1.6+](https://linuxhint.com/bash_jq_command/) for Bash. -- [GitHub’s official command line tool (gh)](https://github.com/cli/cli#installation) -- Specifically on MacOS, you'll need a more recent of `bash` as the default version is rather old. - - To do this you can use homebrew: [`brew install bash`](https://formulae.brew.sh/formula/bash). - - -When these requirements are met, the script can be run using the following command: - -```bash -chmod +x start.sh -./start.sh -``` - -From there on everything else is virtually identical to running the script from inside a container, as described above. - ## What If Something Goes Wrong? It shouldn't happen, but we all know that it does - thank you [Murphy](https://en.wikipedia.org/wiki/Murphy%27s_law)! In most cases, when something goes wrong along the way, all you'll need to do is to run the script once again. The deployment script will skip the parts that have already been completed and re-try the parts that have not. diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/run.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/run.sh index 87b78a9e..38745a91 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/run.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/run.sh @@ -18,11 +18,12 @@ docker run \ --interactive \ --tty \ --rm \ - --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment:ro \ - --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config \ - --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/log/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/log \ - --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/policies/":/asdk/src/Saas.Identity/Saas.IdentityProvider/policies \ - --volume "${repo_base}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment:ro \ + --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/config":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config \ + --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/log":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/log \ + --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/policies":/asdk/src/Saas.Identity/Saas.IdentityProvider/policies \ + --volume "${repo_base}/src/Saas.Lib/Deployment.Script.Modules":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${repo_base}/src/Saas.Lib/Saas.Bicep.Module":/asdk/src/Saas.Lib/Saas.Bicep.Module:ro \ --volume "${repo_base}/.git/":/asdk/.git:ro \ --volume "${HOME}/.azure/":/asdk/.azure:ro \ --volume "${HOME}/asdk/.cache/":/asdk/.cache \ diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/b2c-app-registrations.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/b2c-app-registrations.sh index e0e2ad8c..fde2eca0 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/b2c-app-registrations.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/b2c-app-registrations.sh @@ -41,14 +41,6 @@ echo "Adding app registrations to Azure B2C tenant." \ declare -i scopes_length declare -i permissions_length -# b2c_name="$( get-value ".deployment.azureb2c.name" )" -# prefix="$( get-value ".initConfig.naming.solutionPrefix" )" -# postfix="$( get-value ".deployment.postfix" )" -# solution_name="$( get-value ".initConfig.naming.solutionName" )" -# app_id_uri="api://${b2c_name}/${prefix}-${solution_name}-${postfix}" - -# put-value ".deployment.azureb2c.applicationIdUri" "${app_id_uri}" - # read each item in the JSON array to an item in the Bash array readarray -t app_reg_array < <( jq --compact-output '.appRegistrations[]' "${CONFIG_FILE}") @@ -56,6 +48,20 @@ readarray -t app_reg_array < <( jq --compact-output '.appRegistrations[]' "${CON # declare -i i # i=1 +b2c_tenant_name="$(get-value ".deployment.azureb2c.name" )" \ + || echo "Azure B2C tenant namenot found." \ + | log-output \ + --level error \ + --header "Critical error" \ + || exit 1 + +echo "Setting instance ${b2c_tenant_name}.b2clogin.com" \ + | log-output \ + --level info \ + --header "Azure B2C Instance" + +put-value ".deployment.azureb2c.instance" "https://${b2c_tenant_name}.b2clogin.com" + # iterate through the Bash array of app registrations for app in "${app_reg_array[@]}"; do app_name=$( jq --raw-output '.name' <<< "${app}" ) @@ -74,9 +80,9 @@ for app in "${app_reg_array[@]}"; do display_name="${app_name}" echo "Provisioning app registration for: ${display_name}..." \ - | log-output \ - --level info \ - --header "${display_name}" + | log-output \ + --level info \ + --header "${display_name}" if app-exist "${app_id}"; then echo "App registration for ${app_name} already exist. If you made changes or updated the certificate, you will have to delete the app registration to use this script to update it. " \ diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-azure-b2c.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-azure-b2c.sh index adedd724..318e0051 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-azure-b2c.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-azure-b2c.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -u -e -o pipefail - # shellcheck disable=SC1091 { # include script modules into current shell @@ -42,13 +41,18 @@ if ! resource-exist "${b2c_type_name}" "${b2c_name}" ; then name="${b2c_name}" \ skuName="${b2c_sku_name}" \ tier="${b2c_tier}" \ - || echo "Azure B2C deployment failed." | log-output \ - --level error \ - --header "Critical error" \ + || echo "Azure B2C deployment failed." \ + | log-output \ + --level error \ + --header "Critical error" \ || exit 1 - echo "Provisionning of Azure B2C tenant Successful." | log-output --level success + echo "Provisionning of Azure B2C tenant Successful." | + log-output \ + --level success else - echo "Existing Azure B2C tenant found and will be used." | log-output --level success + echo "Existing Azure B2C tenant found and will be used." | + log-output \ + --level success fi \ No newline at end of file diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-oidc-workflow-github-action.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-oidc-workflow-github-action.sh index e6eb2c1b..1c3f268c 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-oidc-workflow-github-action.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/create-oidc-workflow-github-action.sh @@ -112,9 +112,7 @@ subject="repo:${git_org_project_name}:ref:refs/heads/main" put-value ".oidc.credentials.subject" "${subject}" -federation_id="$( get-value ".oidc.federation.id" )" - -if ! federation-exist "${oidc_app_id}" "${federation_id}"; then +if ! federation-exist "${oidc_app_id}" "${subject}" ; then echo "Creating OIDC Connect Workflow federation..." \ | log-output \ --level info diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/deploy-identity-foundation.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/deploy-identity-foundation.sh index d925e9cf..d842253f 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/deploy-identity-foundation.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/deploy-identity-foundation.sh @@ -10,44 +10,47 @@ set -e -o pipefail source "$SHARED_MODULE_DIR/config-module.sh" } -if ! [[ -f "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" ]]; then - echo "The file ${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE} does not exist, creating it now" \ - | log-output \ +if [[ ! -s "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" || + ! -f "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" ]]; then + + echo "The file ${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE} does not exist or is empty, creating it now" | + log-output \ --level info cp "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_TEMPLATE_FILE}" "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" fi set -u -"${SCRIPT_DIR}/map-identity-paramenters.py" "${CONFIG_FILE}" "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" \ - | log-output \ +"${SCRIPT_DIR}/map-identity-paramenters.py" "${CONFIG_FILE}" "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" | + log-output \ --level info \ - --header "Generating Identity Foundation services parameters..." \ - || echo "Failed to map Identity Foundation services parameters" \ - | log-output \ - --level error \ - --header "Critical Error" \ - || exit 1 + --header "Generating Identity Foundation services parameters..." || + echo "Failed to map Identity Foundation services parameters" | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 -resource_group="$( get-value ".deployment.resourceGroup.name" )" +resource_group="$(get-value ".deployment.resourceGroup.name")" +deployment_name="$(get-value ".deployment.identityFoundation.name")" -echo "Provisioning Identity Foundation services in resource group ${resource_group}..." \ - | log-output \ +echo "Provisioning '${deployment_name}' to resource group ${resource_group}..." | + log-output \ --level info az deployment group create \ --resource-group "${resource_group}" \ - --name "IdentityBicepDeployment" \ + --name "${deployment_name}" \ --template-file "${DEPLOY_IDENTITY_FOUNDATION_FILE}" \ - --parameters "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" \ - | log-output \ - --level info \ - || echo "Failed to deploy Identity Foundation services" \ - | log-output \ - --level error \ - --header "Critical Error" \ - || exit 1 + --parameters "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" | + log-output \ + --level info || + echo "Failed to deploy Identity Foundation services" | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 -echo "Indentity Foundation services successfully provisioned in resource group ${resource_group}..." \ - | log-output \ - --level success \ No newline at end of file +echo "'${deployment_name}' was successfully provisioned to resource group ${resource_group}..." | + log-output \ + --level success diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/generate-ief-policies.py b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/generate-ief-policies.py index 80460a59..bb36cbd4 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/generate-ief-policies.py +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/generate-ief-policies.py @@ -19,7 +19,7 @@ def create_appsettings_file(config_file: str, app_settings_file: str) -> None: # get the values to be added to the appsettings file name = config['environment'] - production = config['production'] + production = config['environment'] == 'Production' tenant = config['deployment']['azureb2c']['domainName'] identityExperienceFrameworkAppId = get_app_value(config, "IdentityExperienceFramework", "appId") proxyIdentityExperienceFrameworkAppId = get_app_value(config, "ProxyIdentityExperienceFramework", "appId") diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init-module.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init-module.sh index 474307aa..03dc459b 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init-module.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init-module.sh @@ -58,7 +58,7 @@ function final-state() { | log-output \ --level error \ --header "Deployment script completion" \ - || echo "Please review the log file for more details: ${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}" \ + || echo "Please review the log file for more details: ${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}" \ | log-output \ --level warn fi @@ -246,27 +246,48 @@ function populate-configuration-manifest() { service_principal_name="${solution_prefix}-usr-sp-${postfix}" put-value ".deployment.azureb2c.servicePrincipal.username" "${service_principal_name}" + admin_api_name="admin-api-${long_solution_name}" + + put-app-value \ + "admin-api" \ + "appServiceName" \ + "${admin_api_name}" + put-app-value \ "admin-api" \ "baseUrl" \ - "api-admin-${long_solution_name}" + "https://${admin_api_name}.azurewebsites.net" put-app-value \ "admin-api" \ "applicationIdUri" \ "api://${b2c_name}/${long_solution_name}/admin-api" + signup_admin_app_name="signupadmin-app-${long_solution_name}" + + put-app-value \ + "signupadmin-app" \ + "appServiceName" \ + "${signup_admin_app_name}" + # adding redirecturl to signupadmin-app put-app-value \ "signupadmin-app" \ "redirectUri" \ - "https://appsignup-${long_solution_name}.azurewebsites.net/signin-oidc" + "https://signupadmin-app-${long_solution_name}.azurewebsites.net/signin-oidc" + + saas_app_name="saas-app-${long_solution_name}" + + put-app-value \ + "saas-app" \ + "appServiceName" \ + "${saas_app_name}" # adding redirecturl to saas-app put-app-value \ "saas-app" \ "redirectUri" \ - "https://saasapp-${long_solution_name}.azurewebsites.net/signin-oidc" + "https://saas-app-${long_solution_name}.azurewebsites.net/signin-oidc" permission_api_name="api-permission-${long_solution_name}" @@ -276,6 +297,16 @@ function populate-configuration-manifest() { "apiName" \ "${permission_api_name}" + put-app-value \ + "permissions-api" \ + "appServiceName" \ + "${permission_api_name}" + + put-app-value \ + "permissions-api" \ + "baseUrl" \ + "https://${permission_api_name}.azurewebsites.net" + # adding permission API Url to permissions-api put-app-value \ "permissions-api" \ diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init.sh index ebb71ccf..117d23da 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/init.sh @@ -14,55 +14,55 @@ set -u -e -o pipefail function check-prerequisites() { - echo "Checking prerequisites..." \ - | log-output \ + echo "Checking prerequisites..." | + log-output \ --level info \ --header "Checking prerequisites" - what_os="$( get-os )" \ - || echo "Unsupported OS: ${what_os}. This script support linux and macos." \ - | log-output \ - --level error \ - --header "Critical Error" \ - || exit 1 + what_os="$(get-os)" || + echo "Unsupported OS: ${what_os}. This script support linux and macos." | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 - echo "Supported operating system: ${what_os}" | \ - log-output \ - --level success \ + echo "Supported operating system: ${what_os}" | + log-output \ + --level success # check if bash version is supported - is-valid-bash "5.0.0" \ - | log-output \ + is-valid-bash "5.0.0" | + log-output \ --level info \ - --header "Checking bash version" \ - || echo "The version of bash is not supported." \ - | log-output \ - --level error \ - --header "Critical error" \ - || exit 1 + --header "Checking bash version" || + echo "The version of bash is not supported." | + log-output \ + --level error \ + --header "Critical error" || + exit 1 # check if az cli version is supported - is-valid-az-cli "2.42.0" \ - | log-output \ + is-valid-az-cli "2.42.0" | + log-output \ --level info \ - --header "Checking az cli version" \ - || echo "The version of az cli is not supported." \ - | log-output \ - --level error \ - --header "Critical error" \ - || exit 1 + --header "Checking az cli version" || + echo "The version of az cli is not supported." | + log-output \ + --level error \ + --header "Critical error" || + exit 1 - is-valid-jq "1.5" \ - | log-output \ + is-valid-jq "1.5" | + log-output \ --level info \ - --header "Checking jq version" \ - || echo "The version of jq is not supported." \ - | log-output \ - --level error \ - --header "Critical error" \ - || exit 1 - - # if running in a container copy the msal token cache + --header "Checking jq version" || + echo "The version of jq is not supported." | + log-output \ + --level error \ + --header "Critical error" || + exit 1 + + # if running in a container copy the msal token cache # so that user may not have to log in again to main tenant. if [ -f /.dockerenv ]; then cp -f /asdk/.azure/msal_token_cache.* /root/.azure/ @@ -77,58 +77,56 @@ function initialize-shell-scripts() { sudo chmod +x ${SCRIPT_DIR}/*.sh sudo chmod +x ${SHARED_MODULE_DIR}/*.py fi - } -function initialize-configuration-manifest-file() -{ - if [[ ! -f "${CONFIG_FILE}" ]]; then +function initialize-configuration-manifest-file() { - echo "It looks like this is the first time you're running this script. Setting things up..." \ - | log-output \ + if [[ ! -s "${CONFIG_FILE}" || ! -f "${CONFIG_FILE}" ]]; then + echo "It looks like this is the first time you're running this script. Setting things up..." | + log-output \ --level info echo - echo "Creating new './config/config.json' from 'config-template.json.'" \ - | log-output \ + echo "Creating new './config/config.json' from 'config-template.json.'" | + log-output \ --level info - + cp "${CONFIG_TEMPLATE_FILE}" "${CONFIG_FILE}" sudo chown -R 666 "${CONFIG_DIR}" echo - echo "Before beginning deployment you must specify initial configuration in the 'initConfig' object:" \ - | log-output \ + echo "Before beginning deployment you must specify initial configuration in the 'initConfig' object:" | + log-output \ --level warning - init_config="$( get-value ".initConfig" )" + init_config="$(get-value ".initConfig")" - echo "${init_config}" \ - | log-output \ - --level msg; + echo "${init_config}" | + log-output \ + --level msg echo - echo "Please add required initial settings to the initConfig object in ./config/config.json and run this script again." \ - | log-output \ + echo "Please add required initial settings to the initConfig object in ./config/config.json and run this script again." | + log-output \ --level warning exit 2 else # Setting configuration variables - echo "Initializing Configuration" \ - | log-output \ + echo "Initializing Configuration" | + log-output \ --level info \ --header "Configation Settings" backup-config-beginning fi - echo "Configuration settings: $CONFIG_FILE." \ - | log-output \ + echo "Configuration settings: $CONFIG_FILE." | + log-output \ --level success } @@ -142,4 +140,4 @@ initialize-shell-scripts initialize-configuration-manifest-file # set to install az cli extensions without prompting -az config set extension.use_dynamic_install=yes_without_prompt &> /dev/null \ No newline at end of file +az config set extension.use_dynamic_install=yes_without_prompt &>/dev/null diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/map-identity-paramenters.py b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/map-identity-paramenters.py index d34b6c15..9c676613 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/script/map-identity-paramenters.py +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/script/map-identity-paramenters.py @@ -17,11 +17,11 @@ def patch_paramenters_file(config_file: str, paramenter_file: str) -> None: parameters['parameters']['version']['value'] \ = config['version'] + parameters['parameters']['environment']['value'] \ + = config['environment'] + parameters['parameters']['devMachineIp']['value'] \ = config['deployment']['devMachine']['ip'] - - parameters['parameters']['appSettingsAdminServiceBaseUrl']['value'] \ - = get_app_value(config, "admin-api", "baseUrl") parameters['parameters']['solutionPostfix']['value'] \ = config['deployment']['postfix'] @@ -35,27 +35,6 @@ def patch_paramenters_file(config_file: str, paramenter_file: str) -> None: parameters['parameters']['keyVaultName']['value'] \ = config['deployment']['keyVault']['name'] - parameters['parameters']['azureB2CDomain']['value'] \ - = config['deployment']['azureb2c']['domainName'] - - parameters['parameters']['azureB2CLoginEndpoint']['value'] \ - = f"https://{config['deployment']['azureb2c']['name']}.b2clogin.com" - - parameters['parameters']['azureB2CTenantId']['value'] \ - = config['deployment']['azureb2c']['tenantId'] - - parameters['parameters']['permissionsApiName']['value'] \ - = get_app_value(config, "permissions-api", "apiName") - - parameters['parameters']['permissionApiClientId']['value'] \ - = get_app_value(config, "permissions-api", "appId") - - parameters['parameters']['permissionInstance']['value'] \ - = get_app_value(config, "permissions-api", "instance") - - parameters['parameters']['permissionCertificateName']['value'] \ - = get_app_value(config, "permissions-api", "certificateKeyName") - parameters['parameters']['permissionApiKey']['value'] \ = 'RestApiKey' diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/setup.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/setup.sh index 689ea5f7..43e28849 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/setup.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/setup.sh @@ -6,15 +6,30 @@ source "./constants.sh" echo "Setting up the deployment environment." echo "Settings execute permissions on necessary scripts files." -sudo chmod +x ./*.sh -sudo chmod +x ./script/*.sh -sudo chmod +x ./script/*.py +( + sudo chmod +x ./*.sh || exit 1 + sudo chmod +x ./script/*.sh || exit 1 + sudo chmod +x ./script/*.py || exit 1 +) || + { + echo "Failed to set execute permissions on the necessary scripts." + exit 1 + } + +repo_base="$(git rev-parse --show-toplevel)" || + { + echo "Failed to get the root of the repository." + exit 1 + } -repo_base="$( git rev-parse --show-toplevel )" docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" # redirect to build.sh in the Deployment.Container folder -sudo chmod +x "${docker_file_folder}/build.sh" +sudo chmod +x "${docker_file_folder}/build.sh" || + { + echo "Failed to set execute permissions on the 'build.sh' script." + exit 1 + } echo "Building the deployment container." ./build.sh || @@ -23,20 +38,28 @@ echo "Building the deployment container." exit 1 } -echo "Setting up log folder..." -mkdir -p "$LOG_FILE_DIR" -sudo chown "${USER}" "$LOG_FILE_DIR" +( + echo "Setting up log folder..." + mkdir -p "$LOG_FILE_DIR" || exit 1 + sudo chown "${USER}" "$LOG_FILE_DIR" || exit 1 -echo "Setting up config folder..." -mkdir -p "${CONFIG_DIR}" -sudo chown "${USER}" "${CONFIG_DIR}" -sudo chown "${USER}" "${CONFIG_FILE}" -sudo chown "${USER}" "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" + echo "Setting up config folder..." + mkdir -p "${CONFIG_DIR}" || exit 1 + sudo chown "${USER}" "${CONFIG_DIR}" || exit 1 + touch "${CONFIG_FILE}" || exit 1 + sudo chown "${USER}" "${CONFIG_FILE}" || exit 1 + touch "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" || exit 1 + sudo chown "${USER}" "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" || exit 1 -echo "Setting up policy folder..." -mkdir -p "${IDENTITY_EXPERIENCE_FRAMEWORK_POLICY_ENVIRONMENT_DIR}" -sudo chown "${USER}" "${IDENTITY_EXPERIENCE_FRAMEWORK_POLICY_ENVIRONMENT_DIR}" -sudo chown "${USER}" "${IDENTITY_EXPERIENCE_FRAMEWORK_POLICY_APP_SETTINGS_FILE}" + echo "Setting up policy folder..." + mkdir -p "${IDENTITY_EXPERIENCE_FRAMEWORK_POLICY_ENVIRONMENT_DIR}" || exit 1 + sudo chown "${USER}" "${IDENTITY_EXPERIENCE_FRAMEWORK_POLICY_ENVIRONMENT_DIR}" || exit 1 + sudo chown "${USER}" "${IDENTITY_EXPERIENCE_FRAMEWORK_POLICY_APP_SETTINGS_FILE}" || exit 1 +) || + { + echo "Failed to setting up folders with permissions." + exit 1 + } echo -echo "Setup complete. You can now run the deployment script using the command './run.sh'." \ No newline at end of file +echo "Setup complete. You can now run the deployment script using the command './run.sh'." diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/start.sh b/src/Saas.Identity/Saas.IdentityProvider/deployment/start.sh index 84aebf9f..cdb13a99 100755 --- a/src/Saas.Identity/Saas.IdentityProvider/deployment/start.sh +++ b/src/Saas.Identity/Saas.IdentityProvider/deployment/start.sh @@ -2,6 +2,12 @@ export ASDK_CACHE_AZ_CLI_SESSIONS=true +# if not running in a container +if ! [ -f /.dockerenv ]; then + echo "Running outside of a container us not supported. Please run the deployment script using './run.sh'." + exit 0 +fi + if [[ -z $ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE ]]; then # repo base echo "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE is not set. Setting it to default value." @@ -31,7 +37,7 @@ set -u -e -o pipefail now=$(date '+%Y-%m-%d--%H-%M-%S') # set run time for deployment script instance -export ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME="${now}" +export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" # create log file directory if it does not exist if ! [ -f /.dockerenv ] && [[ ! -d "${LOG_FILE_DIR}" ]]; then @@ -40,7 +46,7 @@ if ! [ -f /.dockerenv ] && [[ ! -d "${LOG_FILE_DIR}" ]]; then fi # create log file for this deployment script instance -touch "${LOG_FILE_DIR}/deploy-${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}.log" +touch "${LOG_FILE_DIR}/deploy-${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}.log" echo "Welcome to the Azure SaaS Dev Kit - Azure B2C Identity Provider deployment script." \ | log-output \ @@ -70,6 +76,7 @@ if ! [ -f /.dockerenv ]; then # make sure that the init script is executable chmod +x "$SCRIPT_DIR/init.sh" fi + # initialize deployment environment "${SCRIPT_DIR}/init.sh" \ || if [[ $? -eq 2 ]]; then exit 0; fi diff --git a/src/Saas.Identity/Saas.IdentityProvider/policies/.gitignore b/src/Saas.Identity/Saas.IdentityProvider/policies/.gitignore index ed39d9fd..c7e006a0 100644 --- a/src/Saas.Identity/Saas.IdentityProvider/policies/.gitignore +++ b/src/Saas.Identity/Saas.IdentityProvider/policies/.gitignore @@ -1,2 +1,3 @@ Environments/ -appsettings.json \ No newline at end of file +appsettings.json +.vscode \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230105174952120.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230105174952120.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230105174952120.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230105174952120.png diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230107210713030.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230107210713030.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230107210713030.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230107210713030.png diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230112000806828.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230112000806828.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230112000806828.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230112000806828.png diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230112001205528.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230112001205528.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230112001205528.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230112001205528.png diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230112001210631.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230112001210631.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230112001210631.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230112001210631.png diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230120213644846.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230120213644846.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230120213644846.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230120213644846.png diff --git a/src/Saas.Identity/Saas.Permissions/assets/readme/image-20230125141953381.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230125141953381.png similarity index 100% rename from src/Saas.Identity/Saas.Permissions/assets/readme/image-20230125141953381.png rename to src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230125141953381.png diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230126203229729.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230126203229729.png new file mode 100644 index 00000000..6baa5f75 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230126203229729.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230126230446548.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230126230446548.png new file mode 100644 index 00000000..edf0b38e Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230126230446548.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230127151459605.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230127151459605.png new file mode 100644 index 00000000..4cb41523 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230127151459605.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128153624642.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128153624642.png new file mode 100644 index 00000000..5f87309f Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128153624642.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128153659108.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128153659108.png new file mode 100644 index 00000000..450c03c3 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128153659108.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154233104.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154233104.png new file mode 100644 index 00000000..352c76e8 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154233104.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154523352.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154523352.png new file mode 100644 index 00000000..55903ee8 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154523352.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154735945.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154735945.png new file mode 100644 index 00000000..c2206624 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128154735945.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128160051561.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128160051561.png new file mode 100644 index 00000000..133b55a9 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128160051561.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128161036500.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128161036500.png new file mode 100644 index 00000000..1edf45ba Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128161036500.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128161825544.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128161825544.png new file mode 100644 index 00000000..a30d24d3 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128161825544.png differ diff --git a/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128163318970.png b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128163318970.png new file mode 100644 index 00000000..5154d5d3 Binary files /dev/null and b/src/Saas.Identity/Saas.Permissions/.assets/readme/image-20230128163318970.png differ diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Middleware/ApiKeyMiddleware.cs b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Middleware/ApiKeyMiddleware.cs index 76d5a23d..fefe6ba8 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Middleware/ApiKeyMiddleware.cs +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Middleware/ApiKeyMiddleware.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Options; using Saas.Permissions.Service.Models; -using Saas.Permissions.Service.Options; +using Saas.Shared.Options; namespace Saas.Permissions.Service.Middleware; @@ -8,9 +8,9 @@ public class ApiKeyMiddleware { private readonly RequestDelegate _next; private const string API_KEY = "x-api-key"; - private readonly PermissionApiOptions _permissionOptions; + private readonly PermissionsApiOptions _permissionOptions; - public ApiKeyMiddleware(IOptions permissionOptions, RequestDelegate next) { + public ApiKeyMiddleware(IOptions permissionOptions, RequestDelegate next) { _next = next; _permissionOptions = permissionOptions.Value; } diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/PermissionApiOptions.cs b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/PermissionApiOptions.cs deleted file mode 100644 index efab82d2..00000000 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/PermissionApiOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ClientAssertionWithKeyVault.Interface; - -namespace Saas.Permissions.Service.Options; - -public record PermissionApiOptions -{ - public const string SectionName = "PermissionApi"; - - public string? Audience { get; init; } - public string? ApiKey { get; init; } - public string? ClientId { get; init; } - public Certificate[]? Certificates { get; init; } - public string? TenantId { get; init; } - public string? Domain { get; init; } - -} - -public record Certificate : IKeyInfo -{ - public string? SourceType { get; init; } - public string? KeyVaultUrl { get; init; } - public string? KeyVaultCertificateName { get; init; } -} diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Program.cs b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Program.cs index 34971a1b..4208bae3 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Program.cs +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Program.cs @@ -1,14 +1,15 @@ using Azure.Identity; using Saas.Permissions.Service.Data; using Saas.Permissions.Service.Interfaces; -using Saas.Permissions.Service.Options; +using Saas.Shared.Options; using Saas.Permissions.Service.Services; -using Saas.Permissions.Service.Swagger; +using Saas.Swagger; using ClientAssertionWithKeyVault.Interface; using ClientAssertionWithKeyVault; using Saas.Permissions.Service.Middleware; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Polly; +using System.Reflection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddApplicationInsightsTelemetry(); @@ -20,7 +21,7 @@ builder.Services.AddApplicationInsightsTelemetry(); Instead we're utilizing the Azure App Configuration service for storing settings and the Azure Key Vault to store secrets. Azure App Configuration still hold references to the secret, but not the secret themselves. - This approach is more secure, and allows us to have a single source of truth + This approach is more secure and allows us to have a single source of truth for all settings and secrets. The settings and secrets were provisioned to Azure App Configuration and Azure Key Vault @@ -30,7 +31,12 @@ builder.Services.AddApplicationInsightsTelemetry(); on how to set up and run this service in a local development environment - i.e., a local dev machine. */ -var logger = LoggerFactory.Create(config => config.AddConsole()).CreateLogger("Saas.Permissions.API"); +string projectName = Assembly.GetCallingAssembly().GetName().Name + ?? throw new NullReferenceException("Project name cannot be null"); + +var logger = LoggerFactory.Create(config => config.AddConsole()).CreateLogger(projectName); + +logger.LogInformation("001"); if (builder.Environment.IsDevelopment()) { @@ -43,8 +49,11 @@ else // Add configuration settings data using Options Pattern. // For more see: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0 -builder.Services.Configure( - builder.Configuration.GetRequiredSection(PermissionApiOptions.SectionName)); +builder.Services.Configure( + builder.Configuration.GetRequiredSection(PermissionsApiOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(AzureB2CPermissionsApiOptions.SectionName)); builder.Services.Configure( builder.Configuration.GetRequiredSection(SqlOptions.SectionName)); @@ -140,7 +149,7 @@ void InitializeDevEnvironment() { // IMPORTANT // The current version. - // Must corresspond exactly to the version string of our deployment as specificed in the deployment config.json. + // Must correspond exactly to the version string of our deployment as specificed in the deployment config.json. var version = "ver0.8.0"; logger.LogInformation("Version: {version}", version); @@ -167,18 +176,16 @@ void InitializeDevEnvironment() .ConfigureKeyVault(kv => kv.SetCredential(new ChainedTokenCredential(credential))) .Select(KeyFilter.Any, version)); // <-- Important: since we're using labels in our Azure App Configuration store + // Configuring Swagger. + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + // Enabling to option for add the 'x-api-key' header to swagger UI. builder.Services.AddSwaggerGen(option => { option.SwaggerDoc("v1", new() { Title = "Permissions API", Version = "v1.1" }); option.OperationFilter(); }); - - // Configuring Swagger. - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - } void InitializeProdEnvironment() diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.csproj b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.csproj index 18a6ea5d..86bcc58e 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.csproj +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.csproj @@ -26,11 +26,16 @@ - + + + + + + diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.sln b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.sln index 02f7c419..d16a8ac7 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.sln +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.sln @@ -8,7 +8,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Permissions.Service", {5FF9E406-A16E-48B4-B6E2-E04B64F7BF28} = {5FF9E406-A16E-48B4-B6E2-E04B64F7BF28} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientAssertionWithKeyVault", "..\..\..\Saas.Lib\ClientAssertionWithKeyVault\ClientAssertionWithKeyVault.csproj", "{5FF9E406-A16E-48B4-B6E2-E04B64F7BF28}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientAssertionWithKeyVault", "..\..\..\Saas.Lib\ClientAssertionWithKeyVault\ClientAssertionWithKeyVault.csproj", "{5FF9E406-A16E-48B4-B6E2-E04B64F7BF28}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saas.Shared", "..\..\..\Saas.Lib\Saas.Shared\Saas.Shared.csproj", "{D8A87153-45CB-4212-8231-EA1D0FA71554}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -24,6 +26,10 @@ Global {5FF9E406-A16E-48B4-B6E2-E04B64F7BF28}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FF9E406-A16E-48B4-B6E2-E04B64F7BF28}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FF9E406-A16E-48B4-B6E2-E04B64F7BF28}.Release|Any CPU.Build.0 = Release|Any CPU + {D8A87153-45CB-4212-8231-EA1D0FA71554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8A87153-45CB-4212-8231-EA1D0FA71554}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8A87153-45CB-4212-8231-EA1D0FA71554}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8A87153-45CB-4212-8231-EA1D0FA71554}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphAPIService.cs b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphAPIService.cs index e64c507b..b1badd85 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphAPIService.cs +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphAPIService.cs @@ -3,8 +3,8 @@ using Microsoft.Graph; using Saas.Permissions.Service.Exceptions; using Saas.Permissions.Service.Interfaces; using Saas.Permissions.Service.Models; -using Saas.Permissions.Service.Options; using System.Text; +using Saas.Shared.Options; namespace Saas.Permissions.Service.Services; @@ -19,10 +19,10 @@ public class GraphAPIService : IGraphAPIService "Client Assertion Signing Provider"); private readonly GraphServiceClient _graphServiceClient; - private readonly PermissionApiOptions _permissionOptions; + private readonly AzureB2CPermissionsApiOptions _permissionOptions; public GraphAPIService( - IOptions permissionApiOptions, + IOptions permissionApiOptions, IGraphApiClientFactory graphClientFactory, ILogger logger) { @@ -121,8 +121,8 @@ public class GraphAPIService : IGraphAPIService try { var servicePrincipal = await _graphServiceClient.ServicePrincipals.Request() - .Filter($"appId eq '{clientId}'") - .GetAsync(); + .Filter($"appId eq '{clientId}'") + .GetAsync(); return servicePrincipal.SingleOrDefault(); } diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphClientFactory.cs b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphClientFactory.cs index da776242..3ce127ca 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphClientFactory.cs +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphClientFactory.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Options; using Microsoft.Graph; -using Saas.Permissions.Service.Options; +using Saas.Shared.Options; using Saas.Permissions.Service.Interfaces; namespace Saas.Permissions.Service.Services; @@ -24,7 +24,7 @@ public class GraphApiClientFactory : IGraphApiClientFactory public GraphServiceClient Create() => new(_httpClient, _msGraphOptions.BaseUrl) { - AuthenticationProvider = _authenticationProvider, + AuthenticationProvider = _authenticationProvider }; } diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/KeyVaultSigningCredentialsAuthProvider.cs b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/KeyVaultSigningCredentialsAuthProvider.cs index 3e072ca9..f85bdcd2 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/KeyVaultSigningCredentialsAuthProvider.cs +++ b/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/KeyVaultSigningCredentialsAuthProvider.cs @@ -2,7 +2,7 @@ using Microsoft.Graph; using Microsoft.Identity.Client; using Saas.Permissions.Service.Interfaces; -using Saas.Permissions.Service.Options; +using Saas.Shared.Options; using System.Net.Http.Headers; using ClientAssertionWithKeyVault.Interface; using LogLevel = Microsoft.Extensions.Logging.LogLevel; @@ -25,7 +25,7 @@ public class KeyVaultSigningCredentialsAuthProvider : IAuthenticationProvider public KeyVaultSigningCredentialsAuthProvider( IOptions msGraphOptions, - IOptions permissionApiOptions, + IOptions azureAdB2COptions, IClientAssertionSigningProvider clientAssertionSigningProvider, IKeyVaultCredentialService credentialService, ILogger logger) @@ -34,19 +34,19 @@ public class KeyVaultSigningCredentialsAuthProvider : IAuthenticationProvider _msGraphOptions = msGraphOptions.Value; _clientAssertionSigningProvider = clientAssertionSigningProvider; - if (permissionApiOptions?.Value?.Certificates?[0] is null) + if (azureAdB2COptions?.Value?.ClientCertificates?[0] is null) { logger.LogError("Certificate cannot be null."); throw new NullReferenceException("Certificate cannot be null."); } _msalClient = ConfidentialClientApplicationBuilder - .Create(permissionApiOptions.Value.ClientId) - .WithAuthority(AzureCloudInstance.AzurePublic, permissionApiOptions.Value.TenantId) + .Create(azureAdB2COptions.Value.ClientId) + .WithAuthority(AzureCloudInstance.AzurePublic, azureAdB2COptions.Value.TenantId) .WithClientAssertion( (AssertionRequestOptions options) => _clientAssertionSigningProvider.GetClientAssertion( - permissionApiOptions.Value.Certificates[0], + azureAdB2COptions.Value.ClientCertificates[0], options.TokenEndpoint, options.ClientID, credentialService.GetCredential(), diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/.gitignore b/src/Saas.Identity/Saas.Permissions/deployment/act/.gitignore index a4efab88..9f2eaf0d 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/act/.gitignore +++ b/src/Saas.Identity/Saas.Permissions/deployment/act/.gitignore @@ -1,3 +1,5 @@ # nektos/act .secret -.secrets \ No newline at end of file +.secrets +secret +secrets \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/build.sh b/src/Saas.Identity/Saas.Permissions/deployment/act/build.sh deleted file mode 100644 index d98f9e01..00000000 --- a/src/Saas.Identity/Saas.Permissions/deployment/act/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -repo_base="$( git rev-parse --show-toplevel )" -docker_file_folder="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment/act" - -docker build --file "${docker_file_folder}/Dockerfile" --tag act-container:latest . \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/clean.sh b/src/Saas.Identity/Saas.Permissions/deployment/act/clean.sh new file mode 100644 index 00000000..8fb473d9 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/act/clean.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# repo base +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +host_deployment_dir="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" +container_deployment_dir="/asdk/src/Saas.Identity/Saas.Permissions/deployment" + +# running the './act/script/clean-credentials' script using our ASDK deployment script container - i.e., not the act container +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${REPO_BASE}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${HOME}/asdk/act/.secret":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash /asdk/src/Saas.Lib/Deployment.Script.Modules/clean-credentials.sh + +./setup.sh -s diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/deploy.sh b/src/Saas.Identity/Saas.Permissions/deployment/act/deploy.sh new file mode 100644 index 00000000..be7ba9b1 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/act/deploy.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -u -e -o pipefail + +# shellcheck disable=SC1091 +{ + source "./../constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" +} + +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +host_act_secrets_dir="${HOME}/asdk/act/.secret" +host_deployment_dir="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" +container_deployment_dir="/asdk/src/Saas.Identity/Saas.Permissions/deployment" + +# running the './act/script/patch-app-name.sh' script using our ASDK deployment script container - i.e., not the act container +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${host_deployment_dir}/act/workflows/":"${container_deployment_dir}/act/workflows" \ + --volume "${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${REPO_BASE}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${host_act_secrets_dir}":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash /asdk/src/Saas.Lib/Deployment.Script.Modules/deploy-debug.sh + +# run act container to run github action locally, using local workflow file and local code base. +gh act workflow_dispatch \ + --rm \ + --bind \ + --pull=false \ + --secret-file "${host_act_secrets_dir}/secret" \ + --directory "${REPO_BASE}" \ + --workflows "${ACT_LOCAL_WORKFLOW_DEBUG_FILE}" \ + --platform "ubuntu-latest=${ACT_CONTAINER_NAME}" diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/run.sh b/src/Saas.Identity/Saas.Permissions/deployment/act/run.sh deleted file mode 100644 index 774222a6..00000000 --- a/src/Saas.Identity/Saas.Permissions/deployment/act/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -gh act workflow_dispatch --secret-file .secrets -W ./workflows/permissions-api-deploy-debug.yml -P ubuntu-latest=act-container:latest \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/setup.sh b/src/Saas.Identity/Saas.Permissions/deployment/act/setup.sh index 212c4ba2..4c7ceeb4 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/act/setup.sh +++ b/src/Saas.Identity/Saas.Permissions/deployment/act/setup.sh @@ -1 +1,39 @@ -#!/usr/bin/env bash \ No newline at end of file +#!/usr/bin/env bash + +skip_docker_build=false +force_update=false + +while getopts 'sf' flag; do + case "${flag}" in + s) skip_docker_build=true ;; + f) force_update=true ;; + *) skip_docker_build=false ;; + esac +done + +# shellcheck disable=SC1091 +source "../constants.sh" + +echo "Setting up the SaaS Permissions Serivce API Act deployment environment." +echo "Settings execute permissions on necessary scripts files." + +sudo mkdir -p "${ACT_SECRETS_DIR}" + +sudo chmod +x ${ACT_DIR}/*.sh +sudo chmod +x ${SCRIPT_DIR}/*.sh >/dev/null 2>&1 +sudo touch ${ACT_SECRETS_FILE} +sudo chown "${USER}" ${ACT_SECRETS_FILE} +sudo touch ${ACT_SECRETS_FILE_RG} +sudo chown "${USER}" ${ACT_SECRETS_FILE_RG} + +if [ "${skip_docker_build}" = false ]; then + echo "Building the deployment container." + + if [[ "${force_update}" == false ]]; then + "${ACT_CONTAINER_DIR}"/build.sh -n "${ACT_CONTAINER_NAME}" + else + "${ACT_CONTAINER_DIR}"/build.sh -n "${ACT_CONTAINER_NAME}" -f + fi +fi + +echo "SaaS Permissions Service API Act environment setup complete. You can now run the local deployment script using the command './deploy.sh'." diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/workflows/permissions-api-deploy-debug.yml b/src/Saas.Identity/Saas.Permissions/deployment/act/workflows/permissions-api-deploy-debug.yml index 101d3ac6..28a9694a 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/act/workflows/permissions-api-deploy-debug.yml +++ b/src/Saas.Identity/Saas.Permissions/deployment/act/workflows/permissions-api-deploy-debug.yml @@ -1,42 +1,44 @@ +--- name: Deploy Permission API to Azure Web Services on: workflow_dispatch: - # inputs: - # logLevel: - # description: 'Log level' - # required: true - # default: 'warning' - # tags: - # description: 'Test scenario tags' permissions: id-token: write contents: read env: - AZURE_WEBAPP_NAME: 'api-permission-asdk-test-b3yf' # set this to your application's name + APP_NAME: permissions-api + AZURE_WEBAPP_NAME: api-permission-asdk-test-fd4k # set this to your application's name AZURE_WEBAPP_PACKAGE_PATH: "." # set this to the path to your web app project, defaults to the repository root DOTNET_VERSION: 7.x.x PROJECT_DIR: ./src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1 - PROJECT_PATH: ./src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Saas.Permissions.Service.csproj - OUTPUT_PATH: ./publish + PROJECT_PATH: ${{ env.PROJECT_DIR }}/Saas.Permissions.Service.csproj + PUBLISH_PATH: ./publish + OUTPUT_PATH: ${{ env.PUBLISH_PATH }}/${{ env.APP_NAME }}/package + SYMBOLS_PATH: ${{ env.PUBLISH_PATH }}/symbols + BUILD_CONFIGURATION: Debug # setting the configuration manager build configuration value for our workflow. jobs: build-and-deploy: runs-on: ubuntu-latest steps: +################################################## +# this section is specific for local deployment. # +################################################## # Azure login - uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - - # checkout the repo specifying the branch name in 'ref:' - - uses: actions/checkout@v3 - with: - ref: dev #IMPORTANT we're checking out and deploying the 'dev' branch here, not 'main'. - token: ${{ secrets.GITHUB_TOKEN }} + + # checkout the _local_ repository + - name: Checkout + uses: actions/checkout@v3 +################################################# +# end of local deployment specific section. # +################################################# # Setup .NET Core SDK - name: Setup .NET Core @@ -50,19 +52,31 @@ jobs: dotnet restore ${{ env.PROJECT_DIR }} dotnet build ${{ env.PROJECT_PATH }} \ - --configuration Debug + --configuration ${{ env.BUILD_CONFIGURATION }} dotnet publish ${{ env.PROJECT_PATH }} \ - --configuration Debug \ - --output '${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }}' + --configuration ${{ env.BUILD_CONFIGURATION }} \ + --output ${{ env.OUTPUT_PATH }} # Deploy to Azure Web apps - name: "Run Azure webapp deploy action using publish profile credentials" uses: azure/webapps-deploy@v2 with: app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name - package: ${{ env.OUTPUT_PATH }}/{{ env.AZURE_WEBAPP_NAME }} + package: ${{ env.OUTPUT_PATH }} # slot-name: 'PermissionsApi-Staging' + ###################### + # *** Debug only *** # + ###################### + # Copy symbols files (*.pdb)) to local publish folder # rm -rf ${{ env.OUTPUT_PATH }}/${{ env.AZURE_WEBAPP_NAME }} + - name: copy symbols files (*.pdb)) to local publish folder + run: | + mkdir -p ${{ env.PUBLISH_PATH }}/symbols + echo "Copying symbols files to '${{ env.SYMBOLS_PATH }}'" + cp -r ${{ env.OUTPUT_PATH }}/*.pdb ${{ env.SYMBOLS_PATH }} + ###################### + # *** End *** # + ###################### # Azure logout - name: logout diff --git a/src/Saas.Identity/Saas.Permissions/deployment/bicep/Parameters/.gitignore b/src/Saas.Identity/Saas.Permissions/deployment/bicep/Parameters/.gitignore new file mode 100644 index 00000000..39a81f30 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/bicep/Parameters/.gitignore @@ -0,0 +1,2 @@ +*-parameters.json +identity-foundation-outputs.json \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/bicep/Parameters/parameters-template.json b/src/Saas.Identity/Saas.Permissions/deployment/bicep/Parameters/parameters-template.json new file mode 100644 index 00000000..d90c44f3 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/bicep/Parameters/parameters-template.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/src/Saas.Identity/Saas.Permissions/deployment/bicep/deployAppService.bicep b/src/Saas.Identity/Saas.Permissions/deployment/bicep/deployAppService.bicep new file mode 100644 index 00000000..86ce4db7 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/bicep/deployAppService.bicep @@ -0,0 +1,51 @@ +@description('The SaaS Permission API.') +param permissionsapi string + +@description('Version') +param version string + +@description('Environment') +@allowed([ + 'Development' + 'Staging' + 'Production' +]) +param environment string + +@description('The App Service Plan ID.') +param appServicePlanName string + +@description('The Uri of the Key Vault.') +param keyVaultUri string + +@description('The location for all resources.') +param location string + +@description('Azure App Configuration User Assigned Identity Name.') +param userAssignedIdentityName string + +@description('The name of the Azure App Configuration.') +param appConfigurationName string + +@description('The name of the Log Analytics Workspace used by Application Insigths.') +param logAnalyticsWorkspaceName string + +@description('The name of Application Insights.') +param applicationInsightsName string + +module signupAdministrationWebApp './../../../../Saas.Lib/Saas.Bicep.Module/appServiceModuleWithObservability.bicep' = { + name: 'PermissionApi' + params: { + appServiceName: permissionsapi + version: version + environment: environment + appServicePlanName: appServicePlanName + keyVaultUri: keyVaultUri + location: location + userAssignedIdentityName: userAssignedIdentityName + appConfigurationName: appConfigurationName + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + applicationInsightsName: applicationInsightsName + } +} + diff --git a/src/Saas.Identity/Saas.Permissions/deployment/bicep/deployConfigEntries.bicep b/src/Saas.Identity/Saas.Permissions/deployment/bicep/deployConfigEntries.bicep new file mode 100644 index 00000000..9f79bfd3 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/bicep/deployConfigEntries.bicep @@ -0,0 +1,133 @@ +@description('Version') +param version string + +@description('The name of the key vault') +param keyVaultName string + +@description('The URI of the key vault.') +param keyVaultUri string + +@description('Azure B2C Domain Name.') +param azureB2CDomain string + +@description('Azure B2C Tenant Id.') +param azureB2cTenantId string + +@description('Azure AD Instance') +param azureAdInstance string + +@description('The Azure B2C Signed Out Call Back Path.') +param signedOutCallBackPath string + +@description('The Azure B2C Sign up/in Policy Id.') +param signUpSignInPolicyId string + +@description('The Azure B2C Permissions API base Url.') +param baseUrl string + +@description('The Client Id found on registered Permissions API app page.') +param clientId string + +@description('User Identity Name') +param userAssignedIdentityName string + +@description('App Configuration Name') +param appConfigurationName string + +@description('The name of the certificate key.') +param certificateKeyName string + +// Create object with array of objects containing the kayname and value to be stored in Azure App Configuration store. + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { + name: userAssignedIdentityName +} + +var azureB2CKeyName = 'AzureB2C' +var permissionsApiKeyName = 'permissionsApi' + +var certificates = [ + { + SourceType: keyVaultName + KeyVaultUrl: keyVaultUri + KeyVaultCertificateName: certificateKeyName + } +] + +var appConfigStore = { + appConfigurationName: appConfigurationName + keyVaultName: keyVaultName + userAssignedIdentityName: userAssignedIdentity.name + label: version + entries: [ + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:ClientCertificates' + value: ' ${string(certificates)}' // notice the space before the string, this is a necessary hack. https://github.com/Azure/bicep/issues/6167 + isSecret: false + contentType: 'application/json' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:BaseUrl' + value: baseUrl + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:ClientId' + value: clientId + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:TenantId' + value: azureB2cTenantId + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:Domain' + value: azureB2CDomain + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:Instance' + value: azureAdInstance + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:Audience' + value: clientId + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:SignedOutCallbackPath' + value: signedOutCallBackPath + isSecret: false + contentType: 'text/plain' + } + { + key: '${permissionsApiKeyName}:${azureB2CKeyName}:SignUpSignInPolicyId' + value: signUpSignInPolicyId + isSecret: false + contentType: 'text/plain' + } + ] +} + +// Adding App Configuration entries +module appConfigurationSettings './../../../../Saas.Lib/Saas.Bicep.Module/addConfigEntry.bicep' = [ for entry in appConfigStore.entries: { + name: replace('Entry-${entry.key}', ':', '-') + params: { + appConfigurationName: appConfigStore.appConfigurationName + userAssignedIdentityName: appConfigStore.userAssignedIdentityName + keyVaultName: keyVaultName + value: entry.value + contentType: entry.contentType + keyName: entry.key + label: appConfigStore.label + isSecret: entry.isSecret + } +}] diff --git a/src/Saas.Identity/Saas.Permissions/deployment/build.sh b/src/Saas.Identity/Saas.Permissions/deployment/build.sh index 5cf4edc5..69bc6a5a 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/build.sh +++ b/src/Saas.Identity/Saas.Permissions/deployment/build.sh @@ -1,7 +1,22 @@ #!/usr/bin/env bash +force_update=false + +while getopts f flag +do + case "${flag}" in + f) force_update=true;; + *) force_update=false;; + esac +done + repo_base="$( git rev-parse --show-toplevel )" docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + # redirect to build.sh in the Deployment.Container folder -"${docker_file_folder}/build.sh" \ No newline at end of file +if [[ "${force_update}" == false ]]; then + "${docker_file_folder}/build.sh" +else + "${docker_file_folder}/build.sh" -f +fi diff --git a/src/Saas.Identity/Saas.Permissions/deployment/constants.sh b/src/Saas.Identity/Saas.Permissions/deployment/constants.sh index 1a763814..1b1848ec 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/constants.sh +++ b/src/Saas.Identity/Saas.Permissions/deployment/constants.sh @@ -3,23 +3,16 @@ # disable unused variable warning https://www.shellcheck.net/wiki/SC2034 # shellcheck disable=SC2034 -# user directories -BASE_AZURE_CONFIG_DIR="$HOME/.azure" -B2C_USR_AZURE_CONFIG_DIR="${HOME}/b2c/.azure" -SP_USR_AZURE_CONFIG_DIR="${HOME}/sp/.azure" +# app naming +APP_NAME="permissions-api" +APP_DEPLOYMENT_NAME="permissionApi" # repo base -repo_base="$( git rev-parse --show-toplevel )" +repo_base="$(git rev-parse --show-toplevel)" REPO_BASE="${repo_base}" -WORKFLOW_BASE="${REPO_BASE}/.github/workflows" -PERMISSIONS_DEPLOYMENT_WORKFLOW="${WORKFLOW_BASE}/permissions-api-deploy.yml" - -# script directories -BASE_DIR="${ASDK_PERMISSIONS_API_DEPLOYMENT_BASE_DIR}" - -# global script directory -SHARED_MODULE_DIR="${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules" +# project base directory +BASE_DIR="${REPO_BASE}/src/Saas.Identity/Saas.Permissions/deployment" # local script directory SCRIPT_DIR="${BASE_DIR}/script" @@ -27,6 +20,16 @@ SCRIPT_DIR="${BASE_DIR}/script" #local log directory LOG_FILE_DIR="${BASE_DIR}/log" -# configuration manifest for the Identity Foundation deployment, run previously -CONFIG_DIR="${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config" -CONFIG_FILE="${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/config.json" \ No newline at end of file +# act directory +ACT_DIR="${BASE_DIR}/act" + +# GitHub workflows +WORKFLOW_BASE="${REPO_BASE}/.github/workflows" +GITHUB_ACTION_WORKFLOW_FILE="${WORKFLOW_BASE}/permissions-api-deploy.yml" +ACT_LOCAL_WORKFLOW_DEBUG_FILE="${ACT_DIR}/workflows/permissions-api-deploy-debug.yml" + +# global script directory +SHARED_MODULE_DIR="${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules" + +# adding app service global constants +source "${SHARED_MODULE_DIR}/app-service-constants.sh" diff --git a/src/Saas.Identity/Saas.Permissions/deployment/github-action/.gitignore b/src/Saas.Identity/Saas.Permissions/deployment/github-action/.gitignore deleted file mode 100644 index 07a83524..00000000 --- a/src/Saas.Identity/Saas.Permissions/deployment/github-action/.gitignore +++ /dev/null @@ -1 +0,0 @@ -permissions-api.yml \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/github-action/permissions-api-template.yml b/src/Saas.Identity/Saas.Permissions/deployment/github-action/permissions-api-template.yml deleted file mode 100644 index 28d3f793..00000000 --- a/src/Saas.Identity/Saas.Permissions/deployment/github-action/permissions-api-template.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Deploy Permission API to Azure Web Services - -on: - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - tags: - description: 'Test scenario tags' - -permissions: - id-token: write - contents: read - -env: - AZURE_WEBAPP_NAME: my-app # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NUGET_VERSION: '6.x.x' # set this to the dot net version to use - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - # checkout the repo - - uses: actions/checkout@main - - - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Install Nuget - uses: nuget/setup-nuget@v1 - with: - nuget-version: ${{ env.NUGET_VERSION}} - - name: NuGet to restore dependencies as well as project-specific tools that are specified in the project file - run: nuget restore - - - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 - - - name: Run MSBuild - run: msbuild .\SampleWebApplication.sln - - - name: 'Run Azure webapp deploy action using publish profile credentials' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name - package: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}/SampleWebApplication/' - - # Azure logout - - name: logout - run: | - az logout \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/run.sh b/src/Saas.Identity/Saas.Permissions/deployment/run.sh index 632c800b..c1981ccc 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/run.sh +++ b/src/Saas.Identity/Saas.Permissions/deployment/run.sh @@ -1,33 +1,29 @@ #!/usr/bin/env bash -repo_base="$( git rev-parse --show-toplevel )" -git_repo_origin="$( git config --get remote.origin.url )" -git_org_project_name="$( git config --get remote.origin.url | sed 's/.*\/\([^ ]*\/[^.]*\).*/\1/' )" -gh_auth_token="$( gh auth token )" +source "./constants.sh" -if [[ -z "${gh_auth_token}" ]]; then - echo "You are not loggged into your GitHub organization. GitHub auth token is not set and/or you haven't installed GitHub Cli." - echo "Please make sure that GitHub Cli is installed and then run 'gh auth login', before running this script again." - echo "See readme.md for more info." - exit 0 -fi +repo_base="$(git rev-parse --show-toplevel)" +host_act_secrets_dir="${HOME}/asdk/act/.secret" -# using volumes '--volume' to mount only the needed directories to the container. +host_deployment_dir="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" +container_deployment_dir="/asdk/src/Saas.Identity/Saas.Permissions/deployment" + +# using volumes '--volume' to mount only the needed directories to the container. # using ':ro' to make scrip directories etc. read-only. Only config and log directories are writable. docker run \ --interactive \ --tty \ --rm \ - --volume "${repo_base}/src/Saas.Identity/Saas.Permissions/deployment/":/asdk/src/Saas.Identity/Saas.Permissions/deployment:ro \ - --volume "${repo_base}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${host_deployment_dir}/log":"${container_deployment_dir}/log" \ + --volume "${host_deployment_dir}/Bicep/Parameters":"${container_deployment_dir}"/Bicep/Parameters \ --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${repo_base}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${repo_base}/src/Saas.Lib/Saas.Bicep.Module":/asdk/src/Saas.Lib/Saas.Bicep.Module:ro \ --volume "${repo_base}/.github/workflows":/asdk/.github/workflows \ --volume "${repo_base}/.git/":/asdk/.git:ro \ --volume "${HOME}/.azure/":/asdk/.azure:ro \ - --volume "${HOME}/asdk/.cache/":/asdk/.cache \ - --env "ASDK_PERMISSIONS_API_DEPLOYMENT_BASE_DIR=/asdk/src/Saas.Identity/Saas.Permissions/deployment" \ - --env "GIT_REPO_ORIGIN=${git_repo_origin}" \ - --env "GIT_ORG_PROJECT_NAME=${git_org_project_name}" \ - --env "GITHUB_AUTH_TOKEN=${gh_auth_token}" \ - asdk-script-deployment:latest \ - bash /asdk/src/Saas.Identity/Saas.Permissions/deployment/start.sh + --volume "${host_act_secrets_dir}":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash ${container_deployment_dir}/start.sh diff --git a/src/Saas.Identity/Saas.Permissions/deployment/script/add-github-env-variables.sh b/src/Saas.Identity/Saas.Permissions/deployment/script/add-github-env-variables.sh deleted file mode 100644 index a167e94e..00000000 --- a/src/Saas.Identity/Saas.Permissions/deployment/script/add-github-env-variables.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -u -e -o pipefail - -# shellcheck disable=SC1091 -{ - # include script modules into current shell - source "${ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE}/constants.sh" - source "$SHARED_MODULE_DIR/log-module.sh" - source "$SHARED_MODULE_DIR/config-module.sh" - source "$SHARED_MODULE_DIR/github-module.sh" -} \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/script/clean-credentials.sh b/src/Saas.Identity/Saas.Permissions/deployment/script/clean-credentials.sh new file mode 100644 index 00000000..7c814aee --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/script/clean-credentials.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# repo base +repo_base="$( git rev-parse --show-toplevel )" +base_dir="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" + +# shellcheck disable=SC1091 +{ + source "${base_dir}/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" +} + +# initialize az cli +initialize-az-cli "$HOME/.azure" + +sudo rm "${ACT_SECRETS_FILE}" 2> /dev/null + +oidc_app_id="$( get-value ".oidc.appId" )" +oidc_app_name="$( get-value ".oidc.name" )" +oidc_app_name="secret-${oidc_app_name}" + +echo "Deleting the secret based credentials for the OIDC app '${oidc_app_name}'..." +key_id="$( az ad app credential list \ + --id "${oidc_app_id}" \ + --query "[?displayName=='${oidc_app_name}'].keyId | [0]" \ + --output tsv )" + +if [[ -n $key_id ]]; then + az ad app credential delete \ + --id "${oidc_app_id}" \ + --key-id "${key_id}" + + echo "Secret based credentials deleted." +else + echo "No secret based credentials found." +fi \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/script/deploy-debug.sh b/src/Saas.Identity/Saas.Permissions/deployment/script/deploy-debug.sh new file mode 100644 index 00000000..16afb786 --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/script/deploy-debug.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# repo base +repo_base="$( git rev-parse --show-toplevel )" +base_dir="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" + +# shellcheck disable=SC1091 +{ + source "$base_dir/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" +} + +"${SCRIPT_DIR}"/patch-app-name.sh + +# using the az cli settings and cache from the host machine +initialize-az-cli "$HOME/.azure" + +declare secret + +# Creating a new secret for accessing the OIDC app access deployment. +if [[ ! -f "${ACT_SECRETS_FILE}" || ! -s "${ACT_SECRETS_FILE}" ]]; then + oidc_app_name="$( get-value ".oidc.name" )" + oidc_app_id="$( get-value ".oidc.appId" )" + + echo "Creating the OIDC app '${oidc_app_name}'" + + touch "${ACT_SECRETS_FILE}" + + # getting app crentials + secret_json="$( az ad app credential reset \ + --id "${oidc_app_id}" \ + --display-name "secret-${oidc_app_name}" \ + --query "{clientId:appId, clientSecret:password, tenantId:tenant}" \ + 2> /dev/null )" + + subscription_id="$( get-value ".initConfig.subscriptionId" )" + + # adding subscription id to secret json + secret_json="$( jq --arg sub_id "${subscription_id}" \ + '. | .subscriptionId = $sub_id' \ + <<< "${secret_json}" )" + + # outputting secret json to file in compat format with escape double quotes + secret="$( echo "${secret_json}" \ + | jq --compact-output '.' \ + | sed 's/"/\\"/g' )" + + secret="AZURE_CREDENTIALS=\"${secret}\"" + + echo "${secret}" > "${ACT_SECRETS_FILE}" + + echo "Waiting for 30 seconds to allow new secret to propagate." + sleep 30 +else + echo "Using existing secrets file at: '${ACT_SECRETS_FILE}'. If you want to create a new one, please run '/.clean.sh' again and then run './run.sh'." +fi \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/script/map-to-config-entries-parameters.py b/src/Saas.Identity/Saas.Permissions/deployment/script/map-to-config-entries-parameters.py new file mode 100644 index 00000000..bd5a353a --- /dev/null +++ b/src/Saas.Identity/Saas.Permissions/deployment/script/map-to-config-entries-parameters.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +import json +import sys +import re + +def get_b2c_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['azureb2c'][key] + + return { + keyName: { + 'value': value + } + } + +def get_claimTransformer_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['claimToRoleTransformer'][key] + return { + keyName: { + 'value': value + } + } + +def get_deploy_b2c_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['deployment']['azureb2c'][key] + return { + keyName: { + 'value': value + } + } + +def get_app_value( + config: dict, + app_name: str, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + for app in config['appRegistrations']: + if app['name'] == app_name: + return { + keyName: { + 'value': app[key] + } + } + +def get_app_scopes( + config: dict, + app_name: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + scopes = [] + + for app in config['appRegistrations']: + if app['name'] == app_name: + for scope in app['scopes']: + scopes.append(scope['name']) + + return { + keyName: { + 'value': json.dumps(scopes) + } + } + +def get_output_value(outputs: dict, output_name: str) -> 'dict[str, dict[str, str]]': + item = outputs[output_name] + if item : return { + output_name : { + 'value': item['value'] + } + } + +def patch_paramenters_file( + app_name: str, + identity_outputs: str, + paramenter_file: str, + config_file: str) -> None: + + with open(config_file, 'r') as f: + config = json.load(f) + + with open(identity_outputs, 'r') as f: + identity_outputs = json.load(f) + + with open(paramenter_file, 'r') as f: + parameters = json.load(f) + + parameters['parameters'].update(get_output_value(identity_outputs, 'version')) + parameters['parameters'].update(get_output_value(identity_outputs, 'keyVaultName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'keyVaultUri')) + + parameters['parameters'].update(get_output_value(identity_outputs, 'userAssignedIdentityName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'appConfigurationName')) + + parameters['parameters'].update(get_deploy_b2c_value(config, 'domainName', 'azureB2CDomain')) + parameters['parameters'].update(get_deploy_b2c_value(config, 'tenantId', 'azureB2cTenantId')) + parameters['parameters'].update(get_deploy_b2c_value(config, 'instance', 'azureAdInstance')) + + parameters['parameters'].update(get_b2c_value(config, 'signedOutCallBackPath', 'signedOutCallBackPath')) + parameters['parameters'].update(get_b2c_value(config, 'signUpSignInPolicyId', 'signUpSignInPolicyId')) + + parameters['parameters'].update(get_app_value(config, app_name, 'appId', 'clientId')) + parameters['parameters'].update(get_app_value(config, app_name, 'baseUrl', 'baseUrl')) + + parameters['parameters'].update(get_app_value(config, app_name, 'certificateKeyName', 'certificateKeyName')) + + with open(paramenter_file, 'w') as f: + f.write(json.dumps(parameters, indent=4)) + +# Main entry point for the script +if __name__ == "__main__": + app_name = sys.argv[1] + identity_outputs = sys.argv[2] + paramenter_file = sys.argv[3] + config_file = sys.argv[4] + + patch_paramenters_file(app_name, identity_outputs, paramenter_file, config_file) \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/setup.sh b/src/Saas.Identity/Saas.Permissions/deployment/setup.sh index f2896202..6f3e5ce1 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/setup.sh +++ b/src/Saas.Identity/Saas.Permissions/deployment/setup.sh @@ -6,9 +6,47 @@ source "./constants.sh" echo "Setting up the deployment environment." echo "Settings execute permissions on necessary scripts files." -sudo chmod +x ./*.sh -sudo chmod +x ./script/*.sh -sudo chmod +x ./script/*.py +( + sudo chmod +x ./*.sh + sudo chmod +x ./script/*.sh >/dev/null 2>&1 + sudo chmod +x ./script/*.py +) || + { + echo "Failed to set execute permissions on the necessary scripts." + exit 1 + } + +repo_base="$(git rev-parse --show-toplevel)" || + { + echo "Failed to get the root of the repository." + exit 1 + } + +docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + +# redirect to build.sh in the Deployment.Container folder +sudo chmod +x "${docker_file_folder}/build.sh" || + { + echo "Failed to set execute permissions on the 'build.sh' script." + exit 1 + } + +echo "Building the deployment container." +./build.sh || + { + echo "Failed to build the deployment container. Please ensure that Docker is installed and running." + exit 1 + } + +( + echo "Setting up log folder..." + mkdir -p "$LOG_FILE_DIR" + sudo chown "${USER}" "$LOG_FILE_DIR" +) || + { + echo "Failed to set up log folder." + exit 1 + } echo -echo "Setup complete. You can now run the deployment script using the command './run.sh'." \ No newline at end of file +echo "Setup complete. You can now run the deployment script using the command './run.sh'." diff --git a/src/Saas.Identity/Saas.Permissions/deployment/start.sh b/src/Saas.Identity/Saas.Permissions/deployment/start.sh index 187c5b79..c17f72f4 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/start.sh +++ b/src/Saas.Identity/Saas.Permissions/deployment/start.sh @@ -1,27 +1,63 @@ #!/usr/bin/env bash -export ASDK_CACHE_AZ_CLI_SESSIONS=true - -if [[ -z $ASDK_PERMISSIONS_API_DEPLOYMENT_BASE_DIR ]]; then - # repo base - echo "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE is not set. Setting it to default value." - repo_base="$( git rev-parse --show-toplevel )" - project_base="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" - export ASDK_PERMISSIONS_API_DEPLOYMENT_BASE_DIR="${project_base}" -fi - -if [[ -f $CONFIG_FILE ]]; then - echo "The ASDK Identity Foundation has not completed. Please run the Identity Foundation deployment script first." +# if not running in a container +if ! [ -f /.dockerenv ]; then + echo "Running outside of a container us not supported. Please run the script using './run.sh'." exit 0 fi +# shellcheck disable=SC1091 +{ + source "${ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE}/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/log-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" +} + # set bash options to exit on unset variables and errors (exit 1) including pipefail set -u -e -o pipefail -# shellcheck disable=SC1091 -# include script modules into current shell -{ - source "$ASDK_PERMISSIONS_API_DEPLOYMENT_BASE_DIR/constants.sh" -} +if ! [[ -f $CONFIG_FILE ]]; then + echo "The ASDK Identity Foundation has not completed or 'config.json' file from it's deployment is missing. Please run the Identity Foundation deployment script first." + exit 0 +fi -"${SCRIPT_DIR}/patch-workflow.py" "${CONFIG_FILE}" "${PERMISSIONS_DEPLOYMENT_WORKFLOW}" \ No newline at end of file +# get now date and time for backup file name +now=$(date '+%Y-%m-%d--%H-%M-%S') + +# set run time for deployment script instance +export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" + +# using the az cli settings and cache from the host machine +initialize-az-cli "$HOME/.azure" + +echo "Provisioning the SaaS Administration Service API..." | + log-output \ + --level info \ + --header "SaaS Administration Service API" + +"${SHARED_MODULE_DIR}/"deploy-app-service.sh + +"${SHARED_MODULE_DIR}/"deploy-config-entries.sh + +echo "Patching '${APP_NAME}' GitHub Action workflow file." | + log-output \ + --level info \ + --header "SaaS Administration Service API" + +"${SHARED_MODULE_DIR}/patch-github-workflow.py" \ + "${APP_NAME}" \ + "${CONFIG_FILE}" \ + "${GITHUB_ACTION_WORKFLOW_FILE}" || + echo "Failed to patch ${APP_NAME} GitHub Action workflow file" | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 + +git_repo_origin="$(git config --get remote.origin.url)" + +echo "'${APP_NAME}' is ready to be deployed. You have two options:" +echo " a) To deploy to production, use the GitHub Action: ${git_repo_origin::-4}/actions" +echo +echo " b) To deploy for live debugging in Azure; navigate to the act directory ('cd act') and run './setup.sh' and then run './deploy.sh' to deploy for remote debugging." diff --git a/src/Saas.Identity/Saas.Permissions/readme.md b/src/Saas.Identity/Saas.Permissions/readme.md index d6aaed4e..56a25494 100644 --- a/src/Saas.Identity/Saas.Permissions/readme.md +++ b/src/Saas.Identity/Saas.Permissions/readme.md @@ -1,15 +1,43 @@ # SaaS Permissions Service API +The SaaS Permissions Service API is a core component of the authorization system for the Azure SaaS Dev Kit. The API handles role-based authorization of user. + +Once deployed, this service is a web API exposing endpoints to perform CRUD operations on user permission settings. + ## Overview -Section contains the ASDK Permissions Service API for handling role-based authorization of user. The service [depends](https://azure.github.io/azure-saas/components/identity/permissions-service#dependencies) on the Identity Foundation that was deployed a spart of the Identity Foundation and on the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). +Within this folder you will find two subfolders: -For a complete overview, please see the [SaaS Permissions Service](https://azure.github.io/azure-saas/components/identity/permissions-service/) page in the documentation site. +- **Saas.Permissions.Service** - the C# project for the API +- **deployment** - a set of tools for deploying the API for production + - The sub-subfolder **[act](./deployment/act)** is for deploying the API for remote debugging + +## Dependencies + +The service depends on: + +- The **Identity Foundation** that was deployed a spart of the Identity Foundation and on the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). +- The [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview). + +## Provisioning the API + +To work with the SaaS Permissions API it must first be provisions to your Azure ASDK resource group. This is true even if you initially is planning to run the API in your local development environment. The provisioning ensure that configuration and settings to be correctly added to your Azure App Configuration store and readies the API for later deployment to Azure. + +Provisioning is easy: + +1. Navigate to the sub folder `deployment`. + +2. Run these commands: + ```bash + sudo chmod +x setup.sh + ./setup.sh + ./run.sh + ``` + +Now you're ready to move on. ## How to Run Locally -Once deployed, this service is a web API exposing endpoints to perform CRUD operations on user permission settings. - The SaaS Permissions Service API can be run locally during development, testing and learning. ### Requirements @@ -19,12 +47,13 @@ To run the API locally, you must have the following installed on your developer - [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (recommended) or [Visual Studio Code](https://code.visualstudio.com/download). - [.NET 7.0](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) - [ASP.NET Core 7.0](https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-7.0) +- [GitHub’s official command line tool (**gh**)](https://cli.github.com/). For more on installation see [here](https://github.com/cli/cli#installation). -> Tip: .NET 7.0 and ASP.NET Core 7.0 can be installed as part of the latest version Microsoft Visual Studio 2022. +> *Tip*: .NET 7.0 and ASP.NET Core 7.0 can also be installed as part of the latest version Microsoft Visual Studio 2022. You will also need a deployed instance of the [Identity Framework](https://azure.github.io/azure-saas/quick-start/). For details visit the [Deploying the Identify Foundation Services readme](../Saas.Identity.Provider/readme.md). -### App Configuration and Settings +### Configuration, settings and secrets when running locally To manage settings securely and efficiently, settings are being stored in [Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview), while secrets and certificates are being stored in [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview). Furthermore, secrets are represented with a reference (an URI) in Azure App Configuration pointing to the actual secret, which is kept safely and securely in Azure Key Vault. @@ -36,7 +65,7 @@ For running the SaaS Permission API Service in a local development environment, #### Access and Permissions to Azure Key Vault -![image-20230125141953381](assets/readme/image-20230125141953381.png) +![image-20230125141953381](.assets/readme/image-20230125141953381.png) For accessing Azure Key Vault, we will rely on [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) to provide the needed access token. For this to work, you should open a terminal from within Visual Studio (or Visual Studio Code) and run these commands: @@ -77,7 +106,7 @@ az appconfig credential list --name " --qu In the Azure Portal you can find the connection string here: -![image-20230105174952120](assets/readme/image-20230105174952120.png) +![image-20230105174952120](.assets/readme/image-20230105174952120.png) 2. To add the `connection string` to the Secret Manager, run these commands in a terminal in the root directory of the project: @@ -86,7 +115,7 @@ dotnet user-secrets init #initialized your Secret Manager for the project. dotnet user-secrets set ConnectionStrings:AppConfig "" ``` -> Tip: For more details on connecting a local development environment to Azure App Configuration please see: [Connect to the App Configuration store](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-aspnet-core-app?tabs=core6x#connect-to-the-app-configuration-store). +> *Tip*: For more details on connecting a local development environment to Azure App Configuration please see: [Connect to the App Configuration store](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-aspnet-core-app?tabs=core6x#connect-to-the-app-configuration-store). ### Accessing the Azure SQL Server data from your developer environment @@ -96,8 +125,7 @@ During the deployment of the Identity Foundation, the deployment script takes no Adding your public IP address is essential for your local development environment to be able to run. By default the configuration of the SQL Server only allow network access from IP addresses of services running *inside* the Azure environment. This default network security setting is great for production, however since your local development environment is not very likely to be running from within the Azure environment, this firewall restriction gets in the way. -> Tip: You may want to work on you project from multiple locations and development environments, in which case you will need to make changes to the firewall rules of Azure SQL Server, allowing these additional public IPs to access the database. -> +> *Tip*: You may want to work on you project from multiple locations and development environments, in which case you will need to make changes to the firewall rules of Azure SQL Server, allowing these additional public IPs to access the database. To add additional public IP addresses to the Azure SQL Service firewall rule, please do the following: @@ -109,53 +137,195 @@ dig +short myip.opendns.com @resolver1.opendns.com 2. Visit the Azure portal and add the global IP address of the computing you are running the Permission Service from. -![image-20230107210713030](assets/readme/image-20230107210713030.png) +![image-20230107210713030](.assets/readme/image-20230107210713030.png) -## Running the Permissions Service API, Locally +## Running the SaaS Permissions Service API Locally After all of the above have been set up, you're now ready to build and run the SaaS Permissions Services in your local development environment. As you press debug/run, a browser will open and load a Swagger Page: -> Tip: Swagger is only enabled when the API is running locally. You'll find the details in `program.cs`. +> *Tip*: Swagger is only enabled when the API is running locally. You'll find the details in `program.cs`. -![image-20230112000806828](assets/readme/image-20230112000806828.png) +![image-20230112000806828](.assets/readme/image-20230112000806828.png) Now *try it out* by running `GET /api/Permissions/GetTenantUsers` API. The first time you execute the request, it will take about 20-40 seconds to complete the request. This is because the app will need to authenticate itself, including getting a signed assertion from the Key Vault in the Identity Foundation. Enter the `tenantId` of your Azure B2C Tenant (i.e., the `tenant id` of the Azure B2C tenant that was deployed as part of the Identity Foundation). You'll find it in the `config.json` file at `.deployment.azureb2c.tenantId`. -![image-20230112001210631](assets/readme/image-20230112001210631.png) +![image-20230112001210631](.assets/readme/image-20230112001210631.png) -> Tip: After the first run, the access token is cached for the duration of it's life time, so if you try and run the request for a second time, it will be much faster. +> *Tip*: After the first run, the access token is cached for the duration of it's life time, so if you try and run the request for a second time, it will be much faster. -## How to Deploy to Azure +## How to Deploy SaaS Permissions Service API to Azure -For deploying the SaaS Permissions Service API to Azure a [GitHub Action](https://github.com/features/actions) is provide as part of the repo. +A [GitHub Action](https://github.com/features/actions) is provide for deploying the SaaS Permissions Service API to the Azure App Service that was provisioned . -> Tip: Establishing a [CI/CD](CI/CD) pipeline from the onset provides automation which increases security and minimizes operations. We highly recommend using this or some other CI/CD tool. +> *Info #1*: The GitHub Action is defined by a YAML file located in the `./.github/workflows` directory. +> +> *Info #2*: During the deployment of the Identity Foundation, an [OIDC Connection](https://learn.microsoft.com/en-us/azure/app-service/deploy-github-actions?tabs=openid) was established between your Azure resource group and your GitHub repo. This connection enables GitHub action to push updates directly to your Azure App Service in the Azure Resource Group. Leveraging a [OIDC Connection](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect), is the recommended authentication method for automating deployment. OIDC offered hardened security without the need to managing and keeping safe secrets or passwords. +### Setting up for deployment -> Info: During the deployment of the Identity Foundation, an [OIDC Connection](https://learn.microsoft.com/en-us/azure/app-service/deploy-github-actions?tabs=openid) was established between your Azure resource group and your GitHub repo. This connection enables GitHub action to push updates directly to your Azure App Services. Leveraging [OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) is the recommended authentication method for automated deployment, offering hardened security without the need to managing and keeping safe secrets or passwords. +Here are the steps to set things up for deploying the SaaS Permissions Service API to Azure. -To deploy the solution to Azure +1. Before we can use the GitHub Action, we need to update it with the right app name for the SaaS Permissions Service. To do this go to the terminal and change directory: -## How to debug in Azure +```bash +.../src/Saas.Identity/Saas.Permissions/deployment +``` -We're going to deploy for Windows, rather than Linux, because the Windows remote debugging is the most seamless, and we're expecting that you'll want to remote debug into the Azure instance to explore and see how the app runs there. That said, nothing prevents you from deploying to Linux for production, since ASP.NET Core 7 runs equally well on Linux. +2. From the directory, run these commands and bash shell scripts: -For more on remote debugging with Visual Studio 2022 see: [Remote Debug ASP.NET Core on Azure App Service - Visual Studio (Windows) | Microsoft Learn](https://learn.microsoft.com/en-us/visualstudio/debugger/remote-debugging-azure-app-service?view=vs-2022). +```bash +sudo chmod +c ./setup.sh +./setup.sh +./run.sh +``` +This will update the `AZURE_WEBAPP_NAME` environment variable in the GitHub Action YAML file, specifying the name of the SaaS Permissions Service API, as seen here in the file `./.github/workflows/permissions-api-deploy.yaml`: +![image-20230126230446548](assets/readme/image-20230126230446548.png) -## How to Debug in Azure +> *Info*: the SaaS Permissions Service API name is fetched from the `config.json` file that was created when running the Identity Foundation deployment script. -After deploying to Azure everything should work. But what if it doesn't? We could attach a debugger then. Sure, but what if the ASP.NET Core app isn't even starting? +3. To push the update to your GitHub repository do a `git commit` (or merge) of the changes on *main* branch and do a `git push origin` to add the push the changes to the GitHub repo (origin). -### My app won't start +### Deploying -Command line to the rescue. +To deploy the SaaS Permissions Service API to your Azure environment you can run the GitHub action directly from your GitHub repository by: -![image-20230120213644846](assets/readme/image-20230120213644846.png) +1. Press the **Actions** menu tab. +2. Click on the **ASDK Permissions Service API - Deploy to Azure Web Services** workflow. +3. Click on the **Run workflow** drop-down button. +4. Click the green **Run workflow** button. - \ No newline at end of file +![image-20230126203229729](.assets/readme/image-20230126203229729.png) + +> *Tip*: In a real-life CI/CD scenario, we would not use the manual `workflow_dispatch` trigger, but would modify our `.github/workflows/permissions-api-deploy.yml` to use a different [GitHub Action Trigger](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows). + +## Debugging in Azure + +The previous section talked about deploying a *Release* version of the SaaS Permissions Service API from the *main* branch to Azure. + +What if we want to *debug* the web app running in Azure instead? This section is dedicated to that question. + +### Speeding up the inner deployment loop with GitHub Actions + +For testing, debugging and general development, we generally want to do as much work locally as possible, but eventually we also will need to test and understand how the web app runs in the Azure environment as well. There are different way for doing this. We want to pick the a process that a) is as close to production, while b) speeds up our *inner dev loop*. [Credits to @forrestbrazeal at Good Tech Things for humorously illustrating why](https://www.goodtechthings.com/pipeline/) an expedited inner loop is so important. + +![Pipeline](https://www.goodtechthings.com/content/images/size/w1140/2022/12/Pipeline.png) + +Chances are that we don't want to push every little change or test etc. to the *main* branch every time we test, troubleshoot etc. Hence, the first we must do is to create a separate *dev* branch. + +#### Introducing Act + +Working from the *dev* branch means that we need a different GitHub Action that pulls updates from the *dev* branch rather than the *main* branch. We could easily create a 2nd GitHub Action for this, but here we suggest to do something different. Specifically, use [Act](https://github.com/nektos/act). + +Act allows us to run our GitHub Action locally, thus giving us much better speed, control, and insights into what's going on. All of which being exactly what we're looking for when doing debugging, testing and development work. + +Act can be installed as an extension to GitHub cli, by running this command from the terminal: + +```bash +gh extension install nektos/gh-act +``` + +Like our own deployment script, Act uses a container to run GitHub Actions locally. The default container provided by Act doesn't include az cli by default, so we need to extend it. We're created a short bash shell script to make this process easy. + +Here are the steps for getting up and running with Act for doing local deployment: + +1. Go to the sub-directory: + + ```bash + .../src/Saas.Identity/Saas.Permissions/deployment/act + ``` + +2. Set up but running the command: + + ```bash + chmod +x ./setup.sh + ./setup.sh + ``` + + > *Info*: this will set permissions for the scripts and well as extend and build the aforementioned act container + +That's it. From hereon we can deploy our most recent code anytime we want by running this script: + +``` +./deploy.sh +``` + +> *Info*: The code that will be deployed are based on the current file in your project directory. No need neither git commit or git push the code to GitHub first. +> +> ***Important***: We're unable to leverage OIDC when running running deploy with Act. Instead our `./deploy.sh` script creates a credential using a client secret, which is stored in our local dev machine in the directory `$HOME/asdk/.secret/`. You can delete this secret by running `./clean.sh`, which will delete the local file as well as delete the credential in Azure. + +#### Bonus question: Why not use Visual Studio 2022 to publish our code changes directly? + +Why all the hoops and loops of using command line, GitHub actions and Act? After all, Visual Studio 2022 have a build in feature for Publishing directly Azure from the Visual Studio 2022 IDE. + +![image-20230127151459605](.assets/readme/image-20230127151459605.png) + +So wouldn't it be easier to use Visual Studio 2022's build in features? Yes and no. + +When the above have been set up as described, running the `.deploy.sh` shell script each time we want to deploy a change, is both quick and efficient, providing good control and feedback during the deployment process. More importantly, by used (almost) the same deployment pattern for both production automation (CI/CD) and for manual deployment, we will save time and hassle in the long run because our inner loop is the same for both scenarios. + +As a bonus, using Act, we can make changes to the deployment YAML file and test those changes immediately and interactively without having to first go though; a) *git commit*, b) *git merge to c) main* and *git push to origin*, for every change/test we make. Trust us when we say that this *abc* process quickly becomes rather tedious. Not to mention saving us all those *work in progress* commits, messing up our git commit history. You're welcome. + +### How to debug if the web app fails at start-up with Kudo + +The first time you deploy the code to Azure it should all work. Eventually, you might have made a change that breaks the start up of the web app. This section is dedicated to this scenario. + +Azure App Services includes useful tool for debugging the start-up of web app running in Azure App Service. The tool is called [Kudo](https://learn.microsoft.com/en-us/azure/app-service/resources-kudu). + +To use Kudo do this: + +1. Go to the Azure Portal + +2. Find the The Identity Foundation Resource Group provisioned earlier. + +3. Select the SaaS Permission Service API App Service + ![image-20230128154233104](.assets/readme/image-20230128154233104.png) + +4. Scroll the menu on the left down to Development Tools -> Choose Advanced Tools -> press the Go link.![image-20230128153624642](.assets/readme/image-20230128153624642.png) + +5. Choose the **Debug console** drop-down box and choose **CMD**.![image-20230128153659108](.assets/readme/image-20230128153659108.png) + +6. This will open a console on the web page. + ![image-20230128154523352](.assets/readme/image-20230128154523352.png) + +7. To start the SaaS Permissions Service API type: + + ```bash + dotnet /home/site/wwwroot/Saas.Permissions.Service.dll + ``` + +8. If anything fails at start-up the command will exit showing error and exceptions that may have been thrown. If nothing fails, the command will not exit and it will look something like this: + ![image-20230128154735945](.assets/readme/image-20230128154735945.png) + +### How to attached a debugger to an app running in Azure App Service + +Sometimes debugging start-up of an app is not enough. To really see what's going on, we'd like to attach a debugger and set break point etc., the same way we would do when running the web app locally. + +Here are the steps to do this with our SaaS Permissions Service API web app. + +#### Enabling the app for debugging + +> *Important*: For remote debugging to work like outlined here, the web app must be running on a Windows App Service Plan. If the Azure App Service plan is set for Linux, please see details about the [Snapshot debugger](https://learn.microsoft.com/en-us/azure/azure-monitor/snapshot-debugger/snapshot-debugger) instead. Our recommendation is that you stick with Windows for dev, test and debugging. You can still decide to deploy to live production on Linux. + +1. In order to attach a debugger the code must be build for debugging. When using Act for deployment this is already the case. For details see the [GitHub Action workflow YAML file](./deployment/act/workflow/permissions-api-deploy.debug.yml), specifically the environment variable `BUILD_CONFIGURATION`. + +2. After you've made sure that the deployed code is ready for debugging, please see this guide for how to set-up debugging: [Remote Debug ASP.NET Core on Azure App Service - Visual Studio (Windows) | Microsoft Learn](https://learn.microsoft.com/en-us/visualstudio/debugger/remote-debugging-azure-app-service?view=vs-2022). + +3. You will also need to get the right symbol files for the SaaS Permissions Service API. When using Act to deploy for remote debugging, you'll be able to locate those in the `./publish/symbols` directory in the root of your locally cloned GitHub repository, as soon as the deployment has completed. + + To reference the symbol files: In Visual Studio 2022 Navigate to **Debug > Windows > Modules**, sort the list by version and then right click on each our two project dlls and choose **Load Symbols**, to load the symbol information (.pdb). + ![image-20230128163318970](.assets/readme/image-20230128163318970.png) + + *Tip*: The `./publish/symbols` directory is updated every time to you deploy any of the + +#### Bonus tip: Using Kudo to download symbol files for remote debugging + +You can also use Kudo to download symbol files from you deployed project(s): +![image-20230128161825544](.assets/readme/image-20230128161825544.png) + + Happy debugging... diff --git a/src/Saas.Lib/Act.Container/.actrc b/src/Saas.Lib/Act.Container/.actrc new file mode 100644 index 00000000..dea14d6f --- /dev/null +++ b/src/Saas.Lib/Act.Container/.actrc @@ -0,0 +1 @@ +-P ubuntu-latest=act-container:latest \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/deployment/act/Dockerfile b/src/Saas.Lib/Act.Container/Dockerfile similarity index 100% rename from src/Saas.Identity/Saas.Permissions/deployment/act/Dockerfile rename to src/Saas.Lib/Act.Container/Dockerfile diff --git a/src/Saas.Lib/Act.Container/build.sh b/src/Saas.Lib/Act.Container/build.sh new file mode 100644 index 00000000..8633be89 --- /dev/null +++ b/src/Saas.Lib/Act.Container/build.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +force_update=false + +while getopts 'fn:' flag; do + case "${flag}" in + f) + force_update=true + ;; + n) + tag_name="$OPTARG" + ;; + *) + force_update=false + ;; + esac +done + +# shellcheck disable=SC1091 +source "./../constants.sh" + +if [[ "${force_update}" == false ]]; then + echo "Building the deployment container using the cache. To rebuild use the -f flag." + docker build --file "${ACT_CONTAINER_DIR}/Dockerfile" --tag "${tag_name}" . +else + docker build --no-cache --file "${ACT_CONTAINER_DIR}/Dockerfile" --tag "${tag_name}" . +fi + diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionSigningProvider.cs b/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionSigningProvider.cs index ef7a762a..2480db3e 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionSigningProvider.cs +++ b/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionSigningProvider.cs @@ -11,6 +11,7 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Json; +using Saas.Interface; namespace ClientAssertionWithKeyVault; public class ClientAssertionSigningProvider : IClientAssertionSigningProvider diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionWithKeyVault.csproj b/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionWithKeyVault.csproj index c8b5baa0..88856c71 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionWithKeyVault.csproj +++ b/src/Saas.Lib/ClientAssertionWithKeyVault/ClientAssertionWithKeyVault.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IClientAssertionSigningProvider.cs b/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IClientAssertionSigningProvider.cs index eeed6dbd..8cbc94ae 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IClientAssertionSigningProvider.cs +++ b/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IClientAssertionSigningProvider.cs @@ -1,4 +1,5 @@ using Azure.Core; +using Saas.Interface; namespace ClientAssertionWithKeyVault.Interface; diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IPublicX509CertificateDetailProvider.cs b/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IPublicX509CertificateDetailProvider.cs index cf40dcff..c4840aa1 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IPublicX509CertificateDetailProvider.cs +++ b/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IPublicX509CertificateDetailProvider.cs @@ -1,4 +1,5 @@ using Azure.Core; +using Saas.Interface; namespace ClientAssertionWithKeyVault.Interface; diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/Model/KeyInfo.cs b/src/Saas.Lib/ClientAssertionWithKeyVault/Model/KeyInfo.cs index f7bc017a..bcb8e7ae 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/Model/KeyInfo.cs +++ b/src/Saas.Lib/ClientAssertionWithKeyVault/Model/KeyInfo.cs @@ -1,4 +1,4 @@ -using ClientAssertionWithKeyVault.Interface; +using Saas.Interface; namespace ClientAssertionWithKeyVault.Model; internal record KeyInfo : IKeyInfo diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/PublicX509CertificateDetailProvider.cs b/src/Saas.Lib/ClientAssertionWithKeyVault/PublicX509CertificateDetailProvider.cs index 20fb790b..72badf70 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/PublicX509CertificateDetailProvider.cs +++ b/src/Saas.Lib/ClientAssertionWithKeyVault/PublicX509CertificateDetailProvider.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Caching.Memory; using System.Security.Cryptography.X509Certificates; using ClientAssertionWithKeyVault.Util; using Microsoft.Extensions.Logging; +using Saas.Interface; namespace ClientAssertionWithKeyVault; public class PublicX509CertificateDetailProvider : IPublicX509CertificateDetailProvider diff --git a/src/Saas.Lib/Deployment.Container/build.sh b/src/Saas.Lib/Deployment.Container/build.sh index 58dc8466..6ec4c0cf 100755 --- a/src/Saas.Lib/Deployment.Container/build.sh +++ b/src/Saas.Lib/Deployment.Container/build.sh @@ -1,13 +1,33 @@ #!/usr/bin/env bash +force_update=false + +while getopts f flag +do + case "${flag}" in + f) force_update=true;; + *) force_update=false;; + esac +done + repo_base="$( git rev-parse --show-toplevel )" docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" architecture="$( uname -a )" -if [[ "${architecture}" == *"ARM64"* ]]; then - echo "Building for ARM64 (including Apple Sillicon)..." - docker build --file "${docker_file_folder}/Dockerfile.ARM" --tag asdk-script-deployment:latest . +if [[ "${force_update}" == false ]]; then + if [[ "${architecture}" == *"ARM64"* ]]; then + echo "Building for ARM64 (including Apple Sillicon)..." + docker build --file "${docker_file_folder}/Dockerfile.ARM" --tag asdk-script-deployment:latest . + else + docker build --file "${docker_file_folder}/Dockerfile" --tag asdk-script-deployment:latest . + fi else - docker build --file "${docker_file_folder}/Dockerfile" --tag asdk-script-deployment:latest . + if [[ "${architecture}" == *"ARM64"* ]]; then + echo "Building for ARM64 (including Apple Sillicon)..." + docker build --no-cache --file "${docker_file_folder}/Dockerfile.ARM" --tag asdk-script-deployment:latest . + else + docker build --no-cache --file "${docker_file_folder}/Dockerfile" --tag asdk-script-deployment:latest . + fi fi + diff --git a/src/Saas.Lib/Deployment.Script.Modules/act-credentials-module.sh b/src/Saas.Lib/Deployment.Script.Modules/act-credentials-module.sh new file mode 100644 index 00000000..71a1cb1e --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/act-credentials-module.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +{ + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" +} + +function is_resource_group() { + local resource_group="${1}" + + existing_rg="$(cat "${ACT_SECRETS_FILE_RG}")" || + { + false + return + } + + if [[ "${existing_rg}" == "${resource_group}" ]]; then + true + else + false + fi + + return +} + +function setup-act-secret() { + local act_secret_file="${1}" + local resource_group="${2}" + + declare secret + + # Creating a new secret for accessing the OIDC app access deployment + # if no secret exists + # or the secret file is empty + # or it was created for another resource group + if [[ ! -f "${act_secret_file}" || + ! -s "${act_secret_file}" ]] || + ! is_resource_group "${resource_group}"; then + oidc_app_name="$(get-value ".oidc.name")" + oidc_app_id="$(get-value ".oidc.appId")" + + echo "Adding secret for the OIDC app '${oidc_app_name}'" + + touch "${act_secret_file}" + + # getting app crentials + secret_json="$(az ad app credential reset \ + --id "${oidc_app_id}" \ + --display-name "secret-${oidc_app_name}" \ + --query "{clientId:appId, clientSecret:password, tenantId:tenant}" \ + 2>/dev/null)" + + subscription_id="$(get-value ".initConfig.subscriptionId")" + + # adding subscription id to secret json + secret_json="$(jq --arg sub_id "${subscription_id}" \ + '. | .subscriptionId = $sub_id' \ + <<<"${secret_json}")" + + # outputting secret json to file in compat format with escape double quotes + secret="$(echo "${secret_json}" | + jq --compact-output '.' | + sed 's/"/\\"/g')" + + secret="AZURE_CREDENTIALS=\"${secret}\"" + + echo "${secret}" >"${act_secret_file}" + + echo "${resource_group}" >"${ACT_SECRETS_FILE_RG}" + + echo "Waiting for 30 seconds to allow new secret to propagate." + sleep 30 + else + echo "Using existing secrets file at: ${act_secret_file}'. If you want to create a new one, please run '/.clean.sh' again and then run './run.sh' again." + fi +} + +function delete-secret-based-credentials() { + oidc_app_id="$(get-value ".oidc.appId")" + oidc_app_name="$(get-value ".oidc.name")" + oidc_app_name="secret-${oidc_app_name}" + + echo "Deleting the secret based credentials for the OIDC app '${oidc_app_name}'..." + + key_id="$(az ad app credential list \ + --id "${oidc_app_id}" \ + --query "[?displayName=='${oidc_app_name}'].keyId | [0]" \ + --output tsv)" + + if [[ -n $key_id ]]; then + az ad app credential delete \ + --id "${oidc_app_id}" \ + --key-id "${key_id}" + + echo "Secret based credentials deleted." + else + echo "No secret based credentials found." + fi +} diff --git a/src/Saas.Lib/Deployment.Script.Modules/app-service-constants.sh b/src/Saas.Lib/Deployment.Script.Modules/app-service-constants.sh new file mode 100644 index 00000000..13aa495a --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/app-service-constants.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# disable unused variable warning https://www.shellcheck.net/wiki/SC2034 +# shellcheck disable=SC2034 + +# configuration manifest for the Identity Foundation deployment, run previously +CONFIG_DIR="${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config" +CONFIG_FILE="${CONFIG_DIR}/config.json" + +# local bicep script directory +BICEP_DIR="${BASE_DIR}/Bicep" +BICEP_PARAMETERS_DIR="${BASE_DIR}/Bicep/Parameters" +BICEP_APP_SERVICE_DEPLOY_PARAMETERS_FILE="${BICEP_PARAMETERS_DIR}/app-service-parameters.json" +BICEP_CONFIG_ENTRIES_DEPLOY_PARAMETERS_FILE="${BICEP_PARAMETERS_DIR}/config-entries-parameters.json" +BICEP_PARAMETERS_TEMPLATE_FILE="${BICEP_PARAMETERS_DIR}/parameters-template.json" +BICEP_IDENTITY_FOUNDATION_OUTPUT_FILE="${BICEP_PARAMETERS_DIR}/identity-foundation-outputs.json" + +DEPLOY_APP_SERVICE_BICEP_FILE="${BICEP_DIR}/deployAppService.bicep" +DEPLOY_CONFIG_ENTRIES_BICEP_FILE="${BICEP_DIR}/deployConfigEntries.bicep" + +DEPLOYMENT_CONTAINER_NAME="asdk-script-deployment:latest" + +ACT_CONTAINER_DIR="${REPO_BASE}/src/Saas.Lib/Act.Container" + +# act container name +ACT_CONTAINER_NAME="act-container:latest" + +# secrets file +ACT_SECRETS_DIR="/asdk/act/.secret" +ACT_SECRETS_FILE="${ACT_SECRETS_DIR}/secret" +ACT_SECRETS_FILE_RG="${ACT_SECRETS_DIR}/resource_group.txt" \ No newline at end of file diff --git a/src/Saas.Lib/Deployment.Script.Modules/backup-module.sh b/src/Saas.Lib/Deployment.Script.Modules/backup-module.sh index b2f72fb8..27f8a06e 100644 --- a/src/Saas.Lib/Deployment.Script.Modules/backup-module.sh +++ b/src/Saas.Lib/Deployment.Script.Modules/backup-module.sh @@ -7,55 +7,55 @@ source "$SHARED_MODULE_DIR/storage-module.sh" function backup-config-beginning() { set +u - if [[ -z "${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}" ]]; then + if [[ -z "${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}" ]]; then now=$(date '+%Y-%m-%d--%H-%M-%S') - export ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME="${now}" + export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" fi set -u - backup_file="${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}/config.begin.json" + backup_file="${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}/config.begin.json" echo "Backing up existing configuration file to: ${backup_file}" \ | log-output \ --level info \ --header "Backup Configuration" - mkdir -p "${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}" + mkdir -p "${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}" cp "${CONFIG_FILE}" "${backup_file}" } function backup-config-end() { set +u - if [[ -z "${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}" ]]; then + if [[ -z "${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}" ]]; then now=$(date '+%Y-%m-%d--%H-%M-%S') - export ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME="${now}" + export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" fi set -u cp "${CONFIG_FILE}" \ - "${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}/config.end.json" + "${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}/config.end.json" cp "${IDENTITY_FOUNDATION_BICEP_PARAMETERS_FILE}" \ - "${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}/" + "${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}/" cp "${CERTIFICATE_POLICY_FILE}" \ - "${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}/" + "${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}/" } function backup-log() { local script_name="$1" set +u - if [[ -z "${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}" ]]; then + if [[ -z "${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}" ]]; then now=$(date '+%Y-%m-%d--%H-%M-%S') - export ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME="${now}" + export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" fi set -u - mv "${LOG_FILE_DIR}/deploy-${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}.log" \ - "${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}/deploy.log" + mv "${LOG_FILE_DIR}/deploy-${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}.log" \ + "${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}/deploy.log" backup-to-azure-blob-storage \ - "${LOG_FILE_DIR}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}" \ + "${LOG_FILE_DIR}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}" \ "${script_name}" } diff --git a/src/Saas.Lib/Deployment.Script.Modules/clean-credentials.sh b/src/Saas.Lib/Deployment.Script.Modules/clean-credentials.sh new file mode 100644 index 00000000..1a385abd --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/clean-credentials.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# repo base +repo_base="$( git rev-parse --show-toplevel )" +base_dir="${repo_base}/src/Saas.Identity/Saas.Permissions/deployment" + +# shellcheck disable=SC1091 +{ + source "${base_dir}/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" + source "$SHARED_MODULE_DIR/act-credentials-module.sh" +} + +# initialize az cli +initialize-az-cli "$HOME/.azure" + +# remove locally cached secret +sudo rm "${ACT_SECRETS_FILE}" 2> /dev/null + +# delete secret based credential in Azure AD app registration. +delete-secret-based-credentials \ No newline at end of file diff --git a/src/Saas.Lib/Deployment.Script.Modules/deploy-app-service.sh b/src/Saas.Lib/Deployment.Script.Modules/deploy-app-service.sh new file mode 100644 index 00000000..3df07190 --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/deploy-app-service.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -u -e -o pipefail + +# shellcheck disable=SC1091 +{ + # include script modules into current shell + source "${ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE}/constants.sh" + source "$SHARED_MODULE_DIR/log-module.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/deploy-service-module.sh" +} + +prepare-parameters-file "${BICEP_PARAMETERS_TEMPLATE_FILE}" "${BICEP_APP_SERVICE_DEPLOY_PARAMETERS_FILE}" + +resource_group="$(get-value ".deployment.resourceGroup.name")" +identity_foundation_deployment_name="$(get-value ".deployment.identityFoundation.name")" + +echo "Downloading Identity Foundation outputs from Resource Group '${resource_group}' deployment named '${identity_foundation_deployment_name}'..." | + log-output \ + --level info + +get-identity-foundation-deployment-outputs \ + "${resource_group}" \ + "${identity_foundation_deployment_name}" \ + "${BICEP_IDENTITY_FOUNDATION_OUTPUT_FILE}" + +echo "Mapping '${APP_NAME}' parameters..." | + log-output \ + --level info + +# map parameters +"${SHARED_MODULE_DIR}/map-output-parameters-for-app-service.py" \ + "${APP_NAME}" \ + "${BICEP_IDENTITY_FOUNDATION_OUTPUT_FILE}" \ + "${BICEP_APP_SERVICE_DEPLOY_PARAMETERS_FILE}" \ + "${CONFIG_FILE}" | + log-output \ + --level info || + echo "Failed to map ${APP_NAME} services parameters" | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 + +deployment_name="$(get-value ".deployment.${APP_DEPLOYMENT_NAME}.name")" + +echo "Provisioning '${deployment_name}' to resource group ${resource_group}..." | + log-output \ + --level info + +deploy-service \ + "${resource_group}" \ + "${BICEP_APP_SERVICE_DEPLOY_PARAMETERS_FILE}" \ + "${DEPLOY_APP_SERVICE_BICEP_FILE}" \ + "${deployment_name}" + +echo "Done. '${deployment_name}' was successfully provisioned to resource group ${resource_group}..." | + log-output \ + --level success diff --git a/src/Saas.Lib/Deployment.Script.Modules/deploy-config-entries.sh b/src/Saas.Lib/Deployment.Script.Modules/deploy-config-entries.sh new file mode 100644 index 00000000..171c6691 --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/deploy-config-entries.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -u -e -o pipefail + +# shellcheck disable=SC1091 +{ + # include script modules into current shell + source "${ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE}/constants.sh" + source "$SHARED_MODULE_DIR/log-module.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/deploy-service-module.sh" +} + +prepare-parameters-file "${BICEP_PARAMETERS_TEMPLATE_FILE}" "${BICEP_CONFIG_ENTRIES_DEPLOY_PARAMETERS_FILE}" + +resource_group="$( get-value ".deployment.resourceGroup.name" )" +identity_foundation_deployment_name="$( get-value ".deployment.identityFoundation.name" )" + +echo "Downloading Identity Foundation outputs from Resource Group '${resource_group}' deployment named '${identity_foundation_deployment_name}'..." \ + | log-output \ + --level info + +get-identity-foundation-deployment-outputs \ + "${resource_group}" \ + "${identity_foundation_deployment_name}" \ + "${BICEP_IDENTITY_FOUNDATION_OUTPUT_FILE}" + +echo "Provisioning the Saas Administraion Service Configuration Entries." \ + | log-output \ + --level info + +"${SCRIPT_DIR}/"map-to-config-entries-parameters.py \ + "${APP_NAME}" \ + "${BICEP_IDENTITY_FOUNDATION_OUTPUT_FILE}" \ + "${BICEP_CONFIG_ENTRIES_DEPLOY_PARAMETERS_FILE}" \ + "${CONFIG_FILE}" \ + | log-output \ + --level info \ + || echo "Failed to map ${APP_NAME} services parameters" \ + | log-output \ + --level error \ + --header "Critical Error" + +deployment_name="$( get-value ".deployment.${APP_DEPLOYMENT_NAME}.name" )" +deployment_name="${deployment_name}-config-entries" + +echo "Provisioning '${deployment_name}' to resource group ${resource_group}..." \ + | log-output \ + --level info + +deploy-service \ + "${resource_group}" \ + "${BICEP_CONFIG_ENTRIES_DEPLOY_PARAMETERS_FILE}" \ + "${DEPLOY_CONFIG_ENTRIES_BICEP_FILE}" \ + "${deployment_name}" + +echo "Done. '${deployment_name}' was successfully provisioned to resource group ${resource_group}..." \ + | log-output \ + --level success \ No newline at end of file diff --git a/src/Saas.Lib/Deployment.Script.Modules/deploy-debug.sh b/src/Saas.Lib/Deployment.Script.Modules/deploy-debug.sh new file mode 100644 index 00000000..5f23fd80 --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/deploy-debug.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -u -e -o pipefail + +# shellcheck disable=SC1091 +{ + source "$ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" + source "$SHARED_MODULE_DIR/act-credentials-module.sh" +} + +"${SHARED_MODULE_DIR}/patch-github-workflow.py" "${APP_NAME}" "${CONFIG_FILE}" "${ACT_LOCAL_WORKFLOW_DEBUG_FILE}" + +# using the az cli settings and cache from the host machine +initialize-az-cli "$HOME/.azure" + +resource_group_name="$(get-value ".deployment.resourceGroup.name")" + +setup-act-secret "${ACT_SECRETS_FILE}" $"${resource_group_name}" diff --git a/src/Saas.Lib/Deployment.Script.Modules/deploy-service-module.sh b/src/Saas.Lib/Deployment.Script.Modules/deploy-service-module.sh new file mode 100644 index 00000000..a8fa6179 --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/deploy-service-module.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +{ + # include script modules into current shell + source "$SHARED_MODULE_DIR/log-module.sh" +} + +function deploy-service() { + local resource_group="$1" + local parameters_file="$2" + local template_file="$3" + local deployment_name="$4" + + az deployment group create \ + --resource-group "${resource_group}" \ + --name "${deployment_name}" \ + --template-file "${template_file}" \ + --parameters "${parameters_file}" \ + || echo "Failed to deploy to ${APP_NAME}. This sometimes happens for no apperent reason. Please try again." \ + | log-output \ + --level error \ + --header "Critical Error" \ + || exit 1 +} + +function get-identity-foundation-deployment-outputs() { + local resource_group="$1" + local deployment_name="$2" + local output_file="$3" + + deployment_output_parameters="$( az deployment group show \ + --name "${deployment_name}" \ + --resource-group "${resource_group}" \ + --query properties.outputs )" \ + || echo "Failed to get Identity Bicep deployment output parameters" \ + | log-output \ + --level error \ + --header "Critical Error" \ + || exit 1 + + echo "${deployment_output_parameters}" > "${output_file}" +} + +function prepare-parameters-file() { + local template_file="$1" + local parameters_file="$2" + + if ! [[ -f "${parameters_file}" ]]; then + echo "The file ${parameters_file} does not exist, creating it now" \ + | log-output \ + --level info + + cp "${template_file}" "${parameters_file}" + fi +} diff --git a/src/Saas.Lib/Deployment.Script.Modules/log-module.sh b/src/Saas.Lib/Deployment.Script.Modules/log-module.sh index d93819e7..894e0d89 100644 --- a/src/Saas.Lib/Deployment.Script.Modules/log-module.sh +++ b/src/Saas.Lib/Deployment.Script.Modules/log-module.sh @@ -158,9 +158,9 @@ function log-output() { fi set +u - if [[ -z ${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME} ]]; then + if [[ -z ${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME} ]]; then now=$(date '+%Y-%m-%d--%H-%M-%S') - ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME="${now}" + ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" fi set -u @@ -172,7 +172,7 @@ function log-output() { else echo "# ${text}" fi - } >> "${LOG_FILE_DIR}/deploy-${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}.log" + } >> "${LOG_FILE_DIR}/deploy-${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}.log" if [[ $is_error == true ]]; then exit 1 diff --git a/src/Saas.Lib/Deployment.Script.Modules/map-output-parameters-for-app-service.py b/src/Saas.Lib/Deployment.Script.Modules/map-output-parameters-for-app-service.py new file mode 100644 index 00000000..e97512a5 --- /dev/null +++ b/src/Saas.Lib/Deployment.Script.Modules/map-output-parameters-for-app-service.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +import json +import sys +import re + +def get_app_value(config: dict, app_name: str, key: str) -> str: + for item in config['appRegistrations']: + if item['name'] == app_name: return item[key] + +def get_output_value(outputs: dict, output_name: str) -> 'dict[str, dict[str, str]]': + item = outputs[output_name] + if item : return { + output_name : { + 'value': item['value'] + } + } + +def patch_paramenters_file( + app_name: str, + identity_outputs: str, + paramenter_file: str, + config_file: str) -> None: + + with open(config_file, 'r') as f: + config = json.load(f) + + with open(identity_outputs, 'r') as f: + identity_outputs = json.load(f) + + with open(paramenter_file, 'r') as f: + parameters = json.load(f) + + print(f"App Name: {app_name}") + + app_service_name = get_app_value(config, app_name, 'appServiceName') + + app_key_name = re.sub('-', '', app_name) + + parameters['parameters'].update({ + app_key_name: { + 'value': app_service_name + } + }) + + parameters['parameters'].update(get_output_value(identity_outputs, 'version')) + parameters['parameters'].update(get_output_value(identity_outputs, 'environment')) + parameters['parameters'].update(get_output_value(identity_outputs, 'appServicePlanName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'keyVaultUri')) + parameters['parameters'].update(get_output_value(identity_outputs, 'location')) + parameters['parameters'].update(get_output_value(identity_outputs, 'userAssignedIdentityName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'appConfigurationName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'logAnalyticsWorkspaceName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'applicationInsightsName')) + + with open(paramenter_file, 'w') as f: + f.write(json.dumps(parameters, indent=4)) + + +# Main entry point for the script +if __name__ == "__main__": + app_name = sys.argv[1] + identity_outputs = sys.argv[2] + paramenter_file = sys.argv[3] + config_file = sys.argv[4] + + patch_paramenters_file(app_name, identity_outputs, paramenter_file, config_file) \ No newline at end of file diff --git a/src/Saas.Lib/Deployment.Script.Modules/oidc-workflow-module.sh b/src/Saas.Lib/Deployment.Script.Modules/oidc-workflow-module.sh index 90e9d0aa..07c24e3f 100644 --- a/src/Saas.Lib/Deployment.Script.Modules/oidc-workflow-module.sh +++ b/src/Saas.Lib/Deployment.Script.Modules/oidc-workflow-module.sh @@ -5,23 +5,17 @@ source "$SHARED_MODULE_DIR/config-module.sh" function federation-exist() { local app_id="$1" - local federation_credential_id="$2" + local subject="$2" - if [[ -z "${federation_id}" \ - || "${federation_id}" == null \ - || "${federation_id}" == "null" ]]; then - - false - return - fi - - federation_exist="$( az ad app federated-credential show \ + response_subject="$( az ad app federated-credential list \ --id "${app_id}" \ - --federated-credential-id "${federation_credential_id}" \ - --query "id=='${federation_credential_id}'" 2> /dev/null \ + --query "[?issuer == 'https://token.actions.githubusercontent.com'] \ + | [?contains(subject, '${subject}')].subject + | [0]" \ + --output tsv \ || false; return )" - if [ "${federation_exist}" == "true" ]; then + if [[ -n "${response_subject}" ]]; then true return else diff --git a/src/Saas.Identity/Saas.Permissions/deployment/script/patch-workflow.py b/src/Saas.Lib/Deployment.Script.Modules/patch-github-workflow.py similarity index 59% rename from src/Saas.Identity/Saas.Permissions/deployment/script/patch-workflow.py rename to src/Saas.Lib/Deployment.Script.Modules/patch-github-workflow.py index d3dd1b31..73628083 100644 --- a/src/Saas.Identity/Saas.Permissions/deployment/script/patch-workflow.py +++ b/src/Saas.Lib/Deployment.Script.Modules/patch-github-workflow.py @@ -8,13 +8,11 @@ def get_app_value(config: dict, app_name: str, key: str) -> str: for item in config['appRegistrations']: if item['name'] == app_name: return item[key] -def patch_workflow(config_file: str, workflow_yaml: str) -> None: +def patch_workflow(app_name: str, config_file: str, workflow_yaml: str) -> None: with open(config_file, 'r') as f_json: config_json = json.load(f_json) - web_app_name = get_app_value(config_json, "permissions-api", "apiName") - - print(f"Web app name: {web_app_name}") + web_app_name = get_app_value(config_json, app_name, "appServiceName") yaml=YAML() @@ -27,17 +25,16 @@ def patch_workflow(config_file: str, workflow_yaml: str) -> None: env = workflow_yaml['env'] - env.update(dict(AZURE_WEBAPP_NAME=web_app_name)) - - # print(f"Workflow: {workflow_yaml['env']['AZURE_WEBAPP_NAME']}") - - workflow_yaml['env']['AZURE_WEBAPP_NAME'] = web_app_name + env.update(dict(APP_NAME = app_name)) + env.update(dict(AZURE_WEBAPP_NAME = web_app_name)) with open(workflow, 'w+') as stream: yaml.dump(workflow_yaml, stream) # Main entry point for the script if __name__ == "__main__": - config_file = sys.argv[1] - workflow = sys.argv[2] - patch_workflow(config_file, workflow) \ No newline at end of file + app_name = sys.argv[1] + config_file = sys.argv[2] + workflow = sys.argv[3] + + patch_workflow(app_name, config_file, workflow) \ No newline at end of file diff --git a/src/Saas.Lib/Deployment.Script.Modules/storage-module.sh b/src/Saas.Lib/Deployment.Script.Modules/storage-module.sh index 32dbaceb..84f92c62 100644 --- a/src/Saas.Lib/Deployment.Script.Modules/storage-module.sh +++ b/src/Saas.Lib/Deployment.Script.Modules/storage-module.sh @@ -136,7 +136,7 @@ function backup-to-azure-blob-storage() { --level info \ --header "Backup to Azure Blob Storage" - zip_file_name="${directory}/log-${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}.zip" + zip_file_name="${directory}/log-${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}.zip" zip -r -j "${zip_file_name}" "${directory}" > /dev/null \ || echo "Failed to zip logs." \ @@ -148,7 +148,7 @@ function backup-to-azure-blob-storage() { az storage fs file upload \ --account-name "${storage_account_name}" \ --file-system "${container_name}" \ - --path "log/${script_name}/${ASDK_ID_PROVIDER_DEPLOYMENT_RUN_TIME}.zip" \ + --path "log/${script_name}/${ASDK_DEPLOYMENT_SCRIPT_RUN_TIME}.zip" \ --source "${zip_file_name}" \ --auth-mode login \ | log-output \ diff --git a/src/Saas.Lib/Deployment.Script.Modules/user-module.sh b/src/Saas.Lib/Deployment.Script.Modules/user-module.sh index 3fc57973..830895ab 100644 --- a/src/Saas.Lib/Deployment.Script.Modules/user-module.sh +++ b/src/Saas.Lib/Deployment.Script.Modules/user-module.sh @@ -132,4 +132,21 @@ function get-cached-session() { sudo cp -f "$HOME"/asdk/.cache/"$user_name"/azureProfile.json "${azure_context_dir}" fi fi +} + +function get-host-session() { + local azure_context_dir="$1" + + cp -f /asdk/.azure/msal_token_cache.* "${azure_context_dir}" + cp -f /asdk/.azure/azureProfile.json "${azure_context_dir}" +} + + +function initialize-az-cli() { + local azure_context_dir="$1" + + # initialize az cli + az --version > /dev/null + + get-host-session "$azure_context_dir" } \ No newline at end of file diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/RouteBasedRoleCusomizerTests.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/RouteBasedRoleCusomizerTests.cs similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/RouteBasedRoleCusomizerTests.cs rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/RouteBasedRoleCusomizerTests.cs diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/Saas.AspNetCore.Authorization.Tests.csproj b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/Saas.AspNetCore.Authorization.Tests.csproj similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/Saas.AspNetCore.Authorization.Tests.csproj rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization.Tests/Saas.AspNetCore.Authorization.Tests.csproj diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/CustomRoleHandler.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/CustomRoleHandler.cs similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/CustomRoleHandler.cs rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/CustomRoleHandler.cs diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/IRoleCustomizer.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/IRoleCustomizer.cs similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/IRoleCustomizer.cs rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/IRoleCustomizer.cs diff --git a/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleCusomizer.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleCusomizer.cs new file mode 100644 index 00000000..0313660a --- /dev/null +++ b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleCusomizer.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Saas.AspNetCore.Authorization.AuthHandlers; + +public class RouteBasedRoleCusomizer : IRoleCustomizer +{ + private readonly IHttpContextAccessor _httpContextAccessor; + public RouteBasedRoleCusomizer(IHttpContextAccessor httpContextAccessor, string routeName, bool includeOriginals = false) + { + _httpContextAccessor = httpContextAccessor; + IncludeOriginals = includeOriginals; + RouteName = routeName; + } + + public bool IncludeOriginals { get; internal set; } + public string RouteName { get; protected set; } + + public IEnumerable CustomizeRoles(IEnumerable allowedRoles) + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + + string context = httpContext.GetRouteValue(RouteName) as string + ?? throw new NullReferenceException("Routing name cannot be null"); + + if (context is not null && allowedRoles is not null) + { + foreach (string role in allowedRoles) + { + yield return string.Format("{0}.{1}", context, role); + } + if (IncludeOriginals) + { + foreach (string role in allowedRoles) + { + yield return role; + } + } + } + } +} diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleHandlerExtensions.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleHandlerExtensions.cs similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleHandlerExtensions.cs rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/AuthHandlers/RouteBasedRoleHandlerExtensions.cs diff --git a/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformer.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformer.cs new file mode 100644 index 00000000..b1bc9284 --- /dev/null +++ b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformer.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace Saas.AspNetCore.Authorization.ClaimTransformers; + +/// +/// Transforms a custom claim in space delimited format to roles +/// The user principal will factor in the custom roles when IsInRole is called +/// +public class ClaimToRoleTransformer : IClaimsTransformation +{ + private readonly string _sourceClaimType; + private readonly string _roleClaimType; + private readonly string _authenticationType; + + public ClaimToRoleTransformer(IOptions claimToRoleTransformerOptions) + { + _sourceClaimType = claimToRoleTransformerOptions.Value.SourceClaimType + ?? throw new NullReferenceException($"{nameof(claimToRoleTransformerOptions.Value.SourceClaimType)} cannot be null"); + + _roleClaimType = claimToRoleTransformerOptions.Value.AuthenticationType + ?? throw new NullReferenceException($"{nameof(claimToRoleTransformerOptions.Value.AuthenticationType)} cannot be null"); + + _authenticationType = claimToRoleTransformerOptions.Value.RoleClaimType + ?? throw new NullReferenceException($"{nameof(claimToRoleTransformerOptions.Value.RoleClaimType)} cannot be null"); + } + + public Task TransformAsync(ClaimsPrincipal principal) + { + System.Collections.Generic.IEnumerable customClaims = principal.Claims + .Where(c => _sourceClaimType + .Equals(c.Type, StringComparison.OrdinalIgnoreCase)); + + System.Collections.Generic.IEnumerable roleClaims = customClaims + .SelectMany(c => + { + return c.Value.Split(' ').Select(s => + new Claim(_roleClaimType, s)); + }); + + if (!roleClaims.Any()) + { + return Task.FromResult(principal); + } + + ClaimsPrincipal transformed = new(principal); + + ClaimsIdentity rolesIdentity = new( + roleClaims, + _authenticationType, + null, + _roleClaimType); + + transformed.AddIdentity(rolesIdentity); + return Task.FromResult(transformed); + } +} diff --git a/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerOptions.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerOptions.cs new file mode 100644 index 00000000..342bbe72 --- /dev/null +++ b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimTransformers/ClaimToRoleTransformerOptions.cs @@ -0,0 +1,11 @@ + +namespace Saas.AspNetCore.Authorization.ClaimTransformers; + +public record ClaimToRoleTransformerOptions +{ + public const string SectionName = "ClaimToRoleTransformer"; + + public string? AuthenticationType { get; init; } + public string? RoleClaimType { get; init; } + public string? SourceClaimType { get; init; } +} diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimsPrincipalExtensions.cs b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimsPrincipalExtensions.cs similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimsPrincipalExtensions.cs rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/ClaimsPrincipalExtensions.cs diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/README.md b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/README.md similarity index 100% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization/README.md rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/README.md diff --git a/src/Saas.Authorization/Saas.AspNetCore.Authorization/Saas.AspNetCore.Authorization.csproj b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/Saas.AspNetCore.Authorization.csproj similarity index 82% rename from src/Saas.Authorization/Saas.AspNetCore.Authorization/Saas.AspNetCore.Authorization.csproj rename to src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/Saas.AspNetCore.Authorization.csproj index a86b1862..e8d3e430 100644 --- a/src/Saas.Authorization/Saas.AspNetCore.Authorization/Saas.AspNetCore.Authorization.csproj +++ b/src/Saas.Lib/Saas.Authorization/Saas.AspNetCore.Authorization/Saas.AspNetCore.Authorization.csproj @@ -1,18 +1,19 @@  - netstandard2.1 + net7.0 True + enable - + - - - + + + diff --git a/src/Saas.Authorization/samples.http b/src/Saas.Lib/Saas.Authorization/samples.http similarity index 100% rename from src/Saas.Authorization/samples.http rename to src/Saas.Lib/Saas.Authorization/samples.http diff --git a/src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/addConfigEntry.bicep b/src/Saas.Lib/Saas.Bicep.Module/addConfigEntry.bicep similarity index 100% rename from src/Saas.Identity/Saas.IdentityProvider/deployment/bicep/IdentityFoundation/Module/addConfigEntry.bicep rename to src/Saas.Lib/Saas.Bicep.Module/addConfigEntry.bicep diff --git a/src/Saas.Lib/Saas.Bicep.Module/appServiceModuleWithObservability.bicep b/src/Saas.Lib/Saas.Bicep.Module/appServiceModuleWithObservability.bicep new file mode 100644 index 00000000..08e3865d --- /dev/null +++ b/src/Saas.Lib/Saas.Bicep.Module/appServiceModuleWithObservability.bicep @@ -0,0 +1,119 @@ +@description('The SaaS Signup Administration web site name.') +param appServiceName string + +@description('Version') +param version string + +@description('Environment') +@allowed([ + 'Development' + 'Staging' + 'Production' +]) +param environment string + +@description('The App Service Plan ID.') +param appServicePlanName string + +@description('The Uri of the Key Vault.') +param keyVaultUri string + +@description('The location for all resources.') +param location string + +@description('Azure App Configuration User Assigned Identity Name.') +param userAssignedIdentityName string + +@description('The name of the Azure App Configuration.') +param appConfigurationName string + +@description('The name of the Log Analytics Workspace used by Application Insigths.') +param logAnalyticsWorkspaceName string + +@description('The name of Application Insights.') +param applicationInsightsName string + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { + name: userAssignedIdentityName +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { + name: logAnalyticsWorkspaceName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' existing = { + name: appServicePlanName +} + +resource appConfig 'Microsoft.AppConfiguration/configurationStores@2022-05-01' existing = { + name: appConfigurationName +} + +resource signupAdministrationWeb 'Microsoft.Web/sites@2022-03-01' = { + name: appServiceName + location: location + kind: 'app,windows' + properties: { + serverFarmId: appServicePlan.name + httpsOnly: true + // clientCertEnabled: true // https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth?tabs=bicep + clientCertMode: 'Required' + siteConfig: { + ftpsState: 'FtpsOnly' + alwaysOn: true + http20Enabled: true + keyVaultReferenceIdentity: userAssignedIdentity.id // Must specify this when using User Assigned Managed Identity. Read here: https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli#access-vaults-with-a-user-assigned-identity + detailedErrorLoggingEnabled: true + netFrameworkVersion: 'v7.0' + // linuxFxVersion: 'DOTNETCORE|7.0' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } + resource appsettings 'config@2022-03-01' = { + name: 'appsettings' + properties: { + Version: 'ver${version}' + Logging__LogLevel__Default: 'Information' + Logging__LogLevel__Microsoft__AspNetCore: 'Warning' + KeyVault__Url: keyVaultUri + ASPNETCORE_ENVIRONMENT: environment + UserAssignedManagedIdentityClientId: userAssignedIdentity.properties.clientId + AppConfiguration__Endpoint : appConfig.properties.endpoint + APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString // https://learn.microsoft.com/en-us/azure/azure-monitor/app/migrate-from-instrumentation-keys-to-connection-strings + ApplicationInsightsAgent_EXTENSION_VERSION: '~2' + } + } +} + +resource diagnosticsSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'string' + scope: signupAdministrationWeb + properties: { + logs: [ + { + categoryGroup: 'allLogs' + enabled: true + retentionPolicy: { + days: 7 + enabled: true + } + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + workspaceId: logAnalyticsWorkspace.id + } +} diff --git a/src/Saas.Lib/Saas.Bicep.Module/roles.json b/src/Saas.Lib/Saas.Bicep.Module/roles.json new file mode 100644 index 00000000..97404f2d --- /dev/null +++ b/src/Saas.Lib/Saas.Bicep.Module/roles.json @@ -0,0 +1,16 @@ +{ + "roles": { + "Contributor": "b24988ac-6180-42a0-ab88-20f7382dd24c", + "App Configuration Data Reader": "516239f1-63e1-4d78-a4de-a74fb236a071", + "Key Vault Secrets User": "4633458b-17de-408a-b874-0445c86b69e6", + "Key Vault Reader": "21090545-7ca7-4776-b22c-e363652d74d2", + "Azure Service Bus Data Receiver": "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0", + "Key Vault Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "Key Vault Administrator": "00482a5a-887f-4fb3-b363-3b7fe8e74483", + "Key Vault Certificates Officer": "a4417e6f-fecd-4de8-b567-7b0420556985", + "Key Vault Crypto Officer": "14b46e9e-c2b7-41b4-b07b-48a6ebf60603", + "Key Vault Crypto Service Encryption User": "e147488a-f6f5-4113-8e2d-b22465e65bf6", + "Key Vault Crypto User": "12338af0-0e69-4776-bea7-57ae8d297424", + "Key Vault Secrets Officer": "b86a8fe4-44ce-4948-aee5-eccb2c155cd7" + } +} \ No newline at end of file diff --git a/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IKeyInfo.cs b/src/Saas.Lib/Saas.Shared/Interface/IKeyInfo.cs similarity index 67% rename from src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IKeyInfo.cs rename to src/Saas.Lib/Saas.Shared/Interface/IKeyInfo.cs index dea2112d..3a1b7952 100644 --- a/src/Saas.Lib/ClientAssertionWithKeyVault/Interface/IKeyInfo.cs +++ b/src/Saas.Lib/Saas.Shared/Interface/IKeyInfo.cs @@ -1,4 +1,4 @@ -namespace ClientAssertionWithKeyVault.Interface; +namespace Saas.Interface; public interface IKeyInfo { diff --git a/src/Saas.Lib/Saas.Shared/Options/AdminApiOptions.cs b/src/Saas.Lib/Saas.Shared/Options/AdminApiOptions.cs new file mode 100644 index 00000000..0d6ab25c --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Options/AdminApiOptions.cs @@ -0,0 +1,11 @@ + +namespace Saas.Shared.Options; + +public record AdminApiOptions +{ + public const string SectionName = "AdminApi"; + + public string? ApplicationIdUri { get; init; } + public string[]? Scopes { get; init; } + +} diff --git a/src/Saas.Lib/Saas.Shared/Options/AzureAdB2CBase.cs b/src/Saas.Lib/Saas.Shared/Options/AzureAdB2CBase.cs new file mode 100644 index 00000000..acc316bb --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Options/AzureAdB2CBase.cs @@ -0,0 +1,25 @@ + +using Saas.Interface; + +namespace Saas.Shared.Options; +public record AzureAdB2CBase +{ + public string? ClientId { get; init; } + public string? Audience { get; init; } + public string? Domain { get; init; } + public string? Instance { get; init; } + public string? SignedOutCallbackPath { get; init; } + public string? SignUpSignInPolicyId { get; init; } + public string? TenantId { get; init; } + public string? LoginEndpoint { get; init; } + public string? BaseUrl { get; init; } + + public Certificate[]? ClientCertificates { get; init; } +} + +public record Certificate : IKeyInfo +{ + public string? SourceType { get; init; } + public string? KeyVaultUrl { get; init; } + public string? KeyVaultCertificateName { get; init; } +} \ No newline at end of file diff --git a/src/Saas.Lib/Saas.Shared/Options/AzureB2CAdminApiOptions.cs b/src/Saas.Lib/Saas.Shared/Options/AzureB2CAdminApiOptions.cs new file mode 100644 index 00000000..1f43b41e --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Options/AzureB2CAdminApiOptions.cs @@ -0,0 +1,6 @@ + +namespace Saas.Shared.Options; +public record AzureB2CAdminApiOptions : AzureAdB2CBase +{ + public const string SectionName = "AdminApi:AzureB2C"; +} diff --git a/src/Saas.Lib/Saas.Shared/Options/AzureB2CPermissionsApiOptions.cs b/src/Saas.Lib/Saas.Shared/Options/AzureB2CPermissionsApiOptions.cs new file mode 100644 index 00000000..906cedd3 --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Options/AzureB2CPermissionsApiOptions.cs @@ -0,0 +1,6 @@ + +namespace Saas.Shared.Options; +public record AzureB2CPermissionsApiOptions : AzureAdB2CBase +{ + public const string SectionName = "PermissionsApi:AzureB2C"; +} diff --git a/src/Saas.Lib/Saas.Shared/Options/AzureB2CSignupAdminOptions.cs b/src/Saas.Lib/Saas.Shared/Options/AzureB2CSignupAdminOptions.cs new file mode 100644 index 00000000..6d133862 --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Options/AzureB2CSignupAdminOptions.cs @@ -0,0 +1,7 @@ + + +namespace Saas.Shared.Options; +public record AzureB2CSignupAdminOptions : AzureAdB2CBase +{ + public const string SectionName = "SignupAdmin:AzureB2C"; +} diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/MSGraphOptions.cs b/src/Saas.Lib/Saas.Shared/Options/MSGraphOptions.cs similarity index 78% rename from src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/MSGraphOptions.cs rename to src/Saas.Lib/Saas.Shared/Options/MSGraphOptions.cs index 4767bdea..fd565112 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/MSGraphOptions.cs +++ b/src/Saas.Lib/Saas.Shared/Options/MSGraphOptions.cs @@ -1,4 +1,4 @@ -namespace Saas.Permissions.Service.Options; +namespace Saas.Shared.Options; public record MSGraphOptions { diff --git a/src/Saas.Lib/Saas.Shared/Options/PermissionsApiOptions.cs b/src/Saas.Lib/Saas.Shared/Options/PermissionsApiOptions.cs new file mode 100644 index 00000000..7159c0a5 --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Options/PermissionsApiOptions.cs @@ -0,0 +1,9 @@ + +namespace Saas.Shared.Options; + +public class PermissionsApiOptions +{ + public const string SectionName = "PermissionsApi"; + + public string? ApiKey { get; init; } +} \ No newline at end of file diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/SqlOptions.cs b/src/Saas.Lib/Saas.Shared/Options/SqlOptions.cs similarity index 80% rename from src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/SqlOptions.cs rename to src/Saas.Lib/Saas.Shared/Options/SqlOptions.cs index 19c6db08..fd2ecf4f 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Options/SqlOptions.cs +++ b/src/Saas.Lib/Saas.Shared/Options/SqlOptions.cs @@ -1,4 +1,4 @@ -namespace Saas.Permissions.Service.Options; +namespace Saas.Shared.Options; public record SqlOptions { diff --git a/src/Saas.Lib/Saas.Shared/Saas.Shared.csproj b/src/Saas.Lib/Saas.Shared/Saas.Shared.csproj new file mode 100644 index 00000000..3de3b86d --- /dev/null +++ b/src/Saas.Lib/Saas.Shared/Saas.Shared.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Swagger/SwagCustomHeaderFilter.cs b/src/Saas.Lib/Saas.Shared/Swagger/SwagCustomHeaderFilter.cs similarity index 94% rename from src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Swagger/SwagCustomHeaderFilter.cs rename to src/Saas.Lib/Saas.Shared/Swagger/SwagCustomHeaderFilter.cs index 7bcd103d..d127e22e 100644 --- a/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Swagger/SwagCustomHeaderFilter.cs +++ b/src/Saas.Lib/Saas.Shared/Swagger/SwagCustomHeaderFilter.cs @@ -2,7 +2,7 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Saas.Permissions.Service.Swagger; +namespace Saas.Swagger; public class SwagCustomHeaderFilter : IOperationFilter { diff --git a/src/Saas.SignupAdministration/README.md b/src/Saas.SignupAdministration/README.md index 8e1c4195..a3d3a521 100644 --- a/src/Saas.SignupAdministration/README.md +++ b/src/Saas.SignupAdministration/README.md @@ -1,83 +1,70 @@ -# Saas.SignupAdministration.Web +# SaaS Sign-Up Administration Web App -## 1. Module Overview - -This project hosts an application for the onboarding and administration of new tenants in your SaaS ecosystem. It is fully self-contained such that it includes complete copies of all necessary classes for operation. However, keep in mind that some functionality within the app does have [dependencies](https://azure.github.io/azure-saas/components/signup-administration#dependencies) on other services - -For a complete overview, please see the [SaaS.SignupAdministration.Web](https://azure.github.io/azure-saas/components/signup-administration/) page in our documentation site. +This SaaS Sign-up an Administration Web App is an application for the onboarding and administration of tenants. The application has been developed in [MVC](https://docs.microsoft.com/en-us/aspnet/core/mvc/overview?view=aspnetcore-6.0) format, its pages built by respective Controllers paired to Views. See the Views and Controllers directories for relevant service logic or display logic. -## 2. How to Run Locally +For a complete overview, please see the [SaaS.SignupAdministration.Web](https://azure.github.io/azure-saas/components/signup-administration/) page in our documentation site. -Once configured, this app creates new Tenants to be persisted by the Admin service. These tenants can then be administrated and have users added to them for reference from the proper SaaS application. System users in this process do not require any preconfigured roles to use the new tenant workflow. +## Overview -### i. Requirements +Within this folder you will find two subfolders: -To run the web api, you must have the following installed on your machine: +- **Saas.SignupAdministration.Web** - the C# project for the web app. +- **deployment** - a set of tools for deploying the web app for production + - The sub-subfolder **[act](./deployment/act)** is for deploying the web app for remote debugging -- [.NET 6.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -- [ASP.NET Core 6.0](https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0) -- (Recommended) [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Visual Studio Code](https://code.visualstudio.com/download) -- A deployed [Identity Framework](https://azure.github.io/azure-saas/quick-start/) instance - - [Azure AD B2C](https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/) - created automatically with Bicep deployment +## Dependencies -### ii. Development Tools +The service depends on: -- [NSwag](https://github.com/RicoSuter/NSwag) - An NSwag configuration file has been included to generate an appropriate client from the included Admin project. - *Consumes Clients:* - - [admin-service-client-generator.nswag](Saas.SignupAdministration.Web/admin-service-client-generator.nswag) - -### iii. App Settings +- The **Identity Foundation** that was deployed a spart of the Identity Foundation and on the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). +- The **[SaaS Permissions Services API](./../Saas.Identity/Saas.Permissions/readme.md)**. +- The **[SaaS Administration Service API](./../Saas.Admin/readme.md)**. +- The [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview). -In order to run the project locally, the App Settings marked as `secret: true` must be set using the [.NET secrets manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows). If developing in Visual Studio, right click the project in the Solution Explorer and select `Manage User Secrets`. +## Provisioning the Web App -> The secrets indicated here are for Azure AD B2C integration. You must have an existing user store to reference which is configured to redirect to localhost on your debugging port in order to function correctly. +To work with the SaaS Signup Administration Web app it must first be provisions to your Azure ASDK resource group. This is true even if you initially is planning to run the API in your local development environment. The provisioning ensure that configuration and settings to be correctly added to your Azure App Configuration store and readies the API for later deployment to Azure. -When deployed to Azure using the Bicep deployments, these secrets are [loaded from Azure Key Vault](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#secret-storage-in-the-development-environment) instead. -Default values for non secret app settings can be found in [appsettings.json](Saas.SignupAdministration.Web/appsettings.json) +Provisioning is easy: -| AppSetting Key | Description | Secret | Default Value | -| ------------------------------------------- | ------------------------------------------------------------------------------ | ------ | ------------------------------ | -| AppSettings:AdminServiceBaseUrl | URL for downstream admin service | false | https://localhost:7041/ | -| AppSettings:AdminServiceScopeBaseUrl | The Base URL/Prefix for the scopes to request an access token on the downstream admin service for. | false | | -| AppSettings:AdminServiceScopes | List of scopes to authorize user for on the admin service. Space delimited | false | | -| AzureAdB2C:ClientId | The service client corresponding to the Signup Admin application | true | | -| AzureAdB2C:ClientSecret | Unique secret for the application client provided to authenticate the app | true | | -| AzureAdB2C:Domain | Domain name for the Azure AD B2C instance | true | | -| AzureAdB2C:Instance | URL for the root of the Azure AD B2C instance | true | | -| AzureAdB2C:SignedOutCallbackPath | Callback path (not full url) contacted after signout | false | /signout/B2C_1A_SIGNUP_SIGNIN | -| AzureAdB2C:SignUpSignInPolicyId | Name of signup/signin policy | false | B2C_1A_SIGNUP_SIGNIN | -| AzureAdB2C:TenantId | Identifier for the overall Azure AD B2C tenant for the overall SaaS ecosystem | true | | -| EmailOptions:Body | Signup notification email body text | false | | -| EmailOptions:EndPoint | Service endpoint to send confirmation email | true | | -| EmailOptions:FromAddress | Signup notification email source | false | | -| EmailOptions:Subject | Signup notification email subject line | false | | -| KeyVault:Url | KeyVault URL to pull secret values from in production | false | | -| Logging:LogLevel:Default | Logging level when no configured provider is matched | false | Information | -| Logging:LogLevel:Microsoft | Logging level for Microsoft logging | false | Warning | -| Logging:LogLevel:Microsoft.Hosting.Lifetime | Logging level for Hosting Lifetime logging | false | Information | - -### iv. Starting the App +1. Navigate to the sub folder `deployment`. -These instructions cover running the app using its existing implementations and included modules locally. Substituting other module implementations or class implementations may make existing secrets irrelevant. +2. Run these commands: -1. Insert secrets marked as required for running locally into your secrets manager (such as by using provided script). -1. Configure multiple projects to launch: Saas.SignupAdministration.Web, Saas.Permissions.Service and Saas.Admin.Service. In Visual Studio, this can be accomplished by right clicking the project and selecting `Set Startup Projects...` for local debugging. -1. Start apps. Services will launch as presented Swagger APIs. Web app will launch MVC ASP.NET Core application. + ```bash + sudo chmod +x setup.sh + ./setup.sh + ./run.sh + ``` -## 3. Additional Resources +Now you're ready to move on. -### i. Azure AD B2C +## How to Run Locally -You'll need to configure your B2C instance for an external authentication provider. Additional documentation is available [here](https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-azure-ad-multi-tenant?pivots=b2c-user-flow). +Guidelines for getting up and running with SaaS Signup Administration in your local development, are identical to the guidelines found the *[Requirements](./../Saas.Identity/Saas.Permissions/readme.md#Requirements)* and the *[Configuration, settings and secrets when running locally](./../Saas.Identity/Saas.Permissions/readme.md#running-the-saas-permissions-service-api-locally)* section in the [SaaS Permissions Service readme](./../Saas.Identity/Saas.Permissions/readme.md). -### ii. JsonSessionPersistenceProvider +## Running the SaaS Sign-up Administration Web App Locally -The JsonSessionPersistenceProvider maintains the state of the onboarding workflow for each user and allows for forward and backward movment in the app with access to all of the values of the Tenant. Custom providers can be used as long as they inherit from the IPersistenceProvider interface. +--- TODO BEGIN --- -The 2 methods are: -- public void Persist(string key, object value); -- public T Retrieve(string key); +*Needs more investigation.* -The included implementation simply stores the session data within memory of the server app, making unsuitable at scale or when multiple app instances are deployed. A caching service able to perform realtime updates is ideal to maximize scalability. Consider replacing the implementation of the JsonSessionPersistenceProvider class with a new implementation using something like [Azure Cache for Redis](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-overview). Alternatively, you can configure session persistence to ensure your users are always routed to the same server. \ No newline at end of file +*Add some guidelines about how to do this. Probably by leveraging [ngrok](https://ngrok.com/)...* + +---TODO END --- + +## How to Deploy the SaaS Administration Service API to Azure + +The guidelines are identity to *[How to Deploy SaaS Permissions Service API to Azure](./../Saas.Identity/Saas.Permissions/readme.md#how--to-deploy-saas-permissions-service-api-to-azure)*. + +## Debugging in Azure + +The guidelines are identity to *[Debugging in Azure](./../Saas.Identity/Saas.Permissions/readme.md#debugging-in-azure)* for the SaaS Permissions Service API + +## Debugging Azure B2C + + + +[Troubleshoot custom policies with Application Insights - Azure AD B2C | Microsoft Learn](https://learn.microsoft.com/en-us/azure/active-directory-b2c/troubleshoot-with-application-insights?pivots=b2c-custom-policy) \ No newline at end of file diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Deploy-AzureResourceGroup.ps1 b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Deploy-AzureResourceGroup.ps1 deleted file mode 100644 index 8926a049..00000000 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Deploy-AzureResourceGroup.ps1 +++ /dev/null @@ -1,120 +0,0 @@ -#Requires -Version 3.0 - -Param( - [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation, - [string] $ResourceGroupName = 'Saas.Provider', - [switch] $UploadArtifacts, - [string] $StorageAccountName, - [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts', - [string] $TemplateFile = 'azuredeploy.json', - [string] $TemplateParametersFile = 'azuredeploy.parameters.json', - [string] $ArtifactStagingDirectory = '.', - [string] $DSCSourceFolder = 'DSC', - [switch] $ValidateOnly -) - -try { - [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0') -} catch { } - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -function Format-ValidationOutput { - param ($ValidationOutput, [int] $Depth = 0) - Set-StrictMode -Off - return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) }) -} - -$OptionalParameters = New-Object -TypeName Hashtable -$TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile)) -$TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile)) - -if ($UploadArtifacts) { - # Convert relative paths to absolute paths if needed - $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory)) - $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder)) - - # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present - $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json - if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) { - $JsonParameters = $JsonParameters.parameters - } - $ArtifactsLocationName = '_artifactsLocation' - $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken' - $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore - $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore - - # Create DSC configuration archive - if (Test-Path $DSCSourceFolder) { - $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName}) - foreach ($DSCSourceFilePath in $DSCSourceFilePaths) { - $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip' - Publish-AzVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose - } - } - - # Create a storage account name if none was provided - if ($StorageAccountName -eq '') { - $StorageAccountName = 'stage' + ((Get-AzContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19) - } - - $StorageAccount = (Get-AzStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}) - - # Create the storage account if it doesn't already exist - if ($StorageAccount -eq $null) { - $StorageResourceGroupName = 'ARM_Deploy_Staging' - New-AzResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force - $StorageAccount = New-AzStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation" - } - - # Generate the value for artifacts location if it is not provided in the parameter file - if ($OptionalParameters[$ArtifactsLocationName] -eq $null) { - $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName + '/' - } - - # Copy files from the local storage staging location to the storage account container - New-AzStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1 - - $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName} - foreach ($SourcePath in $ArtifactFilePaths) { - Set-AzStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) ` - -Container $StorageContainerName -Context $StorageAccount.Context -Force - } - - # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file - if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) { - $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force ` - (New-AzStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4)) - } -} - -# Create the resource group only when it doesn't already exist -if ((Get-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue) -eq $null) { - New-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop -} - -if ($ValidateOnly) { - $ErrorMessages = Format-ValidationOutput (Test-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName ` - -TemplateFile $TemplateFile ` - -TemplateParameterFile $TemplateParametersFile ` - @OptionalParameters) - if ($ErrorMessages) { - Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.' - } - else { - Write-Output '', 'Template is valid.' - } -} -else { - New-AzResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) ` - -ResourceGroupName $ResourceGroupName ` - -TemplateFile $TemplateFile ` - -TemplateParameterFile $TemplateParametersFile ` - @OptionalParameters ` - -Force -Verbose ` - -ErrorVariable ErrorMessages - if ($ErrorMessages) { - Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") }) - } -} \ No newline at end of file diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Deployment.targets b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Deployment.targets deleted file mode 100644 index 0d792ec6..00000000 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Deployment.targets +++ /dev/null @@ -1,123 +0,0 @@ - - - - Debug - AnyCPU - bin\$(Configuration)\ - false - true - false - None - obj\ - $(BaseIntermediateOutputPath)\ - $(BaseIntermediateOutputPath)$(Configuration)\ - $(IntermediateOutputPath)ProjectReferences - $(ProjectReferencesOutputPath)\ - true - - - - false - false - - - - - - - - - - - Always - - - Never - - - false - Build - - - - - - - - _GetDeploymentProjectContent; - _CalculateContentOutputRelativePaths; - _GetReferencedProjectsOutput; - _CalculateArtifactStagingDirectory; - _CopyOutputToArtifactStagingDirectory; - - - - - - - - - - - - - - - - - Configuration=$(Configuration);Platform=$(Platform) - - - - - - - $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)')) - - - - - - - $(OutDir) - $(OutputPath) - $(ArtifactStagingDirectory)\ - $(ArtifactStagingDirectory)staging\ - $(Build_StagingDirectory) - - - - - - - <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity) - <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', '')) - - - - - $(_RelativePath) - - - - - - - - - PrepareForRun - - - - - - - - - - - diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Saas.SignupAdministration.Web.Deployment.deployproj b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Saas.SignupAdministration.Web.Deployment.deployproj deleted file mode 100644 index 7bb4b85a..00000000 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/Saas.SignupAdministration.Web.Deployment.deployproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Debug - AnyCPU - - - Release - AnyCPU - - - - 6a0836ed-483e-4f7e-8248-ac00fb52e778 - - - - - - - - - - - - - - - False - - - - - \ No newline at end of file diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/azuredeploy.json b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/azuredeploy.json deleted file mode 100644 index 2596356d..00000000 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/azuredeploy.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "SaasProviderName": { - "type": "string", - "defaultValue": "contoso" - }, - "SaasEnvironment": { - "type": "string", - "defaultValue": "dev", - "allowedValues": [ - "dev", - "staging", - "test", - "prod" - ] - }, - "SaasLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for the Cosmos DB account." - } - }, - "SaasInstanceNumber": { - "type": "string", - "defaultValue": "001" - }, - "CosmosDbEndpoint": { - "type": "string" - }, - "CosmosDbAccountKey": { - "type": "string", - "metadata": { - "description": "The account key output of CosmosDB" - } - }, - "CosmosDbAccountName": { - "type": "string", - "defaultValue": "[concat('cosmos-', parameters('SaaSProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]", - "metadata": { - "description": "Cosmos DB account name" - } - }, - "CosmosDbDatabaseName": { - "type": "string", - "defaultValue": "azuresaas", - "metadata": { - "description": "The name for the Core (SQL) database" - } - }, - "CosmosDbConnectionString": { - "type": "string" - }, - "IdentityDbConnectionString": { - "type": "string" - }, - "CatalogDbConnectionString": { - "type": "string" - } - }, - "variables": { - "appServicePlanName": "[concat('app-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]", - "providerWebAppName": "[concat('app-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]" - }, - "resources": [ - { - "name": "[variables('appServicePlanName')]", - "type": "Microsoft.Web/serverfarms", - "location": "[resourceGroup().location]", - "apiVersion": "2015-08-01", - "sku": { - "name": "F1" - }, - "dependsOn": [], - "tags": { - "displayName": "SaaS Provider App Service Plan" - }, - "properties": { - "name": "[variables('appServicePlanName')]", - "numberOfWorkers": 1 - }, - "resources": [ - { - "name": "[variables('providerWebAppName')]", - "type": "Microsoft.Web/sites", - "location": "[resourceGroup().location]", - "apiVersion": "2015-08-01", - "dependsOn": [ - "[concat('Microsoft.Web/serverFarms/', variables('appServicePlanName'))]" - ], - "tags": { - "displayName": "SaaS Provider Web App" - }, - "properties": { - "name": "[variables('providerWebAppName')]", - "serverFarmId": "[resourceId(resourceGroup().name, 'Microsoft.Web/serverFarms', variables('appServicePlanName'))]", - "siteConfig": { - "netFrameworkVersion": "v5.0", - "appSettings": [ - { - "name": "ASPNETCORE_ENVIRONMENT", - "value": "Production" - }, - { - "name": "AppSettings:CosmosDb:Account", - "value": "[parameters('CosmosDbEndpoint')]" - }, - { - "name": "AppSettings:CosmosDb:Key", - "value": "[parameters('CosmosDbAccountKey')]" - }, - { - "name": "AppSettings:CosmosDb:DatabaseName", - "value": "[parameters('CosmosDbDatabaseName')]" - }, - { - "name": "AppSettings:CosmosDb:ContainerName", - "value": "OnboardingFlow" - }, - { - "name": "AppSettings:OnboardingApiBaseUrl", - "value": "[concat('https://api-onboarding-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'), '.azurewebsites.net/')]" - } - ], - "connectionStrings": [ - { - "name": "CosmosDb", - "connectionString": "[parameters('CosmosDbConnectionString')]", - "type": "Custom" - }, - { - "name": "IdentityDbConnection", - "connectionString": "[parameters('IdentityDbConnectionString')]", - "type": "SQLAzure" - } - ] - } - }, - "resources": [ - { - "name": "MSDeploy", - "type": "extensions", - "location": "[resourceGroup().location]", - "apiVersion": "2015-08-01", - "dependsOn": [ "[resourceId('Microsoft.Web/sites', variables('providerWebAppName'))]" ], - "tags": { "displayName": "Deploy" }, - "properties": { - "packageUri": "https://stsaasdev001.blob.core.windows.net/artifacts/saas-provider/Saas.SignupAdministration.Web.zip?sv=2020-04-08&st=2021-06-07T19%3A23%3A20Z&se=2022-06-08T19%3A23%3A00Z&sr=c&sp=rl&sig=kNf0qwTfaCJg02xYeUHlfmHOJvI1bGU1HftjUJ5hl5o%3D" - } - } - ] - } - ] - } - ], - "outputs": { - } -} \ No newline at end of file diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/azuredeploy.parameters.json b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/azuredeploy.parameters.json deleted file mode 100644 index 34cbd1cb..00000000 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web.Deployment/azuredeploy.parameters.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "SaasProviderName": { - "value": "org1" - }, - "SaasEnvironment": { - "value": "dev" - }, - "SaasInstanceNumber": { - "value": "028" - }, - "SaasLocation": { - "value": "eastus" - }, - "CosmosDbConnectionString": { - "value": "AccountEndpoint=https://cosmos-org1-dev-028.documents.azure.com:443/;AccountKey=J92lmfHRM5pGvUJxEqhDqa5LuwGtcMntoV4j9x0sMin52sgMyQKflkFR9kMYtwOockyMVMcYbE4wK6eK1jbL7Q==;" - }, - "CosmosDbAccountKey": { - "value": "J92lmfHRM5pGvUJxEqhDqa5LuwGtcMntoV4j9x0sMin52sgMyQKflkFR9kMYtwOockyMVMcYbE4wK6eK1jbL7Q==" - }, - "CosmosDbEndpoint": { - "value": "https://cosmos-org1-dev-028.documents.azure.com:443/" - }, - "CosmosDbDatabaseName": { - "value": "azuresaas028" - } - } -} \ No newline at end of file diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/HomeController.cs b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/HomeController.cs index 1bde720c..8525bc37 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/HomeController.cs +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/HomeController.cs @@ -1,46 +1,45 @@ using System.Diagnostics; -namespace Saas.SignupAdministration.Web.Controllers +namespace Saas.SignupAdministration.Web.Controllers; + +[AllowAnonymous] +public class HomeController : Controller { - [AllowAnonymous] - public class HomeController : Controller + private readonly ILogger _logger; + + public HomeController(ILogger logger) { - private readonly ILogger _logger; + _logger = logger; + } - public HomeController(ILogger logger) - { - _logger = logger; - } + [HttpGet] + public IActionResult Help() + { + return View(); + } - [HttpGet] - public IActionResult Help() - { - return View(); - } + [HttpGet] + [HttpPost] + public IActionResult Index() + { + return View(); + } - [HttpGet] - [HttpPost] - public IActionResult Index() - { - return View(); - } + [HttpGet] + public IActionResult Pricing() + { + return View(); + } - [HttpGet] - public IActionResult Pricing() - { - return View(); - } + [HttpGet] + public IActionResult Privacy() + { + return View(); + } - [HttpGet] - public IActionResult Privacy() - { - return View(); - } - - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/OnboardingWorkflowController.cs b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/OnboardingWorkflowController.cs index 147ed691..2125f444 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/OnboardingWorkflowController.cs +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Controllers/OnboardingWorkflowController.cs @@ -1,164 +1,163 @@ using Saas.SignupAdministration.Web.Services.StateMachine; -namespace Saas.SignupAdministration.Web.Controllers +namespace Saas.SignupAdministration.Web.Controllers; + +[Authorize()] +public class OnboardingWorkflowController : Controller { - [Authorize()] - public class OnboardingWorkflowController : Controller + private readonly ILogger _logger; + private readonly OnboardingWorkflowService _onboardingWorkflow; + + public OnboardingWorkflowController(ILogger logger, OnboardingWorkflowService onboardingWorkflow) { - private readonly ILogger _logger; - private readonly OnboardingWorkflowService _onboardingWorkflow; + _logger = logger; + _onboardingWorkflow = onboardingWorkflow; + } - public OnboardingWorkflowController(ILogger logger, OnboardingWorkflowService onboardingWorkflow) + // Step 1 - Submit the organization name + [HttpGet] + public IActionResult OrganizationName() + { + ViewBag.OrganizationName = _onboardingWorkflow.OnboardingWorkflowItem.OrganizationName; + return View(); + } + + // Step 1 - Submit the organization name + [ValidateAntiForgeryToken] + [HttpPost] + public IActionResult OrganizationName(string organizationName) + { + _onboardingWorkflow.OnboardingWorkflowItem.OrganizationName = organizationName; + UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnOrganizationNamePosted); + + return RedirectToAction(SR.OrganizationCategoryAction, SR.OnboardingWorkflowController); + } + + // Step 2 - Organization Category + [Route(SR.OnboardingWorkflowOrganizationCategoryRoute)] + [HttpGet] + public IActionResult OrganizationCategory() + { + ViewBag.CategoryId = _onboardingWorkflow.OnboardingWorkflowItem.CategoryId; + return View(ReferenceData.TenantCategories); + } + + // Step 2 Submitted - Organization Category + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult OrganizationCategoryAsync(int categoryId) + { + _onboardingWorkflow.OnboardingWorkflowItem.CategoryId = categoryId; + UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnOrganizationCategoryPosted); + + return RedirectToAction(SR.TenantRouteNameAction, SR.OnboardingWorkflowController); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult OrganizationCategoryBack(int categoryId) + { + return RedirectToAction(SR.OrganizationNameAction, SR.OnboardingWorkflowController); + } + + // Step 3 - Tenant Route Name + [HttpGet] + public IActionResult TenantRouteName() + { + ViewBag.TenantRouteName = _onboardingWorkflow.OnboardingWorkflowItem.TenantRouteName; + return View(); + } + + // Step 3 Submitted - Tenant Route Name + [HttpPost] + [ValidateAntiForgeryToken] + public async Task TenantRouteName(string tenantRouteName) + { + // Need to check whether the route name exists + if (await _onboardingWorkflow.GetRouteExistsAsync(tenantRouteName)) { - _logger = logger; - _onboardingWorkflow = onboardingWorkflow; - } - - // Step 1 - Submit the organization name - [HttpGet] - public IActionResult OrganizationName() - { - ViewBag.OrganizationName = _onboardingWorkflow.OnboardingWorkflowItem.OrganizationName; - return View(); - } - - // Step 1 - Submit the organization name - [ValidateAntiForgeryToken] - [HttpPost] - public IActionResult OrganizationName(string organizationName) - { - _onboardingWorkflow.OnboardingWorkflowItem.OrganizationName = organizationName; - UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnOrganizationNamePosted); - - return RedirectToAction(SR.OrganizationCategoryAction, SR.OnboardingWorkflowController); - } - - // Step 2 - Organization Category - [Route(SR.OnboardingWorkflowOrganizationCategoryRoute)] - [HttpGet] - public IActionResult OrganizationCategory() - { - ViewBag.CategoryId = _onboardingWorkflow.OnboardingWorkflowItem.CategoryId; - return View(ReferenceData.TenantCategories); - } - - // Step 2 Submitted - Organization Category - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult OrganizationCategoryAsync(int categoryId) - { - _onboardingWorkflow.OnboardingWorkflowItem.CategoryId = categoryId; - UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnOrganizationCategoryPosted); - - return RedirectToAction(SR.TenantRouteNameAction, SR.OnboardingWorkflowController); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult OrganizationCategoryBack(int categoryId) - { - return RedirectToAction(SR.OrganizationNameAction, SR.OnboardingWorkflowController); - } - - // Step 3 - Tenant Route Name - [HttpGet] - public IActionResult TenantRouteName() - { - ViewBag.TenantRouteName = _onboardingWorkflow.OnboardingWorkflowItem.TenantRouteName; + ViewBag.TenantRouteExists = true; + ViewBag.TenantNameEntered = tenantRouteName; return View(); } - // Step 3 Submitted - Tenant Route Name - [HttpPost] - [ValidateAntiForgeryToken] - public async Task TenantRouteName(string tenantRouteName) - { - // Need to check whether the route name exists - if (await _onboardingWorkflow.GetRouteExistsAsync(tenantRouteName)) - { - ViewBag.TenantRouteExists = true; - ViewBag.TenantNameEntered = tenantRouteName; - return View(); - } + _onboardingWorkflow.OnboardingWorkflowItem.TenantRouteName = tenantRouteName; + UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnTenantRouteNamePosted); - _onboardingWorkflow.OnboardingWorkflowItem.TenantRouteName = tenantRouteName; - UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnTenantRouteNamePosted); + return RedirectToAction(SR.ServicePlansAction, SR.OnboardingWorkflowController); + } - return RedirectToAction(SR.ServicePlansAction, SR.OnboardingWorkflowController); - } + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult TenantRouteNameBack(string tenantRouteName) + { + return RedirectToAction(SR.OrganizationCategoryAction, SR.OnboardingWorkflowController); + } - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult TenantRouteNameBack(string tenantRouteName) - { - return RedirectToAction(SR.OrganizationCategoryAction, SR.OnboardingWorkflowController); - } + // Step 4 - Service Plan + [HttpGet] + public IActionResult ServicePlans() + { + return View(); + } - // Step 4 - Service Plan - [HttpGet] - public IActionResult ServicePlans() - { - return View(); - } + // Step 4 Submitted - Service Plan + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult ServicePlans(int productId) + { + _onboardingWorkflow.OnboardingWorkflowItem.ProductId = productId; + UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnServicePlanPosted); - // Step 4 Submitted - Service Plan - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult ServicePlans(int productId) - { - _onboardingWorkflow.OnboardingWorkflowItem.ProductId = productId; - UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnServicePlanPosted); + return RedirectToAction(SR.ConfirmationAction, SR.OnboardingWorkflowController); + } - return RedirectToAction(SR.ConfirmationAction, SR.OnboardingWorkflowController); - } + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult ServicePlansBack() + { + return RedirectToAction(SR.TenantRouteNameAction, SR.OnboardingWorkflowController); + } - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult ServicePlansBack() - { - return RedirectToAction(SR.TenantRouteNameAction, SR.OnboardingWorkflowController); - } + // Step 5 - Tenant Created Confirmation + [HttpGet] + public async Task Confirmation() + { + // Deploy the Tenant + await DeployTenantAsync(); + return View(); + } - // Step 5 - Tenant Created Confirmation - [HttpGet] - public async Task Confirmation() - { - // Deploy the Tenant - await DeployTenantAsync(); - return View(); - } + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult LastAction(int categoryId) + { + var action = GetAction(); + return RedirectToAction(action, SR.OnboardingWorkflowController); + } - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult LastAction(int categoryId) - { - var action = GetAction(); - return RedirectToAction(action, SR.OnboardingWorkflowController); - } + private async Task DeployTenantAsync() + { + await _onboardingWorkflow.OnboardTenant(); - private async Task DeployTenantAsync() - { - await _onboardingWorkflow.OnboardTenant(); + UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnTenantDeploymentSuccessful); + } - UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers.OnTenantDeploymentSuccessful); - } + private void UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers trigger) + { + _onboardingWorkflow.TransitionState(trigger); + _onboardingWorkflow.PersistToSession(); + } - private void UpdateOnboardingSessionAndTransitionState(OnboardingWorkflowState.Triggers trigger) - { - _onboardingWorkflow.TransitionState(trigger); - _onboardingWorkflow.PersistToSession(); - } + private string GetAction() + { + var action = SR.OrganizationNameAction; - private string GetAction() - { - var action = SR.OrganizationNameAction; + if (!String.IsNullOrEmpty(_onboardingWorkflow.OnboardingWorkflowItem.TenantRouteName)) + action = SR.ServicePlansAction; + else if (_onboardingWorkflow.OnboardingWorkflowItem.CategoryId > 0) + action = SR.TenantRouteNameAction; - if (!String.IsNullOrEmpty(_onboardingWorkflow.OnboardingWorkflowItem.TenantRouteName)) - action = SR.ServicePlansAction; - else if (_onboardingWorkflow.OnboardingWorkflowItem.CategoryId > 0) - action = SR.TenantRouteNameAction; - - return action; - } + return action; } } diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Program.cs b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Program.cs index 8a2fb613..5ea888d6 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Program.cs +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Program.cs @@ -1,36 +1,73 @@ using Azure.Identity; using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; -using Microsoft.IdentityModel.Logging; using Saas.SignupAdministration.Web; using Microsoft.AspNetCore.HttpOverrides; using Saas.Application.Web; +using System.Reflection; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Saas.Shared.Options; +using Microsoft.IdentityModel.Logging; +using System.Configuration; +using Microsoft.Extensions.DependencyInjection; + +// Hint: For debugging purposes: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/wiki/PII +// IdentityModelEventSource.ShowPII = true; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddApplicationInsightsTelemetry(); -if (builder.Environment.IsProduction()) +string projectName = Assembly.GetCallingAssembly().GetName().Name + ?? throw new NullReferenceException("Project name cannot be null."); + +var logger = LoggerFactory.Create(config => config.AddConsole()).CreateLogger(projectName); + +logger.LogInformation("001"); + +/* IMPORTANT + In the configuration pattern used here, we're seeking to minimize the use of appsettings.json, + as well as eliminate the need for storing local secrets. + + Instead we're utilizing the Azure App Configuration service for storing settings and the Azure Key Vault to store secrets. + Azure App Configuration still hold references to the secret, but not the secret themselves. + + This approach is more secure and allows us to have a single source of truth + for all settings and secrets. + + The settings and secrets are provisioned by the deployment script made available for deploying this service. + Please see the readme for the project for details. + + For local development, please see the ASDK Permission Service readme.md for more + on how to set up and run this service in a local development environment - i.e., a local dev machine. +*/ + +if (builder.Environment.IsDevelopment()) { - // Get Secrets From Azure Key Vault if in production. If not in production, secrets are automatically loaded in from the .NET secrets manager - // https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0 - - // We don't want to fetch all the secrets for the other microservices in the app/solution, so we only fetch the ones with the prefix of "signupadmin-". - // https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#use-a-key-name-prefix - - builder.Configuration.AddAzureKeyVault( - new Uri(builder.Configuration[SR.KeyVaultProperty]), - new DefaultAzureCredential(), - new CustomPrefixKeyVaultSecretManager("signupadmin")); + InitializeDevEnvironment(); +} +else +{ + InitializeProdEnvironment(); } -builder.Services.AddRazorPages(); +builder.Services.Configure( + builder.Configuration.GetRequiredSection(AdminApiOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(AzureB2CAdminApiOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetRequiredSection(AzureB2CSignupAdminOptions.SectionName)); // Load the app settings -builder.Services.Configure(builder.Configuration.GetSection(SR.AppSettingsProperty)); +builder.Services.Configure( + builder.Configuration.GetSection(SR.AppSettingsProperty)); // Load the email settings -builder.Services.Configure(builder.Configuration.GetSection(SR.EmailOptionsProperty)); +builder.Services.Configure( + builder.Configuration.GetSection(SR.EmailOptionsProperty)); + +builder.Services.AddRazorPages(); builder.Services.AddMvc(); @@ -45,7 +82,7 @@ builder.Services.AddScoped(); // Required for the JsonPersistenceProvider // Should be replaced based on the persistence scheme -builder.Services.AddDistributedMemoryCache(); +builder.Services.AddMemoryCache(); // TODO: Replace with your implementation of persistence provider // Session persistence is the default @@ -54,47 +91,56 @@ builder.Services.AddScoped // Add the user details that come back from B2C builder.Services.AddScoped(); +var adminServiceBaseUrl = builder.Configuration.GetRequiredSection(AzureB2CAdminApiOptions.SectionName) + .Get()?.BaseUrl + ?? throw new NullReferenceException($"ApplicationIdUri cannot be null"); + +var scopes = builder.Configuration.GetRequiredSection(AdminApiOptions.SectionName) + .Get()?.Scopes + ?? throw new NullReferenceException("Scopes cannot be null"); + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, AzureB2CSignupAdminOptions.SectionName) + .EnableTokenAcquisitionToCallDownstreamApi(scopes.Select(scope => $"{adminServiceBaseUrl}/{scope}".Trim('/'))) + .AddSessionTokenCaches(); + +builder.Services.Configure(builder.Configuration.GetSection(AzureB2CSignupAdminOptions.SectionName)); + builder.Services.AddHttpClient() - .ConfigureHttpClient(client => - client.BaseAddress = new Uri(builder.Configuration[SR.AdminServiceBaseUrl])); + .ConfigureHttpClient((serviceProvider, client) => + { + using var scope = serviceProvider.CreateScope(); + var adminServiceBaseUrl = scope.ServiceProvider.GetRequiredService>().Value.BaseUrl + ?? throw new NullReferenceException("Permissions Base Url cannot be null"); + + client.BaseAddress = new Uri(adminServiceBaseUrl); + }); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(10); }); -builder.Services.AddApplicationInsightsTelemetry(builder.Configuration[SR.AppInsightsConnectionProperty]); - -// Azure AD B2C requires scope config with a fully qualified url along with an identifier. To make configuring it more manageable and less -// error prone, we store the names of the scopes separately from the base url with identifier and combine them here. -var adminServiceScopes = builder.Configuration[SR.AdminServiceScopesProperty].Split(" "); -var adminServiceScopeBaseUrl = builder.Configuration[SR.AdminServiceScopeBaseUrlProperty].Trim('/'); -for (var i = 0; i < adminServiceScopes.Length; i++) -{ - adminServiceScopes[i] = String.Format("{0}/{1}", adminServiceScopeBaseUrl, adminServiceScopes[i].Trim('/')); -} - -// Set the newly-constructed form into memory for lookup when contacting Azure AD B2C later -builder.Configuration[SR.AdminServiceScopesProperty] = string.Join(' ', adminServiceScopes); - -builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, Constants.AzureAdB2C) - .EnableTokenAcquisitionToCallDownstreamApi(adminServiceScopes) - .AddSessionTokenCaches(); builder.Services.AddControllersWithViews().AddMicrosoftIdentityUI(); // Configuring appsettings section AzureAdB2C, into IOptions builder.Services.AddOptions(); -builder.Services.Configure(builder.Configuration.GetSection(SR.AzureAdB2CProperty)); + +builder.Services.Configure(builder.Configuration.GetSection(AzureB2CSignupAdminOptions.SectionName)); + + +//builder.Services.Configure(options => +//{ +// options. +//}); // This is required for auth to work correctly when running in a docker container because of SSL Termination // Remove this and the subsequent app.UseForwardedHeaders() line below if you choose to run the app without using containers -builder.Services.Configure(options => -{ - options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; - options.ForwardedProtoHeaderName = "X-Forwarded-Proto"; -}); - +//builder.Services.Configure(options => +//{ +// options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; +// options.ForwardedProtoHeaderName = "X-Forwarded-Proto"; +//}); var app = builder.Build(); @@ -113,27 +159,88 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseSession(); -app.UseForwardedHeaders(); +// app.UseForwardedHeaders(); app.UseCookiePolicy(new CookiePolicyOptions { Secure = CookieSecurePolicy.Always }); + app.UseAuthentication(); app.UseAuthorization(); -app.UseEndpoints(endpoints => -{ - // admin - endpoints.MapControllerRoute( - name: "Admin", - pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); +app.MapControllerRoute( + name: "Admin", + pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); - // default - endpoints.MapControllerRoute(name: SR.DefaultName, pattern: SR.MapControllerRoutePattern); +app.MapControllerRoute(name: SR.DefaultName, pattern: SR.MapControllerRoutePattern); - endpoints.MapRazorPages(); -}); +app.MapRazorPages(); AppHttpContext.Services = ((IApplicationBuilder)app).ApplicationServices; -app.Run(); \ No newline at end of file +app.Run(); + + +/*--------------- + local methods +----------------*/ + +void InitializeDevEnvironment() +{ + // IMPORTANT + // The current version. + // Must corresspond exactly to the version string of our deployment as specificed in the deployment config.json. + var version = "ver0.8.0"; + + logger.LogInformation("Version: {version}", version); + logger.LogInformation($"Is Development."); + + // For local development, use the Secret Manager feature of .NET to store a connection string + // and likewise for storing a secret for the permission-api app. + // https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows + + var appConfigurationconnectionString = builder.Configuration.GetConnectionString("AppConfig") + ?? throw new NullReferenceException("App config missing."); + + // Use the connection string to access Azure App Configuration to get access to app settings stored there. + // To gain access to Azure Key Vault use 'Azure Cli: az login' to log into Azure. + // This login on will also now provide valid access tokens to the local development environment. + // For more details and the option to chain and combine multiple credential options with `ChainedTokenCredential` + // please see: https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#define-a-custom-authentication-flow-with-chainedtokencredential + + AzureCliCredential credential = new(); + + builder.Configuration.AddAzureAppConfiguration(options => + options.Connect(appConfigurationconnectionString) + .ConfigureKeyVault(kv => kv.SetCredential(new ChainedTokenCredential(credential))) + .Select(KeyFilter.Any, version)); // <-- Important: since we're using labels in our Azure App Configuration store + + logger.LogInformation($"Initialization complete."); +} + +void InitializeProdEnvironment() +{ + // For procution environment, we'll configured Managed Identities for managing access Azure App Services + // and Key Vault. The Azure App Services endpoint is stored in an environment variable for the web app. + + var version = builder.Configuration.GetRequiredSection("Version")?.Value + ?? throw new NullReferenceException("The Version value cannot be found. Has the 'Version' environment variable been set correctly for the Web App?"); + + logger.LogInformation("Version: {version}", version); + logger.LogInformation($"Is Production."); + + var appConfigurationEndpoint = builder.Configuration.GetRequiredSection("AppConfiguration:Endpoint")?.Value + ?? throw new NullReferenceException("The Azure App Configuration Endpoint cannot be found. Has the endpoint environment variable been set correctly for the Web App?"); + + // Get the ClientId of the UserAssignedIdentity + // If we don't set this ClientID in the ManagedIdentityCredential constructor, it doesn't know it should use the user assigned managed id. + var managedIdentityClientId = builder.Configuration.GetRequiredSection("UserAssignedManagedIdentityClientId")?.Value + ?? throw new NullReferenceException("The Environment Variable 'UserAssignedManagedIdentityClientId' cannot be null. Check the App Service Configuration."); + + ManagedIdentityCredential userAssignedManagedCredentials = new(managedIdentityClientId); + + builder.Configuration.AddAzureAppConfiguration(options => + options.Connect(new Uri(appConfigurationEndpoint), userAssignedManagedCredentials) + .ConfigureKeyVault(kv => kv.SetCredential(userAssignedManagedCredentials)) + .Select(KeyFilter.Any, version)); // <-- Important since we're using labels in our Azure App Configuration store +} \ No newline at end of file diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/SR.cs b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/SR.cs index e6c8c62b..f79db096 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/SR.cs +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/SR.cs @@ -1,140 +1,138 @@ -using System; + +namespace Saas.SignupAdministration.Web; -namespace Saas.SignupAdministration.Web +public static class SR { - public static class SR - { - // Internal Data Names - public const string OnboardingWorkflowName = "Onboarding Workflow"; + // Internal Data Names + public const string OnboardingWorkflowName = "Onboarding Workflow"; - // Prompts - public const string AutomotiveMobilityAndTransportationPrompt = "Automotive, Mobility & Transportation"; - public const string EnergyAndSustainabilityPrompt = "Energy & Sustainability"; - public const string FinancialServicesPrompt = "Financial Services"; - public const string HealthcareAndLifeSciencesPrompt = "Healthcare & Life Sciences"; - public const string ManufacturingAndSupplyChainPrompt = "Manufacturing & Supply Chain"; - public const string MediaAndCommunicationsPrompt = "Media & Communications"; - public const string PublicSectorPrompt = "Public Sector"; - public const string RetailAndConsumerGoodsPrompt = "Retail & Consumer Goods"; - public const string SoftwarePrompt = "Software"; - public const string EmailPrompt = "Email"; - public const string PasswordPrompt = "Password"; + // Prompts + public const string AutomotiveMobilityAndTransportationPrompt = "Automotive, Mobility & Transportation"; + public const string EnergyAndSustainabilityPrompt = "Energy & Sustainability"; + public const string FinancialServicesPrompt = "Financial Services"; + public const string HealthcareAndLifeSciencesPrompt = "Healthcare & Life Sciences"; + public const string ManufacturingAndSupplyChainPrompt = "Manufacturing & Supply Chain"; + public const string MediaAndCommunicationsPrompt = "Media & Communications"; + public const string PublicSectorPrompt = "Public Sector"; + public const string RetailAndConsumerGoodsPrompt = "Retail & Consumer Goods"; + public const string SoftwarePrompt = "Software"; + public const string EmailPrompt = "Email"; + public const string PasswordPrompt = "Password"; - // Service Plans + // Service Plans - public const string FreePlan = "Free"; - public const string BasicPlan = "Basic"; - public const string StandardPlan = "Standard"; + public const string FreePlan = "Free"; + public const string BasicPlan = "Basic"; + public const string StandardPlan = "Standard"; - // API Route Template - public const string ApiRouteTemplate = "api/[controller]"; + // API Route Template + public const string ApiRouteTemplate = "api/[controller]"; - // Tenant Variable Template - public const string TenantTemplate = "{tenant}"; + // Tenant Variable Template + public const string TenantTemplate = "{tenant}"; - // Password Error Message Template - public const string PasswordErrorMessageTemplate = "The {0} must be at least {2} and at max {1} characters long."; + // Password Error Message Template + public const string PasswordErrorMessageTemplate = "The {0} must be at least {2} and at max {1} characters long."; - // Controller Names - public const string OnboardingWorkflowController = "OnboardingWorkflow"; + // Controller Names + public const string OnboardingWorkflowController = "OnboardingWorkflow"; - // Controller Actions - public const string OrganizationNameAction = "OrganizationName"; - public const string OrganizationCategoryAction = "OrganizationCategory"; - public const string ServicePlansAction = "ServicePlans"; - public const string DeployTenantAction = "DeployTenant"; - public const string ConfirmationAction = "Confirmation"; - public const string MerchantAction = "Merchant"; - public const string IndexAction = "Index"; - public const string UsernameAction = "Username"; - public const string TenantRouteNameAction = "TenantRouteName"; + // Controller Actions + public const string OrganizationNameAction = "OrganizationName"; + public const string OrganizationCategoryAction = "OrganizationCategory"; + public const string ServicePlansAction = "ServicePlans"; + public const string DeployTenantAction = "DeployTenant"; + public const string ConfirmationAction = "Confirmation"; + public const string MerchantAction = "Merchant"; + public const string IndexAction = "Index"; + public const string UsernameAction = "Username"; + public const string TenantRouteNameAction = "TenantRouteName"; - // Controller Routes - public const string OnboardingWorkflowDeployRoute = "/" + OnboardingWorkflowController + "/" + DeployTenantAction; - public const string OnboardingWorkflowConfirmationRoute = "/" + OnboardingWorkflowController + "/" + ConfirmationAction; - public const string OnboardingWorkflowOrganizationNameRoute = "/" + OnboardingWorkflowController + "/" + OrganizationNameAction; - public const string OnboardingWorkflowOrganizationCategoryRoute = "/" + OnboardingWorkflowController + "/" + OrganizationCategoryAction; - public const string OnboardingWorkflowServicePlansRoute = "/" + OnboardingWorkflowController + "/" + ServicePlansAction; - public const string OnboardingWorkflowUsernameRoute = "/" + OnboardingWorkflowController + "/" + UsernameAction; - public const string TenantRoute = "/" + SR.TenantTemplate; + // Controller Routes + public const string OnboardingWorkflowDeployRoute = "/" + OnboardingWorkflowController + "/" + DeployTenantAction; + public const string OnboardingWorkflowConfirmationRoute = "/" + OnboardingWorkflowController + "/" + ConfirmationAction; + public const string OnboardingWorkflowOrganizationNameRoute = "/" + OnboardingWorkflowController + "/" + OrganizationNameAction; + public const string OnboardingWorkflowOrganizationCategoryRoute = "/" + OnboardingWorkflowController + "/" + OrganizationCategoryAction; + public const string OnboardingWorkflowServicePlansRoute = "/" + OnboardingWorkflowController + "/" + ServicePlansAction; + public const string OnboardingWorkflowUsernameRoute = "/" + OnboardingWorkflowController + "/" + UsernameAction; + public const string TenantRoute = "/" + SR.TenantTemplate; - // Session Variables - public const string TenantId = "TenantId"; + // Session Variables + public const string TenantId = "TenantId"; - // Header Variables - public const string XApiKey = "X-Api-Key"; + // Header Variables + public const string XApiKey = "X-Api-Key"; - // Onboarding Workflow Properties - public const string OnboardingWorkflowIdProperty = "id"; - public const string OnboardingWorkflowNameProperty = "onboardingWorkflowName"; - public const string OnboardingWorkflowTenantNameProperty = "tenantName"; - public const string OnboardingWorkflowUserIdProperty = "userId"; - public const string OnboardingWorkflowIsExistingUserProperty = "isExistingUser"; - public const string OnboardingWorkflowCategoryIdProperty = "categoryId"; - public const string OnboardingWorkflowProductIdProperty = "productId"; - public const string OnboardingWorkflowIsCompleteProperty = "isComplete"; - public const string OnboardingWorkflowIpAddressProperty = "ipAddress"; - public const string OnboardingWorkflowCreatedProperty = "created"; - public const string OnboardingWorkflowStateProperty = "state"; - public const string OnboardingWorkflowEmailAddressProperty = "emailAddress"; - public const string OnboardingWorkflowOrganizationNameProperty = "organizationName"; - public const string OnboardingWorkflowTenantRouteNameProperty = "tenantRouteName"; - public const string OnboardingWorkflowStateCurrentStateProperty = "currentState"; - public const string OnboardingWorkflowIsActiveProperty = "isActive"; - public const string OnboardingWorkflowIsCancelledProperty = "isCancelled"; - public const string OnboardingWorkflowIsProvisionedProperty = "isProvisioned"; + // Onboarding Workflow Properties + public const string OnboardingWorkflowIdProperty = "id"; + public const string OnboardingWorkflowNameProperty = "onboardingWorkflowName"; + public const string OnboardingWorkflowTenantNameProperty = "tenantName"; + public const string OnboardingWorkflowUserIdProperty = "userId"; + public const string OnboardingWorkflowIsExistingUserProperty = "isExistingUser"; + public const string OnboardingWorkflowCategoryIdProperty = "categoryId"; + public const string OnboardingWorkflowProductIdProperty = "productId"; + public const string OnboardingWorkflowIsCompleteProperty = "isComplete"; + public const string OnboardingWorkflowIpAddressProperty = "ipAddress"; + public const string OnboardingWorkflowCreatedProperty = "created"; + public const string OnboardingWorkflowStateProperty = "state"; + public const string OnboardingWorkflowEmailAddressProperty = "emailAddress"; + public const string OnboardingWorkflowOrganizationNameProperty = "organizationName"; + public const string OnboardingWorkflowTenantRouteNameProperty = "tenantRouteName"; + public const string OnboardingWorkflowStateCurrentStateProperty = "currentState"; + public const string OnboardingWorkflowIsActiveProperty = "isActive"; + public const string OnboardingWorkflowIsCancelledProperty = "isCancelled"; + public const string OnboardingWorkflowIsProvisionedProperty = "isProvisioned"; - // Session Keys - public const string OnboardingWorkflowKey = "OnboardingWorkflow"; - public const string OnboardingWorkflowItemKey = "OnboardingWorkflowItem"; - public const string OnboardingWorkflowStateKey = "OnboardingWorkflowState"; + // Session Keys + public const string OnboardingWorkflowKey = "OnboardingWorkflow"; + public const string OnboardingWorkflowItemKey = "OnboardingWorkflowItem"; + public const string OnboardingWorkflowStateKey = "OnboardingWorkflowState"; - // AppSettings Properties - public const string CatalogDbConnectionProperty = "ConnectionStrings:CatalogDbConnection"; + // AppSettings Properties + public const string CatalogDbConnectionProperty = "ConnectionStrings:CatalogDbConnection"; - // Catalog DB Prperties - public const string CatalogTenantIdProperty = "TenantId"; - public const string CatalogTenantIdParameter = "@" + CatalogTenantIdProperty; - public const string CatalogIdProperty = "Id"; - public const string CatalogCustomerNameProperty = "CustomerName"; - public const string CatalogIsActiveProperty = "IsActive"; - public const string CatalogApiKeyParameter = "@apiKey"; - public const string CatalogCustomerSelectQuery = "SELECT * FROM dbo.Customer Where TenantId = " + CatalogTenantIdParameter; - public const string CatalogTenantSelectQuery = "SELECT Id FROM Tenant WHERE ApiKey = " + CatalogApiKeyParameter; + // Catalog DB Prperties + public const string CatalogTenantIdProperty = "TenantId"; + public const string CatalogTenantIdParameter = "@" + CatalogTenantIdProperty; + public const string CatalogIdProperty = "Id"; + public const string CatalogCustomerNameProperty = "CustomerName"; + public const string CatalogIsActiveProperty = "IsActive"; + public const string CatalogApiKeyParameter = "@apiKey"; + public const string CatalogCustomerSelectQuery = "SELECT * FROM dbo.Customer Where TenantId = " + CatalogTenantIdParameter; + public const string CatalogTenantSelectQuery = "SELECT Id FROM Tenant WHERE ApiKey = " + CatalogApiKeyParameter; - // Azure AD Properties - public const string AzureAdAuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0"; + // Azure AD Properties + public const string AzureAdAuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0"; - // Startup Properties - public const string IdentityDbConnectionProperty = "IdentityDbConnection"; - public const string AppSettingsProperty = "AppSettings"; - public const string AppInsightsConnectionProperty = "APPINSIGHTS_CONNECTIONSTRING"; - public const string CosmosDbProperty = "AppSettings:CosmosDb"; - public const string ErrorRoute = "/Home/Error"; - public const string MapControllerRoutePattern = "{controller=Home}/{action=Index}/{ id ?}"; - public const string DefaultName = "default"; - public const string DatabaseNameProperty = "DatabaseName"; - public const string ContainerNameProperty = "ContainerName"; - public const string AccountProperty = "Account"; - public const string KeyProperty = "Key"; - public const string CosmosNamePartitionKey = "/name"; - public const string AdminServiceBaseUrl = "AppSettings:AdminServiceBaseUrl"; - public const string AdminServiceScopesProperty = "AppSettings:AdminServiceScopes"; - public const string AdminServiceScopeBaseUrlProperty = "AppSettings:AdminServiceScopeBaseUrl"; - public const string AzureAdB2CProperty = "AzureAdB2C"; - public const string KeyVaultProperty = "KeyVault:Url"; - public const string EmailOptionsProperty = "EmailOptions"; + // Startup Properties + public const string IdentityDbConnectionProperty = "IdentityDbConnection"; + public const string AppSettingsProperty = "AppSettings"; + // public const string AppInsightsConnectionProperty = "APPINSIGHTS_CONNECTIONSTRING"; + // public const string CosmosDbProperty = "AppSettings:CosmosDb"; + public const string ErrorRoute = "/Home/Error"; + public const string MapControllerRoutePattern = "{controller=Home}/{action=Index}/{ id ?}"; + public const string DefaultName = "default"; + public const string DatabaseNameProperty = "DatabaseName"; + public const string ContainerNameProperty = "ContainerName"; + public const string AccountProperty = "Account"; + public const string KeyProperty = "Key"; + // public const string CosmosNamePartitionKey = "/name"; + // public const string AdminServiceBaseUrl = "AppSettings:AdminServiceBaseUrl"; + //public const string AdminServiceScopesProperty = "AppSettings:AdminServiceScopes"; + //public const string AdminServiceScopeBaseUrlProperty = "AppSettings:AdminServiceScopeBaseUrl"; + //public const string AzureAdB2CProperty = "AzureAdB2C"; + //public const string KeyVaultProperty = "KeyVault:Url"; + public const string EmailOptionsProperty = "EmailOptions"; - // Error Codes - public const string DuplicateUserNameErrorCode = "DuplicateUserName"; + // Error Codes + public const string DuplicateUserNameErrorCode = "DuplicateUserName"; - // Claim Types - public const string EmailAddressClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - public const string NameIdentifierClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; - public const string AuthenticationClassReferenceClaimType = "http://schemas.microsoft.com/claims/authnclassreference"; - public const string AuthenticationTimeClaimType = "auth_time"; - public const string GivenNamClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"; - public const string SurnameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"; - public const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid"; - } + // Claim Types + public const string EmailAddressClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; + public const string NameIdentifierClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; + public const string AuthenticationClassReferenceClaimType = "http://schemas.microsoft.com/claims/authnclassreference"; + public const string AuthenticationTimeClaimType = "auth_time"; + public const string GivenNamClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"; + public const string SurnameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"; + public const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid"; } diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Saas.SignupAdministration.Web.csproj b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Saas.SignupAdministration.Web.csproj index 44ec68b1..fc4679f9 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Saas.SignupAdministration.Web.csproj +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Saas.SignupAdministration.Web.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 false 7b599cf5-3102-4740-ab34-69dd240f9ea3 /subscriptions/357c83c2-bed7-4fe7-af6a-95835c6e2d91/resourceGroups/rg-saas-dev-001/providers/microsoft.insights/components/app-provider-dev-001 @@ -17,16 +17,17 @@ - + - - + + - - - - - + + + + + + @@ -34,4 +35,8 @@ + + + + diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Services/Clients/OAuthBaseClient.cs b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Services/Clients/OAuthBaseClient.cs index f18dac65..206fe664 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Services/Clients/OAuthBaseClient.cs +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.Web/Services/Clients/OAuthBaseClient.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Options; -using Microsoft.Identity.Web; -using System.Net.Http; +using System.Net.Http; using System.Threading; namespace Saas.SignupAdministration.Web.Services; diff --git a/src/Saas.SignupAdministration/Saas.SignupAdministration.sln b/src/Saas.SignupAdministration/Saas.SignupAdministration.sln index 8a0c11e2..e5e9105a 100644 --- a/src/Saas.SignupAdministration/Saas.SignupAdministration.sln +++ b/src/Saas.SignupAdministration/Saas.SignupAdministration.sln @@ -1,11 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31112.23 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.SignupAdministration.Web", "Saas.SignupAdministration.Web\Saas.SignupAdministration.Web.csproj", "{42415C21-8089-4DA0-8803-4C69C4FFC12F}" EndProject -Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "Saas.SignupAdministration.Web.Deployment", "Saas.SignupAdministration.Web.Deployment\Saas.SignupAdministration.Web.Deployment.deployproj", "{DE61C194-11B4-4868-92C8-E32F9716232D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Shared", "..\Saas.Lib\Saas.Shared\Saas.Shared.csproj", "{7611AF38-07BB-47AA-A002-9BBA79444E90}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,10 +17,10 @@ Global {42415C21-8089-4DA0-8803-4C69C4FFC12F}.Debug|Any CPU.Build.0 = Debug|Any CPU {42415C21-8089-4DA0-8803-4C69C4FFC12F}.Release|Any CPU.ActiveCfg = Release|Any CPU {42415C21-8089-4DA0-8803-4C69C4FFC12F}.Release|Any CPU.Build.0 = Release|Any CPU - {DE61C194-11B4-4868-92C8-E32F9716232D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE61C194-11B4-4868-92C8-E32F9716232D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE61C194-11B4-4868-92C8-E32F9716232D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE61C194-11B4-4868-92C8-E32F9716232D}.Release|Any CPU.Build.0 = Release|Any CPU + {7611AF38-07BB-47AA-A002-9BBA79444E90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7611AF38-07BB-47AA-A002-9BBA79444E90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7611AF38-07BB-47AA-A002-9BBA79444E90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7611AF38-07BB-47AA-A002-9BBA79444E90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Saas.SignupAdministration/deployment/Bicep/Parameters/.gitignore b/src/Saas.SignupAdministration/deployment/Bicep/Parameters/.gitignore new file mode 100644 index 00000000..39a81f30 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/Bicep/Parameters/.gitignore @@ -0,0 +1,2 @@ +*-parameters.json +identity-foundation-outputs.json \ No newline at end of file diff --git a/src/Saas.SignupAdministration/deployment/Bicep/Parameters/parameters-template.json b/src/Saas.SignupAdministration/deployment/Bicep/Parameters/parameters-template.json new file mode 100644 index 00000000..d90c44f3 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/Bicep/Parameters/parameters-template.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/src/Saas.SignupAdministration/deployment/Bicep/deployAppService.bicep b/src/Saas.SignupAdministration/deployment/Bicep/deployAppService.bicep new file mode 100644 index 00000000..1998c3a7 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/Bicep/deployAppService.bicep @@ -0,0 +1,51 @@ +@description('The SaaS Signup Administration web site name.') +param signupadminapp string + +@description('Version') +param version string + +@description('Environment') +@allowed([ + 'Development' + 'Staging' + 'Production' +]) +param environment string + +@description('The App Service Plan ID.') +param appServicePlanName string + +@description('The Uri of the Key Vault.') +param keyVaultUri string + +@description('The location for all resources.') +param location string + +@description('Azure App Configuration User Assigned Identity Name.') +param userAssignedIdentityName string + +@description('The name of the Azure App Configuration.') +param appConfigurationName string + +@description('The name of the Log Analytics Workspace used by Application Insigths.') +param logAnalyticsWorkspaceName string + +@description('The name of Application Insights.') +param applicationInsightsName string + +module signupAdministrationWebApp './../../../Saas.Lib/Saas.Bicep.Module/appServiceModuleWithObservability.bicep' = { + name: 'signupAdministrationWebApp' + params: { + appServiceName: signupadminapp + version: version + environment: environment + appServicePlanName: appServicePlanName + keyVaultUri: keyVaultUri + location: location + userAssignedIdentityName: userAssignedIdentityName + appConfigurationName: appConfigurationName + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + applicationInsightsName: applicationInsightsName + } +} + diff --git a/src/Saas.SignupAdministration/deployment/Bicep/deployConfigEntries.bicep b/src/Saas.SignupAdministration/deployment/Bicep/deployConfigEntries.bicep new file mode 100644 index 00000000..16ac2c99 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/Bicep/deployConfigEntries.bicep @@ -0,0 +1,150 @@ +@description('Version') +param version string + +@description('The name of the key vault') +param keyVaultName string + +@description('The URI of the key vault.') +param keyVaultUri string + +@description('Azure B2C Domain Name.') +param azureB2CDomain string + +@description('Azure B2C Tenant Id.') +param azureB2cTenantId string + +@description('Azure AD Instance') +param azureAdInstance string + +@description('The Azure B2C Signed Out Call Back Path.') +param signedOutCallBackPath string + +@description('The Azure B2C Sign up/in Policy Id.') +param signUpSignInPolicyId string + +@description('The Client Id found on registered Permissions API app page.') +param clientId string + +@description('User Identity Name') +param userAssignedIdentityName string + +@description('App Configuration Name') +param appConfigurationName string + +@description('Indicates the Authentication type for new identity') +param authenticationType string + +@description('Type of the claim to use in the new Identity, works alongside built-in') +param roleClaimType string + +@description('Name of the claim custom roles are in') +param sourceClaimType string + +@description('The name of the certificate key.') +param certificateKeyName string + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' existing = { + name: userAssignedIdentityName +} + +var certificates = [ + { + SourceType: keyVaultName + KeyVaultUrl: keyVaultUri + KeyVaultCertificateName: certificateKeyName + } +] + +var azureB2CKeyName = 'AzureB2C' +var signupAdminKeyName = 'SignupAdmin' +var claimToRoleTransformerKeyName = 'ClaimToRoleTransformer' + +var appConfigStore = { + appConfigurationName: appConfigurationName + keyVaultName: keyVaultName + userAssignedIdentityName: userAssignedIdentity.name + label: version + entries: [ + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:ClientCertificates' + value: ' ${string(certificates)}' // notice the space before the string, this is a necessary hack. https://github.com/Azure/bicep/issues/6167 + isSecret: false + contentType: 'application/json' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:ClientId' + value: clientId + isSecret: false + contentType: 'text/plain' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:TenantId' + value: azureB2cTenantId + isSecret: false + contentType: 'text/plain' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:Domain' + value: azureB2CDomain + isSecret: false + contentType: 'text/plain' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:Instance' + value: azureAdInstance + isSecret: false + contentType: 'text/plain' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:Audience' + value: clientId + isSecret: false + contentType: 'text/plain' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:SignedOutCallbackPath' + value: signedOutCallBackPath + isSecret: false + contentType: 'text/plain' + } + { + key: '${signupAdminKeyName}:${azureB2CKeyName}:SignUpSignInPolicyId' + value: signUpSignInPolicyId + isSecret: false + contentType: 'text/plain' + } + { + key: '${claimToRoleTransformerKeyName}:AuthenticationType' + value: authenticationType + isSecret: false + contentType: 'text/plain' + } + { + key: '${claimToRoleTransformerKeyName}:RoleClaimType' + value: roleClaimType + isSecret: false + contentType: 'text/plain' + } + { + key: '${claimToRoleTransformerKeyName}:SourceClaimType' + value: sourceClaimType + isSecret: false + contentType: 'text/plain' + } + ] +} + +// Adding App Configuration entries +module appConfigurationSettings './../../../Saas.Lib/Saas.Bicep.Module/addConfigEntry.bicep' = [ for entry in appConfigStore.entries: { + name: replace('Entry-${entry.key}', ':', '-') + params: { + appConfigurationName: appConfigStore.appConfigurationName + userAssignedIdentityName: appConfigStore.userAssignedIdentityName + keyVaultName: keyVaultName + value: entry.value + contentType: entry.contentType + keyName: entry.key + label: appConfigStore.label + isSecret: entry.isSecret + } +}] diff --git a/src/Saas.SignupAdministration/deployment/act/.actrc b/src/Saas.SignupAdministration/deployment/act/.actrc new file mode 100644 index 00000000..dea14d6f --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/.actrc @@ -0,0 +1 @@ +-P ubuntu-latest=act-container:latest \ No newline at end of file diff --git a/src/Saas.SignupAdministration/deployment/act/.gitignore b/src/Saas.SignupAdministration/deployment/act/.gitignore new file mode 100644 index 00000000..9f2eaf0d --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/.gitignore @@ -0,0 +1,5 @@ +# nektos/act +.secret +.secrets +secret +secrets \ No newline at end of file diff --git a/src/Saas.SignupAdministration/deployment/act/clean.sh b/src/Saas.SignupAdministration/deployment/act/clean.sh new file mode 100644 index 00000000..c9532786 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/clean.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# repo base +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +host_deployment_dir="${REPO_BASE}/src/Saas.SignupAdministration/deployment" +container_deployment_dir="/asdk/src/Saas.SignupAdministration/deployment" + +# running the './act/script/clean-credentials' script using our ASDK deployment script container - i.e., not the act container +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${REPO_BASE}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${HOME}/asdk/act/.secret":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash /asdk/src/Saas.Lib/Deployment.Script.Modules/clean-credentials.sh + +./setup.sh -s diff --git a/src/Saas.SignupAdministration/deployment/act/deploy.sh b/src/Saas.SignupAdministration/deployment/act/deploy.sh new file mode 100644 index 00000000..54e4fa80 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/deploy.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -u -e -o pipefail + +# shellcheck disable=SC1091 +{ + source "./../constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" +} + +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +host_act_secrets_dir="${HOME}/asdk/act/.secret" +host_deployment_dir="${REPO_BASE}/src/Saas.SignupAdministration/deployment" +container_deployment_dir="/asdk/src/Saas.SignupAdministration/deployment" + +# running the './act/script/patch-app-name.sh' script using our ASDK deployment script container - i.e., not the act container +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${host_deployment_dir}/act/workflows/":"${container_deployment_dir}/act/workflows" \ + --volume "${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules/":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${REPO_BASE}/src/Saas.Identity/Saas.IdentityProvider/deployment/config/":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config:ro \ + --volume "${REPO_BASE}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${host_act_secrets_dir}":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash /asdk/src/Saas.Lib/Deployment.Script.Modules/deploy-debug.sh + +# run act container to run github action locally, using local workflow file and local code base. +gh act workflow_dispatch \ + --rm \ + --bind \ + --pull=false \ + --secret-file "${host_act_secrets_dir}/secret" \ + --directory "${REPO_BASE}" \ + --workflows "${ACT_LOCAL_WORKFLOW_DEBUG_FILE}" \ + --platform "ubuntu-latest=${ACT_CONTAINER_NAME}" diff --git a/src/Saas.SignupAdministration/deployment/act/readme.md b/src/Saas.SignupAdministration/deployment/act/readme.md new file mode 100644 index 00000000..5bdb19a5 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/readme.md @@ -0,0 +1,9 @@ +# Saving Time Running Local GitHub Actions + +GitHub actions are terrific for CI/CD automated deployment. It the *inner loop* for getting GitHub actions right can be a tedious affair - i.e., having to commit, push and run when testing and troubleshoot. + +Luckily, there a solution for this called [act](https://github.com/nektos/act). Act lets you run the a GitHub running locally in a container that mimics what is running in GitHub. You still have to commit your latest code to GitHub, as act will pull it from there when it runs. However, you don't have to commit and push every time you make a change to the GitHub action workflow. This last part can save a lot of time and avoid all this *testing*, *wip* etc. commit and pushes to you main branch. It also allows you to have a slightly different `workflow.yml` file that pulls from for instance your dev branch rather than your main branch. + + + +... bla. bla. \ No newline at end of file diff --git a/src/Saas.SignupAdministration/deployment/act/setup.sh b/src/Saas.SignupAdministration/deployment/act/setup.sh new file mode 100644 index 00000000..16dea670 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/setup.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +skip_docker_build=false +force_update=false + +while getopts 'sf' flag; do + case "${flag}" in + s) skip_docker_build=true ;; + f) force_update=true ;; + *) skip_docker_build=false ;; + esac +done + +# shellcheck disable=SC1091 +source "../constants.sh" + +echo "Setting up the SaaS Signup Administration Web App Act deployment environment." +echo "Settings execute permissions on necessary scripts files." + +sudo mkdir -p "${ACT_SECRETS_DIR}" + +sudo chmod +x ${ACT_DIR}/*.sh +sudo chmod +x ${SCRIPT_DIR}/*.sh >/dev/null 2>&1 +sudo touch ${ACT_SECRETS_FILE} +sudo chown "${USER}" ${ACT_SECRETS_FILE} +sudo touch ${ACT_SECRETS_FILE_RG} +sudo chown "${USER}" ${ACT_SECRETS_FILE_RG} + +if [ "${skip_docker_build}" = false ]; then + echo "Building the deployment container." + + if [[ "${force_update}" == false ]]; then + "${ACT_CONTAINER_DIR}"/build.sh -n "${ACT_CONTAINER_NAME}" + else + "${ACT_CONTAINER_DIR}"/build.sh -n "${ACT_CONTAINER_NAME}" -f + fi +fi + +echo "SaaS SaaS Signup Administration Web App Act environment setup complete. You can now run the local deployment script using the command './deploy.sh'." diff --git a/src/Saas.SignupAdministration/deployment/act/workflows/signup-administration-deploy-debug.yml b/src/Saas.SignupAdministration/deployment/act/workflows/signup-administration-deploy-debug.yml new file mode 100644 index 00000000..ee8b4a6b --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/act/workflows/signup-administration-deploy-debug.yml @@ -0,0 +1,84 @@ +--- +name: ASDK Sign-up Administration Web App - Deploy to Azure Web Services + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + APP_NAME: signupadmin-app + AZURE_WEBAPP_NAME: signupadmin-app-asdk-test-fd4k # set this to your application's name + AZURE_WEBAPP_PACKAGE_PATH: . # set this to the path to your web app project, defaults to the repository root + DOTNET_VERSION: 7.x.x + PROJECT_DIR: ./src/Saas.SignupAdministration/Saas.SignupAdministration.Web + PROJECT_PATH: ${{ env.PROJECT_DIR }}/Saas.SignupAdministration.Web.csproj + PUBLISH_PATH: ./publish + OUTPUT_PATH: ${{ env.PUBLISH_PATH }}/${{ env.APP_NAME }}/package + SYMBOLS_PATH: ${{ env.PUBLISH_PATH }}/symbols + BUILD_CONFIGURATION: Debug # setting the configuration manager build configuration value for our workflow. + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + +################################################## +# this section is specific for local deployment. # +################################################## + # Azure login + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + # checkout the _local_ repository + - name: Checkout + uses: actions/checkout@v3 +################################################# +# end of local deployment specific section. # +################################################# + + # Setup .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + # Run dotnet build and publish + - name: dotnet build and publish + run: | + dotnet restore ${{ env.PROJECT_DIR }} + + dotnet build ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} + + dotnet publish ${{ env.PROJECT_PATH }} \ + --configuration ${{ env.BUILD_CONFIGURATION }} \ + --output ${{ env.OUTPUT_PATH }} + + # Deploy to Azure Web apps + - name: Run Azure webapp deploy action using publish profile credentials + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} # Replace with your app name + package: ${{ env.OUTPUT_PATH }} + + ###################### + # *** Debug only *** # + ###################### + # Copy symbols files (*.pdb)) to local publish folder # rm -rf ${{ env.OUTPUT_PATH }}/${{ env.AZURE_WEBAPP_NAME }} + - name: copy symbols files (*.pdb)) to local publish folder + run: | + mkdir -p ${{ env.PUBLISH_PATH }}/symbols + echo "Copying symbols files to '${{ env.SYMBOLS_PATH }}'" + cp -r ${{ env.OUTPUT_PATH }}/*.pdb ${{ env.SYMBOLS_PATH }} + ###################### + # *** End *** # + ###################### + + # Azure logout + - name: logout + run: | + az logout diff --git a/src/Saas.SignupAdministration/deployment/build.sh b/src/Saas.SignupAdministration/deployment/build.sh new file mode 100644 index 00000000..69bc6a5a --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +force_update=false + +while getopts f flag +do + case "${flag}" in + f) force_update=true;; + *) force_update=false;; + esac +done + +repo_base="$( git rev-parse --show-toplevel )" +docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + + +# redirect to build.sh in the Deployment.Container folder +if [[ "${force_update}" == false ]]; then + "${docker_file_folder}/build.sh" +else + "${docker_file_folder}/build.sh" -f +fi diff --git a/src/Saas.SignupAdministration/deployment/constants.sh b/src/Saas.SignupAdministration/deployment/constants.sh new file mode 100644 index 00000000..9a9fd702 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/constants.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# disable unused variable warning https://www.shellcheck.net/wiki/SC2034 +# shellcheck disable=SC2034 + +# app naming +APP_NAME="signupadmin-app" +APP_DEPLOYMENT_NAME="signupAdministration" + +# repo base +repo_base="$(git rev-parse --show-toplevel)" +REPO_BASE="${repo_base}" + +# project base directory +BASE_DIR="${REPO_BASE}/src/Saas.SignupAdministration/deployment" + +# local script directory +SCRIPT_DIR="${BASE_DIR}/script" + +#local log directory +LOG_FILE_DIR="${BASE_DIR}/log" + +# act directory +ACT_DIR="${BASE_DIR}/act" + +# GitHub workflows +WORKFLOW_BASE="${REPO_BASE}/.github/workflows" +GITHUB_ACTION_WORKFLOW_FILE="${WORKFLOW_BASE}/signup-administration-deploy.yml" +ACT_LOCAL_WORKFLOW_DEBUG_FILE="${ACT_DIR}/workflows/signup-administration-deploy-debug.yml" + +# global script directory +SHARED_MODULE_DIR="${REPO_BASE}/src/Saas.Lib/Deployment.Script.Modules" + +# adding app service global constants +source "${SHARED_MODULE_DIR}/app-service-constants.sh" diff --git a/src/Saas.SignupAdministration/deployment/run.sh b/src/Saas.SignupAdministration/deployment/run.sh new file mode 100644 index 00000000..0372f150 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +source "./constants.sh" + +repo_base="$(git rev-parse --show-toplevel)" +host_act_secrets_dir="${HOME}/asdk/act/.secret" + +host_deployment_dir="${repo_base}/src/Saas.SignupAdministration/deployment" +container_deployment_dir="/asdk/src/Saas.SignupAdministration/deployment" + +# using volumes '--volume' to mount only the needed directories to the container. +# using ':ro' to make scrip directories etc. read-only. Only config and log directories are writable. +docker run \ + --interactive \ + --tty \ + --rm \ + --volume "${host_deployment_dir}":"${container_deployment_dir}":ro \ + --volume "${host_deployment_dir}/log":"${container_deployment_dir}/log" \ + --volume "${host_deployment_dir}/Bicep/Parameters":"${container_deployment_dir}"/Bicep/Parameters \ + --volume "${repo_base}/src/Saas.Identity/Saas.IdentityProvider/deployment/config":/asdk/src/Saas.Identity/Saas.IdentityProvider/deployment/config \ + --volume "${repo_base}/src/Saas.Lib/Deployment.Script.Modules":/asdk/src/Saas.Lib/Deployment.Script.Modules:ro \ + --volume "${repo_base}/src/Saas.Lib/Saas.Bicep.Module":/asdk/src/Saas.Lib/Saas.Bicep.Module:ro \ + --volume "${repo_base}/.github/workflows":/asdk/.github/workflows \ + --volume "${repo_base}/.git/":/asdk/.git:ro \ + --volume "${HOME}/.azure/":/asdk/.azure:ro \ + --volume "${host_act_secrets_dir}":/asdk/act/.secret \ + --env "ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE"="${container_deployment_dir}" \ + "${DEPLOYMENT_CONTAINER_NAME}" \ + bash ${container_deployment_dir}/start.sh diff --git a/src/Saas.SignupAdministration/deployment/script/map-to-config-entries-parameters.py b/src/Saas.SignupAdministration/deployment/script/map-to-config-entries-parameters.py new file mode 100644 index 00000000..85cfc6c1 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/script/map-to-config-entries-parameters.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +import json +import sys +import re + +def get_b2c_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['azureb2c'][key] + + return { + keyName: { + 'value': value + } + } + +def get_claimTransformer_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['claimToRoleTransformer'][key] + return { + keyName: { + 'value': value + } + } + +def get_deploy_b2c_value( + config: dict, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + value = config['deployment']['azureb2c'][key] + return { + keyName: { + 'value': value + } + } + +def get_app_value( + config: dict, + app_name: str, + key: str, + keyName: str) -> 'dict[str, dict[str, str]]': + + for item in config['appRegistrations']: + if item['name'] == app_name: + return { + keyName: { + 'value': item[key] + } + } + +def get_output_value(outputs: dict, output_name: str) -> 'dict[str, dict[str, str]]': + item = outputs[output_name] + if item : return { + output_name : { + 'value': item['value'] + } + } + +def patch_paramenters_file( + app_name: str, + identity_outputs: str, + paramenter_file: str, + config_file: str) -> None: + + with open(config_file, 'r') as f: + config = json.load(f) + + with open(identity_outputs, 'r') as f: + identity_outputs = json.load(f) + + with open(paramenter_file, 'r') as f: + parameters = json.load(f) + + parameters['parameters'].update(get_output_value(identity_outputs, 'version')) + parameters['parameters'].update(get_output_value(identity_outputs, 'keyVaultName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'keyVaultUri')) + + parameters['parameters'].update(get_output_value(identity_outputs, 'userAssignedIdentityName')) + parameters['parameters'].update(get_output_value(identity_outputs, 'appConfigurationName')) + + parameters['parameters'].update(get_deploy_b2c_value(config, 'domainName', 'azureB2CDomain')) + parameters['parameters'].update(get_deploy_b2c_value(config, 'tenantId', 'azureB2cTenantId')) + parameters['parameters'].update(get_deploy_b2c_value(config, 'instance', 'azureAdInstance')) + + parameters['parameters'].update(get_b2c_value(config, 'signedOutCallBackPath', 'signedOutCallBackPath')) + parameters['parameters'].update(get_b2c_value(config, 'signUpSignInPolicyId', 'signUpSignInPolicyId')) + + parameters['parameters'].update(get_app_value(config, app_name, 'appId', 'clientId')) + + parameters['parameters'].update(get_app_value(config, app_name, 'certificateKeyName', 'certificateKeyName')) + + parameters['parameters'].update(get_claimTransformer_value(config, 'authenticationType', 'authenticationType')) + parameters['parameters'].update(get_claimTransformer_value(config, 'roleClaimType', 'roleClaimType')) + parameters['parameters'].update(get_claimTransformer_value(config, 'sourceClaimType', 'sourceClaimType')) + + with open(paramenter_file, 'w') as f: + f.write(json.dumps(parameters, indent=4)) + +# Main entry point for the script +if __name__ == "__main__": + app_name = sys.argv[1] + identity_outputs = sys.argv[2] + paramenter_file = sys.argv[3] + config_file = sys.argv[4] + + patch_paramenters_file(app_name, identity_outputs, paramenter_file, config_file) \ No newline at end of file diff --git a/src/Saas.SignupAdministration/deployment/setup.sh b/src/Saas.SignupAdministration/deployment/setup.sh new file mode 100644 index 00000000..6f3e5ce1 --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/setup.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091 +source "./constants.sh" + +echo "Setting up the deployment environment." +echo "Settings execute permissions on necessary scripts files." + +( + sudo chmod +x ./*.sh + sudo chmod +x ./script/*.sh >/dev/null 2>&1 + sudo chmod +x ./script/*.py +) || + { + echo "Failed to set execute permissions on the necessary scripts." + exit 1 + } + +repo_base="$(git rev-parse --show-toplevel)" || + { + echo "Failed to get the root of the repository." + exit 1 + } + +docker_file_folder="${repo_base}/src/Saas.lib/Deployment.Container" + +# redirect to build.sh in the Deployment.Container folder +sudo chmod +x "${docker_file_folder}/build.sh" || + { + echo "Failed to set execute permissions on the 'build.sh' script." + exit 1 + } + +echo "Building the deployment container." +./build.sh || + { + echo "Failed to build the deployment container. Please ensure that Docker is installed and running." + exit 1 + } + +( + echo "Setting up log folder..." + mkdir -p "$LOG_FILE_DIR" + sudo chown "${USER}" "$LOG_FILE_DIR" +) || + { + echo "Failed to set up log folder." + exit 1 + } + +echo +echo "Setup complete. You can now run the deployment script using the command './run.sh'." diff --git a/src/Saas.SignupAdministration/deployment/start.sh b/src/Saas.SignupAdministration/deployment/start.sh new file mode 100644 index 00000000..88b9f6fa --- /dev/null +++ b/src/Saas.SignupAdministration/deployment/start.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +# if not running in a container +if ! [ -f /.dockerenv ]; then + echo "Running outside of a container us not supported. Please run the deployment script using './run.sh'." + exit 0 +fi + +# shellcheck disable=SC1091 +{ + source "${ASDK_DEPLOYMENT_SCRIPT_PROJECT_BASE}/constants.sh" + source "$SHARED_MODULE_DIR/config-module.sh" + source "$SHARED_MODULE_DIR/log-module.sh" + source "$SHARED_MODULE_DIR/user-module.sh" +} + +# set bash options to exit on unset variables and errors (exit 1) including pipefail +set -u -e -o pipefail + +if ! [[ -f $CONFIG_FILE ]]; then + echo "The ASDK Identity Foundation has not completed or 'config.json' file from it's deployment is missing. Please run the Identity Foundation deployment script first." + exit 0 +fi + +# get now date and time for backup file name +now=$(date '+%Y-%m-%d--%H-%M-%S') + +# set run time for deployment script instance +export ASDK_DEPLOYMENT_SCRIPT_RUN_TIME="${now}" + +# using the az cli settings and cache from the host machine +initialize-az-cli "$HOME/.azure" + +echo "Provisioning the SaaS Signup Administration web app..." | + log-output \ + --level info \ + --header "SaaS Signup Administration" + +"${SHARED_MODULE_DIR}/"deploy-app-service.sh + +"${SHARED_MODULE_DIR}/"deploy-config-entries.sh + +echo "Patching '${APP_NAME}' GitHub Action workflow file." | + log-output \ + --level info \ + --header "SaaS Sign-up Administration Web App" + +"${SHARED_MODULE_DIR}/patch-github-workflow.py" \ + "${APP_NAME}" \ + "${CONFIG_FILE}" \ + "${GITHUB_ACTION_WORKFLOW_FILE}" || + echo "Failed to patch ${APP_NAME} GitHub Action workflow file" | + log-output \ + --level error \ + --header "Critical Error" || + exit 1 + +git_repo_origin="$(git config --get remote.origin.url)" + +echo "'${APP_NAME}' is ready to be deployed. You have two options:" +echo " a) To deploy to production, use the GitHub Action: ${git_repo_origin::-4}/actions" +echo +echo " b) To deploy for live debugging in Azure; navigate to the act directory ('cd act') and run './setup.sh' and then run './deploy.sh' to deploy for remote debugging." diff --git a/src/TestUtilities/AutoDataNSubstituteAttribute.cs b/src/TestUtilities/AutoDataNSubstituteAttribute.cs index 0490321a..86093b30 100644 --- a/src/TestUtilities/AutoDataNSubstituteAttribute.cs +++ b/src/TestUtilities/AutoDataNSubstituteAttribute.cs @@ -30,14 +30,14 @@ public class AutoDataNSubstituteAttribute : AutoDataAttribute public AutoDataNSubstituteAttribute(AutoDataOptions options = AutoDataOptions.Default, params Type[] customizations) : base(GetFactory(options, customizations)) { - this._skipLiveTest = (options & AutoDataOptions.SkipLiveTest) == AutoDataOptions.SkipLiveTest; + _skipLiveTest = (options & AutoDataOptions.SkipLiveTest) == AutoDataOptions.SkipLiveTest; } public AutoDataNSubstituteAttribute(params Type[] customizations) : this(AutoDataOptions.Default, customizations) { } #pragma warning disable CS8603 // Possible null reference return. - public override string Skip => LiveUnitTestUtil.SkipIfLiveUnitTest(this._skipLiveTest); + public override string Skip => LiveUnitTestUtil.SkipIfLiveUnitTest(_skipLiveTest); #pragma warning restore CS8603 // Possible null reference return. private static Func GetFactory(AutoDataOptions options, Type[] cusomizations) @@ -52,12 +52,16 @@ public class AutoDataNSubstituteAttribute : AutoDataAttribute { if (typeof(ISpecimenBuilder).IsAssignableFrom(customizationType)) { - var specimentBuilder = Activator.CreateInstance(customizationType) as ISpecimenBuilder; + ISpecimenBuilder specimentBuilder = Activator.CreateInstance(customizationType) as ISpecimenBuilder + ?? throw new NullReferenceException("SpecimentBuilder cannot be null"); + fixture.Customizations.Add(specimentBuilder); } else if (typeof(ICustomization).IsAssignableFrom(customizationType)) { - var customization = Activator.CreateInstance(customizationType) as ICustomization; + ICustomization customization = Activator.CreateInstance(customizationType) as ICustomization + ?? throw new NullReferenceException("Customization cannot be null"); + fixture = fixture.Customize(customization); } else diff --git a/src/TestUtilities/TestUtilities.csproj b/src/TestUtilities/TestUtilities.csproj index a9526f87..771b91fb 100644 --- a/src/TestUtilities/TestUtilities.csproj +++ b/src/TestUtilities/TestUtilities.csproj @@ -1,21 +1,21 @@ - net6.0 + net7.0 enable enable - + - - + + - +