Skip to content

Commit

Permalink
Updated deployment documentation. Added deploy workflow. #67
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jan 3, 2025
1 parent 3d7928f commit d41f3c9
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 47 deletions.
20 changes: 7 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
- name: Build Custom Actions (Tools.GitHubActions project)
run: |
cd src/Tools.GitHubActions/VariableSubstitution
npm ci --cache .npm --prefer-offline
npm run build
- name: Variable Substitution
uses: ./src/Tools.GitHubActions/VariableSubstitution
with:
files: '**/appsettings.json,**/appsettings.Azure.json,**/appsettings.AWS.json,**/appsettings.Deploy.json'
variables: ${{ toJSON(vars)}}
secrets: ${{ toJSON(secrets)}}
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
Expand All @@ -45,12 +34,17 @@ jobs:
run: dotnet build --configuration ${{env.DEPLOY_BUILD_CONFIGURATION}} "${{env.SOLUTION_PATH}}" /p:HostingPlatform=HOSTEDONAZURE
- name: Build (Backend) for AWS Deploy
run: dotnet build --configuration ${{env.DEPLOY_BUILD_CONFIGURATION}} "${{env.SOLUTION_PATH}}" /p:HostingPlatform=HOSTEDONAWS
- name: Build WebsiteHost (FrontEnd)
- name: Build WebsiteHost (FrontEnd) for Deploy
run: |
cd src/WebsiteHost/ClientApp
cp .env.example .env
npm ci --cache .npm --prefer-offline
npm run build:releasefordeploy
- name: Build Custom GitHub Actions
run: |
cd src/Tools.GitHubActions/VariableSubstitution
npm ci --cache .npm --prefer-offline
npm run build
test:
runs-on: windows-latest
timeout-minutes: 15
Expand Down Expand Up @@ -90,7 +84,7 @@ jobs:
npm ci --cache .npm --prefer-offline
npm run build:releasefordeploy
npm run test:ci
- name: Test VariableSubstitution (GitHubAction)
- name: Test Custom GitHub Actions
continue-on-error: false
run: |
cd src/Tools.GitHubActions/VariableSubstitution
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Build and Deploy

on: [ push ]

permissions:
contents: read
actions: read
checks: write

env:
IS_CI_BUILD: 'true'
SOLUTION_PATH: 'src/SaaStack.sln'
TESTINGONLY_BUILD_CONFIGURATION: 'Release'
DEPLOY_BUILD_CONFIGURATION: 'ReleaseForDeploy'
DOTNET_VERSION: 8.0.302

jobs:
deploy:
runs-on: windows-latest
timeout-minutes: 15
environment: 'Demo'
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{env.DOTNET_VERSION}}
- name: Restore dependencies
run: dotnet restore "${{env.SOLUTION_PATH}}"
- name: Build Custom GitHub Actions
run: |
cd src/Tools.GitHubActions/VariableSubstitution
npm ci --cache .npm --prefer-offline
npm run build
- name: Build (Backend) for Azure Deploy
run: dotnet build --configuration ${{env.DEPLOY_BUILD_CONFIGURATION}} "${{env.SOLUTION_PATH}}" /p:HostingPlatform=HOSTEDONAZURE
- name: Build (Backend) for AWS Deploy
run: dotnet build --configuration ${{env.DEPLOY_BUILD_CONFIGURATION}} "${{env.SOLUTION_PATH}}" /p:HostingPlatform=HOSTEDONAWS
- name: Build WebsiteHost (FrontEnd) for Deploy
run: |
cd src/WebsiteHost/ClientApp
cp .env.example .env
npm ci --cache .npm --prefer-offline
npm run build:releasefordeploy
- name: AppSettings Variable Substitution
uses: ./src/Tools.GitHubActions/VariableSubstitution
with:
files: '**/appsettings.json,**/appsettings.Azure.json,**/appsettings.AWS.json,**/appsettings.Deploy.json'
variables: ${{ toJSON(vars)}}
secrets: ${{ toJSON(secrets)}}
- name: Deploy to Azure
run: echo "Deploy to Azure" #TODO: Deploy steps to Azure
71 changes: 71 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Deployment of SaaStack

This document details the basic steps required to deploy your software into a production environment.

A production environment might be in the cloud or on premise. The deployment process is similar, except for the tools used to perform the deployment.

By default, this deployment is assumed to take place from a GitHub repository using GitHub Actions. However, you can use any CI/CD tool you prefer.

## Automated deployment

The deployment process is automated using GitHub Actions. The deployment process is defined in the `.github/workflows/deploy.yml` file.

## Variables

Most of the required variables should be self-explanatory.

Here are ones that might need a little more explanation:

### Operator Whitelist

Setting name: `HOSTS_ENDUSERSAPI_AUTHORIZATION_OPERATORWHITELIST`

This is a semicolon `;` delimited list of email addresses of user accounts that are authorized to act as operators in the system.
This list is populated before the user accounts are registered and when they are registered later, they are promoted to `operator` status.

**IMPORTANT**: You should always have at least one operator account email address defined in this list before your software goes to production for the first time, otherwise you will have no other way to promote other operators in the system.

