-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/entra id groups #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f0f0aa0
8e9a8f5
2e3e019
bf8bc10
980414e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # 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: | ||
| - `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). | ||
|
|
||
| ## 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 `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`. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. d: I remember having this admin consent thing automated in the past, as this can become really annoying as admins need to approve stuff. maybe worthwhile to discuss this f2f or at least mention it. |
||
| - 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. | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||
| resource "azurerm_resource_group" "backplane" { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f:
Suggested change
würde das nich doppelt benennen, durch das module hat die resource address schon das wort |
||||||
| name = var.name | ||||||
| location = var.location | ||||||
| } | ||||||
|
|
||||||
| resource "azurerm_user_assigned_identity" "backplane" { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| name = var.name | ||||||
| location = var.location | ||||||
| resource_group_name = azurerm_resource_group.backplane.name | ||||||
| } | ||||||
|
|
||||||
| resource "azurerm_federated_identity_credential" "backplane" { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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 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 | ||||||
| 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 | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| provider "azurerm" { | ||
| features {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| 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 "workload_identity_federation" { | ||
| type = object({ | ||
| issuer = string | ||
| subjects = list(string) | ||
| }) | ||
| nullable = false | ||
| description = "WIF issuer and subjects for federated authentication from the meshStack replicator." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| terraform { | ||
| required_version = ">= 1.0.0" | ||
|
|
||
| required_providers { | ||
| azurerm = { | ||
| source = "hashicorp/azurerm" | ||
| version = "~> 4.0" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f: use |
||
| } | ||
| azuread = { | ||
| source = "hashicorp/azuread" | ||
| version = "~> 3.8" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f: use |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ❌ | ✅ | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| 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]) | ||
|
|
||
| unique_user_euids = toset([for user in var.users : user.euid]) | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
|
|
||
| display_name = join("-", concat(local.name_parts, [each.value])) | ||
| security_enabled = true | ||
| mail_enabled = false | ||
| } | ||
|
|
||
| resource "azuread_administrative_unit_member" "project_role" { | ||
| for_each = local.au_id != null ? toset(local.roles) : toset([]) | ||
|
|
||
| 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 = 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| output "group_object_ids" { | ||
| description = "Map of project role name to Entra group object ID." | ||
| value = { for role, g in azuread_group.project_role : role => 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 } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| provider "azuread" {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| variable "prefix" { | ||
| type = string | ||
| default = "" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f: I'm inclined to never have defaults defined for buildingblock module vars, as they should also be shipped by the BBD in see also other vars here in this file and make it consistent. also check if skill / AI instructions (score card review) can be extended accordingly. |
||
| 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." | ||
| } | ||
|
|
||
| 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 | ||
| 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." | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| terraform { | ||
| required_providers { | ||
| azuread = { | ||
| source = "hashicorp/azuread" | ||
| version = "~> 3.8.0" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f: use |
||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f: just swap to have acronum UAMI well defined: