Skip to content

feat: Added Standard Logic App with Managed Identity and IP restriction (for HTTP trigger)#445

Merged
tom-reinders merged 5 commits intodevelopfrom
feature/22793-workflow-po-handler
Feb 11, 2025
Merged

feat: Added Standard Logic App with Managed Identity and IP restriction (for HTTP trigger)#445
tom-reinders merged 5 commits intodevelopfrom
feature/22793-workflow-po-handler

Conversation

@pipalmic
Copy link
Copy Markdown
Contributor

No description provided.

@tom-reinders
Copy link
Copy Markdown
Contributor

For later reviewing of changes to the files compared to base files it probably copied from:

diff --git a/modules/azure/logic_app_standard/main.tf b/modules/azure/logic_app_standard_http_managed_identity/main.tf
index 0b8c7e1..e56e9f4 100644
--- a/modules/azure/logic_app_standard/main.tf
+++ b/modules/azure/logic_app_standard_http_managed_identity/main.tf
@@ -10,6 +10,14 @@ terraform {
       source  = "hashicorp/archive"
       version = "~> 2.3"
     }
+    azapi = {
+      source  = "Azure/azapi"
+      version = "~> 1.4"
+    }
+    azuread = {
+      source  = "hashicorp/azuread"
+      version = "~> 2.36"
+    }
   }
 
   backend "azurerm" {}
@@ -23,8 +31,10 @@ provider "archive" {
 }
 
 locals {
-  identity_type = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null
-  is_linux      = length(regexall("/home/", lower(abspath(path.root)))) > 0
+  identity_type     = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null
+  is_linux          = length(regexall("/home/", lower(abspath(path.root)))) > 0
+  identifiers       = concat(["api://${var.managed_identity_provider.create.application_name}"], var.managed_identity_provider.identifier_uris != null ? var.managed_identity_provider.identifier_uris : [])
+  allowed_audiences = concat(local.identifiers, var.managed_identity_provider.allowed_audiences != null ? var.managed_identity_provider.allowed_audiences : [])
 }
 
 resource "azurerm_logic_app_standard" "app" {
@@ -47,11 +57,36 @@ resource "azurerm_logic_app_standard" "app" {
     ftps_state                = "Disabled"
     elastic_instance_minimum  = var.elastic_instance_minimum
     pre_warmed_instance_count = var.pre_warmed_instance_count
+
+    dynamic "ip_restriction" {
+      for_each = var.ip_restrictions
+
+      content {
+        ip_address                = ip_restriction.value.ip_address
+        service_tag               = ip_restriction.value.service_tag
+        virtual_network_subnet_id = ip_restriction.value.virtual_network_subnet_id
+        name                      = ip_restriction.value.name
+        priority                  = ip_restriction.value.priority
+        action                    = ip_restriction.value.action
+
+        dynamic "headers" {
+          for_each = ip_restriction.value.headers
+
+          content {
+            x_azure_fdid      = headers.value.x_azure_fdid
+            x_fd_health_probe = headers.value.x_fd_health_probe
+            x_forwarded_for   = headers.value.x_forwarded_for
+            x_forwarded_host  = headers.value.x_forwarded_host
+          }
+        }
+      }
+    }
   }
 
   app_settings = merge({
-    WEBSITE_NODE_DEFAULT_VERSION = "~18",
-    FUNCTIONS_WORKER_RUNTIME     = "node",
+    WEBSITE_NODE_DEFAULT_VERSION             = "~18",
+    FUNCTIONS_WORKER_RUNTIME                 = "node",
+    MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = "${var.managed_identity_provider != null ? azuread_application_password.password[0].value : ""}"
   }, var.app_settings)
 
   app_service_plan_id        = var.service_plan_id
@@ -158,3 +193,105 @@ resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting" {
     }
   }
 }
+
+# Managed Identity Provider
+data "azuread_client_config" "current" {}
+
+resource "random_uuid" "oath2_uuid" {}
+
+resource "azuread_application" "application" {
+  count            = var.managed_identity_provider != null ? 1 : 0
+  display_name     = var.managed_identity_provider.create.display_name
+  owners           = var.managed_identity_provider.create.owners != null ? concat([data.azuread_client_config.current.object_id], var.managed_identity_provider.create.owners) : [data.azuread_client_config.current.object_id]
+  sign_in_audience = "AzureADMyOrg"
+  identifier_uris  = local.identifiers
+
+  api {
+    requested_access_token_version = 2
+
+    oauth2_permission_scope {
+      admin_consent_description  = var.managed_identity_provider.create.oauth2_settings.admin_consent_description
+      admin_consent_display_name = var.managed_identity_provider.create.oauth2_settings.admin_consent_display_name
+      enabled                    = var.managed_identity_provider.create.oauth2_settings.enabled
+      id                         = random_uuid.oath2_uuid.result
+      type                       = var.managed_identity_provider.create.oauth2_settings.type
+      user_consent_description   = var.managed_identity_provider.create.oauth2_settings.user_consent_description
+      user_consent_display_name  = var.managed_identity_provider.create.oauth2_settings.user_consent_display_name
+      value                      = var.managed_identity_provider.create.oauth2_settings.role_value
+    }
+  }
+
+  web {
+    redirect_uris = ["https://${var.logic_app_name}.azurewebsites.net/.auth/login/aad/callback"]
+
+    implicit_grant {
+      access_token_issuance_enabled = false
+      id_token_issuance_enabled     = true
+    }
+  }
+
+  required_resource_access {
+    resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
+
+    resource_access {
+      id   = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" # User.Read
+      type = "Scope"
+    }
+  }
+}
+
+resource "null_resource" "always_run" {
+  triggers = {
+    timestamp = "${timestamp()}"
+  }
+}
+
+resource "azapi_update_resource" "setup_auth_settings" {
+  count       = var.managed_identity_provider != null ? 1 : 0
+  type        = "Microsoft.Web/sites/config@2020-12-01"
+  resource_id = "${azurerm_logic_app_standard.app.id}/config/web"
+
+  depends_on = [
+    azurerm_logic_app_standard.app,
+    null_resource.always_run
+  ]
+
+  body = jsonencode({
+    properties = {
+      siteAuthSettingsV2 = {
+        globalValidation = {
+          excludedPaths          = []
+          require_authentication = true,
+          // Even though is looks weird, it is needed. Otherwise, the app and also the designer in Azure Portal are not working
+          // https://techcommunity.microsoft.com/blog/integrationsonazureblog/trigger-workflows-in-standard-logic-apps-with-easy-auth/3207378
+          unauthenticatedClientAction = "AllowAnonymous"
+        },
+        IdentityProviders = {
+          azureActiveDirectory = {
+            enabled = true,
+            registration = {
+              clientId                = azuread_application.application[0].application_id
+              clientSecretSettingName = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET"
+            },
+            validation = {
+              allowedAudiences = local.allowed_audiences
+            }
+          }
+        }
+      }
+    }
+  })
+  lifecycle {
+    /* This action should always be replaces since is works under the hood as an api call
+    * So it does not really track issues with the function app properly
+    */
+    replace_triggered_by = [
+      null_resource.always_run
+    ]
+  }
+}
+
+resource "azuread_application_password" "password" {
+  count                 = var.managed_identity_provider != null ? 1 : 0
+  application_object_id = azuread_application.application[0].object_id
+}
diff --git a/modules/azure/logic_app_standard/variables.tf b/modules/azure/logic_app_standard_http_managed_identity/variables.tf
index 7853ec4..c096e40 100644
--- a/modules/azure/logic_app_standard/variables.tf
+++ b/modules/azure/logic_app_standard_http_managed_identity/variables.tf
@@ -104,3 +104,56 @@ variable "log_analytics_diagnostic_categories" {
   description = "Optional list of diagnostic categories to override the default categories."
   default     = []
 }