> For user accounts that already exist, they can be promoted by existing operator accounts.


## Secrets

You MUST generate new secrets for your deployed services.

**IMPORTANT**: You MUST never re-use the secrets in this repository in a production environment. hey are well known to anyone who has access to this repository.

### HMAC Signing Key

For secrets such as `HOSTS_APIHOST1_HMACAUTHNSECRET` and `HOSTS_ANCILLARYAPI_HMACAUTHNSECRET`:
* Generate a random value using the [HMACSigner.GenerateKey()](https://github.com/jezzsantos/saastack/blob/main/src/Infrastructure.Web.Api.Common/HMACSigner.cs) method.

> Note: You can run the unit tests for this class and copy the value of the generated key in the first test.
**IMPORTANT**: It is vital that all the HMAC signing keys on each deployed host are identical. You can have different values on different hosts, as long as all their client hosts are also updated.

### CSRF secrets

For the CSRF `HOSTS_WEBSITEHOST_CSRFHMACSECRET` secret
* Generate a new random value using the [CSRFToken.GenerateKey()](https://github.com/jezzsantos/saastack/blob/main/src/Infrastructure.Web.Api.Common/HMACSigner.cs) method as above for HMAC secrets.

> Note: You will want to use a different value than the HMAC signing keys.
For the CSRF `HOSTS_WEBSITEHOST_CSRFAESSECRET` secret:
* Generate a new random value using the [AesEncryptionService.GenerateAesSecret()](https://github.com/jezzsantos/saastack/blob/main/src/Infrastructure.Common/DomainServices/AesEncryptionService.cs) method.

> Note: You can run the unit tests for this class and copy the value of the generated key in the first test.
### SSO

For the `APPLICATIONSERVICES_SSOPROVIDERSSERVICE_SSOUSERTOKENS_AESSECRET` secret:
* Generate a new random value using the [AesEncryptionService.GenerateAesSecret()](https://github.com/jezzsantos/saastack/blob/main/src/Infrastructure.Common/DomainServices/AesEncryptionService.cs) method.

> Note: You can run the unit tests for this class and copy the value of the generated key in the first test.
### JWT Signing Key

For the `HOSTS_IDENTITYAPI_JWT_SIGNINGSECRET` secret:
* Generate a new random value using the [JwtTokenService.GenerateSigningKey()](https://github.com/jezzsantos/saastack/blob/main/src/IdentityInfrastructure/ApplicationServices/JWTTokensService.cs) method.

> Note: You can run the unit tests for this class and copy the value of the generated key in the first test.
11 changes: 9 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ As well as a code template, there is custom tooling (tailored to this codebase)

We make extensive use Roslyn Analyzers, Code Fixes and Source Generators and Architecture tests to help you and your team be highly productive in following the established patterns this codebase. And more importantly detect and fix when those principles are violated.

For more details see the [Developer Tooling](design-principles/0140-developer-tooling.md) documentation.
For more details, see the [Developer Tooling](design-principles/0140-developer-tooling.md) documentation.

For example, we make it trivial to define robust REST APIs, and under the covers, the tooling converts those API definitions into MediatR-brokered minimal APIs for you. But you never have to write all that minimal API boilerplate stuff.

Expand All @@ -42,4 +42,11 @@ Lastly, if you are using JetBrains Rider, we have baked in a set of common codin
We also provide you with a number of project templates for adding the various projects for new subdomains.
We also give you several macros in the text editor (a.k.a. Live Templates) for creating certain kinds of classes, like DDD ValueObjects and DDD AggregateRoots, and xUnit test classes.

You can see all of these things in the Platform/Tools projects.
You can see all of these things in the Platform/Tools projects.

## Deployment

The codebase is ready for deployment immediately, from your GitHub repository.
Deployment can be performed by any tool set to any environment, any way you like, we just made it easy for GitHub actions.

For more details, see the [Deployment](DEPLOYMENT.md) documentation.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public JWTTokensServiceSpec()
var settings = new Mock<IConfigurationSettings>();
settings.Setup(s => s.Platform.GetString(JWTTokensService.BaseUrlSettingName, null))
.Returns("https://localhost");
settings.Setup(s => s.Platform.GetString(JWTTokensService.SecretSettingName, null))
settings.Setup(s => s.Platform.GetString(JWTTokensService.SigningSecretSettingName, null))
.Returns("asecretsigningkeyasecretsigningkeyasecretsigningkeyasecretsigningkey");
settings.Setup(s => s.Platform.GetNumber(It.IsAny<string>(), It.IsAny<double>()))
.Returns((string _, double defaultValue) => defaultValue);
Expand All @@ -35,6 +35,16 @@ public JWTTokensServiceSpec()
_service = new JWTTokensService(settings.Object, _tokensService.Object);
}

[Fact]
public void WhenGenerateSigningKey_ThenReturnsKey()
{
#if TESTINGONLY
var result = JWTTokensService.GenerateSigningKey();

result.Should().NotBeNullOrEmpty();
#endif
}

[Fact]
public async Task WhenIssueTokensAsync_ThenReturnsTokens()
{
Expand Down
14 changes: 11 additions & 3 deletions src/IdentityInfrastructure/ApplicationServices/JWTTokensService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text;
using Application.Resources.Shared;
using Common;
Expand All @@ -14,8 +15,8 @@ namespace IdentityInfrastructure.ApplicationServices;
public class JWTTokensService : IJWTTokensService
{
public const string BaseUrlSettingName = "Hosts:IdentityApi:BaseUrl";
public const string DefaultExpirySettingName = "Hosts:IdentityApi:JWT:DefaultExpiryInMinutes";
public const string SecretSettingName = "Hosts:IdentityApi:JWT:SigningSecret";
public const string SigningSecretSettingName = "Hosts:IdentityApi:JWT:SigningSecret";
private const string DefaultExpirySettingName = "Hosts:IdentityApi:JWT:DefaultExpiryInMinutes";
private readonly TimeSpan _accessTokenExpiresAfter;
private readonly string _baseUrl;
private readonly string _signingSecret;
Expand All @@ -24,7 +25,7 @@ public class JWTTokensService : IJWTTokensService
public JWTTokensService(IConfigurationSettings settings, ITokensService tokensService)
{
_tokensService = tokensService;
_signingSecret = settings.Platform.GetString(SecretSettingName);
_signingSecret = settings.Platform.GetString(SigningSecretSettingName);
_baseUrl = settings.Platform.GetString(BaseUrlSettingName);
_accessTokenExpiresAfter =
TimeSpan.FromMinutes(settings.Platform.GetNumber(DefaultExpirySettingName,
Expand All @@ -38,6 +39,13 @@ public Task<Result<AccessTokens, Error>> IssueTokensAsync(EndUserWithMemberships
return Task.FromResult(tokens);
}

#if TESTINGONLY
public static string GenerateSigningKey()
{
return RandomNumberGenerator.GetHexString(64);
}
#endif

private Result<AccessTokens, Error> IssueTokens(EndUserWithMemberships user)
{
var accessTokenExpiresOn = DateTime.UtcNow.Add(_accessTokenExpiresAfter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class AesEncryptionServiceSpec
public AesEncryptionServiceSpec()
{
#if TESTINGONLY
_secret = AesEncryptionService.CreateAesSecret();
_secret = AesEncryptionService.GenerateAesSecret();
#else
_secret = string.Empty;
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ private static SymmetricAlgorithm CreateAes()
}

#if TESTINGONLY
public static string CreateAesSecret()
public static string GenerateAesSecret()
{
CreateKeyAndIv(out var key, out var iv);
GenerateKeyAndIv(out var key, out var iv);
return $"{Convert.ToBase64String(key)}{SecretKeyDelimiter}{Convert.ToBase64String(iv)}";
}

private static void CreateKeyAndIv(out byte[] key, out byte[] iv)
private static void GenerateKeyAndIv(out byte[] key, out byte[] iv)
{
using var aes = CreateAes();
key = aes.Key;
Expand Down
10 changes: 10 additions & 0 deletions src/Infrastructure.Web.Api.Common.UnitTests/HMACSignerSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ public class HMACSignerSpec
[Trait("Category", "Unit")]
public class GivenARequest
{
[Fact]
public void WhenGenerateKey_ThenReturnsRandomKey()
{
#if TESTINGONLY
var result = HMACSigner.GenerateKey();

result.Should().NotBeNullOrEmpty();
#endif
}

[Fact]
public void WhenConstructedWithEmptySecret_ThenThrows()
{
Expand Down
8 changes: 8 additions & 0 deletions src/Infrastructure.Web.Api.Common/HMACSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ public HMACSigner(byte[] data, string secret)
_secret = secret;
}

#if TESTINGONLY
public static string GenerateKey()
{
var algorithm = new HMACSHA256();
return Convert.ToBase64String(algorithm.Key);
}
#endif

public string Sign()
{
var key = SignatureEncoding.GetBytes(_secret);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public CSRFServiceSpec()
settings.Setup(s => s.GetWebsiteHostCSRFSigningSecret())
.Returns("asecret");
#if TESTINGONLY
_encryptionService = new AesEncryptionService(AesEncryptionService.CreateAesSecret());
_encryptionService = new AesEncryptionService(AesEncryptionService.GenerateAesSecret());
#endif

_service = new CSRFService(settings.Object, _encryptionService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public CSRFTokenPairSpec()
{
#if TESTINGONLY
_encryptionService =
new AesEncryptionService(AesEncryptionService.CreateAesSecret());
new AesEncryptionService(AesEncryptionService.GenerateAesSecret());
#endif
}

Expand Down
1 change: 1 addition & 0 deletions src/SaaStack.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\README_DERIVATIVE.md = ..\README_DERIVATIVE.md
..\README.md = ..\README.md
..\CONTRIBUTING.md = ..\CONTRIBUTING.md
..\docs\DEPLOYMENT.md = ..\docs\DEPLOYMENT.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{508E7DA4-4DF2-4201-955D-CCF70C41AD05}"
Expand Down
Loading

0 comments on commit d41f3c9

Please sign in to comment.