From f0f0aa0d89e114319a2dcd8636455a8f2f571e8c Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 9 Jun 2026 09:12:43 +0200 Subject: [PATCH 1/5] feat: entra ID group creation building block --- .../azure/entra-id-groups/backplane/README.md | 28 ++ .../azure/entra-id-groups/backplane/main.tf | 38 +++ .../entra-id-groups/backplane/outputs.tf | 8 + .../entra-id-groups/backplane/variables.tf | 31 +++ .../entra-id-groups/backplane/versions.tf | 14 + .../entra-id-groups/buildingblock/README.md | 47 ++++ .../entra-id-groups/buildingblock/logo.png | Bin 0 -> 9144 bytes .../entra-id-groups/buildingblock/main.tf | 20 ++ .../entra-id-groups/buildingblock/outputs.tf | 9 + .../entra-id-groups/buildingblock/provider.tf | 1 + .../buildingblock/variables.tf | 27 ++ .../entra-id-groups/buildingblock/versions.tf | 8 + .../entra-id-groups/meshstack_integration.tf | 255 ++++++++++++++++++ 13 files changed, 486 insertions(+) create mode 100644 modules/azure/entra-id-groups/backplane/README.md create mode 100644 modules/azure/entra-id-groups/backplane/main.tf create mode 100644 modules/azure/entra-id-groups/backplane/outputs.tf create mode 100644 modules/azure/entra-id-groups/backplane/variables.tf create mode 100644 modules/azure/entra-id-groups/backplane/versions.tf create mode 100644 modules/azure/entra-id-groups/buildingblock/README.md create mode 100644 modules/azure/entra-id-groups/buildingblock/logo.png create mode 100644 modules/azure/entra-id-groups/buildingblock/main.tf create mode 100644 modules/azure/entra-id-groups/buildingblock/outputs.tf create mode 100644 modules/azure/entra-id-groups/buildingblock/provider.tf create mode 100644 modules/azure/entra-id-groups/buildingblock/variables.tf create mode 100644 modules/azure/entra-id-groups/buildingblock/versions.tf create mode 100644 modules/azure/entra-id-groups/meshstack_integration.tf diff --git a/modules/azure/entra-id-groups/backplane/README.md b/modules/azure/entra-id-groups/backplane/README.md new file mode 100644 index 00000000..fcb501d2 --- /dev/null +++ b/modules/azure/entra-id-groups/backplane/README.md @@ -0,0 +1,28 @@ +# Azure Entra ID Groups — Backplane + +This backplane creates the automation identity used to provision Entra security groups for meshStack project roles. + +## What it provisions + +- **Resource Group** — hosts the UAMI in the configured Azure region. +- **User-Assigned Managed Identity (UAMI)** — the automation principal that runs the building block. No client secrets. +- **Workload Identity Federation credentials** — bind the UAMI to the meshStack replicator's OIDC issuer and subject, enabling secret-free authentication. +- **Microsoft Graph app roles** on the UAMI: + - `Group.ReadWrite.All` — create and manage Entra security groups. + - `AdministrativeUnit.ReadWrite.All` — add groups to Administrative Units (used when `administrative_unit_id` is supplied at building block runtime). + +## Required permissions to deploy + +The platform engineer running this backplane needs: + +| Permission | Scope | Why | +|---|---|---| +| `Managed Identity Contributor` | Target subscription | Create and update the UAMI | +| `Owner` or `User Access Administrator` | `var.scope` | Create role assignments on the UAMI | +| `Privileged Role Administrator` (Entra) | Tenant | Grant admin-consented Microsoft Graph app roles | + +## Operational notes + +- The UAMI principal ID maps to a service principal in Entra. The `Group.ReadWrite.All` and `AdministrativeUnit.ReadWrite.All` app role assignments require **admin consent** — ensure a Global Administrator or Privileged Role Administrator approves the assignments in the Entra portal after the first `apply`. +- No secrets are created; the UAMI authenticates via OIDC token exchange. +- The backplane resource group is named after `var.name` and must be unique within the subscription. diff --git a/modules/azure/entra-id-groups/backplane/main.tf b/modules/azure/entra-id-groups/backplane/main.tf new file mode 100644 index 00000000..e92adb50 --- /dev/null +++ b/modules/azure/entra-id-groups/backplane/main.tf @@ -0,0 +1,38 @@ +resource "azurerm_resource_group" "backplane" { + name = var.name + location = var.location +} + +resource "azurerm_user_assigned_identity" "backplane" { + name = var.name + location = var.location + resource_group_name = azurerm_resource_group.backplane.name +} + +resource "azurerm_federated_identity_credential" "backplane" { + for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s } + + name = "subject-${each.key}" + resource_group_name = azurerm_resource_group.backplane.name + parent_id = azurerm_user_assigned_identity.backplane.id + audience = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = each.value +} + +# Grant Microsoft Graph app roles so the UAMI can manage groups and Administrative Unit members. +data "azuread_service_principal" "msgraph" { + client_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph +} + +resource "azuread_app_role_assignment" "group_readwrite_all" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["Group.ReadWrite.All"] + principal_object_id = azurerm_user_assigned_identity.backplane.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} + +resource "azuread_app_role_assignment" "administrative_unit_readwrite_all" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["AdministrativeUnit.ReadWrite.All"] + principal_object_id = azurerm_user_assigned_identity.backplane.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} diff --git a/modules/azure/entra-id-groups/backplane/outputs.tf b/modules/azure/entra-id-groups/backplane/outputs.tf new file mode 100644 index 00000000..940d8f91 --- /dev/null +++ b/modules/azure/entra-id-groups/backplane/outputs.tf @@ -0,0 +1,8 @@ +output "identity" { + description = "UAMI identity attributes consumed by meshstack_integration.tf as static inputs." + value = { + client_id = azurerm_user_assigned_identity.backplane.client_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id + tenant_id = azurerm_user_assigned_identity.backplane.tenant_id + } +} diff --git a/modules/azure/entra-id-groups/backplane/variables.tf b/modules/azure/entra-id-groups/backplane/variables.tf new file mode 100644 index 00000000..26f296f4 --- /dev/null +++ b/modules/azure/entra-id-groups/backplane/variables.tf @@ -0,0 +1,31 @@ +variable "name" { + type = string + nullable = false + description = "Name for the UAMI and related backplane resources. Must match pattern ^[-a-z0-9]+$." + + validation { + condition = can(regex("^[-a-z0-9]+$", var.name)) + error_message = "Only lowercase alphanumeric characters and dashes are allowed." + } +} + +variable "location" { + type = string + nullable = false + description = "Azure region for the backplane resource group and UAMI." +} + +variable "scope" { + type = string + nullable = false + description = "Scope for role assignment (management group or subscription ID)." +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subjects = list(string) + }) + nullable = false + description = "WIF issuer and subjects for federated authentication from the meshStack replicator." +} diff --git a/modules/azure/entra-id-groups/backplane/versions.tf b/modules/azure/entra-id-groups/backplane/versions.tf new file mode 100644 index 00000000..e478efa8 --- /dev/null +++ b/modules/azure/entra-id-groups/backplane/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.8" + } + } +} diff --git a/modules/azure/entra-id-groups/buildingblock/README.md b/modules/azure/entra-id-groups/buildingblock/README.md new file mode 100644 index 00000000..5fec044c --- /dev/null +++ b/modules/azure/entra-id-groups/buildingblock/README.md @@ -0,0 +1,47 @@ +--- +name: Azure Entra ID Groups +supportedPlatforms: + - azure +description: Creates Entra security groups for meshStack project roles, with optional Administrative Unit membership. +--- + +Automatically provision Entra ID security groups for every role in a meshStack project. Groups are named consistently using the workspace identifier, project identifier, an optional prefix, and the role name as suffix — giving your teams a predictable, auditable group structure in Azure Active Directory. + +## When to use it + +Use this building block when you want to: +- Map meshStack project roles (admin, user, reader, or custom roles) to Entra security groups for RBAC assignments in Azure. +- Enforce a standard naming scheme across all projects in your platform. +- Optionally scope groups inside a dedicated Entra Administrative Unit to isolate tenant-level identities from the rest of the directory. + +## Usage examples + +**Default meshStack roles (admin / user / reader):** + +A project `my-project` in workspace `my-workspace` with prefix `plat` produces three groups: +- `plat-my-workspace-my-project-admin` +- `plat-my-workspace-my-project-user` +- `plat-my-workspace-my-project-reader` + +**Custom roles:** + +Set *Project Roles* to `devops,qa,readonly` to create: +- `plat-my-workspace-my-project-devops` +- `plat-my-workspace-my-project-qa` +- `plat-my-workspace-my-project-readonly` + +**With Administrative Unit:** + +Provide the object ID of an existing Entra Administrative Unit. All generated groups are added as members of that AU, restricting who can manage them in the directory. + +## Shared Responsibilities + +| Responsibility | Platform Team | Application Team | +|---|:---:|:---:| +| Deploy and configure the backplane identity | ✅ | ❌ | +| Define the group naming prefix | ✅ | ❌ | +| Create and delete Entra groups | ✅ | ❌ | +| Add the Administrative Unit (optional) | ✅ | ❌ | +| Choose which project roles get groups | ❌ | ✅ | +| Assign users to the generated groups | ❌ | ✅ | +| Use group IDs in downstream RBAC assignments | ❌ | ✅ | diff --git a/modules/azure/entra-id-groups/buildingblock/logo.png b/modules/azure/entra-id-groups/buildingblock/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..86415560d3bbde49071e3d6a93eea2a89ff29238 GIT binary patch literal 9144 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 g.object_id } +} + +output "group_display_names" { + description = "Map of project role name to Entra group display name." + value = { for role, g in azuread_group.project_role : role => g.display_name } +} diff --git a/modules/azure/entra-id-groups/buildingblock/provider.tf b/modules/azure/entra-id-groups/buildingblock/provider.tf new file mode 100644 index 00000000..f2025dcd --- /dev/null +++ b/modules/azure/entra-id-groups/buildingblock/provider.tf @@ -0,0 +1 @@ +provider "azuread" {} diff --git a/modules/azure/entra-id-groups/buildingblock/variables.tf b/modules/azure/entra-id-groups/buildingblock/variables.tf new file mode 100644 index 00000000..a6296064 --- /dev/null +++ b/modules/azure/entra-id-groups/buildingblock/variables.tf @@ -0,0 +1,27 @@ +variable "prefix" { + type = string + default = "" + description = "Optional prefix prepended to all group display names. Leave empty to omit." +} + +variable "workspace_identifier" { + type = string + description = "meshStack workspace identifier included in the group name." +} + +variable "project_identifier" { + type = string + description = "meshStack project identifier included in the group name." +} + +variable "project_roles" { + type = string + default = "admin,user,reader" + description = "Comma-separated list of project role name suffixes. One Entra group is created per role. Defaults to the three standard meshStack roles: admin, user, reader." +} + +variable "administrative_unit_id" { + type = string + default = "" + description = "Object ID of the Entra Administrative Unit to add the groups to. Leave empty to skip AU membership." +} diff --git a/modules/azure/entra-id-groups/buildingblock/versions.tf b/modules/azure/entra-id-groups/buildingblock/versions.tf new file mode 100644 index 00000000..41eb1aaa --- /dev/null +++ b/modules/azure/entra-id-groups/buildingblock/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + azuread = { + source = "hashicorp/azuread" + version = "~> 3.8.0" + } + } +} diff --git a/modules/azure/entra-id-groups/meshstack_integration.tf b/modules/azure/entra-id-groups/meshstack_integration.tf new file mode 100644 index 00000000..fd0c4dcf --- /dev/null +++ b/modules/azure/entra-id-groups/meshstack_integration.tf @@ -0,0 +1,255 @@ +variable "azure_tenant_id" { + type = string + description = "Azure Entra tenant ID where groups will be created." +} + +variable "azure_scope" { + type = string + description = "Azure management group or subscription ID used as the backplane UAMI's role assignment scope." +} + +variable "azure_location" { + type = string + description = "Azure region for the backplane resource group and UAMI (e.g. 'westeurope')." +} + +variable "backplane_name" { + type = string + default = "azure-entra-id-groups" + description = "Name for the backplane resources (resource group, UAMI, role definition). Must match pattern ^[-a-z0-9]+$." + + validation { + condition = can(regex("^[-a-z0-9]+$", var.backplane_name)) + error_message = "Only lowercase alphanumeric characters and dashes are allowed." + } +} + +variable "notification_subscribers" { + type = list(string) + default = [] + description = "Email addresses notified on building block lifecycle events." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to building block definition metadata." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = {} + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition version is kept in draft mode. + EOT +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +data "meshstack_integrations" "integrations" {} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/entra-id-groups/backplane?ref=${var.hub.git_ref}" + name = var.backplane_name + scope = var.azure_scope + location = var.azure_location + + workload_identity_federation = { + issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer + subjects = [ + "${trimsuffix(data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject, ":replicator")}:workspace.${var.meshstack.owning_workspace_identifier}.buildingblockdefinition.${meshstack_building_block_definition.this.metadata.uuid}" + ] + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "Azure Entra ID Groups" + description = "Creates Entra security groups for meshStack project roles, with optional Administrative Unit membership." + support_url = "mailto:support@meshcloud.io" + documentation_url = "https://hub.meshcloud.io/platforms/azure/definitions/azure-entra-id-groups" + notification_subscribers = var.notification_subscribers + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/main/modules/azure/entra-id-groups/buildingblock/logo.png" + target_type = "TENANT_LEVEL" + + readme = chomp(<<-EOT + Automatically provision Entra ID security groups for every role in a meshStack project. Groups are named consistently using the workspace identifier, project identifier, an optional prefix, and the role name as suffix — giving your teams a predictable, auditable group structure in Azure Active Directory. + + ## When to use it + + Use this building block when you want to: + - Map meshStack project roles (admin, user, reader, or custom roles) to Entra security groups for RBAC assignments in Azure. + - Enforce a standard naming scheme across all projects in your platform. + - Optionally scope groups inside a dedicated Entra Administrative Unit to isolate tenant-level identities from the rest of the directory. + + ## Usage examples + + **Default meshStack roles (admin / user / reader):** + + A project `my-project` in workspace `my-workspace` with prefix `plat` produces three groups: + - `plat-my-workspace-my-project-admin` + - `plat-my-workspace-my-project-user` + - `plat-my-workspace-my-project-reader` + + **Custom roles:** + + Set *Project Roles* to `devops,qa,readonly` to create: + - `plat-my-workspace-my-project-devops` + - `plat-my-workspace-my-project-qa` + - `plat-my-workspace-my-project-readonly` + + **With Administrative Unit:** + + Provide the object ID of an existing Entra Administrative Unit. All generated groups are added as members of that AU, restricting who can manage them in the directory. + + ## Shared Responsibilities + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Deploy and configure the backplane identity | ✅ | ❌ | + | Define the group naming prefix | ✅ | ❌ | + | Create and delete Entra groups | ✅ | ❌ | + | Add the Administrative Unit (optional) | ✅ | ❌ | + | Choose which project roles get groups | ❌ | ✅ | + | Assign users to the generated groups | ❌ | ✅ | + | Use group IDs in downstream RBAC assignments | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + + deletion_mode = "DELETE" + + implementation = { + terraform = { + terraform_version = "1.9.0" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/azure/entra-id-groups/buildingblock" + ref_name = var.hub.git_ref + use_mesh_http_backend_fallback = true + } + } + + inputs = { + ARM_CLIENT_ID = { + type = "STRING" + display_name = "ARM Client ID" + description = "Client ID of the UAMI used to authenticate with Azure." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.backplane.identity.client_id) + } + ARM_TENANT_ID = { + type = "STRING" + display_name = "ARM Tenant ID" + description = "Azure Entra tenant ID for authentication." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.azure_tenant_id) + } + ARM_USE_OIDC = { + type = "STRING" + display_name = "ARM Use OIDC" + description = "Enables OIDC-based workload identity federation for the AzureAD provider." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("true") + } + ARM_OIDC_TOKEN_FILE_PATH = { + type = "STRING" + display_name = "ARM OIDC Token File Path" + description = "Path to the OIDC token file used for workload identity federation." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("/var/run/secrets/workload-identity/azure/token") + } + prefix = { + type = "STRING" + display_name = "Group Name Prefix" + description = "Optional prefix prepended to all group display names (e.g. 'plat'). Leave empty to omit." + assignment_type = "USER_INPUT" + argument = jsonencode("") + } + workspace_identifier = { + type = "STRING" + display_name = "Workspace Identifier" + description = "meshStack workspace identifier. Injected automatically from the platform context." + assignment_type = "PLATFORM_TENANT_WORKSPACE_IDENTIFIER" + } + project_identifier = { + type = "STRING" + display_name = "Project Identifier" + description = "meshStack project identifier. Injected automatically from the platform context." + assignment_type = "PLATFORM_TENANT_PROJECT_IDENTIFIER" + } + project_roles = { + type = "STRING" + display_name = "Project Roles" + description = "Comma-separated list of project role name suffixes. One Entra group is created per role. Defaults to the three standard meshStack roles." + assignment_type = "USER_INPUT" + argument = jsonencode("admin,user,reader") + } + administrative_unit_id = { + type = "STRING" + display_name = "Administrative Unit ID" + description = "Object ID of the Entra Administrative Unit to add the groups to. Leave empty to skip AU membership." + assignment_type = "USER_INPUT" + argument = jsonencode("") + } + } + + outputs = { + group_object_ids = { + type = "STRING" + display_name = "Group Object IDs" + description = "JSON map of project role name to Entra group object ID." + assignment_type = "NONE" + } + group_display_names = { + type = "STRING" + display_name = "Group Display Names" + description = "JSON map of project role name to Entra group display name." + assignment_type = "NONE" + } + } + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.21.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.8" + } + } +} From 8e9a8f59c2521a69929e30995cfdf9330e92a12e Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 9 Jun 2026 10:05:56 +0200 Subject: [PATCH 2/5] feat: adding users to groups --- .../entra-id-groups/backplane/provider.tf | 3 +++ .../entra-id-groups/backplane/versions.tf | 2 +- .../entra-id-groups/buildingblock/main.tf | 26 ++++++++++++++++--- .../buildingblock/variables.tf | 14 ++++++++++ .../entra-id-groups/meshstack_integration.tf | 9 ++++++- 5 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 modules/azure/entra-id-groups/backplane/provider.tf diff --git a/modules/azure/entra-id-groups/backplane/provider.tf b/modules/azure/entra-id-groups/backplane/provider.tf new file mode 100644 index 00000000..ab91b248 --- /dev/null +++ b/modules/azure/entra-id-groups/backplane/provider.tf @@ -0,0 +1,3 @@ +provider "azurerm" { + features {} +} diff --git a/modules/azure/entra-id-groups/backplane/versions.tf b/modules/azure/entra-id-groups/backplane/versions.tf index e478efa8..4b5a7c6a 100644 --- a/modules/azure/entra-id-groups/backplane/versions.tf +++ b/modules/azure/entra-id-groups/backplane/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.12.0" + required_version = ">= 1.0.0" required_providers { azurerm = { diff --git a/modules/azure/entra-id-groups/buildingblock/main.tf b/modules/azure/entra-id-groups/buildingblock/main.tf index 28ede8a4..d21f49c6 100644 --- a/modules/azure/entra-id-groups/buildingblock/main.tf +++ b/modules/azure/entra-id-groups/buildingblock/main.tf @@ -1,7 +1,20 @@ locals { - roles = [for r in split(",", var.project_roles) : trimspace(r) if trimspace(r) != ""] - au_id = var.administrative_unit_id != "" ? var.administrative_unit_id : null - name_parts = compact([var.prefix, var.workspace_identifier, var.project_identifier]) + roles = [for r in split(",", var.project_roles) : trimspace(r) if trimspace(r) != ""] + au_id = var.administrative_unit_id != "" ? var.administrative_unit_id : null + name_parts = compact([var.prefix, var.workspace_identifier, var.project_identifier]) + + user_role_assignments = { + for pair in flatten([ + for user in var.users : [ + for role in user.roles : { + key = "${user.euid}-${role}" + euid = user.euid + role = role + } + ] + ]) : pair.key => pair + if contains(local.roles, pair.role) + } } resource "azuread_group" "project_role" { @@ -18,3 +31,10 @@ resource "azuread_administrative_unit_member" "project_role" { administrative_unit_object_id = local.au_id member_object_id = azuread_group.project_role[each.value].object_id } + +resource "azuread_group_member" "project_role" { + for_each = local.user_role_assignments + + group_object_id = azuread_group.project_role[each.value.role].object_id + member_object_id = each.value.euid +} diff --git a/modules/azure/entra-id-groups/buildingblock/variables.tf b/modules/azure/entra-id-groups/buildingblock/variables.tf index a6296064..24ecbc21 100644 --- a/modules/azure/entra-id-groups/buildingblock/variables.tf +++ b/modules/azure/entra-id-groups/buildingblock/variables.tf @@ -25,3 +25,17 @@ variable "administrative_unit_id" { default = "" description = "Object ID of the Entra Administrative Unit to add the groups to. Leave empty to skip AU membership." } + +variable "users" { + type = list(object({ + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + })) + default = [] + description = "Project members from meshStack with their assigned roles. Each user is added to the group matching their role." +} diff --git a/modules/azure/entra-id-groups/meshstack_integration.tf b/modules/azure/entra-id-groups/meshstack_integration.tf index fd0c4dcf..6a45a88f 100644 --- a/modules/azure/entra-id-groups/meshstack_integration.tf +++ b/modules/azure/entra-id-groups/meshstack_integration.tf @@ -129,7 +129,8 @@ resource "meshstack_building_block_definition" "this" { | Create and delete Entra groups | ✅ | ❌ | | Add the Administrative Unit (optional) | ✅ | ❌ | | Choose which project roles get groups | ❌ | ✅ | - | Assign users to the generated groups | ❌ | ✅ | + | Assign users to generated groups (automated via project membership) | ✅ | ❌ | + | Manage which users have which project roles | ❌ | ✅ | | Use group IDs in downstream RBAC assignments | ❌ | ✅ | EOT ) @@ -216,6 +217,12 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "USER_INPUT" argument = jsonencode("") } + users = { + type = "CODE" + display_name = "Users" + description = "Project members from meshStack with their assigned roles. Injected automatically by meshStack." + assignment_type = "USER_PERMISSIONS" + } } outputs = { From 2e3e019485e881351080292ccf067ba42f3ca394 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 9 Jun 2026 10:10:22 +0200 Subject: [PATCH 3/5] fix: adding user based on UPN not GUID --- modules/azure/entra-id-groups/buildingblock/main.tf | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/azure/entra-id-groups/buildingblock/main.tf b/modules/azure/entra-id-groups/buildingblock/main.tf index d21f49c6..2235f449 100644 --- a/modules/azure/entra-id-groups/buildingblock/main.tf +++ b/modules/azure/entra-id-groups/buildingblock/main.tf @@ -3,6 +3,8 @@ locals { au_id = var.administrative_unit_id != "" ? var.administrative_unit_id : null name_parts = compact([var.prefix, var.workspace_identifier, var.project_identifier]) + unique_user_euids = toset([for user in var.users : user.euid]) + user_role_assignments = { for pair in flatten([ for user in var.users : [ @@ -17,6 +19,11 @@ locals { } } +data "azuread_user" "this" { + for_each = local.unique_user_euids + user_principal_name = each.value +} + resource "azuread_group" "project_role" { for_each = toset(local.roles) @@ -36,5 +43,5 @@ resource "azuread_group_member" "project_role" { for_each = local.user_role_assignments group_object_id = azuread_group.project_role[each.value.role].object_id - member_object_id = each.value.euid + member_object_id = data.azuread_user.this[each.value.euid].object_id } From bf8bc105a20198968c55ad2036ac94e387a24af1 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 9 Jun 2026 10:27:17 +0200 Subject: [PATCH 4/5] fix: adding user lookup strategy --- modules/azure/entra-id-groups/backplane/main.tf | 8 +++++++- modules/azure/entra-id-groups/buildingblock/main.tf | 11 ++++++++--- .../azure/entra-id-groups/buildingblock/variables.tf | 10 ++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/azure/entra-id-groups/backplane/main.tf b/modules/azure/entra-id-groups/backplane/main.tf index e92adb50..c9db9c0d 100644 --- a/modules/azure/entra-id-groups/backplane/main.tf +++ b/modules/azure/entra-id-groups/backplane/main.tf @@ -20,11 +20,17 @@ resource "azurerm_federated_identity_credential" "backplane" { subject = each.value } -# Grant Microsoft Graph app roles so the UAMI can manage groups and Administrative Unit members. +# Grant Microsoft Graph app roles so the UAMI can read users, manage groups, and manage Administrative Unit members. data "azuread_service_principal" "msgraph" { client_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph } +resource "azuread_app_role_assignment" "user_read_all" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["User.Read.All"] + principal_object_id = azurerm_user_assigned_identity.backplane.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} + resource "azuread_app_role_assignment" "group_readwrite_all" { app_role_id = data.azuread_service_principal.msgraph.app_role_ids["Group.ReadWrite.All"] principal_object_id = azurerm_user_assigned_identity.backplane.principal_id diff --git a/modules/azure/entra-id-groups/buildingblock/main.tf b/modules/azure/entra-id-groups/buildingblock/main.tf index 2235f449..41b5d2d7 100644 --- a/modules/azure/entra-id-groups/buildingblock/main.tf +++ b/modules/azure/entra-id-groups/buildingblock/main.tf @@ -19,11 +19,16 @@ locals { } } -data "azuread_user" "this" { - for_each = local.unique_user_euids +data "azuread_user" "by_upn" { + for_each = var.user_lookup_attribute == "upn" ? local.unique_user_euids : toset([]) user_principal_name = each.value } +data "azuread_user" "by_email" { + for_each = var.user_lookup_attribute == "email" ? local.unique_user_euids : toset([]) + mail = each.value +} + resource "azuread_group" "project_role" { for_each = toset(local.roles) @@ -43,5 +48,5 @@ resource "azuread_group_member" "project_role" { for_each = local.user_role_assignments group_object_id = azuread_group.project_role[each.value.role].object_id - member_object_id = data.azuread_user.this[each.value.euid].object_id + member_object_id = var.user_lookup_attribute == "upn" ? data.azuread_user.by_upn[each.value.euid].object_id : data.azuread_user.by_email[each.value.euid].object_id } diff --git a/modules/azure/entra-id-groups/buildingblock/variables.tf b/modules/azure/entra-id-groups/buildingblock/variables.tf index 24ecbc21..4784bf54 100644 --- a/modules/azure/entra-id-groups/buildingblock/variables.tf +++ b/modules/azure/entra-id-groups/buildingblock/variables.tf @@ -26,6 +26,16 @@ variable "administrative_unit_id" { description = "Object ID of the Entra Administrative Unit to add the groups to. Leave empty to skip AU membership." } +variable "user_lookup_attribute" { + type = string + default = "upn" + description = "Azure AD attribute used to look up users. 'upn' matches on User Principal Name; 'email' matches on the primary mail address." + validation { + condition = contains(["upn", "email"], var.user_lookup_attribute) + error_message = "Must be 'upn' or 'email'." + } +} + variable "users" { type = list(object({ meshIdentifier = string From 980414ebf3ec45a0f6318187172b5f8b8e14ea79 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 9 Jun 2026 10:52:34 +0200 Subject: [PATCH 5/5] update: update backplane --- modules/azure/entra-id-groups/backplane/README.md | 3 ++- modules/azure/entra-id-groups/backplane/variables.tf | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/modules/azure/entra-id-groups/backplane/README.md b/modules/azure/entra-id-groups/backplane/README.md index fcb501d2..010bb594 100644 --- a/modules/azure/entra-id-groups/backplane/README.md +++ b/modules/azure/entra-id-groups/backplane/README.md @@ -8,6 +8,7 @@ This backplane creates the automation identity used to provision Entra security - **User-Assigned Managed Identity (UAMI)** — the automation principal that runs the building block. No client secrets. - **Workload Identity Federation credentials** — bind the UAMI to the meshStack replicator's OIDC issuer and subject, enabling secret-free authentication. - **Microsoft Graph app roles** on the UAMI: + - `User.Read.All` — look up users by UPN or primary mail address to resolve object IDs for group membership. - `Group.ReadWrite.All` — create and manage Entra security groups. - `AdministrativeUnit.ReadWrite.All` — add groups to Administrative Units (used when `administrative_unit_id` is supplied at building block runtime). @@ -23,6 +24,6 @@ The platform engineer running this backplane needs: ## Operational notes -- The UAMI principal ID maps to a service principal in Entra. The `Group.ReadWrite.All` and `AdministrativeUnit.ReadWrite.All` app role assignments require **admin consent** — ensure a Global Administrator or Privileged Role Administrator approves the assignments in the Entra portal after the first `apply`. +- The UAMI principal ID maps to a service principal in Entra. The `User.Read.All`, `Group.ReadWrite.All`, and `AdministrativeUnit.ReadWrite.All` app role assignments require **admin consent** — ensure a Global Administrator or Privileged Role Administrator approves the assignments in the Entra portal after the first `apply`. - No secrets are created; the UAMI authenticates via OIDC token exchange. - The backplane resource group is named after `var.name` and must be unique within the subscription. diff --git a/modules/azure/entra-id-groups/backplane/variables.tf b/modules/azure/entra-id-groups/backplane/variables.tf index 26f296f4..59ad84b6 100644 --- a/modules/azure/entra-id-groups/backplane/variables.tf +++ b/modules/azure/entra-id-groups/backplane/variables.tf @@ -15,12 +15,6 @@ variable "location" { description = "Azure region for the backplane resource group and UAMI." } -variable "scope" { - type = string - nullable = false - description = "Scope for role assignment (management group or subscription ID)." -} - variable "workload_identity_federation" { type = object({ issuer = string