feat: add CloudEvent validation for webhook handler proxy
Part of #50 (closed)
This MR implements the validation dance for using a EventGrid Topic Subscription using the CloudEvent 1.0 Schema.
Testing
Manual
The easiest way to test this is to run the webhook handler proxy locally by checking out the code and running poe entra
. This will expose the service on your localhost port 8090. I used Tailscale Funnel to temporarily expose this port to the open internet, which provided me with a public accessible URL.
You would then need to go into the Azure Portal, find an EventGrid Topic (I used activate-account-development-azure-ad-topic) and create a subscription. Make you to select the correct event schema and configure a webhook endpoint pointing to your publicly accessible URL.
If the subscription is created, then it did the validation and it all works. You can see in the logs of the application whether it worked.
Through terraform
Alternatively, you can also use Terraform to set things up for you. You can deploy this to development:
locals {
# Future-proofing: if we add more webhook triggers, we can use this to tweak the container template.
webhook = {
image = "europe-west2-docker.pkg.dev/shared-code-meta-2daffe11/public/ucam-faas-python/entra-webhook/50-webhook-handler-proxy-cloud-event-validation:bf725fe0ca3b60d46983171a26f021e7c82487ba"
args = null
env = [
{
name = "ENTRA_APPLICATION_ID"
value = local.azure_application_id
},
{
name = "ENTRA_TENANCY_ID"
value = local.azure_tenant_id
},
{
name = "ENTRA_EXPECTED_PRINCIPAL_OBJECT_ID"
value = data.azuread_service_principal.event_grid.object_id
},
{
name = "PUBSUB_PROJECT_ID"
value = local.project
},
{
name = "PUBSUB_TOPIC_NAME"
value = google_pubsub_topic.test_azure_webhook.name
}
]
}
}
resource "google_service_account" "test_azure_webhook_sa" {
project = local.project
account_id = "test-azure-webhook"
}
resource "google_pubsub_topic" "test_azure_webhook" {
name = "test-azure-webhook"
project = local.project
}
resource "google_pubsub_topic_iam_member" "test_azure_webhook_publisher" {
topic = google_pubsub_topic.test_azure_webhook.name
role = "roles/pubsub.publisher"
member = google_service_account.test_azure_webhook_sa.member
}
resource "google_cloud_run_v2_service" "test_azure_webhook" {
name = "test-azure-webhook"
location = local.region
deletion_protection = false
ingress = "INGRESS_TRAFFIC_ALL"
template {
service_account = google_service_account.test_azure_webhook_sa.email
containers {
image = local.webhook.image
args = local.webhook.args != null ? local.webhook.args : null
dynamic "env" {
for_each = local.webhook.env
content {
name = env.value["name"]
value = env.value["value"]
dynamic "value_source" {
for_each = contains(keys(env.value), "value_source") ? env.value["value_source"] != null ? [env.value["value_source"]] : [] : []
content {
dynamic "secret_key_ref" {
for_each = value_source.value["secret_key_ref"] != null ? [value_source.value["secret_key_ref"]] : []
content {
secret = secret_key_ref.value["secret"]
version = secret_key_ref.value["version"]
}
}
}
}
}
}
resources {
limits = {
cpu = 2
memory = "1024Mi"
}
}
liveness_probe {
http_get {
path = "/healthy"
}
}
startup_probe {
http_get {
path = "/healthy"
}
}
}
scaling {
min_instance_count = 0
max_instance_count = 1
}
}
}
# Allow unauthenticated invocations of the Cloud Run service
resource "google_cloud_run_service_iam_binding" "default" {
location = google_cloud_run_v2_service.test_azure_webhook.location
service = google_cloud_run_v2_service.test_azure_webhook.name
role = "roles/run.invoker"
members = [
"allUsers"
]
}
And then create the subscription like:
resource "azurerm_eventgrid_event_subscription" "entra_event_ce" {
name = "entra-event-subscription-ce"
scope = azurerm_eventgrid_topic.azure_ad_topic.id
event_delivery_schema = "CloudEventSchemaV1_0"
webhook_endpoint {
url = google_cloud_run_v2_service.test_azure_webhook.uri
active_directory_tenant_id = local.azure_tenant_id
active_directory_app_id_or_uri = local.azure_application_id
# Need to explicitly set these to avoid `terraform plan` trying to change them on every run.
# See https://github.com/hashicorp/terraform-provider-azurerm/issues/10922
max_events_per_batch = 1
preferred_batch_size_in_kilobytes = 64
}
}
Once that's created, it means it validated the endpoint (if it's hanging, it's waiting for manual action and the code didn't work).
You can see an example of this here: