Skip to content

saif-application-permissions

This Terraform module manages permissions and roles for an existing Azure Entra ID (Azure Active Directory) application. It is designed to work with applications named using the pattern {project-id}-{environment-short-name}.

✨ Features

  • 📋 API Permissions Management: Configure Microsoft Graph and other API permissions
  • 🎯 Scopes: Define OAuth2 delegated scopes exposed by your application
  • 🔑 App Roles: Create and manage application roles for RBAC
  • 👥 Role Assignments: Assign app roles to users, groups, or service principals
  • ✅ Admin Consent: Automatically grants admin consent for API permissions
  • 🔓 Pre-Authorization: Pre-authorize client applications to access your application's scopes

⚠️ Important Notes

  • This module is for existing applications only - it does not create new applications
  • Default configurations (user_impersonation scope, optional claims, self-authorization) are not applied by this module
  • For new applications with default configurations, use the application module instead
  • Optional claims management is disabled (configure_optional_claims=false) to prevent conflicts with applications created by the application module

📋 Prerequisites

  • An existing Azure AD application (created separately)
  • Appropriate permissions to manage the application
  • Application name following the pattern: {project-id}-{environment-short-name}

🚀 Usage

Basic Example

module "app_permissions" {
  source = "../saif-application-permissions"

  project_id             = "myproject"
  environment_short_name = "dev"

  # Application permissions (app-only, no user context)
  api_roles = [
    {
      api_name        = "Microsoft Graph"
      permission_name = "User.Read.All"
    },
    {
      api_name        = "Microsoft Graph"
      permission_name = "Group.Read.All"
    }
  ]

  # Delegated permissions (requires user context)
  api_scopes = [
    {
      api_name        = "Microsoft Graph"
      permission_name = "User.Read"
    }
  ]

  grant_admin_consent = true
}

Advanced Example - Full Configuration with Cross-Application References

module "app_permissions" {
  source = "../saif-application-permissions"

  project_id             = "myproject"
  environment            = "production"
  environment_short_name = "prod"

  # Application permissions (app-only, no user context)
  api_roles = [
    {
      api_name        = "Microsoft Graph"
      permission_name = "User.Read.All"
    }
  ]

  grant_admin_consent = true

  # scopes (exposing your API) - UUIDs auto-generated!
  scopes = [
    {
      value                      = "Data.Read"  # UUID auto-generated from value
      admin_consent_description  = "Allow the application to read data"
      admin_consent_display_name = "Read data"
      user_consent_description   = "Allow the application to read your data"
      user_consent_display_name  = "Read your data"
      type                       = "User"
    }
  ]

  # App roles (for RBAC) - UUIDs auto-generated!
  app_roles = [
    {
      value                = "Admin"  # UUID auto-generated from value
      allowed_member_types = ["User", "Application"]
      description          = "Administrators have full access"
      display_name         = "Administrator"
    }
  ]

  # App role assignments - Use role values!
  app_role_assignments = [
    {
      role_value          = "Admin"  # Reference role by value
      principal_object_id = "c3d4e5f6-a7b8-9012-cdef-123456789012"
    }
  ]

  # Pre-authorized applications - Cross-application references made easy!
  pre_authorized_applications = [
    {
      project_id   = "clientapp" # Automatically looks up 'clientapp-prod'
      scope_values = ["Data.Read"]
    },
    {
      project_id   = "anotherapp"
      scope_values = ["Data.Read", "Data.Write"]
    }
  ]
}

Example - Exposing an API with Scopes

module "api_scopes" {
  source = "../saif-application-permissions"

  project_id             = "myapi"
  environment_short_name = "dev"