+
+variable "managed_identity_provider" {
+  type = object({
+    existing = optional(object({
+      client_id     = string
+      client_secret = string
+    }))
+    create = optional(object({
+      application_name = string
+      display_name     = string
+      oauth2_settings = object({
+        admin_consent_description  = string
+        admin_consent_display_name = string
+        enabled                    = bool
+        type                       = string
+        user_consent_description   = string
+        user_consent_display_name  = string
+        role_value                 = string
+      })
+      owners        = optional(list(string)) # Deployment user will be added as owner by default
+      redirect_uris = optional(list(string)) # Only for additional URIs, function uri will be added by default
+      group_id      = optional(string)       # Group ID where service principal of the existing application will belong to
+    }))
+    identifier_uris   = optional(list(string)) #  api://<application_name> will be added by default if application is create
+    allowed_audiences = optional(list(string)) # api://<application-name> will be added by default
+  })
+  validation {
+    condition     = var.managed_identity_provider.existing != null || var.managed_identity_provider.create != null
+    error_message = "Variable managed_identity_provider has to provide either an existing managed identity provider or given information to create one"
+  }
+  description = "The managed identity provider to use for connections on this function app"
+  default     = null
+}
+
+variable "ip_restrictions" {
+  type = list(object({
+    ip_address                = optional(string),
+    service_tag               = optional(string),
+    virtual_network_subnet_id = optional(string),
+    name                      = optional(string),
+    priority                  = optional(number),
+    action                    = optional(string),
+
+    headers = optional(list(object({
+      x_azure_fdid      = optional(list(string)),
+      x_fd_health_probe = optional(list(string)),
+      x_forwarded_for   = optional(list(string)),
+      x_forwarded_host  = optional(list(string))
+    })))
+  }))
+  description = "A List of objects representing IP restrictions."
+  default     = []
+}

@pipalmic
Copy link
Copy Markdown
Contributor Author

For later reviewing of changes to the files compared to base files it probably copied from:

diff --git a/modules/azure/logic_app_standard/main.tf b/modules/azure/logic_app_standard_http_managed_identity/main.tf
index 0b8c7e1..e56e9f4 100644
--- a/modules/azure/logic_app_standard/main.tf
+++ b/modules/azure/logic_app_standard_http_managed_identity/main.tf
@@ -10,6 +10,14 @@ terraform {
       source  = "hashicorp/archive"
       version = "~> 2.3"
     }
+    azapi = {
+      source  = "Azure/azapi"
+      version = "~> 1.4"
+    }
+    azuread = {
+      source  = "hashicorp/azuread"
+      version = "~> 2.36"
+    }
   }
 
   backend "azurerm" {}
