Skip to content
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

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

Merged
merged 5 commits into from
Feb 11, 2025

Conversation

pipalmic
Copy link
Contributor

No description provided.

@tom-reinders
Copy link
Member

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
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