Skip to content

docs(multitenant): add requirements, architecture, security, and epic specs - #675

Draft
andrea-putzu wants to merge 34 commits into
mainfrom
feature/SELC-9099
Draft

docs(multitenant): add requirements, architecture, security, and epic specs#675
andrea-putzu wants to merge 34 commits into
mainfrom
feature/SELC-9099

Conversation

@andrea-putzu

@andrea-putzu andrea-putzu commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

List of Changes

  • Added multitenant requirements in apps/docs/Multitenant/Step_0/REQUIREMENTS.md.
  • Added initial architecture in apps/docs/Multitenant/Step_0/ARCHITECTURE.md.
  • Added security baseline and OWASP-based fallback rules in apps/docs/Multitenant/Step_0/SECURITY.md.
  • Added epic breakdown and sub-tasks in apps/docs/Multitenant/Step_0/EPIC.md.

Motivation and Context

The current platform runs separate backend deployments for selfcare.pagopa.it and imprese.notifichedigitali.it.
This PR documents the first aligned specification package for moving to a shared multitenant backend while preserving tenant isolation, APIM-based tenant resolution, and JWT/header tenant consistency across both authentication flows.

How Has This Been Tested?

  • Reviewed content consistency across REQUIREMENTS.md, ARCHITECTURE.md, SECURITY.md, and EPIC.md.
  • Documentation-only change: no runtime code paths were modified.

Screenshots (if appropriate):

N/A

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

@andrea-putzu andrea-putzu changed the title feat: add specs for multitenant architecture in Selfcare microservices docs(multitenant): add requirements, architecture, security, and epic specs Jul 24, 2026
andrea-putzu and others added 3 commits July 28, 2026 18:28
- Added ARCHITECTURE.md detailing the multitenant data isolation strategy for Cosmos DB, Azure Storage, personal data vault, and outbound email.
- Created EPIC.md outlining the Jira epic for the multitenant data isolation rollout, including goals, sub-tasks, and definition of done.
- Introduced REQUIREMENTS.md specifying functional requirements for tenant data isolation across services.
- Established SECURITY.md with security rules and guidelines for implementing multitenant data isolation, referencing OWASP standards.
…laim)

Implements Step_0 sub-task 5 (EPIC.md) in selfcare-sdk-security:

- TenantId: enum AR/PNPG mirroring existing infra naming (-ar/-pnpg)
- TenantConstants: X-Tenant-Id header, tenant_id JWT claim, issuer
  constants (SPID=hub-spid-login, PAGOPA=OneIdentity/auth), default
  tenant PNPG for hub-spid-login when claim is absent (SELC-3.1/3.4)
- TenantContext: request-scoped holder for the validated tenant
- TenantProblem: RFC7807-style error payload local to this library
- TenantValidationFilter: JAX-RS ContainerRequestFilter enforcing
  header/claim presence and consistency for authenticated requests,
  fail-closed on any mismatch, unknown value, or duplicated header

Covered by 14 new unit tests (TenantIdTest, TenantValidationFilterTest).
Verified selfcare-sdk-security compiles cleanly together with all
dependent microservices (mvn --also-make-dependents compile).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…module

Move the AR/PNPG frontend origin -> tenant registry (previously duplicated
across 27 apim_api call sites) into a single local.tenant_frontend_origins
table inside infra/resources/_modules/local-env, exposed as
module.local.config.tenant_ids. Every microservice call site now sets
tenant_ids = module.local.config.tenant_ids instead of repeating the AR/PNPG
origin list inline, following the module's existing domain-aware conditional
pattern (mongo_db, key_vault_name). Own-tenant ordering (used by APIM as the
no-Origin/Referer fallback) is preserved per deployment domain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…task 3)

Resolve tenant from the X-Tenant-Id header at each OneIdentity login
entry point (SAML callback, OIDC exchange, OTP verify) and embed it as
the tenant_id JWT claim on every session token issued by SessionServiceImpl.

- Add TenantHeaderUtils.resolveTenantId: fail-closed (400) if the
  X-Tenant-Id header is missing or references an unknown tenant, reusing
  TenantId/TenantConstants from selfcare-sdk-security.
- Thread tenantId through UserClaims -> SAMLService(Impl), OidcService(Impl),
  OtpFlowService(Impl) -> SessionServiceImpl, which now fails closed (500)
  if a token would be signed without a tenantId.
- Add selfcare-sdk-security dependency to apps/auth.
- Update unit/integration tests for the new signatures; fix two pre-existing
  test bugs surfaced by the change (claim key mismatch in OidcServiceTest,
  missing otpFlow.userId fixture in OtpFlowServiceTest).
- Update EPIC.md sub-task 3 status.

167 unit tests pass in apps/auth (no regressions).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
andrea-putzu and others added 9 commits July 31, 2026 18:56
TenantValidationFilter rejects with 400 any request carrying a JWT but no
X-Tenant-Id. No caller in the monorepo was sending that header, so enabling the
filter had silently broken every internal call. An audit of all outbound code
paths - Quarkus ClientHeadersFactory implementations, Spring Feign interceptor
configurations, and the multi-module web/connector layouts missed by earlier
scans - found and fixed:

- auth to iam: the header is derived from the tenant that went into the session
  token auth just minted, so it can never disagree with the claim it is
  validated against.
- user-ms to onboarding-ms, and the 18 internal Feign clients of dashboard-bff,
  external-api and institution-ms: the header carries the validated tenant from
  the request context, never the raw inbound header.
- institution-send-mail-scheduler and onboarding-cdc have no inbound request:
  the header is decoded from their machine token's own tenant claim, so a
  configuration value cannot drift out of sync with the token. Both provisioned
  JWT_BEARER_TOKENs must be re-issued with a tenant_id claim before release.

Callers authenticating with a subscription key rather than a JWT are exempt from
the filter and were left untouched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The tenant claim name and the data-isolation model were still listed as open
blockers although both are decided and implemented, and sub-task 5 still
described itself as a product-only proof of concept after being rolled out to
every service.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… and user-ms

Extends the document-ms pattern (implicit tenant filtering inside the data-access
layer, never opt-in ...ForTenant variants) to four more services, and adds the
reference deployment consolidation for sub-task 7.

iam: tenantId on UserClaims; reads scoped in the active-record helpers and in the
UserPermissionsRepository aggregation entry matches; writes stamped only when unset.
Roles is left unscoped - it is a global role catalogue, not tenant-owned data.

auth: OTP lookups went through an unscoped UUID query, so a PNPG-issued OTP could be
redeemed from the AR frontend. Closed via a new OtpFlowRepository chokepoint. auth
issues sessions before TenantContext exists, so the tenant comes from the fail-closed
X-Tenant-Id header, not the request context. OtpUtils now takes the distinct-user
count as a supplier so that count is tenant-scoped too.

user-group-ms: scoping applied in UserGroupServiceImpl, the single layer through
which both the MongoRepository writes and the MongoTemplate reads/updates pass.

user-ms: the tenant predicate is injected inside QueryUtils.buildQueryDocument /
buildQueryDocumentByDate, the shared builder ~20 query call sites go through, so a
newly added query starts out scoped rather than unscoped. The five call sites that
bypass it (findById, find(field,value), mongoCollection().updateMany) are scoped
individually, and persistOrUpdate stamps the tenant without ever reassigning one.

infra: infra/resources/iam/dev is the reference consolidation - one container app and
one database serving both tenants, with both APIM APIs kept so neither frontend
changes. It adopts existing resources by import rather than standing up a parallel
stack, and is deliberately not wired into CI until that import happens. Prerequisites
that fail silently (backfill before merge, composite unique index, multi-issuer JWT
key) are documented in its README.

Migration-phase note: every filter still matches "tenant or no tenant", so untagged
legacy records stay visible to both tenants. That branch must be dropped once the
backfill has run; until then this is additive defence, not a hard boundary.

Tests: iam 67, auth 133, user-group-ms 104, user-ms QueryUtilsTenantTest 6/6.
user-ms's Quarkus test bootstrap is broken by a pre-existing SDK classpath conflict
(reproduced at HEAD in a clean worktree), so its new test was also run standalone via
the JUnit console launcher against javac output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replicates the document-ms isolation pattern (tenant filter applied inside
the data-access layer, never as an opt-in variant) to the remaining
Mongo-using microservices: institution-ms, onboarding-ms, delegation-cdc,
user-cdc, user-group-cdc, onboarding-cdc and institution-send-mail-scheduler.

CDC apps and the scheduler receive tenant *propagation* rather than
request-scoped filtering: they have no request context and read a change
stream spanning both tenants, so they carry the source tenantId through to
mirror collections and outbound events.

institution-ms: TenantDataIsolation centralises scoping in
MongoCustomConnectorImpl and the repository defaults without making
connector-dao depend on web. A cross-tenant write now raises
InvalidRequestException instead of silently discarding the caller's data.

product/product-cdc stay unscoped by design: they hold the global product
catalogue, including the dataIsolation block that drives database routing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds infra/resources/product/dev, the second deployment-consolidation
conversion. product is the only app in scope that is simultaneously a
call-graph leaf, zero-divergence between the two legacy stacks, and not
exposed through APIM, which makes it the smallest end-to-end exercise of
the import-based cutover runbook.

Reading the stacks line by line contradicted the stateless/stateful
ordering recorded earlier:

- The two stacks live in different Container App Environments, so every
  conversion needs cross-environment connectivity for its PNPG callers.
- Callers are wired by private DNS name, so conversions are ordered by
  the call graph. BFFs and gateways must go last, not first.

Also records measured per-app config divergence. A non-zero count is
application work, not Terraform work: user-cdc emits no user events on
PNPG today, so consolidating it without a decision would silently start
emitting PNPG events to AR consumers.

Nothing is applied, imported or wired into CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The multitenant design assumed every backend-to-backend call goes direct,
callee private DNS to callee private DNS, carrying a JWT and X-Tenant-Id.
user-cdc does not: its three internal clients go through APIM with an
Ocp-Apim-Subscription-Key. The inbound policy overrides X-Tenant-Id
unconditionally from Origin/Referer, and a CDC process has no Origin, so
setting the header application-side would have been silently discarded and
the call would resolve to default_tenant_id, i.e. 403 fail-closed.

Widening default_tenant_id would reintroduce the silent fallback removed
earlier in sub-task 2. Instead add service_caller_tenants: a map of APIM
subscription id to tenant, evaluated before default_tenant_id and only for
requests with no Origin/Referer, so an s2s caller's tenant is pinned to a
credential it does not control.

Document the finding and the two remaining options in the EPIC, since the
API serving external/internal/v1 is not declared through this module.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…coped

TenantValidationFilter returned early whenever the request had no JWT
issuer, leaving TenantContext unset. CurrentTenantProvider then returned
empty and every tenantScoped(...) call fell through to the unscoped query.

That path is not hypothetical: five clients across auth and user-cdc call
selfcare APIs through APIM with an Ocp-Apim-Subscription-Key and no JWT,
including the login path where auth looks a user up in user-ms. Those calls
were reading and writing across both tenants.

The filter now scopes such a request from a usable X-Tenant-Id header. It
still never rejects unauthenticated requests, so public and health endpoints
are unaffected and the change is strictly additive. Trusting the header is
safe only because APIM overrides it unconditionally on every request it
forwards, and services are not reachable from outside the private network
without passing through APIM.

Also fix four user-ms tests that still stubbed the find(String, Object...)
overload after sub-task 6 moved the production code to find(Document); the
stub silently did not match and PanacheMock returned null.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous commit fixed the unscoped-request defect in the Quarkus filter
in libs/selfcare-sdk-security. The same early-return exists in the six
independent Spring copies of the filter, which were written separately and
carry the same bug: a request with no authenticated principal left the
tenant request attribute unset, so downstream repository queries ran
unscoped across all tenants.

Apply the identical fix to institution-ms, external-api, dashboard-bff,
onboarding-bff, registry-proxy and user-group-ms: scope the request from a
usable X-Tenant-Id when there is no authenticated principal, while still
never rejecting such requests so public and health endpoints keep working.

That the same defect had to be fixed in seven places is an argument for
extracting the Spring filter into a shared library; noted in the EPIC as a
follow-up rather than done here, since it changes the dependency graph of
six services.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@andrea-putzu

Copy link
Copy Markdown
Contributor Author

Step 0 review fixes are available in commit 5932af8c3.

Resolved findings:

  • APIM now prioritizes mapped subscriptions over caller-controlled Origin/Referer and scopes the SAML fallback to loginSaml.
  • External API policies stamp a topology-owned tenant_id claim and overwrite X-Tenant-Id.
  • Document attachments, delegation CDC lookups, onboarding aggregate context and onboarding bulk updates now preserve/enforce tenant identity.
  • Onboarding Functions machine-token propagation validates token/context alignment while preserving legacy tenantless activities on the existing per-tenant deployments.
  • IAM and Product state-transfer runbooks now enforce single Terraform ownership and define safe rollback procedures.

Validation:

  • Targeted document-ms, delegation-cdc and onboarding-functions tests pass.
  • Terraform fmt/docs/validate hooks pass.
  • Final code review findings were resolved.

Migration-dependent items remain explicitly tracked in apps/docs/Multitenant/Step_0/EPIC.md: tenant-qualified user identity, institution collection/index migration, scheduler tenant-specific credentials, remaining tenantless activity payloads, and dedicated-product read routing.

andrea-putzu and others added 7 commits August 4, 2026 11:17
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed clients

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
📋 Pre-commit Output Log
[INFO] Initializing environment for https://github.com/pagopa/dx.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
Lock Terraform Registry modules......................(no files to check)Skipped

Generated on Wed Aug 12 07:42:13 UTC 2026
Run lock_modules on folder: infra/resources/dashboard-bff/dev-ar/

@github-actions

Copy link
Copy Markdown
📋 Pre-commit Output Log
[INFO] Initializing environment for https://github.com/pagopa/dx.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
Lock Terraform Registry modules......................(no files to check)Skipped

Generated on Wed Aug 12 07:42:14 UTC 2026
Run lock_modules on folder: infra/resources/dashboard-bff/dev-pnpg/

@github-actions

Copy link
Copy Markdown
📋 Pre-commit Output Log
[INFO] Initializing environment for https://github.com/pagopa/dx.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
Lock Terraform Registry modules......................(no files to check)Skipped

Generated on Wed Aug 12 07:42:19 UTC 2026
Run lock_modules on folder: infra/resources/auth/dev-ar/

@github-actions

Copy link
Copy Markdown
📋 Pre-commit Output Log
[INFO] Initializing environment for https://github.com/pagopa/dx.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
Lock Terraform Registry modules......................(no files to check)Skipped

Generated on Wed Aug 12 07:42:25 UTC 2026
Run lock_modules on folder: infra/resources/product/dev-ar/

@github-actions

Copy link
Copy Markdown
📋 Pre-commit Output Log
[INFO] Initializing environment for https://github.com/pagopa/dx.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
Lock Terraform Registry modules......................(no files to check)Skipped

Generated on Wed Aug 12 07:42:32 UTC 2026
Run lock_modules on folder: infra/resources/iam/dev-ar/

@github-actions

Copy link
Copy Markdown

Warning

⚠️ Trivy detected forbidden Terraform patterns

The following findings must be resolved before this check becomes enforced.

Severity ID File Line Title
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/_modules/github_repository_environment/main.tf L53 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/dev-ar/data.tf L25 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/dev-ar/data.tf L20 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/dev-ar/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/dev-ar/data.tf L30 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/dev-ar/data.tf L11 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/prod-ar/data.tf L25 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/prod-ar/data.tf L20 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/prod-ar/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/prod-ar/data.tf L30 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/prod-ar/data.tf L11 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/uat-ar/data.tf L25 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/uat-ar/data.tf L20 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/uat-ar/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/uat-ar/data.tf L30 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/bootstrap/uat-ar/data.tf L11 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/cert/_modules/cert/data.tf L7 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/cert/_modules/cert/data.tf L7 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/cert/_modules/cert/data.tf L7 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/cert/_modules/cert/data.tf L7 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/cert/_modules/cert/data.tf L7 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/cert/_modules/cert/data.tf L7 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L45 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L45 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L45 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L55 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L55 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L55 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L40 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L40 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L40 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L71 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L71 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L71 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L50 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L50 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L50 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L60 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L60 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L60 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/apim/data.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L11 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L11 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L11 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L22 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L16 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L28 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L28 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/azure_key_vault_items/main.tf L28 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/docker_credentials/main.tf L6 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/data/docker_credentials/main.tf L1 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L26 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L26 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L26 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L26 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L26 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L26 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L31 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L31 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L31 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L31 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L31 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L31 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L36 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L36 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L36 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L36 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L36 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L36 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L41 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L41 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L41 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L41 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L41 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L41 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L159 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L159 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L159 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L159 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L159 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L159 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L92 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L92 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L92 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L92 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L92 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L92 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L117 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L117 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L117 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L117 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L117 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L117 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L164 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L164 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L164 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L164 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L164 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/key_vault/main.tf L164 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L81 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L81 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L81 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L96 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L96 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L96 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L86 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L86 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L86 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L91 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L91 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/monitor/main.tf L91 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/spid_testenv/spid-testenv.tf L14 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/spid_testenv/spid-testenv.tf L14 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/spid_testenv/spid-testenv.tf L9 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/_modules/spid_testenv/spid-testenv.tf L9 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/dev-pnpg/app.tf L33 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/prod-pnpg/app.tf L24 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/uat-ar/commons.tf L541 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/core/uat-pnpg/app.tf L33 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1679 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1679 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1679 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1684 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1684 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1684 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1689 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1689 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/apim.tf L1689 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L35 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L45 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L45 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L45 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L55 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L55 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L55 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L40 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L40 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L40 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L71 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L71 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L71 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L50 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L50 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L50 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L60 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L60 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L60 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/apim_external_api/data.tf L76 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/dapr/data.tf L17 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/dapr/data.tf L17 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/dapr/data.tf L17 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/functions/function.tf L87 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/functions/function.tf L87 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/functions/function.tf L87 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/functions/function.tf L87 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/functions/function.tf L87 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/functions/function.tf L87 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/namirial_sws/data.tf L15 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/namirial_sws/data.tf L15 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/namirial_sws/data.tf L15 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/namirial_sws/data.tf L10 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/namirial_sws/data.tf L10 Terraform must not read Key Vault secrets via data source
HIGH AVD-DX-0001 infra/resources/_modules/namirial_sws/data.tf L10 Terraform must not read Key Vault secrets via data source

@github-actions

Copy link
Copy Markdown
📋 Pre-commit Output Log
[INFO] Initializing environment for https://github.com/pagopa/dx.
[INFO] Initializing environment for https://github.com/antonbabenko/pre-commit-terraform.
Lock Terraform Registry modules......................(no files to check)Skipped

Generated on Wed Aug 12 07:42:36 UTC 2026
Run lock_modules on folder: infra/resources/registry-proxy/dev-ar/

@github-actions

Copy link
Copy Markdown

📖 Terraform Plan (infra/resources/dashboard-bff/dev-pnpg) - success

Show Plan
  # module.container_app_dashboard_bff_pnpg.azurerm_container_app.container_app will be updated in-place
  ~ resource "azurerm_container_app" "container_app" {
        id                            = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-container-app-rg/providers/Microsoft.App/containerApps/selc-d-pnpg-dashboard-backend-ca"
        name                          = "selc-d-pnpg-dashboard-backend-ca"
        tags                          = {
            "CostCenter"  = "TS310 - PAGAMENTI & SERVIZI"
            "CreatedBy"   = "Terraform"
            "Environment" = "Dev"
            "Owner"       = "Selfcare"
            "Source"      = "https://github.com/pagopa/selfcare"
        }
        # (10 unchanged attributes hidden)

      ~ template {
            # (6 unchanged attributes hidden)

          ~ container {
              ~ image             = "ghcr.io/pagopa/selfcare-dashboard-backend:sha-9069e0e" -> "ghcr.io/pagopa/selfcare-dashboard-backend:sha-$(git r"
                name              = "selc-d-pnpg-dashboard-backend"
                # (5 unchanged attributes hidden)

                # (39 unchanged blocks hidden)
            }

            # (1 unchanged block hidden)
        }

        # (8 unchanged blocks hidden)
    }

  # module.apim_api_bff_dashboard_pnpg.module.apim_api.azurerm_api_management_api_policy.this[0] will be updated in-place
  ~ resource "azurerm_api_management_api_policy" "this" {
        id                  = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-api-v2-rg/providers/Microsoft.ApiManagement/service/selc-d-apim-v2/apis/selc-d-pnpg-api-bff-dashboard"
      ~ xml_content         = <<-EOT
          - <policies>
          - 	<inbound>
          - 		<cors allow-credentials="true">
          - 			<allowed-origins>
          - 				<origin>https://pnpg.dev.selfcare.pagopa.it</origin>
          - 				<origin>https://api-pnpg.dev.selfcare.pagopa.it</origin>
          - 				<origin>http://localhost:3000</origin>
          - 			</allowed-origins>
          - 			<allowed-methods>
          - 				<method>GET</method>
          - 				<method>POST</method>
          - 				<method>PUT</method>
          - 				<method>HEAD</method>
          - 				<method>DELETE</method>
          - 				<method>OPTIONS</method>
          - 			</allowed-methods>
          - 			<allowed-headers>
          - 				<header>*</header>
          - 			</allowed-headers>
          - 		</cors>
          - 		<base />
          - 	</inbound>
          - 	<backend>
          - 		<base />
          - 	</backend>
          - 	<outbound>
          - 		<base />
          - 	</outbound>
          - 	<on-error>
          - 		<base />
          - 	</on-error>
          + <policies>
          +     <inbound>
          +         <cors allow-credentials="true">
          +             <allowed-origins>
          +                 <origin>https://api-pnpg.dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://pnpg.dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://dev.selfcare.pagopa.it</origin>
          + 
          + 
          +                 <origin>http://localhost:3000</origin>
          + 
          +             </allowed-origins>
          +             <allowed-methods>
          +                 <method>GET</method>
          +                 <method>POST</method>
          +                 <method>PUT</method>
          +                 <method>HEAD</method>
          +                 <method>DELETE</method>
          +                 <method>OPTIONS</method>
          +             </allowed-methods>
          +             <allowed-headers>
          +                 <header>*</header>
          +             </allowed-headers>
          +         </cors>
          +         <base />
          +         <!--
          +             Multitenant tenant resolution & propagation.
          +             See apps/docs/Multitenant/Step_0/{REQUIREMENTS,ARCHITECTURE,SECURITY}.md (SELC-1, SELC-2).
          +             Single API group serving every tenant frontend listed in tenant_ids: X-Tenant-Id is
          +             ALWAYS derived here from the calling origin against that origin -> tenant list, never
          +             trusted from the caller.
          + 
          +             Matching is EXACT on the serialised origin (scheme + authority), never a prefix: a
          +             prefix test would let https://selfcare.pagopa.it.attacker.example resolve as the AR
          +             tenant. When the request carries no Origin, the Referer is parsed and reduced to its
          +             scheme + authority before the same exact lookup.
          + 
          +             A subscription listed in var.service_caller_tenants is resolved FIRST, regardless of
          +             Origin/Referer. A non-browser caller controls those headers and must not be able to
          +             override the tenant pinned to its APIM credential by pretending to be a browser.
          +             Note the caller cannot express its tenant any other way:
          +             the policy below OVERRIDES X-Tenant-Id unconditionally, so a header set by the calling
          +             application is discarded — mapping the subscription is the only supported mechanism.
          + 
          +             An unknown subscription then follows browser origin resolution. var.default_tenant_id
          +             applies to origin-less requests, or to the exact operations listed in
          +             var.default_tenant_operation_ids (for example a SAML ACS posted from the IdP origin).
          +             It defaults to null => rejected.
          +         -->
          +         <set-variable name="callerOrigin" value="@{
          +             var origin = context.Request.Headers.GetValueOrDefault("Origin", "");
          +             if (string.IsNullOrEmpty(origin)) {
          +                 var referer = context.Request.Headers.GetValueOrDefault("Referer", "");
          +                 if (!string.IsNullOrEmpty(referer)) {
          +                     try {
          +                         var refererUri = new Uri(referer);
          +                         origin = refererUri.Scheme + "://" + refererUri.Authority;
          +                     } catch (Exception) {
          +                         origin = "";
          +                     }
          +                 }
          +             }
          +             return origin.Trim().ToLowerInvariant();
          +         }" />
          +         <set-variable name="resolvedTenant" value="@{
          +             var tenantByOrigin = new Dictionary<string, string> {
          + 
          +                 { "https://pnpg.dev.selfcare.pagopa.it", "PNPG" },
          + 
          +                 { "https://dev.selfcare.pagopa.it", "AR" },
          + 
          + 
          +                 { "http://localhost:3000", "PNPG" },
          + 
          +             };
          +             var tenantBySubscription = new Dictionary<string, string> {
          + 
          +             };
          +             var subscriptionId = context.Subscription == null ? "" : (context.Subscription.Id ?? "").ToLowerInvariant();
          +             string serviceTenant;
          +             if (!string.IsNullOrEmpty(subscriptionId) && tenantBySubscription.TryGetValue(subscriptionId, out serviceTenant)) {
          +                 return serviceTenant;
          +             }
          + 
          +             var defaultOperations = new HashSet<string> {
          + 
          +             };
          +             var caller = (string)context.Variables["callerOrigin"];
          +             if (string.IsNullOrEmpty(caller)) {
          +                 return defaultOperations.Count == 0 || defaultOperations.Contains(context.Operation.Id)
          +                     ? ""
          +                     : "";
          +             }
          +             string tenant;
          +             if (tenantByOrigin.TryGetValue(caller, out tenant)) {
          +                 return tenant;
          +             }
          +             return defaultOperations.Contains(context.Operation.Id) ? "" : "";
          +         }" />
          +         <choose>
          +             <when condition="@(string.IsNullOrEmpty((string)context.Variables["resolvedTenant"]))">
          +                 <return-response>
          +                     <set-status code="403" reason="Forbidden" />
          +                     <set-header name="Content-Type" exists-action="override">
          +                         <value>application/problem+json</value>
          +                     </set-header>
          +                     <set-body>@("{\"status\":403,\"title\":\"tenant_url_mismatch\",\"detail\":\"Calling URL does not match any known tenant frontend for this API, or the request carried no Origin/Referer and this API defines no default tenant.\"}")</set-body>
          +                 </return-response>
          +             </when>
          +         </choose>
          +         <set-header name="X-Tenant-Id" exists-action="override">
          +             <value>@((string)context.Variables["resolvedTenant"])</value>
          +         </set-header>
          +     </inbound>
          +     <backend>
          +         <base />
          +     </backend>
          +     <outbound>
          +         <base />
          +     </outbound>
          +     <on-error>
          +         <base />
          +     </on-error>
            </policies>
        EOT
        # (3 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

