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 = {