1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-13 06:20:48 -05:00

Updates to README - PR fixes, additional context, tense alignment

This commit is contained in:
Brant DeBow 2025-06-05 18:03:09 -04:00
parent 06fde72131
commit 1ad0d6547a
No known key found for this signature in database
GPG Key ID: 94411BB25947C72B

View File

@ -7,21 +7,19 @@ existing event system without needing to add special handling. By adding a new l
pipeline, it gains an independent stream of events without the need for additional broadcast code. pipeline, it gains an independent stream of events without the need for additional broadcast code.
We want to enable robust handling of failures and retries. By utilizing the two-tier approach We want to enable robust handling of failures and retries. By utilizing the two-tier approach
(described below), we have built-in support at the service level for retries. When we add new integrations, (described below), we build in support at the service level for retries. When we add new integrations,
they can focus solely on the process of sending the specific integration and reporting status, with all the they can focus solely on the integration-specific logic and reporting status, with all the
process of retries and delays offloaded to the messaging system. process of retries and delays managed by the messaging system.
Another goal was to not only support this functionality for the cloud version, but offer it as well to Another goal is to not only support this functionality in the cloud version, but offer it as well to
self-hosted instances. By using RabbitMQ for the self-hosted piece, we have a lightweight way to tie self-hosted instances. RabbitMQ provides a lightweight way for self-hosted instances to tie into the event system
into the event system outside of the Cloud offering (which is using Azure Service Bus) using the same robust using the same robust architecture for integrations without the need for Azure Service Bus.
architecture for integrations locally.
Finally, we wanted to offer organization admins flexibility and control over what events are significant, where Finally, we want to offer organization admins flexibility and control over what events are significant, where
to send events, and the data to be included in the message. The configuration architecture allows Organizations to send events, and the data to be included in the message. The configuration architecture allows Organizations
to customize details of a specific integration; see Integrations and integration configurations below for more to customize details of a specific integration; see Integrations and integration configurations below for more
details on the configuration piece. details on the configuration piece.
# Architecture # Architecture
The entry point for the event integrations is the `IEventWriteService`. By configuring the The entry point for the event integrations is the `IEventWriteService`. By configuring the
@ -37,6 +35,31 @@ When `EventIntegrationEventWriteService` publishes, it posts to the first tier o
approach to handling messages. Each tier is represented in the AMQP stack by a separate exchange approach to handling messages. Each tier is represented in the AMQP stack by a separate exchange
(in RabbitMQ terminology) or topic (in Azure Service Bus). (in RabbitMQ terminology) or topic (in Azure Service Bus).
``` mermaid
flowchart TD
B1[EventService]
B2[EventIntegrationEventWriteService]
B3[Event Exchange / Topic]
B4[EventRepositoryHandler]
B5[WebhookIntegrationHandler]
B6[Events in Database / Azure Tables]
B7[HTTP Server]
B8[SlackIntegrationHandler]
B9[Slack]
B10[EventIntegrationHandler]
B12[Integration Exchange / Topic]
B1 -->|IEventWriteService| B2 --> B3
B3-->|EventListenerService| B4 --> B6
B3-->|EventListenerService| B10
B3-->|EventListenerService| B10
B10 --> B12
B12 -->|IntegrationListenerService| B5
B12 -->|IntegrationListenerService| B8
B5 -->|HTTP POST| B7
B8 -->|HTTP POST| B9
```
### Event tier ### Event tier
In the first tier, events are broadcast in a fan-out to a series of listeners. The message body In the first tier, events are broadcast in a fan-out to a series of listeners. The message body
@ -46,20 +69,20 @@ at this level:
- `EventRepositoryHandler` - `EventRepositoryHandler`
- The `EventRepositoryHandler` is responsible for long term storage of events. It receives all events - The `EventRepositoryHandler` is responsible for long term storage of events. It receives all events
and stores them via an injected `IEventRepository` into the database. and stores them via an injected `IEventRepository` into the database.
- This mirrors the behavior of when event integrations are turned off - Cloud stores to Azure Tables - This mirrors the behavior of when event integrations are turned off - cloud stores to Azure Tables
and self-hosted is stored to the database. and self-hosted is stored to the database.
- `EventIntegrationHandler` - `EventIntegrationHandler`
- The `EventIntegrationHandler` is a generic class that is customized to each integration (via the - The `EventIntegrationHandler` is a generic class that is customized to each integration (via the
configuration details of the integration) and is responsible for determining if there's a configuration configuration details of the integration) and is responsible for determining if there's a configuration
for this event/organization/integration, fetching that configuration, and parsing the details of the for this event / organization / integration, fetching that configuration, and parsing the details of the
event into a template string. event into a template string.
- The `EventIntegrationHandler` uses the injected `IOrganizationIntegrationConfigurationRepository` to pull - The `EventIntegrationHandler` uses the injected `IOrganizationIntegrationConfigurationRepository` to pull
the specific set of configuration and template based on the event type, organization, and integration type. the specific set of configuration and template based on the event type, organization, and integration type.
This configuration is what determines if an integration should be sent, what details are necessary for sending This configuration is what determines if an integration should be sent, what details are necessary for sending
it, and the actual message to send. it, and the actual message to send.
- The output of `EventIntegrationHandler` is a new `IntegrationMessage` with the details of this - The output of `EventIntegrationHandler` is a new `IntegrationMessage`, with the details of this
the configuration necessary to interact with the integration and the message to send (with all the event the configuration necessary to interact with the integration and the message to send (with all the event
details incorporated) published to the integration level of the message bus. details incorporated), published to the integration level of the message bus.
### Integration tier ### Integration tier
@ -68,9 +91,9 @@ will be concrete types of the generic `IntegrationMessage<T>` where `<T>` is the
specific integration for which they've been sent. These messages represent the details required for specific integration for which they've been sent. These messages represent the details required for
sending a specific event to a specific integration, including handling retries and delays. sending a specific event to a specific integration, including handling retries and delays.
Handlers are the integration level are tied directly to the integration (e.g. `SlackIntegrationHandler`, Handlers at the integration level are tied directly to the integration (e.g. `SlackIntegrationHandler`,
`WebhookIntegrationHandler`). These handlers take in `IntegrationMessage<T>` and output `WebhookIntegrationHandler`). These handlers take in `IntegrationMessage<T>` and output
`IntegrationHandlerResult`, which tells the listener what the outcome of the integration was (e.g. success/fail, `IntegrationHandlerResult`, which tells the listener the outcome of the integration (e.g. success / fail,
if it can be retried and any minimum delay that should occur). This makes them easy to unit test in isolation if it can be retried and any minimum delay that should occur). This makes them easy to unit test in isolation
without any of the concerns of AMQP or messaging. without any of the concerns of AMQP or messaging.
@ -80,12 +103,29 @@ Failures will either be sent to the dead letter queue (DLQ) or re-published for
### Retries ### Retries
One of the goals of introducing the integration level was to simplify and enable the process of multiple retries One of the goals of introducing the integration level is to simplify and enable the process of multiple retries
for a specific event integration. For instance, if a service is temporarily down, we don't want one of our handlers for a specific event integration. For instance, if a service is temporarily down, we don't want one of our handlers
blocking the rest of the queue while it waits to retry. In addition, we don't want to retry _all_ integrations for a blocking the rest of the queue while it waits to retry. In addition, we don't want to retry _all_ integrations for a
specific event if only one integration fails nor do we want to re-lookup the configuration details. By splitting specific event if only one integration fails nor do we want to re-lookup the configuration details. By splitting
out the `IntegrationMessage<T>` with the configuration, message, and details around retries, we can process each out the `IntegrationMessage<T>` with the configuration, message, and details around retries, we can process each
event/integration individually and retry easily. event / integration individually and retry easily.
When the `IntegrationHandlerResult.Success` is set to `false` (indicating that the integration attempt failed) the
`Retryable` flag tells the listener whether this failure is temporary or final. If the `Retryable` is `false`, then
the message is immediately sent to the DLQ. If it is `true`, the listener uses the `ApplyRetry(DateTime)` method
in `IntegrationMessage` which handles both incrementing the `RetryCount` and updating the `DelayUntilDate` using
the provided DateTime, but also adding exponential backoff (based on `RetryCount`) and jitter. The listener compares
the `RetryCount` in the `IntegrationMessage` to see if it's over the `MaxRetries` defined in Global Settings. If it
is over the `MaxRetries`, the message is sent to the DLQ. Otherwise, it is scheduled for retry.
``` mermaid
flowchart TD
A[Success == false] --> B{Retryable?}
B -- No --> C[Send to Dead Letter Queue DLQ]
B -- Yes --> D[Check RetryCount vs MaxRetries]
D -->|RetryCount >= MaxRetries| E[Send to Dead Letter Queue DLQ]
D -->|RetryCount < MaxRetries| F[Schedule for Retry]
```
Azure Service Bus supports scheduling messages as part of its core functionality. Retries are scheduled to a specific Azure Service Bus supports scheduling messages as part of its core functionality. Retries are scheduled to a specific
time and then ASB holds the message and publishes it at the correct time. time and then ASB holds the message and publishes it at the correct time.
@ -103,13 +143,12 @@ defaults to `false` which indicates we should use retry queues with a timing che
- This allows us to forego using any retry queues and rely instead on the delay exchange. When a message is - This allows us to forego using any retry queues and rely instead on the delay exchange. When a message is
marked with the header it gets published to the exchange and the exchange handles all the functionality of marked with the header it gets published to the exchange and the exchange handles all the functionality of
holding it until the appropriate time (similar to ASB's built-in support). holding it until the appropriate time (similar to ASB's built-in support).
- The plugin must be setup and enabled before turning this option on (which is why we default to off). - The plugin must be setup and enabled before turning this option on (which is why it defaults to off).
2. Retry queues + timing check 2. Retry queues + timing check
- If the delay plugin setting is off, we push the message to a retry queue which has a fixed amount of time before - If the delay plugin setting is off, we push the message to a retry queue which has a fixed amount of time before
it gets re-published back to the main queue. it gets re-published back to the main queue.
- When a message comes off the queue, we check to see if the `DelayUntilDate` has - When a message comes off the queue, we check to see if the `DelayUntilDate` has already passed.
already passed.
- If it has passed, we then handle the integration normally and retry the request. - If it has passed, we then handle the integration normally and retry the request.
- If it is still in the future, we put the message back on the retry queue for an additional wait. - If it is still in the future, we put the message back on the retry queue for an additional wait.
- While this does use extra processing, it gives us better support for honoring the delays even if the delay plugin - While this does use extra processing, it gives us better support for honoring the delays even if the delay plugin
@ -121,20 +160,20 @@ defaults to `false` which indicates we should use retry queues with a timing che
To make it easy to support multiple AMQP services (RabbitMQ and Azure Service Bus), the act To make it easy to support multiple AMQP services (RabbitMQ and Azure Service Bus), the act
of listening to the stream of messages is decoupled from the act of responding to a message. of listening to the stream of messages is decoupled from the act of responding to a message.
**Listeners** ### Listeners
- Listeners handle the details of the communication platform (i.e. RabbitMQ and Azure Service Bus). - Listeners handle the details of the communication platform (i.e. RabbitMQ and Azure Service Bus).
- There is one listener for each platform (RabbitMQ / ASB) for each of the two levels - i.e. one event listener - There is one listener for each platform (RabbitMQ / ASB) for each of the two levels - i.e. one event listener
and one integration listener and one integration listener.
- Perform all the aspects of setup / teardown, subscription, message acknowledgement, etc. for the messaging platform, - Perform all the aspects of setup / teardown, subscription, message acknowledgement, etc. for the messaging platform,
but do not directly process any events themselves. Instead, they delegate to the handler with which they but do not directly process any events themselves. Instead, they delegate to the handler with which they
are configured. are configured.
- Multiple instances can be configured to run independently, each with its own handler and - Multiple instances can be configured to run independently, each with its own handler and
subscription / queue. subscription / queue.
**Handlers** ### Handlers
- One handler per queue/subscription (e.g. per integration at the integration level). - One handler per queue / subscription (e.g. per integration at the integration level).
- Completely isolated from and know nothing of the messaging platform in use. This allows them to be - Completely isolated from and know nothing of the messaging platform in use. This allows them to be
freely reused across different communication platforms. freely reused across different communication platforms.
- Perform all aspects of handling an event. - Perform all aspects of handling an event.
@ -160,14 +199,14 @@ Organizations can configure integration configurations to send events to differe
handler maps to a specific integration and checks for the configuration when it receives an event. handler maps to a specific integration and checks for the configuration when it receives an event.
Currently, there are integrations / handlers for Slack and webhooks (as mentioned above). Currently, there are integrations / handlers for Slack and webhooks (as mentioned above).
**`OrganizationIntegration`** ### `OrganizationIntegration`
- The top level object that enables a specific integration for the organization. - The top level object that enables a specific integration for the organization.
- Includes any properties that apply to the entire integration across all events. - Includes any properties that apply to the entire integration across all events.
- For Slack, it consists of the token: - For Slack, it consists of the token:
```json ``` json
{ "token": "xoxb-token-from-slack" } { "token": "xoxb-token-from-slack" }
``` ```
@ -175,34 +214,34 @@ Currently, there are integrations / handlers for Slack and webhooks (as mentione
have a webhook `OrganizationIntegration` to enable configuration via have a webhook `OrganizationIntegration` to enable configuration via
`OrganizationIntegrationConfiguration`. `OrganizationIntegrationConfiguration`.
**`OrganizationIntegrationConfiguration`** ### `OrganizationIntegrationConfiguration`
- This contains the configurations specific to each `EventType` for the integration. - This contains the configurations specific to each `EventType` for the integration.
- `Configuration` contains the event-specific configuration. - `Configuration` contains the event-specific configuration.
- For Slack, this would contain what channel to send the message to: - For Slack, this would contain what channel to send the message to:
```json ``` json
{ "channelId": "C123456" } { "channelId": "C123456" }
``` ```
- For Webhook, this is the URL the request should be sent to: - For Webhook, this is the URL the request should be sent to:
```json ``` json
{ "url": "https://api.example.com" } { "url": "https://api.example.com" }
``` ```
- `Template` contains a template string that is expected to be filled in with the contents of the - `Template` contains a template string that is expected to be filled in with the contents of the
actual event. actual event.
- The tokens in the string are wrapped in `#` characters. For instance, the UserId would be - The tokens in the string are wrapped in `#` characters. For instance, the UserId would be
`#UserId#` `#UserId#`.
- The `IntegrationTemplateProcessor` does the actual work of replacing these tokens with - The `IntegrationTemplateProcessor` does the actual work of replacing these tokens with
introspected values from the provided `EventMessage`. introspected values from the provided `EventMessage`.
- The template does not enforce any structure -- it could be a freeform text message to send via - The template does not enforce any structure -- it could be a freeform text message to send via
Slack, or a JSON body to send via webhook; it is simply stored and used as a string for the most Slack, or a JSON body to send via webhook; it is simply stored and used as a string for the most
flexibility. flexibility.
**`OrganizationIntegrationConfigurationDetails`** ### `OrganizationIntegrationConfigurationDetails`
- This is the combination of both the `OrganizationIntegration` and `OrganizationIntegrationConfiguration` into - This is the combination of both the `OrganizationIntegration` and `OrganizationIntegrationConfiguration` into
a single object. The combined contents tell the integration's handler all the details needed to send to an a single object. The combined contents tell the integration's handler all the details needed to send to an
@ -216,37 +255,41 @@ These are all the pieces required in the process of building out a new integrati
clarity in naming, these assume a new integration called "Example". clarity in naming, these assume a new integration called "Example".
## IntegrationType ## IntegrationType
Add a new type to `IntegrationType` for the new integration
Add a new type to `IntegrationType` for the new integration.
## Configuration Models ## Configuration Models
The configuration models are the classes that will determine what is stored in the database for The configuration models are the classes that will determine what is stored in the database for
`OrganizationIntegration` and `OrganizationIntegrationConfiguration`. The `Configuration` columns are the `OrganizationIntegration` and `OrganizationIntegrationConfiguration`. The `Configuration` columns are the
serialized version of the corresponding objects and represent the coonfiguration details for this integration serialized version of the corresponding objects and represent the coonfiguration details for this integration
and event type. and event type.
1. `ExampleIntegration` 1. `ExampleIntegration`
- Configuration details for the whole integration (e.g. a token in Slack) - Configuration details for the whole integration (e.g. a token in Slack).
- Applies to every event type configuration defined for this integration - Applies to every event type configuration defined for this integration.
- Maps to the JSON structure stored in `Configuration` in ``OrganizationIntegration` - Maps to the JSON structure stored in `Configuration` in ``OrganizationIntegration`.
2. `ExampleIntegrationConfiguration` 2. `ExampleIntegrationConfiguration`
- Configuration details that could change from event to event (e.g. channelId in Slack) - Configuration details that could change from event to event (e.g. channelId in Slack).
- Maps to the JSON structure stored in `Configuration` in `OrganizationIntegrationConfiguration` - Maps to the JSON structure stored in `Configuration` in `OrganizationIntegrationConfiguration`.
3. `ExampleIntegrationConfigurationDetails` 3. `ExampleIntegrationConfigurationDetails`
- Combined configuration of both Integration _and_ IntegrationConfiguration - Combined configuration of both Integration _and_ IntegrationConfiguration.
- This will be the deserialized version of the `MergedConfiguration` in `OrganizationIntegrationConfigurationDetails` - This will be the deserialized version of the `MergedConfiguration` in
`OrganizationIntegrationConfigurationDetails`.
## Request Models ## Request Models
1. Add a new case to the switch method in `OrganizationIntegrationRequestModel.Validate` 1. Add a new case to the switch method in `OrganizationIntegrationRequestModel.Validate`.
2. Add a new case to the switch method in `OrganizationIntegrationConfigurationRequestModel.IsValidForType` 2. Add a new case to the switch method in `OrganizationIntegrationConfigurationRequestModel.IsValidForType`.
## Integration Handler ## Integration Handler
e.g. `ExampleIntegrationHandler` e.g. `ExampleIntegrationHandler`
- This is where the actual code will go to perform the integration (i.e. send an HTTP request, etc.) - This is where the actual code will go to perform the integration (i.e. send an HTTP request, etc.).
- Handlers receive an `IntegrationMessage<T>` where `<T>` is the `ExampleIntegrationConfigurationDetails` - Handlers receive an `IntegrationMessage<T>` where `<T>` is the `ExampleIntegrationConfigurationDetails`
defined above. This has the Configuration as well as the rendered template message to be sent. defined above. This has the Configuration as well as the rendered template message to be sent.
- Handlers return an `IntegrationHandlerResult` with details about if the request was - Handlers return an `IntegrationHandlerResult` with details about if the request - success / failure,
successful, if it can be retried, when it should be delayed until, etc. if it can be retried, when it should be delayed until, etc.
- The scope of the handler is simply to do the integration and report the result. - The scope of the handler is simply to do the integration and report the result.
Everything else (such as how many times to retry, when to retry, what to do with failures) Everything else (such as how many times to retry, when to retry, what to do with failures)
is done in the Listener. is done in the Listener.
@ -274,23 +317,23 @@ integration below
#### Service Bus Emulator, local config #### Service Bus Emulator, local config
In order to create ASB resources locally, we need to also update the `servicebusemulator_config.json` file In order to create ASB resources locally, we need to also update the `servicebusemulator_config.json` file
to include any new subscriptions. to include any new subscriptions.
- Under the existing event topic (`event-logging`) add a - Under the existing event topic (`event-logging`) add a subscription for the event level for this
subscription for the event level for this new integration (`events-example-subscription`). new integration (`events-example-subscription`).
- Under the existing integration topic (`event-integrations`) add a new subscription for the integration - Under the existing integration topic (`event-integrations`) add a new subscription for the integration
level messages (`integration-example-subscription`). level messages (`integration-example-subscription`).
- Copy the correlation filter from the other integration level subscriptions. It should filter based on - Copy the correlation filter from the other integration level subscriptions. It should filter based on
the `IntegrationType.ToRoutingKey`, or in this example `example`. the `IntegrationType.ToRoutingKey`, or in this example `example`.
These names added here are what must match the values provided in the secrets or the defaults provided These names added here are what must match the values provided in the secrets or the defaults provided
in Global Settings. This must be in place (and the local ASB emulater restarted) before you can use any in Global Settings. This must be in place (and the local ASB emulator restarted) before you can use any
code locally that accesses ASB resources. code locally that accesses ASB resources.
## ServiceCollectionExtensions ## ServiceCollectionExtensions
In our `ServiceCollectionExtensions`, we pull all the pieces together that have been built to start In our `ServiceCollectionExtensions`, we pull all the above pieces together to start listeners on each message
listeners on each message tier, with handlers to process the integration. There are a number of helper tier with handlers to process the integration. There are a number of helper methods in here to make this simple
methods in here to make this simple to add a new integration - one call per platform. to add a new integration - one call per platform.
Also note that if an integration needs a custom singleton/service defined, the add listeners method is a Also note that if an integration needs a custom singleton / service defined, the add listeners method is a
good place to set that up. For instance, `SlackIntegrationHandler` needs a `SlackService`, so the singleton good place to set that up. For instance, `SlackIntegrationHandler` needs a `SlackService`, so the singleton
declaration is right above the add integration method for slack. Same thing for webhooks when it comes to declaration is right above the add integration method for slack. Same thing for webhooks when it comes to
defining a custom HttpClient by name. defining a custom HttpClient by name.
@ -303,7 +346,6 @@ defining a custom HttpClient by name.
globalSettings.EventLogging.RabbitMq.ExampleIntegrationRetryQueueName, globalSettings.EventLogging.RabbitMq.ExampleIntegrationRetryQueueName,
globalSettings.EventLogging.RabbitMq.MaxRetries, globalSettings.EventLogging.RabbitMq.MaxRetries,
IntegrationType.Example); IntegrationType.Example);
``` ```
2. In `AddAzureServiceBusListeners` add the integration: 2. In `AddAzureServiceBusListeners` add the integration:
@ -313,7 +355,6 @@ services.AddAzureServiceBusIntegration<ExampleIntegrationConfigurationDetails, E
integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleIntegrationSubscriptionName, integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleIntegrationSubscriptionName,
integrationType: IntegrationType.Example, integrationType: IntegrationType.Example,
globalSettings: globalSettings); globalSettings: globalSettings);
``` ```
# Deploying a new integration # Deploying a new integration
@ -339,7 +380,7 @@ code.
1. `ExmpleEventSubscriptionName` 1. `ExmpleEventSubscriptionName`
- This subscription is a fan-out subscription from the main event topic. - This subscription is a fan-out subscription from the main event topic.
- As such, it will start receiving all the events as soon as it is declared. - As such, it will start receiving all the events as soon as it is declared.
- This can create a backlog before the integration-specific handler is declared and deployed - This can create a backlog before the integration-specific handler is declared and deployed.
- One strategy to avoid this is to create the subscription with a false filter (e.g. `1 = 0`). - One strategy to avoid this is to create the subscription with a false filter (e.g. `1 = 0`).
- This will create the subscription, but the filter will ensure that no messages - This will create the subscription, but the filter will ensure that no messages
actually land in the subscription. actually land in the subscription.
@ -351,5 +392,5 @@ code.
2. `ExmpleIntegrationSubscriptionName` 2. `ExmpleIntegrationSubscriptionName`
- This subscription must be created before the new integration code can be deployed. - This subscription must be created before the new integration code can be deployed.
- However, it is not fan-out, but rather a filter based on the `IntegrationType.ToRoutingKey`. - However, it is not fan-out, but rather a filter based on the `IntegrationType.ToRoutingKey`.
- Therefore, it won't start receiving messages until Organizations have active configurations. - Therefore, it won't start receiving messages until organizations have active configurations.
This means there's no risk of building up a backlog by declaring it ahead of time. This means there's no risk of building up a backlog by declaring it ahead of time.