@@ -23,8 +31,10 @@ provider "archive" {
 }
 
 locals {
-  identity_type = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null
-  is_linux      = length(regexall("/home/", lower(abspath(path.root)))) > 0
+  identity_type     = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null
+  is_linux          = length(regexall("/home/", lower(abspath(path.root)))) > 0
+  identifiers       = concat(["api://${var.managed_identity_provider.create.application_name}"], var.managed_identity_provider.identifier_uris != null ? var.managed_identity_provider.identifier_uris : [])
+  allowed_audiences = concat(local.identifiers, var.managed_identity_provider.allowed_audiences != null ? var.managed_identity_provider.allowed_audiences : [])
 }
 
 resource "azurerm_logic_app_standard" "app" {
@@ -47,11 +57,36 @@ resource "azurerm_logic_app_standard" "app" {
     ftps_state                = "Disabled"
     elastic_instance_minimum  = var.elastic_instance_minimum
     pre_warmed_instance_count = var.pre_warmed_instance_count
+
+    dynamic "ip_restriction" {
+      for_each = var.ip_restrictions
+
+      content {
+        ip_address                = ip_restriction.value.ip_address
+        service_tag               = ip_restriction.value.service_tag
+        virtual_network_subnet_id = ip_restriction.value.virtual_network_subnet_id
+        name                      = ip_restriction.value.name
+        priority                  = ip_restriction.value.priority
+        action                    = ip_restriction.value.action
+
+        dynamic "headers" {
+          for_each = ip_restriction.value.headers
+
+          content {
+            x_azure_fdid      = headers.value.x_azure_fdid
+            x_fd_health_probe = headers.value.x_fd_health_probe
+            x_forwarded_for   = headers.value.x_forwarded_for
+            x_forwarded_host  = headers.value.x_forwarded_host
+          }
+        }
+      }
+    }
   }
 
   app_settings = merge({
-    WEBSITE_NODE_DEFAULT_VERSION = "~18",
-    FUNCTIONS_WORKER_RUNTIME     = "node",
+    WEBSITE_NODE_DEFAULT_VERSION             = "~18",
+    FUNCTIONS_WORKER_RUNTIME                 = "node",
+    MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = "${var.managed_identity_provider != null ? azuread_application_password.password[0].value : ""}"
   }, var.app_settings)
 
   app_service_plan_id        = var.service_plan_id
@@ -158,3 +193,105 @@ resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting" {
     }
   }
 }
+
+# Managed Identity Provider
+data "azuread_client_config" "current" {}
+
+resource "random_uuid" "oath2_uuid" {}
+
+resource "azuread_application" "application" {
+  count            = var.managed_identity_provider != null ? 1 : 0
+  display_name     = var.managed_identity_provider.create.display_name
+  owners           = var.managed_identity_provider.create.owners != null ? concat([data.azuread_client_config.current.object_id], var.managed_identity_provider.create.owners) : [data.azuread_client_config.current.object_id]
+  sign_in_audience = "AzureADMyOrg"
+  identifier_uris  = local.identifiers
+
+  api {
+    requested_access_token_version = 2
+
+    oauth2_permission_scope {
+      admin_consent_description  = var.managed_identity_provider.create.oauth2_settings.admin_consent_description
+      admin_consent_display_name = var.managed_identity_provider.create.oauth2_settings.admin_consent_display_name
+      enabled                    = var.managed_identity_provider.create.oauth2_settings.enabled
+      id                         = random_uuid.oath2_uuid.result
+      type                       = var.managed_identity_provider.create.oauth2_settings.type
+      user_consent_description   = var.managed_identity_provider.create.oauth2_settings.user_consent_description
+      user_consent_display_name  = var.managed_identity_provider.create.oauth2_settings.user_consent_display_name
+      value                      = var.managed_identity_provider.create.oauth2_settings.role_value
+    }
+  }
+
+  web {
+    redirect_uris = ["https://${var.logic_app_name}.azurewebsites.net/.auth/login/aad/callback"]
+
+    implicit_grant {
+      access_token_issuance_enabled = false
+      id_token_issuance_enabled     = true
+    }
+  }
+
+  required_resource_access {
+    resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
+
+    resource_access {
+      id   = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" # User.Read
+      type = "Scope"
+    }
+  }
+}
+
+resource "null_resource" "always_run" {
+  triggers = {
+    timestamp = "${timestamp()}"
+  }
+}
+
+resource "azapi_update_resource" "setup_auth_settings" {
+  count       = var.managed_identity_provider != null ? 1 : 0
+  type        = "Microsoft.Web/sites/config@2020-12-01"
+  resource_id = "${azurerm_logic_app_standard.app.id}/config/web"
+
+  depends_on = [
+    azurerm_logic_app_standard.app,
+    null_resource.always_run
+  ]
+
+  body = jsonencode({
+    properties = {
+      siteAuthSettingsV2 = {
+        globalValidation = {
+          excludedPaths          = []
+          require_authentication = true,
+          // Even though is looks weird, it is needed. Otherwise, the app and also the designer in Azure Portal are not working
+          // https://techcommunity.microsoft.com/blog/integrationsonazureblog/trigger-workflows-in-standard-logic-apps-with-easy-auth/3207378
+          unauthenticatedClientAction = "AllowAnonymous"
+        },
+        IdentityProviders = {
+          azureActiveDirectory = {
+            enabled = true,
+            registration = {
+              clientId                = azuread_application.application[0].application_id
+              clientSecretSettingName = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET"
+            },
+            validation = {
+              allowedAudiences = local.allowed_audiences
+            }
+          }
+        }
+      }
+    }
+  })
+  lifecycle {
+    /* This action should always be replaces since is works under the hood as an api call
+    * So it does not really track issues with the function app properly
+    */
+    replace_triggered_by = [
+      null_resource.always_run
+    ]
+  }
+}
+
+resource "azuread_application_password" "password" {
+  count                 = var.managed_identity_provider != null ? 1 : 0
+  application_object_id = azuread_application.application[0].object_id
+}
diff --git a/modules/azure/logic_app_standard/variables.tf b/modules/azure/logic_app_standard_http_managed_identity/variables.tf
index 7853ec4..c096e40 100644
--- a/modules/azure/logic_app_standard/variables.tf
+++ b/modules/azure/logic_app_standard_http_managed_identity/variables.tf
@@ -104,3 +104,56 @@ variable "log_analytics_diagnostic_categories" {
   description = "Optional list of diagnostic categories to override the default categories."
   default     = []
 }
+
+variable "managed_identity_provider" {
+  type = object({
+    existing = optional(object({
+      client_id     = string
+      client_secret = string
+    }))
+    create = optional(object({
+      application_name = string
+      display_name     = string
+      oauth2_settings = object({
+        admin_consent_description  = string
+        admin_consent_display_name = string
+        enabled                    = bool
+        type                       = string
+        user_consent_description   = string
+        user_consent_display_name  = string
+        role_value                 = string
+      })
+      owners        = optional(list(string)) # Deployment user will be added as owner by default
+      redirect_uris = optional(list(string)) # Only for additional URIs, function uri will be added by default
+      group_id      = optional(string)       # Group ID where service principal of the existing application will belong to
+    }))
+    identifier_uris   = optional(list(string)) #  api:// will be added by default if application is create
+    allowed_audiences = optional(list(string)) # api:// will be added by default
+  })
+  validation {
+    condition     = var.managed_identity_provider.existing != null || var.managed_identity_provider.create != null
+    error_message = "Variable managed_identity_provider has to provide either an existing managed identity provider or given information to create one"
+  }
+  description = "The managed identity provider to use for connections on this function app"
+  default     = null
+}
+
+variable "ip_restrictions" {
+  type = list(object({
+    ip_address                = optional(string),
+    service_tag               = optional(string),
+    virtual_network_subnet_id = optional(string),
+    name                      = optional(string),
+    priority                  = optional(number),
+    action                    = optional(string),
+
+    headers = optional(list(object({
+      x_azure_fdid      = optional(list(string)),
+      x_fd_health_probe = optional(list(string)),
+      x_forwarded_for   = optional(list(string)),
+      x_forwarded_host  = optional(list(string))
+    })))
+  }))
+  description = "A List of objects representing IP restrictions."
+  default     = []
+}

This was created as a merge of the function app module and the standard logic app module. Based on Artiom's suggestion, I am checking now if we can put this change in the already existing standard logic app module. So far it seems it is possible, I am just ensuring there are no breaking changes and I'll push it soon

@tom-reinders tom-reinders added this to the v3.15.0 milestone Feb 11, 2025
@tom-reinders tom-reinders merged commit 9f54751 into develop Feb 11, 2025
2 checks passed
@tom-reinders tom-reinders deleted the feature/22793-workflow-po-handler branch February 11, 2025 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants