Skip to content

Commit 3a3355f

Browse files
committed
chore(infra): IaC, compose, radix, redis, oauth2-proxy baseline
Infra & deployment topology: - IaC: app-registration + resources deploy scripts, bicep updates - docker-compose: align dev/override, wire oauth2-proxy service - radixconfig: updated config - redis: standalone Dockerfile + entrypoint - secrets: README and .gitignore - root: env template, gitattributes, release-please, mise lint task, dependabot, pre-commit hooks OAuth2 proxy & nginx: - web/oauth2: dedicated Dockerfile, entrypoint, oauth2-proxy config - web/nginx: oauth2 auth_request/redirect snippets, security headers, default.conf updates - web/Dockerfile: align with oauth2 split
1 parent a555b38 commit 3a3355f

31 files changed

Lines changed: 1335 additions & 113 deletions

.env-template

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,13 @@ MONGODB_USERNAME=root
1515
MONGODB_PASSWORD=mongodb
1616
MONGODB_HOSTNAME=db
1717
MONGODB_PORT=27017
18+
# monitoring (Application Insights)
19+
# Leave empty to disable telemetry locally; set to a real connection string
20+
# to ship traces/page-views/exceptions through the API to Azure Monitor.
21+
APPINSIGHTS_CONSTRING=
22+
# Optional: service principal used to authenticate App Insights ingestion
23+
# (mirrors CoreDM). When all three (AZURE_TENANT_ID, OAUTH_CLIENT_ID,
24+
# OAUTH_CLIENT_SECRET) are set, ingestion uses ClientSecretCredential.
25+
# Otherwise it falls back to the instrumentation key in the connection
26+
# string above.
27+
OAUTH_CLIENT_SECRET=

.github/dependabot.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,9 @@ updates:
1313
interval: "monthly" # when template is used, recommended interval is "weekly"
1414
groups:
1515
web:
16-
exclude-patterns:
17-
- "@hey-api/openapi-ts"
1816
update-types:
1917
- "minor"
2018
- "patch"
21-
openapi-ts:
22-
patterns:
23-
- "@hey-api/openapi-ts"
2419

2520
- package-ecosystem: "docker"
2621
directories:

.github/release-please-config.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3-
"packages": {
4-
".": {
5-
"release-type": "simple",
6-
"changelog-path": "docs/docs/changelog/changelog.md",
7-
"extra-files": ["web/package.json"]
8-
}
9-
}
2+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3+
"packages": {
4+
".": {
5+
"release-type": "simple",
6+
"changelog-path": "docs/docs/changelog/changelog.md",
7+
"extra-files": ["web/package.json"]
8+
}
9+
}
1010
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{".":"1.5.0"}
1+
{ ".": "1.5.0" }

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ repos:
7676
files: ^api/.*\.py$
7777

7878
- repo: https://github.com/biomejs/pre-commit
79-
rev: v2.4.14
79+
rev: v2.4.13
8080
hooks:
8181
- id: biome-check
8282
name: "Lint : biome (ts/js)"

IaC/app-registration.bicep

Lines changed: 134 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,150 @@
1-
extension microsoftGraph
2-
param applicationName string = 'template-fastapi-react'
3-
param repositoryName string = 'template-fastapi-react'
1+
extension 'br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:1.0.0'
2+
targetScope = 'subscription'
43

5-
// The Entra ID application
6-
// Resource format https://learn.microsoft.com/en-us/graph/templates/reference/applications?view=graph-bicep-1.0
7-
resource app 'Microsoft.Graph/applications@v1.0' = {
8-
displayName: '${applicationName}'
4+
// Provisions the two Entra ID app registrations required by the BFF auth setup:
5+
// * <name>-api-<env> — resource server. FastAPI validates JWTs whose
6+
// `aud` claim equals this app's appId.
7+
// * <name>-oauth2-<env> — OIDC client used by oauth2-proxy.
8+
//
9+
// Run via ./deploy-app-registration.sh (which also mints the BFF client secret).
10+
11+
@description('Lowercase application slug, used as both the unique app name and to derive Radix URLs.')
12+
param applicationName string
13+
14+
@description('Environment slug. Affects the registration suffix and the redirect URIs registered.')
15+
@allowed(['dev', 'test', 'prod'])
16+
param environment string
17+
18+
@description('ServiceNow Configuration Item / Business Application ID. Required by Equinor IAM compliance for production use; may be empty for sandbox deployments.')
19+
param serviceManagementReference string = ''
20+
21+
@description('Object IDs of users/groups that should own both App Registrations.')
22+
param ownerObjectIds string[]
23+
24+
@description('Extra production hostnames whose /oauth2/callback should be a valid redirect URI.')
25+
param productionHostnames string[] = []
26+
27+
var apiAccessScopeId = guid('api-access-${applicationName}')
28+
var adminRoleId = guid('admin-role-${applicationName}')
29+
30+
var productionRedirectUris = [for host in productionHostnames: 'https://${host}/oauth2/callback']
31+
32+
var bffRedirectUris = concat(
33+
['https://proxy-${applicationName}-${environment}.radix.equinor.com/oauth2/callback'],
34+
environment == 'prod'
35+
? concat(
36+
['https://${applicationName}.app.radix.equinor.com/oauth2/callback'],
37+
productionRedirectUris
38+
)
39+
: environment == 'dev' ? ['http://localhost/oauth2/callback'] : []
40+
)
41+
42+
var graphAppId = '00000003-0000-0000-c000-000000000000'
43+
44+
// ------------------------------------------------------------------
45+
// API app registration — the resource server.
46+
// ------------------------------------------------------------------
47+
resource apiApp 'Microsoft.Graph/applications@v1.0' = {
48+
displayName: '${applicationName}-api-${environment}'
49+
uniqueName: '${applicationName}-api-${environment}'
950
signInAudience: 'AzureADMyOrg'
10-
uniqueName: '${applicationName}'
11-
spa: {
12-
// The callback URL is the URL that the user is redirected to after the login,
13-
// and it contains the URL of the application that is registered in Radix and localhost for doing development.
14-
redirectUris: [
15-
// Development
16-
'https://proxy-${applicationName}-dev.radix.equinor.com/api/docs/oauth2-redirect'
17-
'https://proxy-${applicationName}-dev.radix.equinor.com'
18-
'https://proxy-${applicationName}-dev.radix.equinor.com/'
19-
// Staging
20-
'https://proxy-${applicationName}-staging.radix.equinor.com/api/docs/oauth2-redirect'
21-
'https://proxy-${applicationName}-staging.radix.equinor.com'
22-
'https://proxy-${applicationName}-staging.radix.equinor.com/'
23-
// Production
24-
'https://${applicationName}.app.radix.equinor.com/api/docs/oauth2-redirect'
25-
'https://proxy-${applicationName}-prod.radix.equinor.com/'
26-
'https://${applicationName}.app.radix.equinor.com/'
27-
'https://${applicationName}.app.radix.equinor.com'
28-
// For development
29-
'http://localhost/api/docs/oauth2-redirect'
30-
'http://localhost/'
31-
'http://localhost:5000/docs/oauth2-redirect'
32-
]
51+
serviceManagementReference: empty(serviceManagementReference) ? null : serviceManagementReference
52+
owners: {
53+
relationships: ownerObjectIds
54+
relationshipSemantics: 'replace'
3355
}
56+
identifierUris: [
57+
'https://${environment}.${applicationName}.equinor.com/api'
58+
]
3459
api: {
35-
// In version 2 the audience is always the client id, and does not contain the api:// in the decoded JWT.
36-
// It is important to know this because the API expects a JWT token with a specific signature for validation,
37-
// and this is specified in the configuration settings and must match.
60+
// v2 access tokens carry the appId (a GUID) in `aud`, not api://...
3861
requestedAccessTokenVersion: 2
39-
// To allow OpenAPI and clients to talk to the API, we need to add the scope to the API.
4062
oauth2PermissionScopes: [
41-
{
42-
id: '31a61854-0d6d-4c60-918b-efffd4fac373'
43-
adminConsentDescription: 'Allow users to access the API'
44-
adminConsentDisplayName: 'Read'
45-
isEnabled: true
46-
type: 'User'
47-
userConsentDescription: 'Access the API'
48-
userConsentDisplayName: 'Access the API'
49-
value: 'api${app.appId}'
50-
}
63+
{
64+
id: apiAccessScopeId
65+
adminConsentDescription: 'Allow users to access the API'
66+
adminConsentDisplayName: 'Read'
67+
isEnabled: true
68+
type: 'User'
69+
userConsentDescription: 'Access the API'
70+
userConsentDisplayName: 'Access the API'
71+
value: 'access'
72+
}
5173
]
5274
}
5375
appRoles: [
5476
{
55-
id: '31a61854-0d6d-4c60-918b-efffd4fac379'
56-
allowedMemberTypes: [
57-
'User'
58-
'Application'
59-
]
60-
description: '${applicationName} administrators. Access to all fields. Permission to edit admin values.'
61-
displayName: 'Admin'
62-
isEnabled: true
63-
value: 'admin'
64-
}
77+
id: adminRoleId
78+
allowedMemberTypes: ['User', 'Application']
79+
description: '${applicationName} administrators.'
80+
displayName: 'Admin'
81+
isEnabled: true
82+
value: 'admin'
83+
}
6584
]
66-
// Resource format https://learn.microsoft.com/en-us/graph/templates/reference/federatedidentitycredentials?view=graph-bicep-1.0
67-
resource githubFic 'federatedIdentityCredentials' = {
68-
name: '${app.uniqueName}/githubFic'
69-
audiences: [
70-
'api://AzureADTokenExchange'
71-
]
72-
description: 'Federated Identity Credentials for Github Actions to access Entra protected resources'
73-
issuer: 'https://token.actions.githubusercontent.com'
74-
// Subject is checked before issuing an Entra ID access token to access Azure resources.
75-
// GitHub Actions subject examples can be found in https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims
76-
subject: 'repo:equinor/${repositoryName}:ref:refs/heads/main'
85+
requiredResourceAccess: [
86+
{
87+
resourceAppId: graphAppId
88+
resourceAccess: [
89+
// User.Read
90+
{ id: 'e1fe6dd8-ba31-4d61-89e7-88639da4683d', type: 'Scope' }
91+
]
92+
}
93+
]
94+
}
95+
96+
resource apiAppSP 'Microsoft.Graph/servicePrincipals@v1.0' = {
97+
appId: apiApp.appId
98+
owners: {
99+
relationships: ownerObjectIds
100+
relationshipSemantics: 'replace'
77101
}
78102
}
79103

80-
// The Service Principle (or Enterprise App)
81-
resource appSP 'Microsoft.Graph/servicePrincipals@v1.0' = {
82-
appId: app.appId
83-
displayName: '${applicationName}'
104+
// ------------------------------------------------------------------
105+
// BFF (oauth2-proxy) app registration — the OIDC client.
106+
// ------------------------------------------------------------------
107+
resource oauth2App 'Microsoft.Graph/applications@v1.0' = {
108+
displayName: '${applicationName}-oauth2-${environment}'
109+
uniqueName: '${applicationName}-oauth2-${environment}'
110+
signInAudience: 'AzureADMyOrg'
111+
serviceManagementReference: empty(serviceManagementReference) ? null : serviceManagementReference
112+
owners: {
113+
relationships: ownerObjectIds
114+
relationshipSemantics: 'replace'
115+
}
116+
web: {
117+
redirectUris: bffRedirectUris
118+
}
119+
requiredResourceAccess: [
120+
{
121+
// The matching API registration.
122+
resourceAppId: apiApp.appId
123+
resourceAccess: [
124+
{ id: apiAccessScopeId, type: 'Scope' }
125+
]
126+
}
127+
{
128+
resourceAppId: graphAppId
129+
resourceAccess: [
130+
{ id: '37f7f235-527c-4136-accd-4a02d197296e', type: 'Scope' } // openid
131+
{ id: '14dad69e-099b-42c9-810b-d002981feec1', type: 'Scope' } // profile
132+
{ id: '64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0', type: 'Scope' } // email
133+
{ id: '7427e0e9-2fba-42fe-b0c0-848c9e6a8182', type: 'Scope' } // offline_access
134+
{ id: 'e1fe6dd8-ba31-4d61-89e7-88639da4683d', type: 'Scope' } // User.Read
135+
]
136+
}
137+
]
138+
}
84139

140+
resource oauth2AppSP 'Microsoft.Graph/servicePrincipals@v1.0' = {
141+
appId: oauth2App.appId
142+
owners: {
143+
relationships: ownerObjectIds
144+
relationshipSemantics: 'replace'
145+
}
85146
}
147+
148+
output apiApplicationId string = apiApp.appId
149+
output apiScope string = 'api://${apiApp.appId}/access'
150+
output oauth2ApplicationId string = oauth2App.appId

IaC/bicepconfig.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
2-
"experimentalFeaturesEnabled": {
3-
"extensibility": true
4-
}
2+
"experimentalFeaturesEnabled": {
3+
"extensibility": true
4+
},
5+
"extensions": {
6+
"MicrosoftGraph": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.2.0-preview"
7+
}
58
}

0 commit comments

Comments
 (0)