Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,11 @@ override.tf.json
*_override.tf.json

# MacOS
.DS_Store
.DS_Store

# Local database copies — may contain production personal data. Never commit.
.bacpacs/
*.bacpac
# Local developer config overrides (may contain local credentials). Never commit.
appsettings.Local.json
appsettings.*.Local.json
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Repo for Altinn infoportal
This repository is organized into distinct applications for the frontend and backend:

- **Frontend**: Running instructions and documentation for the frontend app can be found in the [Astro Infoportal README](./astro-infoportal/README.md).
- **Backend**: Running instructions and documentation for the CMS app can be found in the [Umbraco Infoportal README](./umbraco-infoportal/README.md).
- **Backend**: Running instructions and documentation for the CMS app can be found in the [Umbraco Infoportal README](./umbraco-infoportal/README.md).
- **Local database**: To run Umbraco against a copy of a real environment's database, see the [local database copy README](./scripts/db/README.md).
18 changes: 17 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,24 @@ services:
- umbraco_logs:/app/umbraco/Logs
- umbraco_data:/app/umbraco/Data
- umbraco_media:/app/wwwroot/media


mssql:
image: ${MSSQL_IMAGE:-mcr.microsoft.com/mssql/server:2022-latest}
platform: ${MSSQL_PLATFORM:-linux/amd64}
container_name: infoportal-mssql
profiles: ["localdb"]
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
MSSQL_PID: "Developer"
MSSQL_SA_PASSWORD: "${MSSQL_SA_PASSWORD:-}"
ports:
- "${MSSQL_PORT:-1433}:1433"
volumes:
- mssql_data:/var/opt/mssql

volumes:
umbraco_logs:
umbraco_data:
umbraco_media:
mssql_data:
147 changes: 147 additions & 0 deletions scripts/db/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Local copy of the Umbraco database

Copy an environment's database to your machine and run Umbraco against it.

```bash
scripts/db/refresh-local-db.sh at22 --auth token
```

Then start Umbraco. The connection string is written for you.

## 1. Get access

You need to be a member of the Entra group **`DIS AzSQL Admin Dev IP`**, which is the Entra
admin on the dev SQL servers. Ask the platform team. Prod is a separate grant.

You also need to be **connected to the VPN** — the databases are Private Link addresses and
do not resolve otherwise.

## 2. Install the tools

**All platforms**

```bash
dotnet tool install -g microsoft.sqlpackage # ensure ~/.dotnet/tools is on PATH
az login # used for database authentication
```

**macOS** — Microsoft ships no arm64 SQL Server image, and it only runs under Rosetta-backed
emulation, so use Colima rather than Docker Desktop or podman:

```bash
brew install colima docker docker-compose sqlcmd
colima start --vm-type=vz --vz-rosetta --cpu 4 --memory 8 --disk 60
```

**Linux**

```bash
sudo apt-get install -y docker.io sqlcmd # or your distro's equivalent
```

**Windows** — work inside WSL2 (Ubuntu); the scripts are not run from PowerShell. Either
enable Docker Desktop's WSL2 integration, or install Docker inside the distro, then follow the
Linux steps. Keep the repo on the Linux filesystem, not `/mnt/c`, or everything will be slow.

## 3. Run it

```bash
scripts/db/refresh-local-db.sh at22 --auth token
```

Exports the database, starts SQL Server locally, imports, and writes the connection string to
`umbraco-infoportal/appsettings.Local.json`, which Umbraco loads automatically in Development.

**That file is git-ignored and contains a password. Never commit it.**

## 4. Start Umbraco

```bash
cd umbraco-infoportal
dotnet run
```

Then, to log in:

1. **Turn off the Entra auto-redirect.** In
`umbraco-infoportal/App_Plugins/ExternalLoginProviders/umbraco-package.json`, set
`"autoRedirect"` to `false`.

Without this, `/umbraco` immediately redirects to Microsoft Entra ID, which is not
configured locally, and you get
`No authentication handler is registered for the scheme 'Umbraco.MicrosoftEntraId'`.

2. **Open https://localhost:44391/umbraco** and log in as `alv.gullbrand.lia@digdir.no` /
`Test1234`. Use the local username and password form, not the Microsoft button.

The backoffice requires HTTPS — plain `http://` returns
`error:invalid_request ... This server only accepts HTTPS requests`. If the browser
distrusts the certificate, run `dotnet dev-certs https --trust` once.

3. **Revert `umbraco-package.json` before committing.** It is a tracked file, so the change
shows up in `git status` and must not be pushed.

Passwords come from the copied environment. If that account does not work in the environment
you copied, ask the team for a test account or reset one against your local container.

Use plain `dotnet run` so it starts in Development: that is what loads `appsettings.Local.json`
and keeps Key Vault disabled. Running with `--no-launch-profile` or a Production environment
fails with `Configuration value 'KeyVault:AkvUri' must be configured`.

The generated `appsettings.Local.json` also turns uSync's automatic export off
(`ExportOnSave: "None"`). Without it, working against a copied environment writes that
environment's entire schema and content to `umbraco-infoportal/uSync/` — thousands of
generated files sitting next to tracked ones. Exporting by hand from the backoffice still
works, which is how uSync files are meant to be produced.

Useful flags:

| Flag | Effect |
|---|---|
| `--skip-export` | reuse the newest `.bacpac` instead of downloading again |
| `--keep-container` | import into the running container instead of recreating it |
| `--auth password` | prompt for an Entra password instead of using an `az` token |

The four scripts also run standalone: `export-bacpac.sh`, `start-sqlserver.sh`,
`import-bacpac.sh`, `refresh-local-db.sh`. Pass `--help` to any of them.

## What you get, and what you don't

- **Images will be broken.** Media lives in Azure Blob Storage, not in the database.
- **Log in with the copied environment's backoffice users**, not local ones.
- For doctype and schema work, use uSync instead — it is lighter and involves no production
data.

## Handling the data

A copy of a real environment contains real accounts and personal data.

- Keep it on an encrypted disk.
- Delete the `.bacpac` when you are done.
- Do not share it. `.bacpacs/` and `*.bacpac` are git-ignored; leave it that way.

Remote environments are strictly read-only: these scripts only ever run a connectivity check
and `sqlpackage /Action:Export` against them, and the import refuses any target that is not
localhost.

## If something fails

| Symptom | Cause and fix |
|---|---|
| `cannot resolve <host>` | Not on the VPN. |
| `Login failed for user '<token-identified principal>'` | You are not in the `DIS AzSQL Admin Dev IP` group. |
| `AADSTS50126` | Password auth is blocked for your account — use `--auth token`. |
| `SQL Server segfaulted on startup (exit 139)` | Your runtime emulates with qemu, not Rosetta. Use Colima as above, or set the fallback below. |
| `x509: negative serial number` | Only affects hand-run `sqlcmd`; prefix it with `GODEBUG=x509negativeserial=1`. |
| Import fails on a foreign key | The export caught the source mid-write. Re-export. |

**Without Rosetta** (arm64 machines that cannot use Colima) put this in `scripts/db/.env` to use
the arm64-native Azure SQL Edge instead. It works, but it is a reduced engine and not what the
environments run:

```
MSSQL_IMAGE=mcr.microsoft.com/azure-sql-edge
MSSQL_PLATFORM=linux/arm64
```

To point the scripts at a specific container runtime, set `CONTAINER_CLI` in `scripts/db/.env`.
10 changes: 10 additions & 0 deletions scripts/db/environments.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Azure SQL servers per environment. Private Link addresses — reachable only over VPN.
# Source: Confluence, "Systemdokumentasjon Altinn Infoportal" -> AzureSQL
#
# Only at22 is active. Enable another environment by uncommenting its line.
#
# env host catalog
at22 infop-at22-vllfov-server.privatelink.database.windows.net umbraco
#at23 infop-at23-vtjzsk-server.privatelink.database.windows.net umbraco
#tt02 infop-tt02-ksalae-server.privatelink.database.windows.net umbraco
#prod infop-prod-hxhapc-server.privatelink.database.windows.net umbraco
139 changes: 139 additions & 0 deletions scripts/db/export-bacpac.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
#
# Export an environment's Umbraco database to a local .bacpac file.
#
# READ-ONLY against the remote server. This script performs exactly two remote
# operations: a TCP reachability check and sqlpackage /Action:Export.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
. "$SCRIPT_DIR/lib.sh"

usage() {
cat >&2 <<EOF
usage: export-bacpac.sh <env> [--out DIR] [--user ENTRA_EMAIL] [--auth METHOD]

<env> environment name; one of: $(list_environments)
--out DIR output directory (default: <repo>/.bacpacs)
--user EMAIL Entra ID account (default: \$INFOPORTAL_DB_USER, or
git config user.email when it is a @digdir.no address)
--auth METHOD password (default) or token.

'password' uses Authentication=Active Directory Password and
prompts. If Entra answers AADSTS50126 even with the correct
password, ROPC is blocked for your account -- use token.

'token' takes an Entra access token from the Azure CLI
(az login required). Nothing is prompted.
EOF
exit 1
}

