Terraforming your way through Azure AD Entitlement Management

The Azure AD Terraform Provider has finally gotten support for Entitlement Management, let’s test it out! But let’s first discuss a few scenarios where this can come in handy.

Scenario 1 – Azure Landing Zones

In an Azure Landing Zones environment, you may have a large set of landing zones where your different developer teams may have access. Some teams may be internal, some external and some mixed. A nice way to establish these landing zones is by using the concept of subscription vending machine, where you use Terraform to establish everything ranging from the subscription, virtual network, virtual hub connection, policies, enterprise scale archetype association, service principals for deployment and so on. Now you can also create access packages, where you automatically provision access packages for each of your landing zones, such as “Azure Landing Zone – Service X – Operators” or similar.

Scenario 2 – An access package for each “something”

Imagine you want to create a standardized access package for each “something” you have, such as Teams, GitHub Repos, Azure Key Vault, Azure Subscription, etc. Now you can!

Terraform has powerful features for looping through lists, creating resources (an access package is a resource in Terraform) for each of them, with different properties. This means that you could have Terraform create an access package per GitHub Repo in your environment, or similar.

Scenario 3 – Have you tried the Graph API?

Unless you are a super expert in PowerShell and Microsoft Graph, creating access packages, policies, etc. is a nightmare! With Terraform, this becomes much easier.

Let me show you

We will run Terraform locally this time, but we can easily run this is Azure DevOps Pipelines, GitHub Actions, or similar. All code is available here.

Let’s start by creating a few Azure AD groups:

locals {
  security_groups = ["Group 1", "Group 2", "Group 3"]
}

resource "azuread_group" "security_groups" {
  for_each         = toset(local.security_groups)
  display_name     = each.key
  security_enabled = true
  mail_enabled     = false
  owners           = [data.azuread_client_config.current.object_id]
}

Let’s also create a few applications:

locals {
  applications = ["Application 1", "Application 2", "Application 3"]
}

resource "azuread_application" "applications" {
  for_each     = toset(local.applications)
  display_name = each.key
  owners           = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "applications" {
  for_each       = azuread_application.applications
  application_id = each.value.application_id
}

Now we are ready to create our catalog, and link our groups and applications:

resource "azuread_access_package_catalog" "blogpost" {
  display_name = "Blogpost"
  description  = "Catalog for blogpost"
  published    = true
}

# Assign all security groups we have created to our catalog
resource "azuread_access_package_resource_catalog_association" "blogpost_groups" {
  for_each               = azuread_group.security_groups
  catalog_id             = azuread_access_package_catalog.blogpost.id
  resource_origin_id     = each.value.object_id
  resource_origin_system = "AadGroup"
}

# Assign all security groups we have created to our catalog
resource "azuread_access_package_resource_catalog_association" "blogpost_applications" {
  for_each               = azuread_service_principal.applications
  catalog_id             = azuread_access_package_catalog.blogpost.id
  resource_origin_id     = each.value.object_id
  resource_origin_system = "AadApplication"
}

At this point, we have our catalog created, and three groups and three applications linked as resources:

Let’s create our access packages!

# Create an access package for our three applications
resource "azuread_access_package" "all_applications" {
  catalog_id   = azuread_access_package_catalog.blogpost.id
  display_name = "All applications"
  description  = "Provides access to the three applications we created"
}

# Associate all applications to our above acess package
resource "azuread_access_package_resource_package_association" "all_applications" {
  for_each                        = azuread_access_package_resource_catalog_association.blogpost_applications
  access_package_id               = azuread_access_package.all_applications.id
  catalog_resource_association_id = each.value.id
}

# Create an access package for our three groups
resource "azuread_access_package" "all_groups" {
  catalog_id   = azuread_access_package_catalog.blogpost.id
  display_name = "All groups"
  description  = "Provides access to the three groups we created"
}

# Associate all groups to our above acess package
resource "azuread_access_package_resource_package_association" "all_groups" {
  for_each                        = azuread_access_package_resource_catalog_association.blogpost_groups
  access_package_id               = azuread_access_package.all_groups.id
  catalog_resource_association_id = each.value.id
}

# Create an access package for all groups and applications we have created
resource "azuread_access_package" "everything" {
  catalog_id   = azuread_access_package_catalog.blogpost.id
  display_name = "Everything"
  description  = "Provides access to the three groups and the three applications we created"
}

# Associate all groups and applications to our above acess package
resource "azuread_access_package_resource_package_association" "everything" {
  for_each                        = merge(azuread_access_package_resource_catalog_association.blogpost_groups, azuread_access_package_resource_catalog_association.blogpost_applications)
  access_package_id               = azuread_access_package.everything.id
  catalog_resource_association_id = each.value.id
}

Almost awesome! I actually now see that the developer that contributed the entitlement management feature to the Azure AD provider has made a poor assumption, that the following roles are present:

This is only true for groups, and will not work for applications. I’ll report this back and get it fixed, or fix it myself.

So let’s continue for now, only with access packages that contains groups:

# Create an access package for our three applications
resource "azuread_access_package" "all_applications" {
  catalog_id   = azuread_access_package_catalog.blogpost.id
  display_name = "All applications"
  description  = "Provides access to the three applications we created"
}

# Create an access package for our three groups
resource "azuread_access_package" "all_groups" {
  catalog_id   = azuread_access_package_catalog.blogpost.id
  display_name = "All groups"
  description  = "Provides access to the three groups we created"
}

# Associate all groups to our above acess package
resource "azuread_access_package_resource_package_association" "all_groups" {
  for_each                        = azuread_access_package_resource_catalog_association.blogpost_groups
  access_package_id               = azuread_access_package.all_groups.id
  catalog_resource_association_id = each.value.id
}

# Create an access package for all groups and applications we have created
resource "azuread_access_package" "everything" {
  catalog_id   = azuread_access_package_catalog.blogpost.id
  display_name = "Everything"
  description  = "Provides access to the three groups and the three applications we created"
}

# Associate all groups and applications to our above acess package
resource "azuread_access_package_resource_package_association" "everything" {
  for_each                        = azuread_access_package_resource_catalog_association.blogpost_groups
  access_package_id               = azuread_access_package.everything.id
  catalog_resource_association_id = each.value.id
}

Now, the last thing remaining is to create a policy for one of our access packages:

resource "azuread_access_package_assignment_policy" "all_applications" {
  access_package_id = azuread_access_package.all_applications.id
  display_name      = "Everyone can request"
  description       = "Everyone can request"
  duration_in_days  = 90

  requestor_settings {
    scope_type = "AllExistingDirectoryMemberUsers"
  }

  approval_settings {
    approval_required = true

    approval_stage {
      approval_timeout_in_days = 14

      primary_approver {
        object_id    = azuread_group.security_groups["Group 1"].object_id
        subject_type = "groupMembers"
      }
    }
  }

  assignment_review_settings {
    enabled                        = true
    review_frequency               = "weekly"
    duration_in_days               = 3
    review_type                    = "Self"
    access_review_timeout_behavior = "keepAccess"
  }
}

And now we can find that policy, working like a charm:

This is a great addition to the Azure AD provider for Terraform, and I’m looking forward to seeing a few added features, such as:

  • Fixing the ability to assign apps to access packages
  • Support for auto assignment policies

You can find the code on GitHub

One thought on “Terraforming your way through Azure AD Entitlement Management

  1. Definitely agree that terraform support is a good step forward. Do you know if they worked through the issue of being able to remove resources in access packages through the tmf code?

    Additionally have you checked out another project called “Tenant Management Framework”, our teams have been using it for a while and initial support for auto-assignment policies was just added
    https://github.com/ATVWGS/tenant-management-framework

    Also I noticed Microsoft365DSC now appears to have support for Entitlement manage

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s