Skip to content

Commit 06c0573

Browse files
committed
Add BackOfficeAdmins capability and simplify mock identities to admin and plain user
1 parent 02c6753 commit 06c0573

19 files changed

Lines changed: 309 additions & 92 deletions

File tree

.github/workflows/_deploy-container.yml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ on:
1818
image_name:
1919
required: true
2020
type: string
21+
container_app_name:
22+
required: false
23+
type: string
24+
default: ""
2125
version:
2226
required: true
2327
type: string
@@ -69,7 +73,7 @@ jobs:
6973

7074
# For production, import image from staging instead of building
7175
- name: Import Container Image from Staging to Production
72-
if: inputs.azure_environment == 'prod'
76+
if: inputs.azure_environment == 'prod' && inputs.container_app_name == ''
7377
run: |
7478
STAGING_REGISTRY_ID="/subscriptions/${{ vars.STAGING_SUBSCRIPTION_ID }}/resourceGroups/${{ env.UNIQUE_PREFIX }}-stage-global/providers/Microsoft.ContainerRegistry/registries/${{ env.UNIQUE_PREFIX }}stage"
7579
@@ -82,11 +86,11 @@ jobs:
8286
8387
# For staging, build and push the image
8488
- name: Setup Docker Buildx
85-
if: inputs.azure_environment == 'stage'
89+
if: inputs.azure_environment == 'stage' && inputs.container_app_name == ''
8690
uses: docker/setup-buildx-action@v4
8791

8892
- name: Build and Push Container Image
89-
if: inputs.azure_environment == 'stage'
93+
if: inputs.azure_environment == 'stage' && inputs.container_app_name == ''
9094
working-directory: ${{ inputs.docker_context }}
9195
run: |
9296
docker buildx create --use
@@ -102,14 +106,15 @@ jobs:
102106
run: |
103107
CLUSTER_RESOURCE_GROUP_NAME="${{ env.UNIQUE_PREFIX }}-${{ env.ENVIRONMENT }}-${{ env.CLUSTER_LOCATION_ACRONYM }}"
104108
SUFFIX=$(echo "${{ inputs.version }}" | sed 's/\./-/g')
105-
az containerapp update --name ${{ inputs.image_name }} --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --image "${{ env.UNIQUE_PREFIX }}${{ env.ENVIRONMENT }}.azurecr.io/${{ inputs.image_name }}:${{ inputs.version }}" --revision-suffix $SUFFIX
109+
TARGET_APP="${{ inputs.container_app_name != '' && inputs.container_app_name || inputs.image_name }}"
110+
az containerapp update --name "$TARGET_APP" --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --image "${{ env.UNIQUE_PREFIX }}${{ env.ENVIRONMENT }}.azurecr.io/${{ inputs.image_name }}:${{ inputs.version }}" --revision-suffix $SUFFIX
106111
107112
echo "Waiting for the new revision to be active..."
108113
for i in {1..10}; do
109114
sleep 15
110115
111-
RUNNING_STATUS=$(az containerapp revision list --name ${{ inputs.image_name }} --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --query "[?contains(name, '$SUFFIX')].properties.runningState" --output tsv)
112-
HEALTH_STATUS=$(az containerapp revision list --name ${{ inputs.image_name }} --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --query "[?contains(name, '$SUFFIX')].properties.healthState" --output tsv)
116+
RUNNING_STATUS=$(az containerapp revision list --name "$TARGET_APP" --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --query "[?contains(name, '$SUFFIX')].properties.runningState" --output tsv)
117+
HEALTH_STATUS=$(az containerapp revision list --name "$TARGET_APP" --resource-group "$CLUSTER_RESOURCE_GROUP_NAME" --query "[?contains(name, '$SUFFIX')].properties.healthState" --output tsv)
113118
if [[ "$HEALTH_STATUS" == "Healthy" ]]; then
114119
echo "New revision is healthy. Running state: $RUNNING_STATUS"
115120
exit 0

.github/workflows/_deploy-infrastructure.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ on:
3737
back_office_entra_client_id:
3838
required: true
3939
type: string
40+
back_office_admins_group_id:
41+
required: false
42+
type: string
43+
default: "-"
4044
postgres_admin_object_id:
4145
required: true
4246
type: string
@@ -88,6 +92,7 @@ jobs:
8892
env:
8993
POSTGRES_ADMIN_OBJECT_ID: ${{ inputs.postgres_admin_object_id }}
9094
BACK_OFFICE_ENTRA_CLIENT_ID: ${{ inputs.back_office_entra_client_id }}
95+
BACK_OFFICE_ADMINS_GROUP_ID: ${{ inputs.back_office_admins_group_id }}
9196
GOOGLE_OAUTH_CLIENT_ID: ${{ vars.GOOGLE_OAUTH_CLIENT_ID }}
9297
GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
9398
STRIPE_PUBLISHABLE_KEY: ${{ vars.STRIPE_PUBLISHABLE_KEY }}
@@ -174,6 +179,7 @@ jobs:
174179
env:
175180
POSTGRES_ADMIN_OBJECT_ID: ${{ inputs.postgres_admin_object_id }}
176181
BACK_OFFICE_ENTRA_CLIENT_ID: ${{ inputs.back_office_entra_client_id }}
182+
BACK_OFFICE_ADMINS_GROUP_ID: ${{ inputs.back_office_admins_group_id }}
177183
GOOGLE_OAUTH_CLIENT_ID: ${{ vars.GOOGLE_OAUTH_CLIENT_ID }}
178184
GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
179185
STRIPE_PUBLISHABLE_KEY: ${{ vars.STRIPE_PUBLISHABLE_KEY }}

.github/workflows/account.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,25 @@ jobs:
279279
docker_context: ./application/account
280280
docker_file: ./Workers/Dockerfile
281281

282+
back-office-stage:
283+
name: Back Office Staging
284+
if: ${{ needs.build-and-test.outputs.deploy_staging == 'true' }}
285+
needs: [build-and-test, database-migrations-stage, api-stage]
286+
uses: ./.github/workflows/_deploy-container.yml
287+
secrets: inherit
288+
with:
289+
azure_environment: "stage"
290+
cluster_location_acronym: ${{ vars.STAGING_CLUSTER_LOCATION_ACRONYM }}
291+
service_principal_id: ${{ vars.STAGING_SERVICE_PRINCIPAL_ID }}
292+
subscription_id: ${{ vars.STAGING_SUBSCRIPTION_ID }}
293+
image_name: account-api
294+
container_app_name: back-office
295+
version: ${{ needs.build-and-test.outputs.version }}
296+
artifacts_name: account-api
297+
artifacts_path: application/account/Api/publish
298+
docker_context: ./application/account
299+
docker_file: ./Api/Dockerfile
300+
282301
database-migrations-prod1:
283302
name: Database Production
284303
if: ${{ needs.build-and-test.outputs.deploy_production == 'true' }}
@@ -331,3 +350,22 @@ jobs:
331350
artifacts_path: application/account/Workers/publish
332351
docker_context: ./application/account
333352
docker_file: ./Workers/Dockerfile
353+
354+
back-office-prod1:
355+
name: Back Office Production
356+
if: ${{ needs.build-and-test.outputs.deploy_production == 'true' }}
357+
needs: [build-and-test, database-migrations-prod1, api-prod1]
358+
uses: ./.github/workflows/_deploy-container.yml
359+
secrets: inherit
360+
with:
361+
azure_environment: "prod"
362+
cluster_location_acronym: ${{ vars.PRODUCTION_CLUSTER1_LOCATION_ACRONYM }}
363+
service_principal_id: ${{ vars.PRODUCTION_SERVICE_PRINCIPAL_ID }}
364+
subscription_id: ${{ vars.PRODUCTION_SUBSCRIPTION_ID }}
365+
image_name: account-api
366+
container_app_name: back-office
367+
version: ${{ needs.build-and-test.outputs.version }}
368+
artifacts_name: account-api
369+
artifacts_path: application/account/Api/publish
370+
docker_context: ./application/account
371+
docker_file: ./Api/Dockerfile

.github/workflows/cloud-infrastructure.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
domain_name: ${{ vars.STAGING_DOMAIN_NAME }}
4040
back_office_domain_name: ${{ vars.STAGING_BACK_OFFICE_DOMAIN_NAME || '-' }}
4141
back_office_entra_client_id: ${{ vars.STAGING_BACK_OFFICE_ENTRA_CLIENT_ID }}
42+
back_office_admins_group_id: ${{ vars.STAGING_BACK_OFFICE_ADMINS_GROUP_ID || '-' }}
4243
postgres_admin_object_id: ${{ vars.STAGING_POSTGRES_ADMIN_OBJECT_ID }}
4344
production_service_principal_object_id: ${{ vars.PRODUCTION_SERVICE_PRINCIPAL_OBJECT_ID }}
4445

@@ -59,5 +60,6 @@ jobs:
5960
domain_name: ${{ vars.PRODUCTION_DOMAIN_NAME }}
6061
back_office_domain_name: ${{ vars.PRODUCTION_BACK_OFFICE_DOMAIN_NAME || '-' }}
6162
back_office_entra_client_id: ${{ vars.PRODUCTION_BACK_OFFICE_ENTRA_CLIENT_ID }}
63+
back_office_admins_group_id: ${{ vars.PRODUCTION_BACK_OFFICE_ADMINS_GROUP_ID || '-' }}
6264
postgres_admin_object_id: ${{ vars.PRODUCTION_POSTGRES_ADMIN_OBJECT_ID }}
6365
tenant_id: ${{ vars.TENANT_ID }}

application/AppHost/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using AppHost;
44
using Azure.Storage.Blobs;
55
using Projects;
6+
using SharedKernel.Authentication.MockEasyAuth;
67
using SharedKernel.Configuration;
78

89
// Read the port allocation before CreateBuilder so we can set Aspire's dashboard env vars
@@ -91,6 +92,7 @@
9192
.WithEnvironment("OAUTH_PUBLIC_URL", "https://localhost:" + ports.AppGateway)
9293
.WithEnvironment("Hostnames__App", appHostname)
9394
.WithEnvironment("BackOffice__Host", backOfficeHostname)
95+
.WithEnvironment("BackOffice__AdminsGroupId", MockEasyAuthIdentities.MockAdminsGroupId)
9496
.WithReference(accountDatabase)
9597
.WithReference(azureStorage)
9698
.WithEnvironment("OAuth__Google__ClientId", googleOAuthClientId)

application/account/BackOfficeWebApp/routes/login.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useState } from "react";
1111
import logoMarkUrl from "@/shared/images/logo-mark.svg";
1212
import { HorizontalHeroLayout } from "@/shared/layouts/HorizontalHeroLayout";
1313

14-
const MOCK_IDENTITY_IDS = ["admin", "support", "readonly", "plain"] as const;
14+
const MOCK_IDENTITY_IDS = ["admin", "user"] as const;
1515

1616
interface MockLoginSearch {
1717
returnPath: string;
@@ -84,13 +84,9 @@ function MockLoginPage() {
8484
function getIdentityName(id: string) {
8585
switch (id) {
8686
case "admin":
87-
return <Trans>Admin User</Trans>;
88-
case "support":
89-
return <Trans>Support User</Trans>;
90-
case "readonly":
91-
return <Trans>Read Only</Trans>;
92-
case "plain":
93-
return <Trans>Plain User</Trans>;
87+
return <Trans>Admin</Trans>;
88+
case "user":
89+
return <Trans>User</Trans>;
9490
default:
9591
return id;
9692
}
@@ -100,11 +96,7 @@ function getIdentityDescription(id: string) {
10096
switch (id) {
10197
case "admin":
10298
return <Trans>Log in with admin rights</Trans>;
103-
case "support":
104-
return <Trans>Log in with support rights</Trans>;
105-
case "readonly":
106-
return <Trans>Log in with read-only rights</Trans>;
107-
case "plain":
99+
case "user":
108100
return <Trans>Log in without group claims</Trans>;
109101
default:
110102
return null;

application/account/BackOfficeWebApp/shared/translations/locale/da-DK.po

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ msgstr "Konti"
1919
msgid "Accounts (coming soon)"
2020
msgstr "Konti (kommer snart)"
2121

22-
msgid "Admin User"
23-
msgstr "Administrator"
22+
msgid "Admin"
23+
msgstr "Admin"
2424

2525
msgid "An unexpected error occurred while processing your request."
2626
msgstr "Der opstod en uventet fejl ved behandlingen."
@@ -91,12 +91,6 @@ msgstr "Log ind"
9191
msgid "Log in with admin rights"
9292
msgstr "Log ind med administratorrettigheder"
9393

94-
msgid "Log in with read-only rights"
95-
msgstr "Log ind med skrivebeskyttede rettigheder"
96-
97-
msgid "Log in with support rights"
98-
msgstr "Log ind med supportrettigheder"
99-
10094
msgid "Log in without group claims"
10195
msgstr "Log ind uden gruppeclaims"
10296

@@ -127,9 +121,6 @@ msgstr "Ingen"
127121
msgid "Page not found"
128122
msgstr "Siden blev ikke fundet"
129123

130-
msgid "Plain User"
131-
msgstr "Almindelig bruger"
132-
133124
msgid "PlatformPlatform logo"
134125
msgstr "PlatformPlatform logo"
135126

@@ -139,9 +130,6 @@ msgstr "Tjek venligst URL'en eller vend tilbage til forsiden."
139130
msgid "Please try again or return to the home page."
140131
msgstr "Prøv venligst igen eller vend tilbage til forsiden."
141132

142-
msgid "Read Only"
143-
msgstr "Skrivebeskyttet"
144-
145133
msgid "Screenshots of the dashboard project with desktop and mobile versions"
146134
msgstr "Skærmbilleder af dashboard-projektet i desktop- og mobilversioner"
147135

@@ -160,9 +148,6 @@ msgstr "Support"
160148
msgid "Support (coming soon)"
161149
msgstr "Support (kommer snart)"
162150

163-
msgid "Support User"
164-
msgstr "Supportbruger"
165-
166151
msgid "System"
167152
msgstr "System"
168153

@@ -175,6 +160,9 @@ msgstr "Tema"
175160
msgid "Try again"
176161
msgstr "Prøv igen"
177162

163+
msgid "User"
164+
msgstr "Bruger"
165+
178166
msgid "User menu"
179167
msgstr "Brugermenu"
180168

application/account/BackOfficeWebApp/shared/translations/locale/en-US.po

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ msgstr "Accounts"
1919
msgid "Accounts (coming soon)"
2020
msgstr "Accounts (coming soon)"
2121

22-
msgid "Admin User"
23-
msgstr "Admin User"
22+
msgid "Admin"
23+
msgstr "Admin"
2424

2525
msgid "An unexpected error occurred while processing your request."
2626
msgstr "An unexpected error occurred while processing your request."
@@ -91,12 +91,6 @@ msgstr "Log in"
9191
msgid "Log in with admin rights"
9292
msgstr "Log in with admin rights"
9393

94-
msgid "Log in with read-only rights"
95-
msgstr "Log in with read-only rights"
96-
97-
msgid "Log in with support rights"
98-
msgstr "Log in with support rights"
99-
10094
msgid "Log in without group claims"
10195
msgstr "Log in without group claims"
10296

@@ -127,9 +121,6 @@ msgstr "None"
127121
msgid "Page not found"
128122
msgstr "Page not found"
129123

130-
msgid "Plain User"
131-
msgstr "Plain User"
132-
133124
msgid "PlatformPlatform logo"
134125
msgstr "PlatformPlatform logo"
135126

@@ -139,9 +130,6 @@ msgstr "Please check the URL or return to the home page."
139130
msgid "Please try again or return to the home page."
140131
msgstr "Please try again or return to the home page."
141132

142-
msgid "Read Only"
143-
msgstr "Read Only"
144-
145133
msgid "Screenshots of the dashboard project with desktop and mobile versions"
146134
msgstr "Screenshots of the dashboard project with desktop and mobile versions"
147135

@@ -160,9 +148,6 @@ msgstr "Support"
160148
msgid "Support (coming soon)"
161149
msgstr "Support (coming soon)"
162150

163-
msgid "Support User"
164-
msgstr "Support User"
165-
166151
msgid "System"
167152
msgstr "System"
168153

@@ -175,6 +160,9 @@ msgstr "Theme"
175160
msgid "Try again"
176161
msgstr "Try again"
177162

163+
msgid "User"
164+
msgstr "User"
165+
178166
msgid "User menu"
179167
msgstr "User menu"
180168

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Security.Claims;
2+
using FluentAssertions;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.Extensions.Options;
5+
using SharedKernel.Authentication.BackOfficeIdentity;
6+
using Xunit;
7+
8+
namespace Account.Tests.BackOffice;
9+
10+
// Verifies the optional BackOffice:AdminsGroupId capability gate. When unset, no one is admin.
11+
// When set, the principal must carry a 'groups' claim matching the configured value.
12+
public sealed class BackOfficeAdminRequirementTests
13+
{
14+
[Fact]
15+
public async Task HandleRequirement_WhenAdminsGroupIdUnset_ShouldFail()
16+
{
17+
var handler = CreateHandler(null);
18+
var requirement = new BackOfficeAdminRequirement();
19+
var context = CreateAuthorizationContext(requirement, ["BackOfficeAdmins"]);
20+
21+
await handler.HandleAsync(context);
22+
23+
context.HasSucceeded.Should().BeFalse();
24+
}
25+
26+
[Fact]
27+
public async Task HandleRequirement_WhenPrincipalCarriesMatchingGroup_ShouldSucceed()
28+
{
29+
var handler = CreateHandler("BackOfficeAdmins");
30+
var requirement = new BackOfficeAdminRequirement();
31+
var context = CreateAuthorizationContext(requirement, ["BackOfficeAdmins"]);
32+
33+
await handler.HandleAsync(context);
34+
35+
context.HasSucceeded.Should().BeTrue();
36+
}
37+
38+
[Fact]
39+
public async Task HandleRequirement_WhenPrincipalLacksMatchingGroup_ShouldFail()
40+
{
41+
var handler = CreateHandler("BackOfficeAdmins");
42+
var requirement = new BackOfficeAdminRequirement();
43+
var context = CreateAuthorizationContext(requirement, []);
44+
45+
await handler.HandleAsync(context);
46+
47+
context.HasSucceeded.Should().BeFalse();
48+
}
49+
50+
private static BackOfficeAdminAuthorizationHandler CreateHandler(string? adminsGroupId)
51+
{
52+
var options = Options.Create(new BackOfficeHostOptions { Host = "back-office.test.localhost", AdminsGroupId = adminsGroupId });
53+
return new BackOfficeAdminAuthorizationHandler(options);
54+
}
55+
56+
private static AuthorizationHandlerContext CreateAuthorizationContext(IAuthorizationRequirement requirement, string[] groups)
57+
{
58+
var claims = groups.Select(group => new Claim(BackOfficeIdentityDefaults.GroupsClaimType, group));
59+
var identity = new ClaimsIdentity(claims, BackOfficeIdentityDefaults.AuthenticationScheme);
60+
var principal = new ClaimsPrincipal(identity);
61+
return new AuthorizationHandlerContext([requirement], principal, null);
62+
}
63+
}

0 commit comments

Comments
 (0)