diff --git a/modules/stackit/meshstack_integration.tf b/modules/stackit/meshstack_integration.tf index ba588900..060e259d 100644 --- a/modules/stackit/meshstack_integration.tf +++ b/modules/stackit/meshstack_integration.tf @@ -8,6 +8,18 @@ variable "stackit_organization_id" { description = "STACKIT organization ID where the service account will be granted permissions." } +variable "stackit_organization_member_role" { + type = string + default = "organization.viewer" + description = "STACKIT organization role assigned best-effort to all meshStack project users before project role assignments are applied." +} + +variable "stackit_organization_onboarding_enabled" { + type = bool + default = true + description = "Whether the building block adds meshStack project users to the STACKIT organization (with `stackit_organization_member_role`) before applying project-level role assignments. Disable if organization membership is managed outside this building block." +} + variable "stackit_parent_container_id" { type = string description = "Default parent container ID (organization or folder) for project creation." @@ -68,9 +80,10 @@ variable "hub" { module "backplane" { source = "github.com/meshcloud/meshstack-hub//modules/stackit/project/backplane?ref=${var.hub.git_ref}" - project_id = var.stackit_project_id - organization_id = var.stackit_organization_id - service_account_name = coalesce(var.stackit_service_account_name, "mesh-project") + project_id = var.stackit_project_id + organization_id = var.stackit_organization_id + service_account_name = coalesce(var.stackit_service_account_name, "mesh-project") + organization_onboarding_enabled = var.stackit_organization_onboarding_enabled workload_identity_federation = { issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer @@ -179,6 +192,9 @@ resource "meshstack_building_block_definition" "this" { ref_name = var.hub.git_ref async = false use_mesh_http_backend_fallback = true + pre_run_script = <<-SH + exec python3 "./prerun.py" "$@" + SH } } @@ -217,6 +233,42 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode("/var/run/secrets/workload-identity/azure/token") } + STACKIT_SERVICE_ACCOUNT_EMAIL = { + display_name = "STACKIT Service Account Email" + description = "Service account email used by the pre-run script for WIF token exchange." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.backplane.service_account_email) + } + + STACKIT_ORGANIZATION_ID = { + display_name = "STACKIT Organization ID" + description = "STACKIT organization where meshStack project users are added best-effort before project role assignments are applied." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.stackit_organization_id) + } + + STACKIT_ORGANIZATION_MEMBER_ROLE = { + display_name = "STACKIT Organization Member Role" + description = "STACKIT organization role assigned best-effort to all meshStack project users." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.stackit_organization_member_role) + } + + STACKIT_ORGANIZATION_ONBOARDING_ENABLED = { + display_name = "STACKIT Organization Onboarding Enabled" + description = "Whether the pre-run script adds meshStack project users to the STACKIT organization before project role assignments are applied." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.stackit_organization_onboarding_enabled ? "1" : "0") + } + project_name = { display_name = "Project Name" description = "Name of the STACKIT project." @@ -262,6 +314,12 @@ resource "meshstack_building_block_definition" "this" { project_name = { display_name = "Project Name" type = "STRING" + assignment_type = "NONE" + } + + summary = { + display_name = "Summary" + type = "STRING" assignment_type = "SUMMARY" } } diff --git a/modules/stackit/project/backplane/README.md b/modules/stackit/project/backplane/README.md index f054d9b7..8ddd2dfe 100644 --- a/modules/stackit/project/backplane/README.md +++ b/modules/stackit/project/backplane/README.md @@ -5,6 +5,7 @@ It creates a dedicated service account with a Workload Identity Federation (WIF) and the permissions required to create and manage STACKIT projects under a given organization: - **`resource-manager.admin`** — allows creating and managing projects within the organization. +- **`iam.member-admin`** — allows the building block pre-run script to add meshStack project users to the STACKIT organization before project-level role assignments are applied. Only granted when `organization_onboarding_enabled` is `true` (the default). Authentication uses WIF (OIDC token exchange) — no long-lived service account key is created or stored. @@ -51,6 +52,7 @@ No modules. | Name | Type | |------|------| +| [stackit_authorization_organization_role_assignment.member_admin](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_organization_role_assignment) | resource | | [stackit_authorization_organization_role_assignment.project_admin](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_organization_role_assignment) | resource | | [stackit_service_account.building_block](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account) | resource | | [stackit_service_account_federated_identity_provider.building_block](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account_federated_identity_provider) | resource | @@ -60,6 +62,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [organization\_id](#input\_organization\_id) | STACKIT organization ID where the service account will be granted permissions to create and manage projects. | `string` | n/a | yes | +| [organization\_onboarding\_enabled](#input\_organization\_onboarding\_enabled) | Whether the service account is granted `iam.member-admin` so the building block pre-run script can add meshStack project users to the STACKIT organization. | `bool` | `true` | no | | [project\_id](#input\_project\_id) | STACKIT project ID where the service account will be created. | `string` | n/a | yes | | [service\_account\_name](#input\_service\_account\_name) | Name of the service account created in the STACKIT project. Override when deploying multiple backplane instances in the same project. | `string` | `"mesh-project"` | no | | [workload\_identity\_federation](#input\_workload\_identity\_federation) | WIF issuer URL and subject list for the meshStack building block identity provider. |
object({
issuer = string
subjects = list(string)
}) | n/a | yes |
diff --git a/modules/stackit/project/backplane/main.tf b/modules/stackit/project/backplane/main.tf
index 6e4eab0b..fb6851dc 100644
--- a/modules/stackit/project/backplane/main.tf
+++ b/modules/stackit/project/backplane/main.tf
@@ -30,3 +30,16 @@ resource "stackit_authorization_organization_role_assignment" "project_admin" {
role = "resource-manager.admin"
subject = stackit_service_account.building_block.email
}
+
+moved {
+ from = stackit_authorization_organization_role_assignment.member_admin
+ to = stackit_authorization_organization_role_assignment.member_admin[0]
+}
+
+resource "stackit_authorization_organization_role_assignment" "member_admin" {
+ count = var.organization_onboarding_enabled ? 1 : 0
+
+ resource_id = var.organization_id
+ role = "iam.member-admin"
+ subject = stackit_service_account.building_block.email
+}
diff --git a/modules/stackit/project/backplane/variables.tf b/modules/stackit/project/backplane/variables.tf
index 93bcaf42..969b21bf 100644
--- a/modules/stackit/project/backplane/variables.tf
+++ b/modules/stackit/project/backplane/variables.tf
@@ -25,3 +25,10 @@ variable "service_account_name" {
nullable = false
description = "Name of the service account created in the STACKIT project. Override when deploying multiple backplane instances in the same project."
}
+
+variable "organization_onboarding_enabled" {
+ type = bool
+ default = true
+ nullable = false
+ description = "Whether the service account is granted `iam.member-admin` so the building block pre-run script can add meshStack project users to the STACKIT organization."
+}
diff --git a/modules/stackit/project/buildingblock/APP_TEAM_README.md b/modules/stackit/project/buildingblock/APP_TEAM_README.md
index a70ecdfe..c515d086 100644
--- a/modules/stackit/project/buildingblock/APP_TEAM_README.md
+++ b/modules/stackit/project/buildingblock/APP_TEAM_README.md
@@ -1,7 +1,7 @@
# StackIt Project
## Description
-This building block creates a new STACKIT project and manages user access permissions with configurable role mapping. It provides application teams with a secure, isolated environment for deploying their workloads while ensuring proper access controls.
+This building block creates a new STACKIT project and manages user access permissions with configurable role mapping. It also performs best-effort STACKIT organization onboarding for all assigned users before project permissions are applied. It provides application teams with a secure, isolated environment for deploying their workloads while ensuring proper access controls.
## Usage Motivation
This building block is designed for application teams that need to:
@@ -85,6 +85,14 @@ role_mapping = {
Unknown meshStack roles are ignored. If a user has multiple meshStack roles, all mapped STACKIT roles are assigned once.
+### STACKIT Organization Membership
+
+Before applying project-level role assignments, the building block runs a best-effort pre-run step that adds all assigned meshStack users to the STACKIT organization with the organization role configured by the platform team. The default organization role is `organization.viewer`. The platform team can disable this step entirely; in that case, organization membership must be managed outside this building block.
+
+This onboarding step is apply-only and does not remove organization memberships during destroy because organization membership can be shared by multiple projects. If the onboarding request fails, the building block logs a warning and continues; project-level role assignment remains authoritative and may still fail if STACKIT rejects a user that is not an organization member.
+
+The building block summary lists each user with a status: a checkmark if the required organization role is assigned, or details about what went wrong otherwise (e.g. a failed add request or a role that is still missing). If any user's status is not a checkmark, the summary includes remediation guidance for the platform team.
+
### Environment-Based Parent Container Selection
The building block supports automatic parent container selection based on environment type:
diff --git a/modules/stackit/project/buildingblock/README.md b/modules/stackit/project/buildingblock/README.md
index 4c762461..095b324a 100644
--- a/modules/stackit/project/buildingblock/README.md
+++ b/modules/stackit/project/buildingblock/README.md
@@ -8,7 +8,7 @@ description: |
# STACKIT Project Building Block
-This Terraform module provisions a STACKIT project with user access control. meshStack roles from the `users` input are mapped to STACKIT project roles via the configurable `role_mapping` input.
+This Terraform module provisions a STACKIT project with user access control. meshStack roles from the `users` input are mapped to STACKIT project roles via the configurable `role_mapping` input. When used through the meshStack integration, a pre-run script performs best-effort STACKIT organization onboarding for all assigned users before project-level role assignments are applied and writes a summary that marks users with ✅ when the required organization role is currently assigned.
## Requirements
@@ -78,4 +78,5 @@ No modules.
| [project\_id](#output\_project\_id) | The UUID of the created StackIt project. |
| [project\_name](#output\_project\_name) | The name of the created StackIt project. |
| [project\_url](#output\_project\_url) | The deep link URL to access the project in the StackIt portal. |
+| [summary](#output\_summary) | Summary of STACKIT organization membership onboarding for assigned project users. |
\ No newline at end of file
diff --git a/modules/stackit/project/buildingblock/outputs.tf b/modules/stackit/project/buildingblock/outputs.tf
index 177df76d..9bec4a92 100644
--- a/modules/stackit/project/buildingblock/outputs.tf
+++ b/modules/stackit/project/buildingblock/outputs.tf
@@ -13,8 +13,12 @@ output "project_name" {
description = "The name of the created StackIt project."
}
-
output "project_url" {
value = "https://portal.stackit.cloud/projects/${stackit_resourcemanager_project.project.project_id}"
description = "The deep link URL to access the project in the StackIt portal."
+}
+
+output "summary" {
+ value = fileexists("${path.module}/stackit_organization_membership_summary.md") ? file("${path.module}/stackit_organization_membership_summary.md") : "STACKIT organization membership summary was not generated."
+ description = "Summary of STACKIT organization membership onboarding for assigned project users."
}
\ No newline at end of file
diff --git a/modules/stackit/project/buildingblock/prerun.py b/modules/stackit/project/buildingblock/prerun.py
new file mode 100755
index 00000000..51bf8aa1
--- /dev/null
+++ b/modules/stackit/project/buildingblock/prerun.py
@@ -0,0 +1,285 @@
+#!/usr/bin/env python3
+"""Best-effort STACKIT organization membership onboarding."""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+import urllib.error
+import urllib.parse
+import urllib.request
+from pathlib import Path
+
+SUMMARY_FILE = Path("stackit_organization_membership_summary.md")
+AUTHORIZATION_ENDPOINT = "https://authorization.api.stackit.cloud"
+TOKEN_ENDPOINT = "https://accounts.stackit.cloud/oauth/v2/token"
+DEFAULT_ORGANIZATION_ROLE = "organization.viewer"
+DEFAULT_FEDERATED_TOKEN_FILE = "/var/run/secrets/stackit.cloud/serviceaccount/token"
+WIF_CLIENT_ASSERTION_TYPE = "urn:schwarz:params:oauth:client-assertion-type:workload-jwt"
+
+
+def info(message: str) -> None:
+ print(f"[stackit-org-membership] {message}")
+
+
+def warn(message: str) -> None:
+ print(f"[stackit-org-membership] WARN: {message}", file=sys.stderr)
+ if user_message_file := os.environ.get("MESHSTACK_USER_MESSAGE"):
+ with open(user_message_file, "a", encoding="utf-8") as handle:
+ handle.write(f"STACKIT organization membership warning: {message}\n")
+
+
+def request(
+ method: str,
+ url: str,
+ *,
+ token: str | None = None,
+ json_body: dict | None = None,
+ form_body: dict | None = None,
+) -> tuple[int, str]:
+ headers = {}
+ data = None
+
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+
+ if json_body is not None:
+ headers["Content-Type"] = "application/json"
+ data = json.dumps(json_body).encode("utf-8")
+
+ if form_body is not None:
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
+ data = urllib.parse.urlencode(form_body).encode("utf-8")
+
+ try:
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
+ with urllib.request.urlopen(req, timeout=30) as response:
+ return response.status, response.read().decode("utf-8", errors="replace")
+ except urllib.error.HTTPError as error:
+ return error.code, error.read().decode("utf-8", errors="replace")
+ except urllib.error.URLError as error:
+ return 0, str(error.reason)
+
+
+def extract_emails(run_input: str) -> list[str]:
+ run = json.loads(run_input)
+ inputs = run["spec"]["buildingBlock"]["spec"]["inputs"]
+ users_input = next(item for item in inputs if item["key"] == "users")
+ users = json.loads(users_input["value"])
+ return sorted({user["email"].strip().lower() for user in users})
+
+
+def get_access_token() -> str:
+ if token := os.environ.get("STACKIT_ACCESS_TOKEN"):
+ return token
+
+ token_file = Path(os.environ.get("STACKIT_FEDERATED_TOKEN_FILE", DEFAULT_FEDERATED_TOKEN_FILE))
+ code, body = request(
+ "POST",
+ os.environ.get("STACKIT_IDP_TOKEN_ENDPOINT", TOKEN_ENDPOINT),
+ form_body={
+ "grant_type": "client_credentials",
+ "client_assertion_type": WIF_CLIENT_ASSERTION_TYPE,
+ "client_assertion": token_file.read_text(encoding="utf-8").strip(),
+ "client_id": os.environ["STACKIT_SERVICE_ACCOUNT_EMAIL"],
+ },
+ )
+
+ if not 200 <= code < 300:
+ warn(f"token exchange returned HTTP {code}: {body[:500]}")
+ return ""
+
+ token = json.loads(body).get("access_token", "")
+ if not token:
+ warn("token exchange response did not contain an access_token")
+ return token
+
+
+def add_member(organization_id: str, role: str, email: str, token: str) -> dict[str, str]:
+ # This PATCH is STACKIT's `AddMembers` operation (Authorization API v2): it grants the given
+ # (subject, role) pair without touching the subject's other roles or other members' grants.
+ # Removal uses a separate `RemoveMembers` endpoint; there is no full-list-replace endpoint.
+ code, body = request(
+ "PATCH",
+ f"{AUTHORIZATION_ENDPOINT}/v2/{urllib.parse.quote(organization_id, safe='')}/members",
+ token=token,
+ json_body={"resourceType": "organization", "members": [{"subject": email, "role": role}]},
+ )
+
+ if 200 <= code < 300:
+ return {"status": "succeeded", "details": "Add request succeeded."}
+
+ details = f"Add request returned HTTP {code}: {body[:300]}"
+ warn(f"{email}: {details}")
+ return {"status": "failed", "details": details}
+
+
+def get_assignment(organization_id: str, role: str, email: str, token: str) -> dict[str, str]:
+ org = urllib.parse.quote(organization_id, safe="")
+ subject = urllib.parse.quote(email, safe="")
+ code, body = request(
+ "GET",
+ f"{AUTHORIZATION_ENDPOINT}/v2/organization/{org}/members?subject={subject}",
+ token=token,
+ )
+
+ if not 200 <= code < 300:
+ return {
+ "status": "unknown",
+ "details": f"Could not query organization membership. STACKIT API returned HTTP '{code}'.",
+ }
+
+ roles = sorted(
+ {
+ member["role"]
+ for member in json.loads(body).get("members", [])
+ if member["subject"].lower() == email
+ }
+ )
+
+ if role in roles:
+ return {"status": "assigned", "details": "Required organization role is assigned."}
+
+ if roles:
+ current_roles = ", ".join(f"`{current_role}`" for current_role in roles)
+ return {
+ "status": "missing",
+ "details": f"Current organization roles: {current_roles}; missing required role `{role}`.",
+ }
+
+ return {
+ "status": "missing",
+ "details": "No organization membership for this user was returned by STACKIT.",
+ }
+
+
+def make_row(
+ email: str,
+ role: str,
+ add_status: str,
+ add_details: str,
+ assignment_status: str,
+ assignment_details: str,
+) -> dict[str, str]:
+ return {
+ "email": email,
+ "role": role,
+ "add_status": add_status,
+ "add_details": add_details,
+ "assignment_status": assignment_status,
+ "assignment_details": assignment_details,
+ }
+
+
+def markdown_cell(value: str) -> str:
+ return value.replace("|", "\\|").replace("\n", " ")
+
+
+def write_summary(organization_id: str, role: str, rows: list[dict[str, str]]) -> None:
+ lines = [
+ "# STACKIT Organization Membership",
+ "",
+ "The pre-run script adds all meshStack users assigned to this project to the STACKIT organization with the required organization role, then checks the resulting membership status before project-level role assignments are applied.",
+ "",
+ f"- **Organization ID**: `{organization_id}`",
+ f"- **Required organization role**: `{role}`",
+ "",
+ "| User | Status |",
+ "|------|--------|",
+ ]
+
+ for row in rows:
+ if row["assignment_status"] == "assigned":
+ status = "✅"
+ else:
+ details = " ".join([row["add_details"], row["assignment_details"]]).strip()
+ status = markdown_cell(details)
+ lines.append(f"| `{markdown_cell(row['email'])}` | {status} |")
+
+ has_problem = any(row["assignment_status"] != "assigned" for row in rows)
+ if has_problem:
+ lines += [
+ "",
+ "## How to fix missing access",
+ "",
+ "- If the add request failed, verify that the building block service account has the STACKIT organization role `iam.member-admin` and that Workload Identity Federation is configured correctly. Then re-run this building block.",
+ f"- If the required role is missing, ask a STACKIT organization administrator to add the user to organization `{organization_id}` with role `{role}`, then re-run this building block.",
+ "- If membership could not be checked, verify API access for querying organization members and re-run this building block.",
+ ]
+
+ SUMMARY_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+def write_disabled_summary() -> None:
+ SUMMARY_FILE.write_text(
+ "# STACKIT Organization Membership\n\n"
+ "Organization membership onboarding is disabled for this building block.\n",
+ encoding="utf-8",
+ )
+
+
+def main() -> None:
+ if len(sys.argv) > 1 and sys.argv[1].upper() == "DESTROY":
+ info("destroy run detected; skipping organization membership onboarding")
+ return
+
+ if os.environ.get("STACKIT_ORGANIZATION_ONBOARDING_ENABLED", "1") != "1":
+ info("organization membership onboarding is disabled; skipping")
+ write_disabled_summary()
+ return
+
+ organization_id = os.environ["STACKIT_ORGANIZATION_ID"]
+ role = os.environ.get("STACKIT_ORGANIZATION_MEMBER_ROLE", DEFAULT_ORGANIZATION_ROLE)
+ emails = extract_emails(sys.stdin.read())
+
+ if not emails:
+ info("no users found; skipping organization membership onboarding")
+ write_summary(organization_id, role, [])
+ return
+
+ info(f"ensuring {len(emails)} user(s) have organization role '{role}' on '{organization_id}'")
+
+ token = get_access_token()
+ if not token:
+ write_summary(
+ organization_id,
+ role,
+ [
+ make_row(
+ email,
+ role,
+ "skipped",
+ "Could not obtain a STACKIT access token, so no add request was made.",
+ "unknown",
+ "Membership could not be checked.",
+ )
+ for email in emails
+ ],
+ )
+ return
+
+ rows = []
+ for email in emails:
+ add = add_member(organization_id, role, email, token)
+ assignment = get_assignment(organization_id, role, email, token)
+ rows.append(
+ make_row(
+ email,
+ role,
+ add["status"],
+ add["details"],
+ assignment["status"],
+ assignment["details"],
+ )
+ )
+
+ write_summary(organization_id, role, rows)
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc: # best-effort pre-run must not block apply
+ warn(f"unexpected pre-run failure: {exc}")
+ sys.exit(0)
diff --git a/modules/stackit/project/buildingblock/prerun.sh b/modules/stackit/project/buildingblock/prerun.sh
new file mode 100755
index 00000000..dc64855d
--- /dev/null
+++ b/modules/stackit/project/buildingblock/prerun.sh
@@ -0,0 +1 @@
+exec python3 "./prerun.py" "$@"
diff --git a/reference-architectures/stackit-sandbox-landingzone/buildingblock/README.md b/reference-architectures/stackit-sandbox-landingzone/buildingblock/README.md
index ee4ef593..4145d4dd 100644
--- a/reference-architectures/stackit-sandbox-landingzone/buildingblock/README.md
+++ b/reference-architectures/stackit-sandbox-landingzone/buildingblock/README.md
@@ -50,6 +50,7 @@ The user-facing readme is maintained inline in the `readme` field of the
| [platform\_identifier](#input\_platform\_identifier) | Identifier for the STACKIT sandbox platform created in meshStack (letters, digits and dashes only). | `string` | n/a | yes |
| [role\_mapping](#input\_role\_mapping) | Default mapping from meshStack roles to STACKIT project roles for the nested STACKIT Project integration. Values can be built-in STACKIT roles or custom STACKIT role names. | `map(list(string))` | {
"admin": [
"owner"
],
"reader": [
"reader"
],
"user": [
"editor"
]
} | no |
| [stackit\_org](#input\_stackit\_org) | STACKIT organization UUID under which the landing-zone folder, backplane project and tenant projects are created. | `string` | n/a | yes |
+| [stackit\_organization\_onboarding\_enabled](#input\_stackit\_organization\_onboarding\_enabled) | Whether the nested STACKIT Project integration adds meshStack project users to the STACKIT organization before applying project-level role assignments. Disable if organization membership is managed outside this landing zone. | `bool` | `true` | no |
| [stackit\_owner\_email](#input\_stackit\_owner\_email) | Owner email assigned to the STACKIT resourcemanager folder and backplane project. | `string` | n/a | yes |
| [stackit\_service\_account\_key](#input\_stackit\_service\_account\_key) | STACKIT service account key JSON with `resource-manager.admin` on the organization. Used to create the landing-zone folder and backplane project. | `string` | n/a | yes |
| [tags](#input\_tags) | Tags forwarded to the nested STACKIT Project integration. `landingzone` tags are applied to the default landing zone; `building_block` tags are applied to the nested building block definition. | object({
landingzone = map(list(string))
building_block = map(list(string))
}) | {
"building_block": {},
"landingzone": {}
} | no |
diff --git a/reference-architectures/stackit-sandbox-landingzone/buildingblock/main.tf b/reference-architectures/stackit-sandbox-landingzone/buildingblock/main.tf
index 78a95fde..0d2fa207 100644
--- a/reference-architectures/stackit-sandbox-landingzone/buildingblock/main.tf
+++ b/reference-architectures/stackit-sandbox-landingzone/buildingblock/main.tf
@@ -29,11 +29,12 @@ resource "stackit_resourcemanager_project" "backplane" {
module "stackit_integration" {
source = "github.com/meshcloud/meshstack-hub//modules/stackit?ref=${var.git_ref}"
- stackit_organization_id = var.stackit_org
- stackit_parent_container_id = stackit_resourcemanager_folder.this.container_id
- stackit_project_id = stackit_resourcemanager_project.backplane.project_id
- stackit_service_account_name = substr(var.platform_identifier, 0, 20)
- role_mapping = var.role_mapping
+ stackit_organization_id = var.stackit_org
+ stackit_parent_container_id = stackit_resourcemanager_folder.this.container_id
+ stackit_project_id = stackit_resourcemanager_project.backplane.project_id
+ stackit_service_account_name = substr(var.platform_identifier, 0, 20)
+ role_mapping = var.role_mapping
+ stackit_organization_onboarding_enabled = var.stackit_organization_onboarding_enabled
hub = {
git_ref = var.git_ref
diff --git a/reference-architectures/stackit-sandbox-landingzone/buildingblock/variables.tf b/reference-architectures/stackit-sandbox-landingzone/buildingblock/variables.tf
index e48d3b62..711057bc 100644
--- a/reference-architectures/stackit-sandbox-landingzone/buildingblock/variables.tf
+++ b/reference-architectures/stackit-sandbox-landingzone/buildingblock/variables.tf
@@ -61,6 +61,12 @@ variable "role_mapping" {
}
}
+variable "stackit_organization_onboarding_enabled" {
+ type = bool
+ default = true
+ description = "Whether the nested STACKIT Project integration adds meshStack project users to the STACKIT organization before applying project-level role assignments. Disable if organization membership is managed outside this landing zone."
+}
+
variable "git_ref" {
type = string
default = "main"
diff --git a/reference-architectures/stackit-sandbox-landingzone/meshstack_integration.tf b/reference-architectures/stackit-sandbox-landingzone/meshstack_integration.tf
index cb9f4b62..cd3ba642 100644
--- a/reference-architectures/stackit-sandbox-landingzone/meshstack_integration.tf
+++ b/reference-architectures/stackit-sandbox-landingzone/meshstack_integration.tf
@@ -183,6 +183,14 @@ resource "meshstack_building_block_definition" "this" {
}))
}
+ stackit_organization_onboarding_enabled = {
+ display_name = "STACKIT Organization Onboarding Enabled"
+ description = "If true, the nested STACKIT Project integration adds meshStack project users to the STACKIT organization before applying project-level role assignments."
+ type = "BOOLEAN"
+ assignment_type = "USER_INPUT"
+ default_value = jsonencode(true)
+ }
+
# ── meshStack context ──
workspace = {