ENV_NAME=""
OUT_DIR="$INFOPORTAL_REPO_ROOT/.bacpacs"
DB_USER="${INFOPORTAL_DB_USER:-}"
AUTH_METHOD="password"

while [ $# -gt 0 ]; do
case "$1" in
--out) [ $# -ge 2 ] || usage; OUT_DIR="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || usage; DB_USER="$2"; shift 2 ;;
--auth) [ $# -ge 2 ] || usage; AUTH_METHOD="$2"; shift 2 ;;
-h|--help) usage ;;
-*) log "unknown option: $1"; usage ;;
*) [ -z "$ENV_NAME" ] || usage; ENV_NAME="$1"; shift ;;
esac
done

case "$AUTH_METHOD" in
password|token) ;;
*) log "unknown --auth value '$AUTH_METHOD'"; usage ;;
esac

[ -n "$ENV_NAME" ] || usage

require_sqlpackage
SQLPACKAGE="$(find_sqlpackage)"

ENV_OUTPUT="$(resolve_environment "$ENV_NAME")"
read -r HOST CATALOG <<EOF
$ENV_OUTPUT
EOF

if [ "$ENV_NAME" = "prod" ]; then
log ""
log "You are about to export from PRODUCTION."
log "The export is read-only, but it puts read load on a live server and"
log "produces a file containing real personal data."
printf 'Type the word prod to continue: ' >&2
read -r CONFIRM
[ "$CONFIRM" = "prod" ] || die "aborted"
fi

log "checking connectivity to $HOST:1433 ..."
require_remote_reachable "$HOST" 1433

CONN="Server=tcp:$HOST,1433; Initial Catalog=$CATALOG; Encrypt=True; TrustServerCertificate=True"

if [ "$AUTH_METHOD" = "token" ]; then
command -v az >/dev/null 2>&1 || die "the token auth method needs the Azure CLI.
Install it, run 'az login', then retry."

log "requesting an Entra access token for database.windows.net ..."
# Never echo, log or persist the token. It goes straight into sqlpackage's
# argv and nowhere else.
DB_TOKEN="$(az account get-access-token --resource https://database.windows.net/ \
--query accessToken -o tsv 2>/dev/null || true)"
[ -n "$DB_TOKEN" ] || die "could not get an access token. Run 'az login' and check
that 'az account show' names the tenant that owns this database."
else
if [ -z "$DB_USER" ]; then
DB_USER="$(git -C "$INFOPORTAL_REPO_ROOT" config user.email 2>/dev/null || true)"
case "$DB_USER" in *@digdir.no) ;; *) DB_USER="" ;; esac
fi
printf 'Entra ID email%s: ' "${DB_USER:+ [$DB_USER]}" >&2
read -r USER_INPUT
[ -n "$USER_INPUT" ] && DB_USER="$USER_INPUT"
[ -n "$DB_USER" ] || die "no Entra ID email given"

printf 'Password for %s: ' "$DB_USER" >&2
read -rs DB_PASSWORD
printf '\n' >&2
[ -n "$DB_PASSWORD" ] || die "no password given"

CONN="$CONN; Authentication=Active Directory Password; User ID=$DB_USER; Password=$DB_PASSWORD"
fi

mkdir -p "$OUT_DIR"
TARGET="$OUT_DIR/$ENV_NAME-umbraco-$(date +%Y%m%d-%H%M%S).bacpac"

log "exporting $ENV_NAME ($CATALOG) -> $TARGET"
log "this reads the whole database and can take several minutes ..."

if [ "$AUTH_METHOD" = "token" ]; then
# An access token lasts about an hour. A very large export can outlive it;
# if that happens, re-run — sqlpackage cannot refresh it mid-flight.
"$SQLPACKAGE" /Action:Export \
/SourceConnectionString:"$CONN" \
/AccessToken:"$DB_TOKEN" \
/TargetFile:"$TARGET" \
|| die "export failed.

If it says \"Login failed for user '<token-identified principal>'\", your Entra
account authenticated but has no login on this server. Ask for access to be
granted on $HOST."
else
"$SQLPACKAGE" /Action:Export \
/SourceConnectionString:"$CONN" \
/TargetFile:"$TARGET" \
|| die "export failed.

If it says AADSTS50126, Entra rejected the password. That usually means password
(ROPC) authentication is blocked for your account rather than the password being
wrong. Retry with: $(basename "$0") $ENV_NAME --auth token"
fi

log "exported $(du -h "$TARGET" | cut -f1) to:"
[ -s "$TARGET" ] || die "export reported success but $TARGET is missing or empty"
printf '%s\n' "$TARGET"
Loading