@github-actions

Copy link
Copy Markdown

📖 Terraform Plan (infra/resources/dashboard-bff/dev-ar) - success

Show Plan
  # module.container_app_dashboard_bff.azurerm_container_app.container_app will be updated in-place
  ~ resource "azurerm_container_app" "container_app" {
        id                            = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-container-app-002-rg/providers/Microsoft.App/containerApps/selc-d-dashboard-backend-ca"
        name                          = "selc-d-dashboard-backend-ca"
        tags                          = {
            "CostCenter"  = "TS310 - PAGAMENTI & SERVIZI"
            "CreatedBy"   = "Terraform"
            "Environment" = "Dev"
            "Owner"       = "Selfcare"
            "Source"      = "https://github.com/pagopa/selfcare"
        }
        # (10 unchanged attributes hidden)

      ~ template {
            # (6 unchanged attributes hidden)

          ~ container {
              ~ image             = "ghcr.io/pagopa/selfcare-dashboard-backend:sha-9069e0e" -> "ghcr.io/pagopa/selfcare-dashboard-backend:sha-$(git r"
                name              = "selc-d-dashboard-backend"
                # (5 unchanged attributes hidden)

                # (42 unchanged blocks hidden)
            }

            # (2 unchanged blocks hidden)
        }

        # (9 unchanged blocks hidden)
    }

  # module.apim_api_bff_dashboard.module.apim_api.azurerm_api_management_api_policy.this[0] will be updated in-place
  ~ resource "azurerm_api_management_api_policy" "this" {
        id                  = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-api-v2-rg/providers/Microsoft.ApiManagement/service/selc-d-apim-v2/apis/selc-d-api-bff-dashboard"
      ~ xml_content         = <<-EOT
          - <policies>
          - 	<inbound>
          - 		<cors allow-credentials="true">
          - 			<allowed-origins>
          - 				<origin>https://dev.selfcare.pagopa.it</origin>
          - 				<origin>https://api.dev.selfcare.pagopa.it</origin>
          - 				<origin>http://localhost:3000</origin>
          - 			</allowed-origins>
          - 			<allowed-methods>
          - 				<method>GET</method>
          - 				<method>POST</method>
          - 				<method>PUT</method>
          - 				<method>HEAD</method>
          - 				<method>DELETE</method>
          - 				<method>OPTIONS</method>
          - 			</allowed-methods>
          - 			<allowed-headers>
          - 				<header>*</header>
          - 			</allowed-headers>
          - 		</cors>
          - 		<base />
          - 	</inbound>
          - 	<backend>
          - 		<base />
          - 	</backend>
          - 	<outbound>
          - 		<base />
          - 	</outbound>
          - 	<on-error>
          - 		<base />
          - 	</on-error>
          + <policies>
          +     <inbound>
          +         <cors allow-credentials="true">
          +             <allowed-origins>
          +                 <origin>https://api.dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://pnpg.dev.selfcare.pagopa.it</origin>
          + 
          + 
          +                 <origin>http://localhost:3000</origin>
          + 
          +             </allowed-origins>
          +             <allowed-methods>
          +                 <method>GET</method>
          +                 <method>POST</method>
          +                 <method>PUT</method>
          +                 <method>HEAD</method>
          +                 <method>DELETE</method>
          +                 <method>OPTIONS</method>
          +             </allowed-methods>
          +             <allowed-headers>
          +                 <header>*</header>
          +             </allowed-headers>
          +         </cors>
          +         <base />
          +         <!--
          +             Multitenant tenant resolution & propagation.
          +             See apps/docs/Multitenant/Step_0/{REQUIREMENTS,ARCHITECTURE,SECURITY}.md (SELC-1, SELC-2).
          +             Single API group serving every tenant frontend listed in tenant_ids: X-Tenant-Id is
          +             ALWAYS derived here from the calling origin against that origin -> tenant list, never
          +             trusted from the caller.
          + 
          +             Matching is EXACT on the serialised origin (scheme + authority), never a prefix: a
          +             prefix test would let https://selfcare.pagopa.it.attacker.example resolve as the AR
          +             tenant. When the request carries no Origin, the Referer is parsed and reduced to its
          +             scheme + authority before the same exact lookup.
          + 
          +             A subscription listed in var.service_caller_tenants is resolved FIRST, regardless of
          +             Origin/Referer. A non-browser caller controls those headers and must not be able to
          +             override the tenant pinned to its APIM credential by pretending to be a browser.
          +             Note the caller cannot express its tenant any other way:
          +             the policy below OVERRIDES X-Tenant-Id unconditionally, so a header set by the calling
          +             application is discarded — mapping the subscription is the only supported mechanism.
          + 
          +             An unknown subscription then follows browser origin resolution. var.default_tenant_id
          +             applies to origin-less requests, or to the exact operations listed in
          +             var.default_tenant_operation_ids (for example a SAML ACS posted from the IdP origin).
          +             It defaults to null => rejected.
          +         -->
          +         <set-variable name="callerOrigin" value="@{
          +             var origin = context.Request.Headers.GetValueOrDefault("Origin", "");
          +             if (string.IsNullOrEmpty(origin)) {
          +                 var referer = context.Request.Headers.GetValueOrDefault("Referer", "");
          +                 if (!string.IsNullOrEmpty(referer)) {
          +                     try {
          +                         var refererUri = new Uri(referer);
          +                         origin = refererUri.Scheme + "://" + refererUri.Authority;
          +                     } catch (Exception) {
          +                         origin = "";
          +                     }
          +                 }
          +             }
          +             return origin.Trim().ToLowerInvariant();
          +         }" />
          +         <set-variable name="resolvedTenant" value="@{
          +             var tenantByOrigin = new Dictionary<string, string> {
          + 
          +                 { "https://dev.selfcare.pagopa.it", "AR" },
          + 
          +                 { "https://pnpg.dev.selfcare.pagopa.it", "PNPG" },
          + 
          + 
          +                 { "http://localhost:3000", "AR" },
          + 
          +             };
          +             var tenantBySubscription = new Dictionary<string, string> {
          + 
          +             };
          +             var subscriptionId = context.Subscription == null ? "" : (context.Subscription.Id ?? "").ToLowerInvariant();
          +             string serviceTenant;
          +             if (!string.IsNullOrEmpty(subscriptionId) && tenantBySubscription.TryGetValue(subscriptionId, out serviceTenant)) {
          +                 return serviceTenant;
          +             }
          + 
          +             var defaultOperations = new HashSet<string> {
          + 
          +             };
          +             var caller = (string)context.Variables["callerOrigin"];
          +             if (string.IsNullOrEmpty(caller)) {
          +                 return defaultOperations.Count == 0 || defaultOperations.Contains(context.Operation.Id)
          +                     ? ""
          +                     : "";
          +             }
          +             string tenant;
          +             if (tenantByOrigin.TryGetValue(caller, out tenant)) {
          +                 return tenant;
          +             }
          +             return defaultOperations.Contains(context.Operation.Id) ? "" : "";
          +         }" />
          +         <choose>
          +             <when condition="@(string.IsNullOrEmpty((string)context.Variables["resolvedTenant"]))">
          +                 <return-response>
          +                     <set-status code="403" reason="Forbidden" />
          +                     <set-header name="Content-Type" exists-action="override">
          +                         <value>application/problem+json</value>
          +                     </set-header>
          +                     <set-body>@("{\"status\":403,\"title\":\"tenant_url_mismatch\",\"detail\":\"Calling URL does not match any known tenant frontend for this API, or the request carried no Origin/Referer and this API defines no default tenant.\"}")</set-body>
          +                 </return-response>
          +             </when>
          +         </choose>
          +         <set-header name="X-Tenant-Id" exists-action="override">
          +             <value>@((string)context.Variables["resolvedTenant"])</value>
          +         </set-header>
          +     </inbound>
          +     <backend>
          +         <base />
          +     </backend>
          +     <outbound>
          +         <base />
          +     </outbound>
          +     <on-error>
          +         <base />
          +     </on-error>
            </policies>
        EOT
        # (3 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

@github-actions

Copy link
Copy Markdown

📖 Terraform Plan (infra/resources/auth/dev-ar) - success

Show Plan
  # module.container_app_auth_ms.azurerm_container_app.container_app will be updated in-place
  ~ resource "azurerm_container_app" "container_app" {
        id                            = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-container-app-002-rg/providers/Microsoft.App/containerApps/selc-d-auth-ms-ca"
        name                          = "selc-d-auth-ms-ca"
        tags                          = {
            "CostCenter"  = "TS310 - PAGAMENTI & SERVIZI"
            "CreatedBy"   = "Terraform"
            "Environment" = "Dev"
            "Owner"       = "Selfcare"
            "Source"      = "https://github.com/pagopa/selfcare"
        }
        # (10 unchanged attributes hidden)

      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      - secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + secret {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }

      ~ template {
            # (6 unchanged attributes hidden)

          ~ container {
              ~ image             = "ghcr.io/pagopa/selfcare-auth-ms:sha-c1cedff" -> "ghcr.io/pagopa/selfcare-auth-ms:sha-$(git r"
                name              = "selc-d-auth-ms"
                # (5 unchanged attributes hidden)

              ~ env {
                  ~ name        = "ONE_MAIL_URL" -> "APPLICATIONINSIGHTS_CONNECTION_STRING"
                  + secret_name = "appinsights-connection-string"
                  - value       = "https://uat.onemail.pagopa.it" -> null
                }
              ~ env {
                  ~ name        = "MAIL_SENDER_ADDRESS" -> "FEATURE_FLAG_OTP_BETA_USERS"
                  + secret_name = "feature-flag-otp-beta-users"
                  - value       = "noreply@selfcare.pagopa.it" -> null
                }
              ~ env {
                  ~ name        = "APPLICATIONINSIGHTS_CONNECTION_STRING" -> "INTERNAL-API-KEY"
                  ~ secret_name = "appinsights-connection-string" -> "internal-api-key"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "FEATURE_FLAG_OTP_BETA_USERS" -> "INTERNAL-MS-USER-API-KEY"
                  ~ secret_name = "feature-flag-otp-beta-users" -> "internal-ms-user-api-key"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "INTERNAL-API-KEY" -> "MONGODB-CONNECTION-STRING"
                  ~ secret_name = "internal-api-key" -> "mongodb-connection-string"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "INTERNAL-MS-USER-API-KEY" -> "ONE_IDENTITY_CLIENT_ID"
                  ~ secret_name = "internal-ms-user-api-key" -> "oneidentity-client-id"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "MONGODB-CONNECTION-STRING" -> "ONE_IDENTITY_CLIENT_SECRET"
                  ~ secret_name = "mongodb-connection-string" -> "oneidentity-client-secret"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "ONE-MAIL-API-KEY" -> "SAML_IDP_CERT"
                  ~ secret_name = "onemail-api-key" -> "saml-idp-cert"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "ONE_IDENTITY_CLIENT_ID" -> "SAML_IDP_ENTITY_ID"
                  ~ secret_name = "oneidentity-client-id" -> "saml-idp-entity-id"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "ONE_IDENTITY_CLIENT_SECRET" -> "SAML_IDP_METADATA"
                  ~ secret_name = "oneidentity-client-secret" -> "saml-idp-metadata"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "SAML_IDP_CERT" -> "SESSION_TOKEN_PRIVATE_KEY"
                  ~ secret_name = "saml-idp-cert" -> "jwt-private-key-pkcs8"
                    # (1 unchanged attribute hidden)
                }
              ~ env {
                  ~ name        = "SAML_IDP_ENTITY_ID" -> "USER-REGISTRY-API-KEY"
                  ~ secret_name = "saml-idp-entity-id" -> "user-registry-api-key"
                    # (1 unchanged attribute hidden)
                }
              - env {
                  - name        = "SAML_IDP_METADATA" -> null
                  - secret_name = "saml-idp-metadata" -> null
                    # (1 unchanged attribute hidden)
                }
              - env {
                  - name        = "SESSION_TOKEN_PRIVATE_KEY" -> null
                  - secret_name = "jwt-private-key-pkcs8" -> null
                    # (1 unchanged attribute hidden)
                }
              - env {
                  - name        = "USER-REGISTRY-API-KEY" -> null
                  - secret_name = "user-registry-api-key" -> null
                    # (1 unchanged attribute hidden)
                }

                # (20 unchanged blocks hidden)
            }

            # (1 unchanged block hidden)
        }

        # (2 unchanged blocks hidden)
    }

  # module.apim_api_auth.module.apim_api.azurerm_api_management_api.this will be updated in-place
  ~ resource "azurerm_api_management_api" "this" {
        id                    = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-api-v2-rg/providers/Microsoft.ApiManagement/service/selc-d-apim-v2/apis/selc-d-api-auth;rev=1"
        name                  = "selc-d-api-auth"
        # (17 unchanged attributes hidden)

      ~ import {
          ~ content_value  = jsonencode(
              ~ {
                  ~ components = {
                      ~ schemas         = {
                          - Date                      = {
                              - examples = [
                                  - "2022-03-10",
                                ]
                              - format   = "date"
                              - type     = "string"
                            }
                          - MailStatusHistory         = {
                              - properties = {
                                  - changedAt = {
                                      - "$ref" = "#/components/schemas/OffsetDateTime"
                                    }
                                  - status    = {
                                      - type = "string"
                                    }
                                }
                              - type       = "object"
                            }
                          - ObjectId                  = {
                              - properties = {
                                  - date      = {
                                      - "$ref" = "#/components/schemas/Date"
                                    }
                                  - nonce     = {
                                      - format = "int64"
                                      - type   = "integer"
                                    }
                                  - timestamp = {
                                      - format = "int32"
                                      - type   = "integer"
                                    }
                                }
                              - type       = "object"
                            }
                          - OffsetDateTime            = {
                              - examples = [
                                  - "2022-03-10T12:15:50-04:00",
                                ]
                              - format   = "date-time"
                              - type     = "string"
                            }
                          - OtpFlow                   = {
                              - properties = {
                                  - attempts      = {
                                      - format = "int32"
                                      - type   = "integer"
                                    }
                                  - createdAt     = {
                                      - "$ref" = "#/components/schemas/OffsetDateTime"
                                    }
                                  - expiresAt     = {
                                      - "$ref" = "#/components/schemas/OffsetDateTime"
                                    }
                                  - id            = {
                                      - "$ref" = "#/components/schemas/ObjectId"
                                    }
                                  - mailRequestId = {
                                      - type = "string"
                                    }
                                  - otp           = {
                                      - type = "string"
                                    }
                                  - status        = {
                                      - "$ref" = "#/components/schemas/OtpStatus"
                                    }
                                  - updatedAt     = {
                                      - "$ref" = "#/components/schemas/OffsetDateTime"
                                    }
                                  - userId        = {
                                      - type = "string"
                                    }
                                  - uuid          = {
                                      - type = "string"
                                    }
                                }
                              - type       = "object"
                            }
                          - OtpMailInfoResponse       = {
                              - properties = {
                                  - attempts      = {
                                      - format = "int32"
                                      - type   = "integer"
                                    }
                                  - history       = {
                                      - items = {
                                          - "$ref" = "#/components/schemas/MailStatusHistory"
                                        }
                                      - type  = "array"
                                    }
                                  - mailRequestId = {
                                      - type = "string"
                                    }
                                  - recipient     = {
                                      - type = "string"
                                    }
                                  - status        = {
                                      - type = "string"
                                    }
                                }
                              - type       = "object"
                            }
                            # (10 unchanged attributes hidden)
                        }
                        # (1 unchanged attribute hidden)
                    }
                  ~ paths      = {
                      - "/otp/info"                      = {
                          - get = {
                              - description = "Retrieve OTP information for a user"
                              - operationId = "getOtpInfo"
                              - parameters  = [
                                  - {
                                      - in     = "query"
                                      - name   = "status"
                                      - schema = {
                                          - "$ref" = "#/components/schemas/OtpStatus"
                                        }
                                    },
                                  - {
                                      - in       = "query"
                                      - name     = "userId"
                                      - required = true
                                      - schema   = {
                                          - pattern = "\\S"
                                          - type    = "string"
                                        }
                                    },
                                ]
                              - responses   = {
                                  - "200" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/OtpFlow"
                                                }
                                            }
                                        }
                                      - description = "OK"
                                    }
                                  - "400" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/Problem"
                                                }
                                            }
                                        }
                                      - description = "Bad Request"
                                    }
                                  - "404" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/Problem"
                                                }
                                            }
                                        }
                                      - description = "Not Found"
                                    }
                                  - "500" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/Problem"
                                                }
                                            }
                                        }
                                      - description = "Internal Server Error"
                                    }
                                }
                              - summary     = "Get OTP information"
                              - tags        = [
                                  - "OTP",
                                ]
                            }
                        }
                      - "/otp/mail-info/{mailRequestId}" = {
                          - get = {
                              - description = "Retrieve the processing status of an email managed by OneMail"
                              - operationId = "getOtpMailInfo"
                              - parameters  = [
                                  - {
                                      - in       = "path"
                                      - name     = "mailRequestId"
                                      - required = true
                                      - schema   = {
                                          - type = "string"
                                        }
                                    },
                                ]
                              - responses   = {
                                  - "200" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/OtpMailInfoResponse"
                                                }
                                            }
                                        }
                                      - description = "OK"
                                    }
                                  - "400" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/Problem"
                                                }
                                            }
                                        }
                                      - description = "Bad Request"
                                    }
                                  - "404" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/Problem"
                                                }
                                            }
                                        }
                                      - description = "Not Found"
                                    }
                                  - "500" = {
                                      - content     = {
                                          - "application/json" = {
                                              - schema = {
                                                  - "$ref" = "#/components/schemas/Problem"
                                                }
                                            }
                                        }
                                      - description = "Internal Server Error"
                                    }
                                }
                              - summary     = "Get OTP mail information"
                              - tags        = [
                                  - "OTP",
                                ]
                            }
                        }
                        # (4 unchanged attributes hidden)
                    }
                    tags       = [
                        {
                            name = "OIDC"
                        },
                        {
                            name = "OTP"
                        },
                        {
                            name = "SAML"
                        },
                    ]
                    # (3 unchanged attributes hidden)
                }
            )
            # (1 unchanged attribute hidden)
        }

        # (1 unchanged block hidden)
    }

  # module.apim_api_auth.module.apim_api.azurerm_api_management_api_policy.this[0] will be updated in-place
  ~ resource "azurerm_api_management_api_policy" "this" {
        id                  = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-api-v2-rg/providers/Microsoft.ApiManagement/service/selc-d-apim-v2/apis/selc-d-api-auth"
      ~ xml_content         = <<-EOT
          - <policies>
          - 	<inbound>
          - 		<cors allow-credentials="true">
          - 			<allowed-origins>
          - 				<origin>https://dev.selfcare.pagopa.it</origin>
          - 				<origin>https://api.dev.selfcare.pagopa.it</origin>
          - 				<origin>http://localhost:3000</origin>
          - 			</allowed-origins>
          - 			<allowed-methods>
          - 				<method>GET</method>
          - 				<method>POST</method>
          - 				<method>PUT</method>
          - 				<method>HEAD</method>
          - 				<method>DELETE</method>
          - 				<method>OPTIONS</method>
          - 			</allowed-methods>
          - 			<allowed-headers>
          - 				<header>*</header>
          - 			</allowed-headers>
          - 		</cors>
          - 		<base />
          - 	</inbound>
          - 	<backend>
          - 		<base />
          - 	</backend>
          - 	<outbound>
          - 		<base />
          - 	</outbound>
          - 	<on-error>
          - 		<base />
          - 	</on-error>
          + <policies>
          +     <inbound>
          +         <cors allow-credentials="true">
          +             <allowed-origins>
          +                 <origin>https://api.dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://pnpg.dev.selfcare.pagopa.it</origin>
          + 
          + 
          +                 <origin>http://localhost:3000</origin>
          + 
          +             </allowed-origins>
          +             <allowed-methods>
          +                 <method>GET</method>
          +                 <method>POST</method>
          +                 <method>PUT</method>
          +                 <method>HEAD</method>
          +                 <method>DELETE</method>
          +                 <method>OPTIONS</method>
          +             </allowed-methods>
          +             <allowed-headers>
          +                 <header>*</header>
          +             </allowed-headers>
          +         </cors>
          +         <base />
          +         <!--
          +             Multitenant tenant resolution & propagation.
          +             See apps/docs/Multitenant/Step_0/{REQUIREMENTS,ARCHITECTURE,SECURITY}.md (SELC-1, SELC-2).
          +             Single API group serving every tenant frontend listed in tenant_ids: X-Tenant-Id is
          +             ALWAYS derived here from the calling origin against that origin -> tenant list, never
          +             trusted from the caller.
          + 
          +             Matching is EXACT on the serialised origin (scheme + authority), never a prefix: a
          +             prefix test would let https://selfcare.pagopa.it.attacker.example resolve as the AR
          +             tenant. When the request carries no Origin, the Referer is parsed and reduced to its
          +             scheme + authority before the same exact lookup.
          + 
          +             A subscription listed in var.service_caller_tenants is resolved FIRST, regardless of
          +             Origin/Referer. A non-browser caller controls those headers and must not be able to
          +             override the tenant pinned to its APIM credential by pretending to be a browser.
          +             Note the caller cannot express its tenant any other way:
          +             the policy below OVERRIDES X-Tenant-Id unconditionally, so a header set by the calling
          +             application is discarded — mapping the subscription is the only supported mechanism.
          + 
          +             An unknown subscription then follows browser origin resolution. var.default_tenant_id
          +             applies to origin-less requests, or to the exact operations listed in
          +             var.default_tenant_operation_ids (for example a SAML ACS posted from the IdP origin).
          +             It defaults to null => rejected.
          +         -->
          +         <set-variable name="callerOrigin" value="@{
          +             var origin = context.Request.Headers.GetValueOrDefault("Origin", "");
          +             if (string.IsNullOrEmpty(origin)) {
          +                 var referer = context.Request.Headers.GetValueOrDefault("Referer", "");
          +                 if (!string.IsNullOrEmpty(referer)) {
          +                     try {
          +                         var refererUri = new Uri(referer);
          +                         origin = refererUri.Scheme + "://" + refererUri.Authority;
          +                     } catch (Exception) {
          +                         origin = "";
          +                     }
          +                 }
          +             }
          +             return origin.Trim().ToLowerInvariant();
          +         }" />
          +         <set-variable name="resolvedTenant" value="@{
          +             var tenantByOrigin = new Dictionary<string, string> {
          + 
          +                 { "https://dev.selfcare.pagopa.it", "AR" },
          + 
          +                 { "https://pnpg.dev.selfcare.pagopa.it", "PNPG" },
          + 
          + 
          +                 { "http://localhost:3000", "AR" },
          + 
          +             };
          +             var tenantBySubscription = new Dictionary<string, string> {
          + 
          +             };
          +             var subscriptionId = context.Subscription == null ? "" : (context.Subscription.Id ?? "").ToLowerInvariant();
          +             string serviceTenant;
          +             if (!string.IsNullOrEmpty(subscriptionId) && tenantBySubscription.TryGetValue(subscriptionId, out serviceTenant)) {
          +                 return serviceTenant;
          +             }
          + 
          +             var defaultOperations = new HashSet<string> {
          + 
          +                 "loginSaml",
          + 
          +             };
          +             var caller = (string)context.Variables["callerOrigin"];
          +             if (string.IsNullOrEmpty(caller)) {
          +                 return defaultOperations.Count == 0 || defaultOperations.Contains(context.Operation.Id)
          +                     ? "AR"
          +                     : "";
          +             }
          +             string tenant;
          +             if (tenantByOrigin.TryGetValue(caller, out tenant)) {
          +                 return tenant;
          +             }
          +             return defaultOperations.Contains(context.Operation.Id) ? "AR" : "";
          +         }" />
          +         <choose>
          +             <when condition="@(string.IsNullOrEmpty((string)context.Variables["resolvedTenant"]))">
          +                 <return-response>
          +                     <set-status code="403" reason="Forbidden" />
          +                     <set-header name="Content-Type" exists-action="override">
          +                         <value>application/problem+json</value>
          +                     </set-header>
          +                     <set-body>@("{\"status\":403,\"title\":\"tenant_url_mismatch\",\"detail\":\"Calling URL does not match any known tenant frontend for this API, or the request carried no Origin/Referer and this API defines no default tenant.\"}")</set-body>
          +                 </return-response>
          +             </when>
          +         </choose>
          +         <set-header name="X-Tenant-Id" exists-action="override">
          +             <value>@((string)context.Variables["resolvedTenant"])</value>
          +         </set-header>
          +     </inbound>
          +     <backend>
          +         <base />
          +     </backend>
          +     <outbound>
          +         <base />
          +     </outbound>
          +     <on-error>
          +         <base />
          +     </on-error>
            </policies>
        EOT
        # (3 unchanged attributes hidden)
    }

Plan: 0 to add, 3 to change, 0 to destroy.

@github-actions

Copy link
Copy Markdown

📖 Terraform Plan (infra/resources/product/dev-ar) - success

Show Plan
  # module.container_app_product_ms.azurerm_container_app.container_app will be updated in-place
  ~ resource "azurerm_container_app" "container_app" {
        id                            = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-container-app-002-rg/providers/Microsoft.App/containerApps/selc-d-product-ms-ca"
        name                          = "selc-d-product-ms-ca"
        tags                          = {
            "CostCenter"  = "TS310 - PAGAMENTI & SERVIZI"
            "CreatedBy"   = "Terraform"
            "Environment" = "Dev"
            "Owner"       = "Selfcare"
            "Source"      = "https://github.com/pagopa/selfcare"
        }
        # (10 unchanged attributes hidden)

      ~ template {
            # (6 unchanged attributes hidden)

          ~ container {
              ~ image             = "ghcr.io/pagopa/selfcare-product-ms:sha-bb3e485" -> "ghcr.io/pagopa/selfcare-product-ms:sha-$(git r"
                name              = "selc-d-product-ms"
                # (5 unchanged attributes hidden)

                # (17 unchanged blocks hidden)
            }

            # (1 unchanged block hidden)
        }

        # (8 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

@github-actions

Copy link
Copy Markdown

📖 Terraform Plan (infra/resources/iam/dev-ar) - success

Show Plan
  # module.container_app_iam_ms.azurerm_container_app.container_app will be updated in-place
  ~ resource "azurerm_container_app" "container_app" {
        id                            = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-container-app-002-rg/providers/Microsoft.App/containerApps/selc-d-iam-ms-ca"
        name                          = "selc-d-iam-ms-ca"
        tags                          = {
            "CostCenter"  = "TS310 - PAGAMENTI & SERVIZI"
            "CreatedBy"   = "Terraform"
            "Environment" = "Dev"
            "Owner"       = "Selfcare"
            "Source"      = "https://github.com/pagopa/selfcare"
        }
        # (10 unchanged attributes hidden)

      ~ template {
            # (6 unchanged attributes hidden)

          ~ container {
              ~ image             = "ghcr.io/pagopa/selfcare-iam-ms:sha-9069e0e" -> "ghcr.io/pagopa/selfcare-iam-ms:sha-$(git r"
                name              = "selc-d-iam-ms"
                # (5 unchanged attributes hidden)

                # (15 unchanged blocks hidden)
            }

            # (1 unchanged block hidden)
        }

        # (7 unchanged blocks hidden)
    }

  # module.apim_api.module.apim_api.azurerm_api_management_api_policy.this[0] will be updated in-place
  ~ resource "azurerm_api_management_api_policy" "this" {
        id                  = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-api-v2-rg/providers/Microsoft.ApiManagement/service/selc-d-apim-v2/apis/selc-d-api-iam"
      ~ xml_content         = <<-EOT
          - <policies>
          - 	<inbound>
          - 		<cors allow-credentials="true">
          - 			<allowed-origins>
          - 				<origin>https://dev.selfcare.pagopa.it</origin>
          - 				<origin>https://api.dev.selfcare.pagopa.it</origin>
          - 				<origin>http://localhost:3000</origin>
          - 			</allowed-origins>
          - 			<allowed-methods>
          - 				<method>GET</method>
          - 				<method>POST</method>
          - 				<method>PUT</method>
          - 				<method>HEAD</method>
          - 				<method>DELETE</method>
          - 				<method>OPTIONS</method>
          - 			</allowed-methods>
          - 			<allowed-headers>
          - 				<header>*</header>
          - 			</allowed-headers>
          - 		</cors>
          - 		<base />
          - 	</inbound>
          - 	<backend>
          - 		<base />
          - 	</backend>
          - 	<outbound>
          - 		<base />
          - 	</outbound>
          - 	<on-error>
          - 		<base />
          - 	</on-error>
          + <policies>
          +     <inbound>
          +         <cors allow-credentials="true">
          +             <allowed-origins>
          +                 <origin>https://api.dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://pnpg.dev.selfcare.pagopa.it</origin>
          + 
          + 
          +                 <origin>http://localhost:3000</origin>
          + 
          +             </allowed-origins>
          +             <allowed-methods>
          +                 <method>GET</method>
          +                 <method>POST</method>
          +                 <method>PUT</method>
          +                 <method>HEAD</method>
          +                 <method>DELETE</method>
          +                 <method>OPTIONS</method>
          +             </allowed-methods>
          +             <allowed-headers>
          +                 <header>*</header>
          +             </allowed-headers>
          +         </cors>
          +         <base />
          +         <!--
          +             Multitenant tenant resolution & propagation.
          +             See apps/docs/Multitenant/Step_0/{REQUIREMENTS,ARCHITECTURE,SECURITY}.md (SELC-1, SELC-2).
          +             Single API group serving every tenant frontend listed in tenant_ids: X-Tenant-Id is
          +             ALWAYS derived here from the calling origin against that origin -> tenant list, never
          +             trusted from the caller.
          + 
          +             Matching is EXACT on the serialised origin (scheme + authority), never a prefix: a
          +             prefix test would let https://selfcare.pagopa.it.attacker.example resolve as the AR
          +             tenant. When the request carries no Origin, the Referer is parsed and reduced to its
          +             scheme + authority before the same exact lookup.
          + 
          +             A subscription listed in var.service_caller_tenants is resolved FIRST, regardless of
          +             Origin/Referer. A non-browser caller controls those headers and must not be able to
          +             override the tenant pinned to its APIM credential by pretending to be a browser.
          +             Note the caller cannot express its tenant any other way:
          +             the policy below OVERRIDES X-Tenant-Id unconditionally, so a header set by the calling
          +             application is discarded — mapping the subscription is the only supported mechanism.
          + 
          +             An unknown subscription then follows browser origin resolution. var.default_tenant_id
          +             applies to origin-less requests, or to the exact operations listed in
          +             var.default_tenant_operation_ids (for example a SAML ACS posted from the IdP origin).
          +             It defaults to null => rejected.
          +         -->
          +         <set-variable name="callerOrigin" value="@{
          +             var origin = context.Request.Headers.GetValueOrDefault("Origin", "");
          +             if (string.IsNullOrEmpty(origin)) {
          +                 var referer = context.Request.Headers.GetValueOrDefault("Referer", "");
          +                 if (!string.IsNullOrEmpty(referer)) {
          +                     try {
          +                         var refererUri = new Uri(referer);
          +                         origin = refererUri.Scheme + "://" + refererUri.Authority;
          +                     } catch (Exception) {
          +                         origin = "";
          +                     }
          +                 }
          +             }
          +             return origin.Trim().ToLowerInvariant();
          +         }" />
          +         <set-variable name="resolvedTenant" value="@{
          +             var tenantByOrigin = new Dictionary<string, string> {
          + 
          +                 { "https://dev.selfcare.pagopa.it", "AR" },
          + 
          +                 { "https://pnpg.dev.selfcare.pagopa.it", "PNPG" },
          + 
          + 
          +                 { "http://localhost:3000", "AR" },
          + 
          +             };
          +             var tenantBySubscription = new Dictionary<string, string> {
          + 
          +             };
          +             var subscriptionId = context.Subscription == null ? "" : (context.Subscription.Id ?? "").ToLowerInvariant();
          +             string serviceTenant;
          +             if (!string.IsNullOrEmpty(subscriptionId) && tenantBySubscription.TryGetValue(subscriptionId, out serviceTenant)) {
          +                 return serviceTenant;
          +             }
          + 
          +             var defaultOperations = new HashSet<string> {
          + 
          +             };
          +             var caller = (string)context.Variables["callerOrigin"];
          +             if (string.IsNullOrEmpty(caller)) {
          +                 return defaultOperations.Count == 0 || defaultOperations.Contains(context.Operation.Id)
          +                     ? ""
          +                     : "";
          +             }
          +             string tenant;
          +             if (tenantByOrigin.TryGetValue(caller, out tenant)) {
          +                 return tenant;
          +             }
          +             return defaultOperations.Contains(context.Operation.Id) ? "" : "";
          +         }" />
          +         <choose>
          +             <when condition="@(string.IsNullOrEmpty((string)context.Variables["resolvedTenant"]))">
          +                 <return-response>
          +                     <set-status code="403" reason="Forbidden" />
          +                     <set-header name="Content-Type" exists-action="override">
          +                         <value>application/problem+json</value>
          +                     </set-header>
          +                     <set-body>@("{\"status\":403,\"title\":\"tenant_url_mismatch\",\"detail\":\"Calling URL does not match any known tenant frontend for this API, or the request carried no Origin/Referer and this API defines no default tenant.\"}")</set-body>
          +                 </return-response>
          +             </when>
          +         </choose>
          +         <set-header name="X-Tenant-Id" exists-action="override">
          +             <value>@((string)context.Variables["resolvedTenant"])</value>
          +         </set-header>
          +     </inbound>
          +     <backend>
          +         <base />
          +     </backend>
          +     <outbound>
          +         <base />
          +     </outbound>
          +     <on-error>
          +         <base />
          +     </on-error>
            </policies>
        EOT
        # (3 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

@github-actions

Copy link
Copy Markdown

📖 Terraform Plan (infra/resources/registry-proxy/dev-ar) - success

Show Plan
  # module.container_app_registry_proxy_ms.azurerm_container_app.container_app will be updated in-place
  ~ resource "azurerm_container_app" "container_app" {
        id                            = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-container-app-002-rg/providers/Microsoft.App/containerApps/selc-d-party-reg-proxy-ca"
        name                          = "selc-d-party-reg-proxy-ca"
        tags                          = {
            "CostCenter"  = "TS310 - PAGAMENTI & SERVIZI"
            "CreatedBy"   = "Terraform"
            "Environment" = "Dev"
            "Owner"       = "Selfcare"
            "Source"      = "https://github.com/pagopa/selfcare"
        }
        # (10 unchanged attributes hidden)

      ~ template {
            # (6 unchanged attributes hidden)

          ~ container {
              ~ image             = "ghcr.io/pagopa/selfcare-registry-proxy-ms:sha-f5b052e" -> "ghcr.io/pagopa/selfcare-registry-proxy-ms:sha-$(git r"
                name              = "selc-d-party-reg-proxy"
                # (5 unchanged attributes hidden)

                # (61 unchanged blocks hidden)
            }

            # (1 unchanged block hidden)
        }

        # (23 unchanged blocks hidden)
    }

  # module.apim_api_registry_proxy.module.apim_api.azurerm_api_management_api_policy.this[0] will be updated in-place
  ~ resource "azurerm_api_management_api_policy" "this" {
        id                  = "/subscriptions/1ab5e788-3b98-4c63-bd05-de0c7388c853/resourceGroups/selc-d-api-v2-rg/providers/Microsoft.ApiManagement/service/selc-d-apim-v2/apis/selc-d-api-bff-proxy"
      ~ xml_content         = <<-EOT
          - <policies>
          - 	<inbound>
          - 		<cors allow-credentials="true">
          - 			<allowed-origins>
          - 				<origin>https://dev.selfcare.pagopa.it</origin>
          - 				<origin>https://api.dev.selfcare.pagopa.it</origin>
          - 				<origin>http://localhost:3000</origin>
          - 			</allowed-origins>
          - 			<allowed-methods>
          - 				<method>GET</method>
          - 				<method>POST</method>
          - 				<method>PUT</method>
          - 				<method>HEAD</method>
          - 				<method>DELETE</method>
          - 				<method>OPTIONS</method>
          - 			</allowed-methods>
          - 			<allowed-headers>
          - 				<header>*</header>
          - 			</allowed-headers>
          - 		</cors>
          - 		<base />
          - 	</inbound>
          - 	<backend>
          - 		<base />
          - 	</backend>
          - 	<outbound>
          - 		<base />
          - 	</outbound>
          - 	<on-error>
          - 		<base />
          - 	</on-error>
          + <policies>
          +     <inbound>
          +         <cors allow-credentials="true">
          +             <allowed-origins>
          +                 <origin>https://api.dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://dev.selfcare.pagopa.it</origin>
          + 
          +                 <origin>https://pnpg.dev.selfcare.pagopa.it</origin>
          + 
          + 
          +                 <origin>http://localhost:3000</origin>
          + 
          +             </allowed-origins>
          +             <allowed-methods>
          +                 <method>GET</method>
          +                 <method>POST</method>
          +                 <method>PUT</method>
          +                 <method>HEAD</method>
          +                 <method>DELETE</method>
          +                 <method>OPTIONS</method>
          +             </allowed-methods>
          +             <allowed-headers>
          +                 <header>*</header>
          +             </allowed-headers>
          +         </cors>
          +         <base />
          +         <!--
          +             Multitenant tenant resolution & propagation.
          +             See apps/docs/Multitenant/Step_0/{REQUIREMENTS,ARCHITECTURE,SECURITY}.md (SELC-1, SELC-2).
          +             Single API group serving every tenant frontend listed in tenant_ids: X-Tenant-Id is
          +             ALWAYS derived here from the calling origin against that origin -> tenant list, never
          +             trusted from the caller.
          + 
          +             Matching is EXACT on the serialised origin (scheme + authority), never a prefix: a
          +             prefix test would let https://selfcare.pagopa.it.attacker.example resolve as the AR
          +             tenant. When the request carries no Origin, the Referer is parsed and reduced to its
          +             scheme + authority before the same exact lookup.
          + 
          +             A subscription listed in var.service_caller_tenants is resolved FIRST, regardless of
          +             Origin/Referer. A non-browser caller controls those headers and must not be able to
          +             override the tenant pinned to its APIM credential by pretending to be a browser.
          +             Note the caller cannot express its tenant any other way:
          +             the policy below OVERRIDES X-Tenant-Id unconditionally, so a header set by the calling
          +             application is discarded — mapping the subscription is the only supported mechanism.
          + 
          +             An unknown subscription then follows browser origin resolution. var.default_tenant_id
          +             applies to origin-less requests, or to the exact operations listed in
          +             var.default_tenant_operation_ids (for example a SAML ACS posted from the IdP origin).
          +             It defaults to null => rejected.
          +         -->
          +         <set-variable name="callerOrigin" value="@{
          +             var origin = context.Request.Headers.GetValueOrDefault("Origin", "");
          +             if (string.IsNullOrEmpty(origin)) {
          +                 var referer = context.Request.Headers.GetValueOrDefault("Referer", "");
          +                 if (!string.IsNullOrEmpty(referer)) {
          +                     try {
          +                         var refererUri = new Uri(referer);
          +                         origin = refererUri.Scheme + "://" + refererUri.Authority;
          +                     } catch (Exception) {
          +                         origin = "";
          +                     }
          +                 }
          +             }
          +             return origin.Trim().ToLowerInvariant();
          +         }" />
          +         <set-variable name="resolvedTenant" value="@{
          +             var tenantByOrigin = new Dictionary<string, string> {
          + 
          +                 { "https://dev.selfcare.pagopa.it", "AR" },
          + 
          +                 { "https://pnpg.dev.selfcare.pagopa.it", "PNPG" },
          + 
          + 
          +                 { "http://localhost:3000", "AR" },
          + 
          +             };
          +             var tenantBySubscription = new Dictionary<string, string> {
          + 
          +             };
          +             var subscriptionId = context.Subscription == null ? "" : (context.Subscription.Id ?? "").ToLowerInvariant();
          +             string serviceTenant;
          +             if (!string.IsNullOrEmpty(subscriptionId) && tenantBySubscription.TryGetValue(subscriptionId, out serviceTenant)) {
          +                 return serviceTenant;
          +             }
          + 
          +             var defaultOperations = new HashSet<string> {
          + 
          +             };
          +             var caller = (string)context.Variables["callerOrigin"];
          +             if (string.IsNullOrEmpty(caller)) {
          +                 return defaultOperations.Count == 0 || defaultOperations.Contains(context.Operation.Id)
          +                     ? ""
          +                     : "";
          +             }
          +             string tenant;
          +             if (tenantByOrigin.TryGetValue(caller, out tenant)) {
          +                 return tenant;
          +             }
          +             return defaultOperations.Contains(context.Operation.Id) ? "" : "";
          +         }" />
          +         <choose>
          +             <when condition="@(string.IsNullOrEmpty((string)context.Variables["resolvedTenant"]))">
          +                 <return-response>
          +                     <set-status code="403" reason="Forbidden" />
          +                     <set-header name="Content-Type" exists-action="override">
          +                         <value>application/problem+json</value>
          +                     </set-header>
          +                     <set-body>@("{\"status\":403,\"title\":\"tenant_url_mismatch\",\"detail\":\"Calling URL does not match any known tenant frontend for this API, or the request carried no Origin/Referer and this API defines no default tenant.\"}")</set-body>
          +                 </return-response>
          +             </when>
          +         </choose>
          +         <set-header name="X-Tenant-Id" exists-action="override">
          +             <value>@((string)context.Variables["resolvedTenant"])</value>
          +         </set-header>
          +     </inbound>
          +     <backend>
          +         <base />
          +     </backend>
          +     <outbound>
          +         <base />
          +     </outbound>
          +     <on-error>
          +         <base />
          +     </on-error>
            </policies>
        EOT
        # (4 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.

Warning: Redundant ignore_changes element

  on ../../_modules/ai_search_ipa/ipa.tf line 37, in resource "restapi_object" "ipa_institution_index":
  37: resource "restapi_object" "ipa_institution_index" {

Adding an attribute name to ignore_changes tells Terraform to ignore future
changes to the argument in configuration after the object has been created,
retaining the value originally configured.

The attribute api_data is decided by the provider alone and therefore there
can be no configured value to compare with. Including this attribute in
ignore_changes has no effect. Remove the attribute from ignore_changes to
quiet this warning.

(and 9 more similar warnings elsewhere)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant