Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/+IFC-2272.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for custom HTTP headers on webhooks. Users can create reusable key-value pairs (static or environment-variable-based) and associate them with webhooks.
3 changes: 2 additions & 1 deletion dev/knowledge/backend/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ Similar to pull requests, proposed changes allow reviewing and approving data mo
| Core | Business logic, domain models | `core/` |
| Database | Query execution, connection mgmt | `database/` |
| Workers | Async task processing | `workers/`, `task_manager/` |
| Events | Pub/sub, triggers, webhooks | `events/`, `message_bus/` |
| Events | Pub/sub, triggers | `events/`, `message_bus/` |
| Webhooks | HTTP notification delivery | `webhook/` |

## Entry Points

Expand Down
1 change: 1 addition & 0 deletions dev/knowledge/backend/async-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,5 @@ Available dependencies:
- [ADR-0003: Asynchronous Tasks](../../adr/0003-asynchronous-tasks.md) - Why we use Prefect
- [Creating Workflows Guide](../../guides/backend/creating-async-tasks.md) - How to create a new workflow
- [Events System](events.md) - Event-driven workflow triggers
- [Webhooks](webhooks.md) - Primary consumer of events and async tasks
- [Backend Architecture](architecture.md) - Overall backend structure
1 change: 1 addition & 0 deletions dev/knowledge/backend/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,5 @@ Events can be queried through:

- [ADR-0002: Prefect Events System](../../adr/0002-events-system.md) - Why we use Prefect Events
- [Creating Events Guide](../../guides/backend/creating-events.md) - How to create a new event
- [Webhooks](webhooks.md) - HTTP notification delivery triggered by events
- [Backend Architecture](architecture.md) - Overall backend structure
24 changes: 21 additions & 3 deletions dev/knowledge/backend/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ configure_webhook flow
└─ WebhookAction.RECONCILE_ALL ──► _reconcile_all()
└──► Full sync via setup_triggers_specific()

KeyValue update (header value changed)
Built-in Trigger (TRIGGER_KEYVALUE_WEBHOOK_INVALIDATE)
invalidate_webhook_headers flow
└──► Queries webhooks referencing the changed KeyValue
└──► Invalidates their cache entries

┌─────────────────────┐
Application Event (e.g. node.created) ──► │ Prefect Automation │
│ (event matching) │
Expand Down Expand Up @@ -75,17 +85,23 @@ Bridges Infrahub webhooks to Prefect automations. Extends `TriggerDefinition` wi

Normalized representation of the event extracted from Prefect's raw event payload. Contains `id`, `branch`, `account_id`, `occured_at`, and `event` type. Created via `from_event()` which parses the nested context structure from Prefect.

### `WebhookHeader`

Pydantic model for a custom HTTP header: `key` (str), `value` (str), `kind` (Literal `"static"` | `"environment"`). The `resolve()` method returns the header value — for `"static"` it returns the value directly, for `"environment"` it looks up the environment variable and raises `WebhookHeaderResolutionError` if the variable is missing (the caller catches this and skips the header with a warning log).

### `Webhook` class hierarchy

`Webhook` (base) → `StandardWebhook`, `CustomWebhook`, `TransformWebhook`

The base class handles:

- Payload preparation (`_prepare_payload`)
- Header assignment with optional HMAC signing (`_assign_headers`)
- Header assignment with custom headers and optional HMAC signing (`_assign_headers`)
- HTTP delivery via `send()`
- Cache serialization (`to_cache` / `from_cache`)

The `custom_headers: list[WebhookHeader]` field on the base `Webhook` class holds headers loaded from the `CoreWebhook.headers` relationship. During `_assign_headers()`, custom headers are applied after system defaults (Accept, Content-Type) but before HMAC signature headers. Static headers use the value directly; environment headers resolve from `os.environ` at send time (missing vars are skipped with a warning log).

## Schema (GraphQL)

Defined in `backend/infrahub/core/schema/definitions/core/webhook.py`:
Expand Down Expand Up @@ -203,7 +219,7 @@ Two built-in triggers in `triggers.py` react to webhook-related node lifecycle e

- **`TRIGGER_WEBHOOK_CONFIGURE`**: Fires on `infrahub.node.created`, `infrahub.node.updated`, and `infrahub.node.deleted` for `CoreCustomWebhook` and `CoreStandardWebhook` nodes. Invokes `WEBHOOK_CONFIGURE` with the event type and node data. The `configure_webhook` flow uses `WebhookConfigureParams` and the `EVENT_TO_ACTION` mapping to route to the correct handler.

- **`TRIGGER_KEYVALUE_WEBHOOK_INVALIDATE`**: Fires on `infrahub.node.created`, `infrahub.node.updated`, and `infrahub.node.deleted` for `CoreStaticKeyValue` and `CoreEnvironmentVariableKeyValue` nodes. Invokes `WEBHOOK_INVALIDATE_HEADERS` which resolves which webhooks reference the changed KeyValue (via `KeyValueGetWebhooksQuery` traversing the `webhook__headers` relationship) and clears their cache entries.
- **`TRIGGER_KEYVALUE_WEBHOOK_INVALIDATE`**: Fires on `infrahub.node.updated` for `CoreStaticKeyValue` and `CoreEnvironmentVariableKeyValue` nodes. Invokes `WEBHOOK_INVALIDATE_HEADERS` with the event type and node data. The `invalidate_webhook_headers` flow resolves which webhooks reference the changed KeyValue (via `NodeManager.query` with `headers__ids` filter) and clears their cache entries.

## Key Locations

Expand All @@ -220,7 +236,9 @@ Two built-in triggers in `triggers.py` react to webhook-related node lifecycle e
| Schema definitions | `backend/infrahub/core/schema/definitions/core/webhook.py` |
| GraphQL mutations | `backend/infrahub/graphql/mutations/webhook.py` |
| Workflow catalogue | `backend/infrahub/workflows/catalogue.py` |
| Unit tests | `backend/tests/unit/webhook/test_models.py` |
| KeyValue schema | `backend/infrahub/core/schema/definitions/core/key_value.py` |
| Unit tests (models) | `backend/tests/unit/webhook/test_models.py` |
| Unit tests (triggers) | `backend/tests/unit/webhook/test_triggers.py` |
| Functional tests (configure) | `backend/tests/functional/webhook/test_configure.py` |
| Functional tests (process) | `backend/tests/functional/webhook/test_process.py` |
| Mutation tests | `backend/tests/component/graphql/mutations/test_webhook.py` |
Expand Down
24 changes: 10 additions & 14 deletions dev/specs/infp-445-webhook-headers/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@

### User Story 1 - Attach Authentication Headers to a Webhook (Priority: P1)

An infrastructure administrator configures a webhook in Infrahub to notify an external system (e.g., Ansible Automation Platform) about infrastructure changes. The external system requires an authentication header such as `Authorization: Bearer <token>` to accept incoming requests. The administrator creates a key-value pair containing the sensitive header credentials, associates it with the webhook, and when events fire, the custom header is automatically included in the HTTP request.
An infrastructure administrator configures a webhook in Infrahub to notify an external system (e.g., Ansible Automation Platform) about infrastructure changes. The external system requires an authentication header such as `Authorization: Bearer <token>` to accept incoming requests. The administrator creates a key-value pair containing the header credentials, associates it with the webhook, and when events fire, the custom header is automatically included in the HTTP request.

**Why this priority**: This is the core use case driving the feature. Without it, customers cannot integrate Infrahub webhooks with any system requiring header-based authentication, which is the majority of modern APIs and automation platforms.

**Independent Test**: Can be fully tested by creating a key-value pair with a sensitive header value, linking it to a webhook, triggering an event, and verifying the target system receives the request with the correct authentication header.
**Independent Test**: Can be fully tested by creating a key-value pair with a header value, linking it to a webhook, triggering an event, and verifying the target system receives the request with the correct authentication header.

**Acceptance Scenarios**:

1. **Given** a webhook configured to notify an external system and a key-value pair containing a sensitive authentication header, **When** the administrator links the key-value pair to the webhook and an event fires, **Then** the HTTP request to the external system includes the custom authentication header with the correct value.
2. **Given** a key-value pair with a sensitive header value (e.g., API key or bearer token), **When** the administrator views the key-value pair via the UI, **Then** the sensitive value is masked and not displayed in cleartext.
3. **Given** a webhook with a linked authentication header, **When** the administrator queries the webhook configuration, **Then** the header relationship is visible.
1. **Given** a webhook configured to notify an external system and a key-value pair containing an authentication header, **When** the administrator links the key-value pair to the webhook and an event fires, **Then** the HTTP request to the external system includes the custom authentication header with the correct value.
2. **Given** a webhook with a linked authentication header, **When** the administrator queries the webhook configuration, **Then** the header relationship is visible.

---

Expand Down Expand Up @@ -56,18 +55,18 @@ An administrator has multiple webhooks that all target systems within the same o

---

### User Story 4 - Add Non-Sensitive Custom Headers to a Webhook (Priority: P3)
### User Story 4 - Add Plain-Text Custom Headers to a Webhook (Priority: P3)

An administrator needs to include non-sensitive identification or routing headers (e.g., `X-Source-System: infrahub`, `X-Tenant-Id: acme-corp`) in webhook requests. They create a plain-text key-value pair where the value is stored and displayed without masking, and associate it with their webhook.
An administrator needs to include identification or routing headers (e.g., `X-Source-System: infrahub`, `X-Tenant-Id: acme-corp`) in webhook requests. They create a plain-text key-value pair and associate it with their webhook.

**Why this priority**: Supports simpler use cases where headers carry non-sensitive metadata, providing flexibility without requiring secret management overhead.
**Why this priority**: Supports use cases where headers carry metadata like system identifiers or routing information.

**Independent Test**: Can be fully tested by creating a plain-text key-value pair, linking it to a webhook, triggering an event, and verifying the header appears in the request.

**Acceptance Scenarios**:

1. **Given** a plain-text key-value pair linked to a webhook, **When** a webhook event fires, **Then** the HTTP request includes the custom header with the literal value.
2. **Given** a plain-text key-value pair, **When** the administrator views it via the API or UI, **Then** the value is displayed in cleartext (not masked).
2. **Given** a plain-text key-value pair, **When** the administrator views it via the API or UI, **Then** the value is displayed as-is.

---

Expand All @@ -85,12 +84,11 @@ An administrator needs to include non-sensitive identification or routing header
### Functional Requirements

- **FR-001**: System MUST allow users to create key-value pairs representing custom HTTP headers with a human-friendly name (globally unique across all key-value pair types, consistent with standard generic behavior in Infrahub), a header name (the actual HTTP header field name), and a header value.
- **FR-002**: System MUST support three types of key-value pairs: plain-text (value stored and displayed as-is), password/sensitive (value uses a Password attribute kind and is masked in the UI), and environment-variable-based (value resolved from worker environment at send time).
- **FR-002**: System MUST support two types of key-value pairs: plain-text (value stored and displayed as-is) and environment-variable-based (value resolved from worker environment at send time).
- **FR-003**: System MUST allow associating zero or more key-value pairs with any webhook (both Standard and Custom Webhooks).
- **FR-004**: System MUST support many-to-many relationships between key-value pairs and webhooks, allowing one key-value pair to be referenced by multiple webhooks and one webhook to reference multiple key-value pairs.
- **FR-005**: System MUST include all associated custom headers in webhook HTTP requests when events fire.
- **FR-006**: System MUST merge custom headers with default system headers (Content-Type, Accept, HMAC signature headers) when sending webhook requests. In case of name conflicts, the user's custom header value MUST take precedence over the system default.
- **FR-007**: System MUST mask sensitive header values (password type) in the UI, consistent with how existing Password kind attributes are displayed.
- **FR-008**: System MUST resolve environment-variable-based header values from the worker process environment at the time the webhook request is sent, not at configuration time.
- **FR-009**: When an environment-variable-based header references a variable that does not exist in the worker environment, the system MUST skip that header, include all remaining resolvable headers in the request, and log a warning identifying the missing variable name.
- **FR-010**: System MUST invalidate cached webhook data when associated key-value pairs are created, updated, deleted, or when the relationship between a key-value pair and a webhook changes.
Expand All @@ -101,8 +99,7 @@ An administrator needs to include non-sensitive identification or routing header
### Key Entities

- **Key-Value Pair (Generic)**: A reusable generic configuration object representing a key-value pair. Has a globally unique human-friendly name for identification (enforced across all key-value pair types, per standard Infrahub generic behavior), a key name (e.g., an HTTP header field name like "Authorization" or "X-Auth-Token"), and a value. Serves as the base generic entity with three specialized node types that inherit from it, differing only in how the value is stored and resolved.
- **Static Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The value is stored as plain text and displayed without masking. Used for non-sensitive data like system identifiers or routing metadata.
- **Password Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The value uses a Password attribute kind and is masked in the UI. Used for sensitive data like API keys and bearer tokens.
- **Static Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The value is stored as plain text and displayed as-is. Used for data like system identifiers, routing metadata, or static authentication tokens.
- **Environment Variable Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The stored value is the name of an environment variable. The actual value is resolved from the worker process environment at the time of use. Used when secrets are managed externally (Kubernetes secrets, vault solutions).
- **Webhook**: An existing generic entity (CoreWebhook) from which both Standard and Custom Webhook types inherit. A new optional `headers` relationship (cardinality=many) to the Key-Value Pair generic MUST be defined on this webhook generic, so that all webhook types automatically inherit the ability to reference zero or more key-value pairs.

Expand All @@ -129,7 +126,6 @@ An administrator needs to include non-sensitive identification or routing header

- **SC-001**: Users can successfully send webhook requests to external systems requiring custom authentication headers (e.g., Ansible Automation Platform) without any workarounds or intermediary proxies.
- **SC-002**: Users can configure a webhook with custom headers and trigger a successful authenticated request to an external endpoint. This should be verified after the configuration has been updated and applied within Prefect.
- **SC-003**: Sensitive header values (password type) are masked in the UI and never appear in application logs.
- **SC-004**: A single key-value pair update propagates to all linked webhooks on the next event trigger, with zero manual intervention required per webhook.
- **SC-005**: Environment-variable-based headers resolve correctly at send time, and missing variables produce actionable error messages that identify the specific variable name.
- **SC-006**: Custom header functionality works identically for both Standard and Custom Webhook types with no behavioral differences.
68 changes: 68 additions & 0 deletions docs/docs/guides/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,71 @@ mutation CreateCustomWebhook {
2. Check that your external endpoint received the transformed payload.

For troubleshooting, check the Infrahub logs for webhook delivery status and any Transformation errors.

# Adding custom headers

Webhooks can include custom HTTP headers for authentication, routing metadata, or any other headers the receiving system requires. Headers are defined as reusable key-value pair entities — either static values or references to environment variables — and can be shared across multiple webhooks. For details on header types, merge behavior, and environment variable resolution, see the [webhooks topic](../topics/webhooks.mdx#custom-headers).

<Tabs groupId="method" queryString>
<TabItem value="web" label="Via the Web Interface" default>

1. Navigate to **Integrations** > **Key/Value**.
2. Click **+ Add Key Value**.
3. Fill in the fields:
- **Name**: A human-friendly identifier, such as "API Auth Token"
- **Key**: The HTTP header field name, such as `Authorization`
- **Value**: The header value, such as `Bearer eyJhbGci...`
4. Click **Save**.
5. Navigate to **Integrations** > **Webhooks** and select your webhook.
6. In the **Headers** relationship, associate the key-value pair you created.
7. Click **Save**.

</TabItem>

<TabItem value="graphql" label="Via the GraphQL Interface">

First, create a key-value pair for the header:

```graphql
mutation CreateHeader {
CoreStaticKeyValueCreate(
data: {
name: {value: "API Auth Token"}
key: {value: "Authorization"}
value: {value: "Bearer eyJhbGci..."}
}
) {
object {
id
display_label
}
}
}
```

Then, update your webhook to associate the header:

```graphql
mutation AddHeaderToWebhook {
CoreStandardWebhookUpdate(
data: {
id: "<webhook-id>"
headers: [{id: "<header-id>"}]
}
) {
object {
id
display_label
}
}
}
```

</TabItem>
</Tabs>

:::info

Authentication header values must include the full value including any type prefix. For example, for Bearer token authentication, set the value to `Bearer eyJhbGci...` (not just the token). For environment-variable-based headers that resolve secrets at send time, use `CoreEnvKeyValue` instead — see the [webhooks topic](../topics/webhooks.mdx#environment-variable-resolution) for details.

:::
Loading
Loading