diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..d652e74 --- /dev/null +++ b/README.en.md @@ -0,0 +1,117 @@ +# spip-serverless + +[Français](README.md) · **English** + +Run [SPIP](https://www.spip.net/) — the French free-software CMS — **serverless on AWS**: +PHP on AWS Lambda (via [Bref](https://bref.sh/)), an Aurora DSQL database, sessions in +DynamoDB, static assets on S3, all fronted by CloudFront. No servers to patch, scale to +zero, pay per request. + +This repo is a **generic, reusable platform** — bring your own content, plugins and +skeletons. Default region: **eu-west-3** (Paris) 🇫🇷. + +## Architecture + +``` + ┌────────────┐ + visitor ──────▶│ CloudFront │ + └─────┬──────┘ + /IMG,/plugins-dist,… everything else + │ │ + ┌─────▼─────┐ ┌─────▼───────────┐ + │ S3 assets │ │ API Gateway │ + └───────────┘ │ (proxy, X-Ray) │ + └─────┬───────────┘ + │ AWS_PROXY + ┌─────▼───────────┐ IAM-auth token + │ Lambda (SPIP) │──────────────▶ Aurora DSQL + │ Bref, PHP 8.5 │──────────────▶ DynamoDB (sessions) + └─────┬───────────┘──────────────▶ S3 (media, read/write) + │ + SSM (spip keys), CloudWatch, X-Ray +``` + +- **`spip/`** — the SPIP runtime image. SPIP core is **fetched at build time** (pinned in + `spip/SPIP_VERSION`), not vendored. Overlays adapt core to Lambda/DSQL; two custom + plugins (`s3upload`, `sessions_dynamodb`) and `logs_stderr` are baked in. +- **`iac/spip/static/`** — Terraform: DSQL cluster, S3 bucket, ECR repo, DynamoDB table, + SSM key parameter. +- **`iac/spip/app/`** — Terraform: Lambda + alias, API Gateway (transparent proxy), + CloudFront (+ optional custom domain via ACM/Route53). +- **`docs/`** — how everything fits together. + +## Prerequisites + +- An AWS account with permission to create the resources above (Aurora DSQL is available + in eu-west-3 among others). +- **Docker** installed and **running** (buildx; the image is `linux/arm64`). +- **Terraform ≥ 1.14** (an older 1.5.x on your PATH will fail the `required_version` check). +- **AWS CLI v2** with an **active session** for your profile — `aws sso login --profile

` + or exported credentials. The principal must be able to create DSQL, S3, ECR, DynamoDB, + SSM, Lambda, API Gateway, CloudFront and IAM roles. +- **make**, `git`, `curl`, `unzip`. +- Aurora DSQL, Bedrock etc. beyond the above are **not** used by the bare platform. + +## Quick start + +```bash +# 0. pick an environment name + AWS profile +export ENV=test AWS_PROFILE=your-profile + +# 1. create the S3 bucket that will hold Terraform state (once per account) +aws s3 mb s3://your-tfstate-bucket --region eu-west-3 + +# 2. create per-env config from the template +for s in static app; do cp -r iac/spip/$s/var/example iac/spip/$s/var/$ENV; done +# then edit iac/spip/*/var/$ENV/{values.tfvars,backend.tfbackend}: +# set the state bucket (both backend.tfbackend + app's static_state_bucket), +# region, and optionally a custom domain. + +# 3. base infra (DSQL, S3, ECR, DynamoDB, SSM) +make deploy-static ENV=$ENV + +# 4. fill the SPIP key material in SSM → see docs/db-bootstrap.md + +# 5. build + push image, sync assets, deploy the app stack +make deploy ENV=$ENV + +# 6. create the schema + admin author → see docs/db-bootstrap.md +``` + +`make deploy` prints the CloudFront URL; the admin is at `/ecrire`. Verified end-to-end +on a fresh AWS account (eu-west-3): CloudFront serves the public site and `/spip.php?page=login`. + +## Working with SPIP core locally + +Core is git-ignored. To index it in your IDE or run it with Apache locally: + +```bash +make fetch-spip # downloads the pinned version into spip/src/ +make run-local # SPIP on http://localhost:8080 (Apache) +``` + +## Common tasks + +| Task | Doc | +|---|---| +| Add a plugin (third-party or custom) | [docs/plugins.md](docs/plugins.md) | +| Upgrade SPIP core | [docs/spip-upgrade.md](docs/spip-upgrade.md) | +| Add / configure an environment | [docs/environments.md](docs/environments.md) | +| Initialise the database + admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) | +| Reset an admin password | [docs/spip-passwords.md](docs/spip-passwords.md) | +| DSQL, sessions, S3, secrets, tracing, logging, cron | `docs/*.md` | + +## Security notes + +- API Gateway is a **transparent proxy** — it does not authenticate. **SPIP** enforces + auth for the back-office (`/ecrire`). If you add your own API routes, add an authorizer + in `iac/spip/app/` yourself. +- SPIP secret keys live in SSM (SecureString) and are written back by the Lambda on first + boot; the S3 bucket is private (CloudFront OAC only); DSQL uses short-lived IAM auth + tokens (no stored DB password). +- You can turn the public site off (admin/login only) with `spip_public_disabled = true`. + +## License + +The SPIP core fetched at build time is GPL-3.0 (© the SPIP community). This repo's own +glue code is provided under the terms in [LICENSE](LICENSE). diff --git a/README.md b/README.md index c51e336..2e71f71 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,119 @@ # spip-serverless -Run [SPIP](https://www.spip.net/) — the French free-software CMS — **serverless on AWS**: -PHP on AWS Lambda (via [Bref](https://bref.sh/)), an Aurora DSQL database, sessions in -DynamoDB, static assets on S3, all fronted by CloudFront. No servers to patch, scale to -zero, pay per request. +**Français** · [English](README.en.md) -This repo is a **generic, reusable platform** — bring your own content, plugins and -skeletons. Default region: **eu-west-3** (Paris) 🇫🇷. +Faire tourner [SPIP](https://www.spip.net/) — le CMS libre français — **serverless sur AWS** : +PHP sur AWS Lambda (via [Bref](https://bref.sh/)), une base Aurora DSQL, les sessions dans +DynamoDB, les assets statiques sur S3, le tout derrière CloudFront. Aucun serveur à patcher, +scale-to-zero, facturation à l'usage. + +Ce dépôt est une **plateforme générique et réutilisable** — apportez votre contenu, vos +plugins et vos squelettes. Région par défaut : **eu-west-3** (Paris) 🇫🇷. ## Architecture ``` ┌────────────┐ - visitor ──────▶│ CloudFront │ + visiteur ─────▶│ CloudFront │ └─────┬──────┘ - /IMG,/plugins-dist,… everything else - │ │ - ┌─────▼─────┐ ┌─────▼───────────┐ - │ S3 assets │ │ API Gateway │ + /IMG,/plugins-dist,… tout le reste + │ │ + ┌─────▼─────┐ ┌─────────▼───────┐ + │ assets S3 │ │ API Gateway │ └───────────┘ │ (proxy, X-Ray) │ └─────┬───────────┘ │ AWS_PROXY - ┌─────▼───────────┐ IAM-auth token + ┌─────▼───────────┐ jeton IAM │ Lambda (SPIP) │──────────────▶ Aurora DSQL │ Bref, PHP 8.5 │──────────────▶ DynamoDB (sessions) - └─────┬───────────┘──────────────▶ S3 (media, read/write) + └─────┬───────────┘──────────────▶ S3 (média, lecture/écriture) │ - SSM (spip keys), CloudWatch, X-Ray + SSM (clés spip), CloudWatch, X-Ray ``` -- **`spip/`** — the SPIP runtime image. SPIP core is **fetched at build time** (pinned in - `spip/SPIP_VERSION`), not vendored. Overlays adapt core to Lambda/DSQL; two custom - plugins (`s3upload`, `sessions_dynamodb`) and `logs_stderr` are baked in. -- **`iac/spip/static/`** — Terraform: DSQL cluster, S3 bucket, ECR repo, DynamoDB table, - SSM key parameter. -- **`iac/spip/app/`** — Terraform: Lambda + alias, API Gateway (transparent proxy), - CloudFront (+ optional custom domain via ACM/Route53). -- **`docs/`** — how everything fits together. - -## Prerequisites - -- An AWS account with permission to create the resources above (Aurora DSQL is available - in eu-west-3 among others). -- Docker (buildx, ARM64), Terraform ≥ 1.14, the AWS CLI, and `make`. -- Aurora DSQL, Bedrock etc. are **not** used by the bare platform — only DSQL, S3, - DynamoDB, Lambda, API Gateway, CloudFront, ECR, SSM. - -## Quick start +- **`spip/`** — l'image runtime SPIP. Le cœur SPIP est **récupéré au build** (épinglé dans + `spip/SPIP_VERSION`), non vendorisé. Des overlays adaptent le cœur à Lambda/DSQL ; deux + plugins maison (`s3upload`, `sessions_dynamodb`) et `logs_stderr` sont embarqués. +- **`iac/spip/static/`** — Terraform : cluster DSQL, bucket S3, dépôt ECR, table DynamoDB, + paramètre SSM (clés). +- **`iac/spip/app/`** — Terraform : Lambda + alias, API Gateway (proxy transparent), + CloudFront (+ domaine custom optionnel via ACM/Route53). +- **`docs/`** — comment tout s'articule. + +## Prérequis + +- Un compte AWS autorisé à créer les ressources ci-dessus (Aurora DSQL est disponible en + eu-west-3, entre autres). +- **Docker** installé et **démarré** (buildx ; l'image est `linux/arm64`). +- **Terraform ≥ 1.14** (un vieux 1.5.x dans le PATH échoue sur le contrôle `required_version`). +- **AWS CLI v2** avec une **session active** pour votre profil — `aws sso login --profile

` + ou des credentials exportés. Le principal doit pouvoir créer DSQL, S3, ECR, DynamoDB, SSM, + Lambda, API Gateway, CloudFront et des rôles IAM. +- **make**, `git`, `curl`, `unzip`. +- Au-delà, Aurora DSQL, Bedrock, etc. ne sont **pas** utilisés par la plateforme nue. + +## Démarrage rapide ```bash -# 0. pick an environment name + AWS profile -export ENV=test AWS_PROFILE=your-profile +# 0. choisir un nom d'environnement + un profil AWS +export ENV=test AWS_PROFILE=votre-profil + +# 1. créer le bucket S3 qui contiendra l'état Terraform (une fois par compte) +aws s3 mb s3://votre-bucket-tfstate --region eu-west-3 -# 1. create per-env config from the template +# 2. créer la config par env depuis le modèle for s in static app; do cp -r iac/spip/$s/var/example iac/spip/$s/var/$ENV; done -# then edit iac/spip/*/var/$ENV/{values.tfvars,backend.tfbackend} -# (state bucket, region, optional domain) +# puis éditer iac/spip/*/var/$ENV/{values.tfvars,backend.tfbackend} : +# renseigner le bucket d'état (les deux backend.tfbackend + static_state_bucket de app), +# la région, et éventuellement un domaine custom. -# 2. base infra (DSQL, S3, ECR, DynamoDB, SSM) +# 3. infra de base (DSQL, S3, ECR, DynamoDB, SSM) make deploy-static ENV=$ENV -# 3. fill the SPIP key material in SSM → see docs/db-bootstrap.md +# 4. remplir les clés SPIP dans SSM → voir docs/db-bootstrap.md -# 4. build + push image, sync assets, deploy the app stack +# 5. build + push de l'image, sync des assets, déploiement de la stack app make deploy ENV=$ENV -# 5. create the schema + admin author → see docs/db-bootstrap.md +# 6. créer le schéma + l'auteur admin → voir docs/db-bootstrap.md ``` -`make deploy` prints the CloudFront URL; the admin is at `/ecrire`. +`make deploy` affiche l'URL CloudFront ; l'admin est sur `/ecrire`. Vérifié de bout en bout +sur un compte AWS vierge (eu-west-3) : CloudFront sert le site public et `/spip.php?page=login`. -## Working with SPIP core locally +## Travailler avec le cœur SPIP en local -Core is git-ignored. To index it in your IDE or run it with Apache locally: +Le cœur est git-ignoré. Pour l'indexer dans votre IDE ou le lancer avec Apache en local : ```bash -make fetch-spip # downloads the pinned version into spip/src/ -make run-local # SPIP on http://localhost:8080 (Apache) +make fetch-spip # télécharge la version épinglée dans spip/src/ +make run-local # SPIP sur http://localhost:8080 (Apache) ``` -## Common tasks +## Tâches courantes -| Task | Doc | +| Tâche | Doc | |---|---| -| Add a plugin (third-party or custom) | [docs/plugins.md](docs/plugins.md) | -| Upgrade SPIP core | [docs/spip-upgrade.md](docs/spip-upgrade.md) | -| Add / configure an environment | [docs/environments.md](docs/environments.md) | -| Initialise the database + admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) | -| Reset an admin password | [docs/spip-passwords.md](docs/spip-passwords.md) | -| DSQL, sessions, S3, secrets, tracing, logging, cron | `docs/*.md` | - -## Security notes - -- API Gateway is a **transparent proxy** — it does not authenticate. **SPIP** enforces - auth for the back-office (`/ecrire`). If you add your own API routes, add an authorizer - in `iac/spip/app/` yourself. -- SPIP secret keys live in SSM (SecureString) and are written back by the Lambda on first - boot; the S3 bucket is private (CloudFront OAC only); DSQL uses short-lived IAM auth - tokens (no stored DB password). -- You can turn the public site off (admin/login only) with `spip_public_disabled = true`. - -## License - -The SPIP core fetched at build time is GPL-3.0 (© the SPIP community). This repo's own -glue code is provided under the terms in [LICENSE](LICENSE). +| Ajouter un plugin (tiers ou maison) | [docs/plugins.md](docs/plugins.md) | +| Mettre à jour le cœur SPIP | [docs/spip-upgrade.md](docs/spip-upgrade.md) | +| Ajouter / configurer un environnement | [docs/environments.md](docs/environments.md) | +| Initialiser la base + l'admin | [docs/db-bootstrap.md](docs/db-bootstrap.md) | +| Réinitialiser un mot de passe admin | [docs/spip-passwords.md](docs/spip-passwords.md) | +| DSQL, sessions, S3, secrets, tracing, logs, cron | `docs/*.md` | + +> Les docs détaillées sous `docs/*.md` sont en anglais pour l'instant. + +## Notes de sécurité + +- L'API Gateway est un **proxy transparent** — elle n'authentifie pas. C'est **SPIP** qui + gère l'auth du back-office (`/ecrire`). Si vous ajoutez vos propres routes API, ajoutez + vous-même un authorizer dans `iac/spip/app/`. +- Les clés secrètes SPIP vivent dans SSM (SecureString) et sont réécrites par le Lambda au + premier boot ; le bucket S3 est privé (OAC CloudFront uniquement) ; DSQL utilise des jetons + IAM à courte durée (aucun mot de passe DB stocké). +- Vous pouvez couper le site public (admin/login seulement) avec `spip_public_disabled = true`. + +## Licence + +Le cœur SPIP récupéré au build est en GPL-3.0 (© la communauté SPIP). Le code de glue propre +à ce dépôt est fourni selon les termes du fichier [LICENSE](LICENSE). diff --git a/docs/db-bootstrap.md b/docs/db-bootstrap.md index e729b8a..6af969f 100644 --- a/docs/db-bootstrap.md +++ b/docs/db-bootstrap.md @@ -43,12 +43,13 @@ eval "$(AWS_PROFILE=$PROFILE aws configure export-credentials --format env)" docker run --rm \ -e SPIP_DSQL_CLUSTER=$CLUSTER \ -e SPIP_TABLE_PREFIX=spip \ + -e SPIP_PUBLIC_URL=https:// \ -e SPIP_CLES="$SPIP_CLES" \ -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \ -e AWS_REGION=$REGION \ --entrypoint php \ .dkr.ecr.$REGION.amazonaws.com/spip-serverless: \ - php -d auto_prepend_file= /var/task/scripts/bootstrap-db.php \ + -d auto_prepend_file= /var/task/scripts/bootstrap-db.php \ --admin-login=admin --admin-email=you@example.org --admin-pass='' ``` diff --git a/docs/environments.md b/docs/environments.md index c148a7c..ea9c531 100644 --- a/docs/environments.md +++ b/docs/environments.md @@ -34,8 +34,17 @@ Fill in: - the state S3 bucket in both `backend.tfbackend` files (create it once per account) - `static_state_bucket` / `static_state_region` in the app stack (point at the static stack's state) -- optionally `domain_name` + `hosted_zone_name` for a custom domain (Route53 zone must - exist in the same account) +- optionally `domain_name` + `hosted_zone_name` for a custom domain. Setting + `domain_name` (via Terraform, not the CloudFront console) drives everything from one + variable: the CloudFront alias, a us-east-1 ACM certificate (DNS-validated against the + `hosted_zone_name` Route53 zone, which must exist **in the same account**), the Route53 + A-record, **and** the Lambda's `SPIP_PUBLIC_URL` (so SPIP builds its absolute links on + that domain). Leave both empty to use the default `*.cloudfront.net` domain. + + > If you change `domain_name` on an already-bootstrapped environment, the runtime host + > (prepend.php / `SPIP_PUBLIC_URL`) follows automatically, but the stored `adresse_site` + > meta does not — re-run the bootstrap (or `UPDATE spip_meta SET valeur='https://' + > WHERE nom='adresse_site'`). See `docs/db-bootstrap.md`. ## First bring-up (per environment) diff --git a/docs/plugins.md b/docs/plugins.md index 819aabe..61ac7ae 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -34,6 +34,25 @@ Everything placed under `plugins-dist/` is scanned and activated by SPIP on cold SVP registers it in the DB (`spip_paquets` with `actif='oui'`) and wires the `paquet.xml` pipeline declarations automatically. +### `plugins/` vs `plugins-dist/` — why the distinction disappears at runtime + +In a **normal** SPIP install the two directories mean different things: + +- `plugins-dist/` — plugins **shipped with the SPIP core**, always active, no activation step. +- `plugins/` — plugins **you added**, which must be **activated** (registered in the DB via + SVP, normally by clicking "activate" in the private area). + +That activation step is interactive and writes to disk — neither is possible on an +**immutable, read-only Lambda** that scales to zero. So this build deliberately **copies our +own plugins into `plugins-dist/`** (see the Dockerfile), where they are always active from +the first cold start, with no manual step. + +Consequently, at **runtime** the `plugins/` vs `plugins-dist/` distinction no longer exists — +everything lives in `/var/task/plugins-dist/`. In the **repo**, the separate source folders +(`spip/plugins/` = ours, `spip/plugins-vendor/` = third-party) are kept only for +organisation and provenance; there is no `spip/plugins-dist/` (that name belongs to the +fetched SPIP core). + ## Add a THIRD-PARTY plugin 1. Download the plugin into `spip/plugins-vendor//` (it must have a valid diff --git a/docs/spip-upgrade.md b/docs/spip-upgrade.md index 1de202a..1f8e62a 100644 --- a/docs/spip-upgrade.md +++ b/docs/spip-upgrade.md @@ -4,6 +4,20 @@ SPIP core is **not vendored** in this repo. It is fetched at build time from the archive (`files.spip.net`) at a single pinned version. Upgrading SPIP is therefore just: **bump the pinned version, rebuild, test on a non-prod environment, promote.** +## Repo side vs online side (the mental model) + +The image is **immutable and stateless**. Two clean halves: + +| You change it in the REPO (→ build → deploy) | It happens ONLINE, automatically, in DSQL | +|---|---| +| SPIP version (`spip/SPIP_VERSION`), plugins, overlays, config | The SPIP/plugin **DB schema migration** replays on the **first authenticated `/ecrire` visit** after deploy (if `spip_version_base` bumped) — no manual DB step | +| Rebuild the Docker image, `make deploy` / CI | Your **content** (articles, media, users) stays in DSQL, untouched by a deploy | + +So: **all code/version changes are repo → build → deploy; all data/migration changes are +runtime**, triggered by the first admin request (same cold-start mechanism as the initial +plugin migrations — see `docs/db-bootstrap.md`). You never run a DB migration by hand for +an upgrade; you deploy the image and open the admin once. + ## Where the version is pinned One source of truth: **`spip/SPIP_VERSION`** (e.g. `4.4.22`). @@ -35,7 +49,7 @@ It is consumed by: Our customisations live **outside** SPIP core, so a core upgrade never touches them: - `spip/overlay/**` — files that override core at build time (connect.php, mes_options*, - install.php, dsql.php, SpipCles.php, prepend.php, router.php) + install.php, dsql.php, prepend.php, router.php) - `spip/plugins/**`, `spip/plugins-vendor/**` — our + third-party plugins - `spip/scripts/patch-documents.php` — the S3 patch applied to `ecrire/inc/documents.php` diff --git a/iac/spip/app/cloudfront.tf b/iac/spip/app/cloudfront.tf index 21d1e48..4d6d82e 100644 --- a/iac/spip/app/cloudfront.tf +++ b/iac/spip/app/cloudfront.tf @@ -77,7 +77,7 @@ resource "aws_cloudfront_distribution" "spip" { cached_methods = ["GET", "HEAD"] target_origin_id = "apigw" cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # Managed-CachingDisabled - origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3" # Managed-AllViewer + origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" # Managed-AllViewerExceptHostHeader viewer_protocol_policy = "redirect-to-https" } diff --git a/iac/spip/app/cron.tf b/iac/spip/app/cron.tf new file mode 100644 index 0000000..88e3737 --- /dev/null +++ b/iac/spip/app/cron.tf @@ -0,0 +1,39 @@ +# EventBridge rule that runs SPIP's cron/queue every 5 minutes. +# +# On Lambda, SPIP's queue is disabled on normal web requests (see prepend.php, +# _DEBUG_BLOCK_QUEUE) so page loads stay fast. This rule invokes the Lambda with a +# synthetic API Gateway event for `/spip.php?action=cron`, which is the one path that +# runs the queue — so scheduled jobs fire deterministically, independent of traffic. +resource "aws_cloudwatch_event_rule" "spip_cron" { + name = "${local.app_name}-cron" + schedule_expression = "rate(5 minutes)" + state = "ENABLED" +} + +resource "aws_cloudwatch_event_target" "spip_cron" { + rule = aws_cloudwatch_event_rule.spip_cron.name + target_id = "spip-cron" + arn = aws_lambda_alias.spip_live.arn + + input = jsonencode({ + version = "2.0" + routeKey = "GET /spip.php" + rawPath = "/spip.php" + rawQueryString = "action=cron" + headers = { "x-spip-cron" = "1" } + queryStringParameters = { action = "cron" } + requestContext = { + http = { method = "GET", path = "/spip.php" } + } + isBase64Encoded = false + }) +} + +resource "aws_lambda_permission" "eventbridge_cron" { + statement_id = "AllowEventBridgeCron" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.spip.function_name + qualifier = aws_lambda_alias.spip_live.name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.spip_cron.arn +} diff --git a/iac/spip/app/locals.tf b/iac/spip/app/locals.tf index fac8d63..63e4028 100644 --- a/iac/spip/app/locals.tf +++ b/iac/spip/app/locals.tf @@ -56,6 +56,10 @@ locals { CF_DISTRIBUTION_ID = aws_cloudfront_distribution.spip.id APP_NAME = local.app_name + # Public base URL SPIP builds its absolute links from (see prepend.php). Uses the + # custom domain when set, else the CloudFront default domain. + SPIP_PUBLIC_URL = "https://${var.domain_name != "" ? var.domain_name : aws_cloudfront_distribution.spip.domain_name}" + # When "1", the public SPIP site (skeletons) returns a 404 "off" page. # API/admin paths stay available. SPIP_PUBLIC_DISABLED = var.spip_public_disabled ? "1" : "0" diff --git a/spip/Dockerfile b/spip/Dockerfile index 88a0013..17b28da 100644 --- a/spip/Dockerfile +++ b/spip/Dockerfile @@ -128,7 +128,6 @@ COPY --from=vendors /app/ /var/task/ # Overlays that replace core files (DSQL connection, install wizard, options, router). COPY spip/overlay/ecrire/exec/install.php /var/task/ecrire/exec/install.php COPY spip/overlay/ecrire/req/dsql.php /var/task/ecrire/req/dsql.php -COPY spip/overlay/ecrire/src/Chiffrer/SpipCles.php /var/task/ecrire/src/Chiffrer/SpipCles.php COPY spip/overlay/config/connect.php /var/task/config/connect.php COPY spip/overlay/config/mes_options_lambda.php /var/task/config/mes_options.php COPY spip/overlay/php/prepend.php /var/task/php/prepend.php diff --git a/spip/overlay/ecrire/src/Chiffrer/SpipCles.php b/spip/overlay/ecrire/src/Chiffrer/SpipCles.php deleted file mode 100644 index f45b9b8..0000000 --- a/spip/overlay/ecrire/src/Chiffrer/SpipCles.php +++ /dev/null @@ -1,155 +0,0 @@ -getSecretSite(); - } - - private function __construct(string $file = '') { - if ($file) { - $this->file = $file; - } - $this->cles = new Cles($this->read()); - } - - public function getSecretSite(bool $autoInit = true): ?string { - $key = $this->getKey('secret_du_site', $autoInit); - $meta = $this->getMetaKey('secret_du_site', $autoInit); - return $key ^ $meta; - } - - public function getSecretAuth(bool $autoInit = false): ?string { - return $this->getKey('secret_des_auth', $autoInit); - } - - public function save(): bool { - $ok = ecrire_fichier_securise($this->file, $this->cles->toJson()); - // Persist to SSM so keys survive cold starts - if ($ok) { - $this->saveToSsm(); - } - return $ok; - } - - private function saveToSsm(): void { - $param = getenv('SPIP_CLES_SSM_NAME'); - if (!$param) { - $param = '/spip-serverless/' . (getenv('SPIP_ENV') ?: 'test') . '/spip/cles'; - } - try { - require_once '/var/task/vendor/autoload.php'; - $ssm = new \Aws\Ssm\SsmClient(['region' => getenv('AWS_REGION') ?: 'us-east-1', 'version' => 'latest']); - $ssm->putParameter([ - 'Name' => $param, - 'Value' => $this->cles->toJson(), - 'Type' => 'SecureString', - 'Overwrite' => true, - ]); - } catch (\Throwable $e) { - spip_log('SSM save failed: ' . $e->getMessage(), 'chiffrer' . _LOG_ERREUR); - } - } - - public function backup( - #[\SensitiveParameter] - string $withKey - ): string { - if (count($this->cles)) { - return Chiffrement::chiffrer($this->cles->toJson(), $withKey); - } - return ''; - } - - public function restore( - string $backup, - #[\SensitiveParameter] - string $password_clair, - #[\SensitiveParameter] - string $password_hash, - int $id_auteur - ): bool { - if (empty($backup)) { - return false; - } - $sauvegarde = Chiffrement::dechiffrer($backup, $password_clair); - $json = json_decode($sauvegarde, true); - if (!$json) { - return false; - } - $cles_potentielles = array_map('base64_decode', $json); - if (!empty($cles_potentielles['secret_des_auth'])) { - if (!Password::verifier($password_clair, $password_hash, $cles_potentielles['secret_des_auth'])) { - spip_log("Restauration de la cle `secret_des_auth` par id_auteur $id_auteur erronnee, on ignore", 'chiffrer' . _LOG_INFO_IMPORTANTE); - unset($cles_potentielles['secret_des_auth']); - } - } - $restauration = false; - foreach ($cles_potentielles as $name => $key) { - if (!$this->cles->has($name)) { - $this->cles->set($name, $key); - spip_log("Restauration de la cle $name par id_auteur $id_auteur", 'chiffrer' . _LOG_INFO_IMPORTANTE); - $restauration = true; - } - } - return $restauration; - } - - private function getKey(string $name, bool $autoInit): ?string { - if ($this->cles->has($name)) { - return $this->cles->get($name); - } - if ($autoInit) { - $this->cles->generate($name); - if ($this->save()) { - return $this->cles->get($name); - } - spip_log('Echec ecriture du fichier cle ' . $this->file . " ; impossible de generer une cle $name", 'chiffrer' . _LOG_ERREUR); - $this->cles->delete($name); - } - return null; - } - - private function getMetaKey(string $name, bool $autoInit = true): ?string { - if (!isset($GLOBALS['meta'][$name])) { - include_spip('base/abstract_sql'); - $GLOBALS['meta'][$name] = sql_getfetsel('valeur', 'spip_meta', 'nom = ' . sql_quote($name, '', 'string')); - } - $key = base64_decode($GLOBALS['meta'][$name] ?? ''); - if (strlen($key) === \SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { - return $key; - } - if (!$autoInit) { - return null; - } - $key = Chiffrement::keygen(); - ecrire_meta($name, base64_encode($key), 'non'); - lire_metas(); - return $key; - } - - private function read(): array { - lire_fichier_securise($this->file, $json); - if ( - $json - and $json = \json_decode($json, true) - and is_array($json) - ) { - return array_map('base64_decode', $json); - } - return []; - } -} diff --git a/spip/overlay/php/prepend.php b/spip/overlay/php/prepend.php index 0b3ef77..47d853f 100644 --- a/spip/overlay/php/prepend.php +++ b/spip/overlay/php/prepend.php @@ -14,6 +14,25 @@ $_SERVER['DOCUMENT_ROOT'] = '/var/task'; } +// ── 0b. Pin SPIP's public base URL (host + scheme) ───────────────────────── +// Behind CloudFront + API Gateway the origin sees the execute-api host (CloudFront +// strips the viewer Host so API Gateway accepts the request), so SPIP's url_de_base() +// would build absolute URLs (login "converser", redirects, canonical, emails) on the +// wrong host, without the stage path. Force the real public host/scheme here so every +// absolute URL SPIP emits points at the CloudFront (or custom) domain. +$publicUrl = getenv('SPIP_PUBLIC_URL'); +if ($publicUrl && ($p = parse_url($publicUrl)) && !empty($p['host'])) { + $_SERVER['HTTP_HOST'] = $p['host'] . (isset($p['port']) ? ':' . $p['port'] : ''); + $_SERVER['SERVER_NAME'] = $p['host']; + if (($p['scheme'] ?? 'https') === 'https') { + $_SERVER['HTTPS'] = 'on'; + $_SERVER['SERVER_PORT'] = '443'; + } + unset($p); +} +unset($publicUrl); + + // ── 1. Create writable dirs ──────────────────────────────────────────────── if (!is_dir('/tmp/spip/cache')) { @mkdir('/tmp/spip/cache/skel', 0777, true); diff --git a/spip/scripts/bootstrap-db.php b/spip/scripts/bootstrap-db.php index 42a392c..f47a212 100644 --- a/spip/scripts/bootstrap-db.php +++ b/spip/scripts/bootstrap-db.php @@ -154,6 +154,20 @@ function _out(string $k, string $v): void { echo str_pad($k, 22) . $v . "\n"; } _out('aleas', $has_alea ? 'already present' : 'renouvelle_alea unavailable'); } +// ── 2b. Site public URL (adresse_site meta) ────────────────────────────────── +// The CLI install cannot infer the site's public URL (no HTTP host), so creer_base +// leaves adresse_site as "http://" → SPIP emits "http:///" for logo/home/canonical +// links and transactional emails. Take it from SPIP_PUBLIC_URL (same value the Lambda +// forces as HTTP_HOST at runtime; see prepend.php). +$publicUrl = rtrim((string) getenv('SPIP_PUBLIC_URL'), '/'); +if ($publicUrl !== '' && function_exists('ecrire_meta')) { + ecrire_meta('adresse_site', $publicUrl, 'non'); + _out('adresse_site', $publicUrl); +} else { + _out('adresse_site', 'skipped (SPIP_PUBLIC_URL unset)'); +} +unset($publicUrl); + // ── 3. Plugin install/upgrade (plugin tables + your plugin migrations) ──────────── // Two steps, mirroring what the first authenticated visit to the private area does: // a) actualise_plugins_actifs(): refresh the active-plugins list (meta 'plugin') @@ -173,6 +187,10 @@ function _out(string $k, string $v): void { echo str_pad($k, 22) . $v . "\n"; } _out('plugin_installes', 'seeded empty (SVP guard)'); } if (function_exists('plugin_installes_meta')) { + // plugin_installes_meta() runs plugin install boxes that call template helpers + // like typo()/propre(); load them (not auto-loaded in this CLI context). + include_spip('inc/texte'); + include_spip('inc/filtres'); // plugin_installes_meta() may echo install boxes; capture and drop that output. ob_start(); plugin_installes_meta();