  # Define scopes that client applications can request (UUIDs auto-generated!)
  scopes = [
    {
      value                      = "Orders.Read"  # UUID auto-generated from value
      admin_consent_description  = "Allow the application to read orders"
      admin_consent_display_name = "Read orders"
      user_consent_description   = "Allow the application to read your orders"
      user_consent_display_name  = "Read your orders"
      type                       = "User"
    },
    {
      value                      = "Orders.Write"  # UUID auto-generated from value
      admin_consent_description  = "Allow the application to write orders"
      admin_consent_display_name = "Write orders"
      type                       = "Admin"  # Requires admin consent
    }
  ]
}

Example - Application Roles for RBAC

module "app_rbac" {
  source = "../saif-application-permissions"

  project_id             = "myapp"
  environment_short_name = "prod"

  # Define roles for your application (UUIDs auto-generated!)
  app_roles = [
    {
      value                = "Reader"  # UUID auto-generated from value
      allowed_member_types = ["User"]
      description          = "Can read data"
      display_name         = "Data Reader"
    },
    {
      value                = "Writer"  # UUID auto-generated from value
      allowed_member_types = ["User"]
      description          = "Can read and write data"
      display_name         = "Data Writer"
    },
    {
      value                = "ServiceAccount"  # UUID auto-generated from value
      allowed_member_types = ["Application"]
      description          = "Service account access"
      display_name         = "Service Account"
    }
  ]

  # Assign roles to users/service principals
  app_role_assignments = [
    {
      role_value    = "Reader"
      business_role = "DataReader"
    },
    {
      role_value    = "Writer"
      business_role = "DataWriter"
    }
  ]
}

📊 Inputs

Name Description Type Default Required
project_id The project identifier (e.g., 'myproject'). Used to construct '{project_id}-{environment_short_name}' string n/a ✅
environment The full environment name (e.g., 'development', 'production'). Used for tagging and documentation. string n/a ✅
environment_short_name The short environment name (e.g., 'dev', 'prod') to construct application name string n/a ✅
application Optional application identity object { id, client_id } (e.g. { id = module.saif-appservices.application_id, client_id = module.saif-appservices.application_client_id }). Skips the display-name lookup and avoids needing a coarse depends_on on the module that created the app. Validated to reject null/empty fields. object({ id = string, client_id = string }) null ❌
service_principal Optional service principal identity object { object_id, client_id } (e.g. { object_id = module.saif-appservices.application_principal_object_id, client_id = module.saif-appservices.application_principal_client_id }). Skips the service-principal lookup. Validated to reject null/empty fields. object({ object_id = string, client_id = string }) null ❌
api_roles List of API application permissions (Role type) - app-only, no user context required list(object) [] ❌
api_scopes List of API delegated permissions (Scope type) - requires user context list(object) [] ❌
grant_admin_consent Whether to grant admin consent for API permissions (applies to api_roles) bool true ❌
scopes List of OAuth2 scopes to expose from this application list(object) [] ❌
app_roles List of app roles to define for this application list(object) [] ❌
app_role_assignments List of app role assignments to users, groups, or service principals list(object) [] ❌
pre_authorized_applications List of applications to pre-authorize. Reference by 'project_id' list(object) [] ❌

📤 Outputs

Name Description
application_object_id The object ID of the Azure AD application
application_id The application ID of the Azure AD application
client_id The client ID of the Azure AD application
service_principal_object_id The object ID of the service principal
api_permissions List of API permissions configured
scopes List of OAuth2 scopes exposed
app_roles List of app roles defined
app_role_assignments Map of app role assignments
pre_authorized_applications List of pre-authorized applications
granted_permissions Map of granted API permissions (when admin consent is granted)
summary Summary of application permissions configuration

💡 Tips

🎯 Default Configurations

This module automatically configures several critical settings for all Entra ID applications:

  1. 🔐 user_impersonation Scope: Every application automatically exposes a user_impersonation scope that allows client applications to access the API on behalf of a signed-in user. This scope:
  2. Is added by default (no configuration needed)
  3. Uses type "User" (users can consent)
  4. Follows Azure naming conventions
  5. Is automatically pre-authorized for the application itself

  6. 👥 Groups Claim Configuration: Optional claims are configured to include groups in tokens with the following properties:

  7. Groups are included in access tokens, ID tokens, and SAML tokens
  8. Groups include sam_account_name for on-premises Active Directory groups
  9. Groups are emitted as roles for easier claim processing
  10. This allows your application to check group membership using the groups claim

🔑 Token Type Claim: The idtyp claim is included in access and ID tokens to indicate whether the token is for an application (app) or a user (user), making it easy to distinguish between service-to-service and user-delegated authentication.

  1. ✅ Self-Authorization: The application automatically grants admin consent to itself for the user_impersonation scope, simplifying the consent flow for common scenarios.

⚠️ Important Notes

  1. ✨ Auto-Generated UUIDs: Scope IDs and role IDs are automatically generated from their values - no manual UUID management!
  2. 🎯 Platform-First Design: Use project_id and environment_short_name to reference applications - no need for UUIDs!
  3. 📝 Intuitive API Permissions:
  4. Use api_roles for application permissions (app-to-app, no user context)
  5. Use api_scopes for delegated permissions (requires user signin)
  6. No need to specify type - the variable name makes it clear!
  7. 🔗 Cross-Application References: Reference other applications by project_id - automatically looks up {project_id}-{environment_short_name}:
pre_authorized_applications = [
  {
    project_id   = "otherapp"  # Automatically looks up 'otherapp-{env}'
    scope_values = ["Data.Read"]
  }
]
  1. 📋 Admin Consent: Defaults to true since application permissions require consent to be functional
  2. 👥 Role Assignments: Assign roles using business role names - automatically maps to bus-role.{name} (prod) or bus-role-np.{name} (non-prod) groups
  3. 🔄 Scope Types:
  4. User: Users can consent themselves
  5. Admin: Requires admin consent

🏗️ Architecture

This module manages permissions for an existing application by:

  1. Looking up the application by display name
  2. Configuring API permissions the application needs (consuming APIs)
  3. Defining scopes the application exposes (for other apps to consume)
  4. Creating app roles for RBAC within your application
  5. Assigning roles to users/groups/service principals
  6. Pre-authorizing trusted client applications

🔗 Resources

  • azuread_application_registration (data source)
  • azuread_service_principal (data source)
  • azuread_application_api_access
  • azuread_app_role_assignment
  • azuread_application_permission_scope
  • azuread_application_app_role
  • azuread_application_pre_authorized

Note: This module is part of the SAIF Platform Terraform modules. Follow the terraform skill in .github/skills/terraform/ for development guidelines.

Features

  • 📋 API Permissions Management: Configure Microsoft Graph and other API permissions
  • 🎯 scopes: Define OAuth2 delegated scopes exposed by your application
  • 🔑 App Roles: Create and manage application roles for RBAC
  • 👥 Role Assignments: Assign app roles to users, groups, or service principals
  • ✅ Admin Consent: Optionally grant admin consent for API permissions
  • 🔓 Pre-Authorization: Pre-authorize client applications to access your application's scopes

Usage

Basic Example

module "app_permissions" {
  source = "../saif-application-permissions"

  application_name = "myproject-dev"

  api_permissions = [
    {
      api_name        = "Microsoft Graph"
      permission_name = "User.Read.All"
      type            = "Role"
    }
  ]

  grant_admin_consent = true
}

Requirements

Requirements

Name Version
terraform >= 1.0.0, < 2.0.0
azuread >= 3.0, < 4.0

Providers

Providers

Name Version
azuread 3.9.0

Inputs

Inputs

Name Description Type Default Required
app_role_assignments List of app role assignments to business role groups. Groups are looked up by business role name and environment.
list(object({
role_value = string # Value of the app role to assign (e.g., "Admin")
business_role = string # Business role name (e.g., "TechSupport") - will be converted to group display name
}))
[] no
app_roles App roles that this application exposes to other applications or users
list(object({
value = string # The role value (e.g., "Admin") - UUID will be auto-generated
allowed_member_types = list(string) # ["User"], ["Application"], or both
description = string # Description of the role
display_name = string # Display name of the role
}))
[] no
application Optional application identity to pass in directly instead of looking up the application by display name. When set, both id (the application object ID) and client_id must be provided together, e.g. { id = module.saif-appservices.application_id, client_id = module.saif-appservices.application_client_id }. Passing a single object (instead of separate id/client_id/boolean inputs) lets count safely branch on var.application == null even when the id/client_id values themselves are unknown at plan time (e.g. because the source module's application resource is being created or replaced) — an object literal with unknown attribute values is still a known, non-null object, so there's no risk of an 'Invalid count argument' error. Leave null to look the application up by '{project_id}-{environment_short_name}'.
object({
id = string
client_id = string
})
null no
environment The full environment name (e.g., 'development', 'production'). Used for tagging and documentation purposes. string n/a yes
environment_short_name The short environment name used to construct the application name as '{project_id}-{environment_short_name}' (e.g., 'dev', 'prod'). string n/a yes
pre_authorized_applications List of applications to pre-authorize for accessing this application's scopes and/or app roles. Reference applications by their project_id.
list(object({
project_id = string # Project ID to look up application as '{project_id}-{environment_short_name}'
scope_values = list(string) # List of scope values (not UUIDs) to authorize (e.g., ["Data.Read", "Data.Write"])
app_role_values = optional(list(string), []) # List of app role values (application permissions) for service-to-service calls (e.g., ["App.Read"])
}))
[] no
project_id The project identifier for the application (e.g., 'myproject'). Used to construct the application name as '{project_id}-{environment_short_name}'. string n/a yes
requested_application_permissions Application permissions (Role type) to request from other APIs like Microsoft Graph. These are app-only permissions that don't require user context.
list(object({
api_name = string # e.g., "Microsoft Graph"
permission_name = string # e.g., "User.Read.All"
}))
[] no
requested_delegated_permissions Delegated permissions (Scope type) to request from other APIs like Microsoft Graph. These permissions require user context.
list(object({
api_name = string # e.g., "Microsoft Graph"
permission_name = string # e.g., "User.Read"
}))
[] no
scopes OAuth2 scopes that this application exposes to other applications
list(object({
value = string # The scope value (e.g., "Data.Read") - UUID will be auto-generated
admin_consent_description = string # Description shown to admins
admin_consent_display_name = string # Display name shown to admins
user_consent_description = optional(string, "") # Description shown to users
user_consent_display_name = optional(string, "") # Display name shown to users
type = optional(string, "Admin") # "Admin" or "User" - defaults to "Admin" for automatic consent
}))
[] no
service_principal Optional service principal identity to pass in directly instead of looking it up via the application's client ID. When set, both object_id and client_id must be provided together, e.g. { object_id = module.saif-appservices.application_principal_object_id, client_id = module.saif-appservices.application_principal_client_id }. See application for why a single object is used instead of separate id/client_id/boolean inputs. Leave null to look the service principal up automatically.
object({
object_id = string
client_id = string
})
null no

Outputs

Outputs

Name Description
api_permissions List of API permissions configured for the application
app_role_assignments Map of app role assignments
app_roles List of app roles defined for the application
application_id The application ID (client ID) of the Azure AD application
application_object_id The object ID of the Azure AD application
client_id The client ID of the Azure AD application
granted_permissions Map of granted API permissions with their details (admin consent granted)
pre_authorized_applications List of pre-authorized applications
scopes List of OAuth2 scopes the application
service_principal_object_id The object ID of the service principal
summary Summary of application permissions configuration

Resources

Resources

Name Type
azuread_application.application data source
azuread_group.role_assignments data source
azuread_service_principal.application_sp data source

View source on GitHub