diff --git a/backend/src/ee/services/event/event-sse-service.ts b/backend/src/ee/services/event/event-sse-service.ts index f8c025e77..bb85f667c 100644 --- a/backend/src/ee/services/event/event-sse-service.ts +++ b/backend/src/ee/services/event/event-sse-service.ts @@ -13,11 +13,9 @@ const AUTH_REFRESH_INTERVAL = 60 * 1000; const HEART_BEAT_INTERVAL = 15 * 1000; export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => { - let heartbeatInterval: NodeJS.Timeout | null = null; - const clients = new Set(); - heartbeatInterval = setInterval(() => { + const heartbeatInterval = setInterval(() => { for (const client of clients) { if (client.stream.closed) continue; void client.ping(); diff --git a/backend/src/ee/services/event/event-sse-stream.ts b/backend/src/ee/services/event/event-sse-stream.ts index d73eccb42..258783c2e 100644 --- a/backend/src/ee/services/event/event-sse-stream.ts +++ b/backend/src/ee/services/event/event-sse-stream.ts @@ -66,15 +66,24 @@ export type EventStreamClient = { }; export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient { - const rules = options.registered.map((r) => ({ - subject: options.type, - action: "subscribe", - conditions: { - eventType: r.event, - secretPath: r.conditions?.secretPath ?? "/", - environment: r.conditions?.environmentSlug - } - })); + const rules = options.registered.map((r) => { + const secretPath = r.conditions?.secretPath; + const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath; + + return { + subject: options.type, + action: "subscribe", + conditions: { + eventType: r.event, + ...(hasConditions + ? { + environment: r.conditions?.environmentSlug ?? "", + secretPath: { $glob: secretPath } + } + : {}) + } + }; + }); const id = `sse-${nanoid()}`; const control = new AbortController(); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b4bdcb4fa..0cc272f15 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -70,7 +70,8 @@ export enum ApiDocsTags { SecretScanning = "Secret Scanning", OidcSso = "OIDC SSO", SamlSso = "SAML SSO", - LdapSso = "LDAP SSO" + LdapSso = "LDAP SSO", + Events = "Event Subscriptions" } export const GROUPS = { @@ -2872,3 +2873,10 @@ export const LdapSso = { caCert: "The CA certificate to use when verifying the LDAP server certificate." } }; + +export const EventSubscriptions = { + SUBSCRIBE_PROJECT_EVENTS: { + projectId: "The ID of the project to subscribe to events for.", + register: "List of events you want to subscribe to" + } +}; diff --git a/backend/src/server/routes/v1/event-router.ts b/backend/src/server/routes/v1/event-router.ts index b8a043db5..19dc296bf 100644 --- a/backend/src/server/routes/v1/event-router.ts +++ b/backend/src/server/routes/v1/event-router.ts @@ -7,6 +7,7 @@ import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream"; import { EventRegisterSchema } from "@app/ee/services/event/types"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs"; import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -20,10 +21,14 @@ export const registerEventRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Events], + description: "Subscribe to project events", body: z.object({ - projectId: z.string().trim(), - register: z.array(EventRegisterSchema).max(10) - }) + projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId), + register: z.array(EventRegisterSchema).min(1).max(10) + }), + produces: ["text/event-stream"] }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, reply) => { @@ -75,13 +80,15 @@ export const registerEventRouter = async (server: FastifyZodProvider) => { } req.body.register.forEach((r) => { + const fields = { + environment: r.conditions?.environmentSlug ?? "", + secretPath: r.conditions?.secretPath ?? "/", + eventType: r.event + }; + const allowed = info.permission.can( ProjectPermissionSecretActions.Subscribe, - subject(ProjectPermissionSub.Secrets, { - environment: r.conditions?.environmentSlug ?? "", - secretPath: r.conditions?.secretPath ?? "/", - eventType: r.event - }) + subject(ProjectPermissionSub.Secrets, fields) ); if (!allowed) { @@ -89,9 +96,9 @@ export const registerEventRouter = async (server: FastifyZodProvider) => { name: "PermissionDenied", message: `You are not allowed to subscribe on secrets`, details: { - event: r.event, - environmentSlug: r.conditions?.environmentSlug, - secretPath: r.conditions?.secretPath ?? "/" + event: fields.eventType, + environmentSlug: fields.environment, + secretPath: fields.secretPath } }); } diff --git a/docs/api-reference/endpoints/events/project-events.mdx b/docs/api-reference/endpoints/events/project-events.mdx new file mode 100644 index 000000000..9ceb0439e --- /dev/null +++ b/docs/api-reference/endpoints/events/project-events.mdx @@ -0,0 +1,4 @@ +--- +title: "Project Events" +openapi: "POST /api/v1/events/subscribe/project-events" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 6b406a517..960c41727 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -41,6 +41,8 @@ "group": "Platform Reference", "pages": [ "documentation/platform/organization", + "documentation/platform/event-subscriptions", + "documentation/platform/folder", { "group": "Projects", "pages": [ @@ -764,6 +766,10 @@ "group": "Admin", "pages": ["api-reference/endpoints/admin/bootstrap-instance"] }, + { + "group": "Events", + "pages": ["api-reference/endpoints/events/project-events"] + }, { "group": "Identities", "pages": [ diff --git a/docs/documentation/platform/event-subscriptions.mdx b/docs/documentation/platform/event-subscriptions.mdx new file mode 100644 index 000000000..ca6ceebd1 --- /dev/null +++ b/docs/documentation/platform/event-subscriptions.mdx @@ -0,0 +1,118 @@ +--- +title: "Event Subscriptions" +sidebarTitle: "Events" +description: "Subscribe to events in Infisical for real-time updates" +--- + + + **Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under + the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [sales@infisical.com](mailto:sales@infisical.com) + to purchase an enterprise license. + + +Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**. + +## How It Works + +- Server receives message over pubsub connection indicating changes have occurred +- Server processes the change notification +- Updated data is synchronized across all connected Infisical instances +- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) +- All servers maintain consistent state without manual intervention + +This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization. + + + Event Subscriptions are designed for real-time communication and do not include persistence or replay + capabilities—events are delivered once and are not stored for future retrieval. + + +## Supported Resources + +You can currently subscribe to notifications for the following resources and event types: + +- **Secrets** + - `secret:created`: Triggered when a secret is created + - `secret:updated`: Triggered when a secret is updated + - `secret:deleted`: Triggered when a secret is deleted + +## Permissions Setup + +To receive events on a supported resource, the identity must have `Subscribe` action permission on that resource. + +Follow these steps to set up the necessary permissions: + + + + ![Select Project](/images/platform/events/select-project.png) + +On your project page, open **Project Settings** from the sidebar. + +In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL: +`https://app.infisical.com/project//settings` + + + + + ![Project Detail](/images/platform/events/project-detail.png) ![Project + Access](/images/platform/events/project-access.png) Navigate to **Access Management**, then select **Project Roles**. + + + + ![Project Role](/images/platform/events/project-role.png) You can either edit an existing role or create a new role + for event subscriptions. + + + + ![Role Detail](/images/platform/events/role-detail.png) Select the specific resources that the role should have access + to. ![Add policy](/images/platform/events/add-policy.png) + + + + ![Policy setting](/images/platform/events/policy-setting.png) + + Ensure the **Subscribe** action is selected for the relevant resources and events. + + ## Conditions + + By default, the role will have access to all events for the selected resources in this project. + + + + ![Policy setting](/images/platform/events/access-full.png) + + + ![Policy setting](/images/platform/events/access-path.png) + + + ![Policy setting](/images/platform/events/access-dev.png) + + + + + +## Getting Started + +Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more. + +### API Usage + +You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods). + +#### Creating a Subscription + +![Postman Subscription](/images/platform/events/postman-subscribe.png) + +**Request Parameters:** + +- `projectId`: Project whose events you want to subscribe to +- `register`: List of event filters + - `conditions`: Conditions to filter events on + - `environmentSlug`: Project environment + - `secretPath`: Path of the secrets + +![Postman Subscription Response](/images/platform/events/postman-sse-response.png) + +The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming. + +For more specific details, please refer to our [API Reference](/api-reference/endpoints/events). diff --git a/docs/images/platform/events/access-dev.png b/docs/images/platform/events/access-dev.png new file mode 100644 index 000000000..3f44fc51b Binary files /dev/null and b/docs/images/platform/events/access-dev.png differ diff --git a/docs/images/platform/events/access-full.png b/docs/images/platform/events/access-full.png new file mode 100644 index 000000000..d41260c17 Binary files /dev/null and b/docs/images/platform/events/access-full.png differ diff --git a/docs/images/platform/events/access-path.png b/docs/images/platform/events/access-path.png new file mode 100644 index 000000000..3bfa9b28b Binary files /dev/null and b/docs/images/platform/events/access-path.png differ diff --git a/docs/images/platform/events/add-auth-method-form.png b/docs/images/platform/events/add-auth-method-form.png new file mode 100644 index 000000000..ba181ff48 Binary files /dev/null and b/docs/images/platform/events/add-auth-method-form.png differ diff --git a/docs/images/platform/events/add-auth-method.png b/docs/images/platform/events/add-auth-method.png new file mode 100644 index 000000000..583f13d09 Binary files /dev/null and b/docs/images/platform/events/add-auth-method.png differ diff --git a/docs/images/platform/events/add-conditions.png b/docs/images/platform/events/add-conditions.png new file mode 100644 index 000000000..bf0580b22 Binary files /dev/null and b/docs/images/platform/events/add-conditions.png differ diff --git a/docs/images/platform/events/add-policy.png b/docs/images/platform/events/add-policy.png new file mode 100644 index 000000000..00e6b76f0 Binary files /dev/null and b/docs/images/platform/events/add-policy.png differ diff --git a/docs/images/platform/events/copy-generated-token.png b/docs/images/platform/events/copy-generated-token.png new file mode 100644 index 000000000..1fb061017 Binary files /dev/null and b/docs/images/platform/events/copy-generated-token.png differ diff --git a/docs/images/platform/events/generate-auth-token-add.png b/docs/images/platform/events/generate-auth-token-add.png new file mode 100644 index 000000000..2057e2509 Binary files /dev/null and b/docs/images/platform/events/generate-auth-token-add.png differ diff --git a/docs/images/platform/events/generate-auth-token-create.png b/docs/images/platform/events/generate-auth-token-create.png new file mode 100644 index 000000000..9bccbb16f Binary files /dev/null and b/docs/images/platform/events/generate-auth-token-create.png differ diff --git a/docs/images/platform/events/generate-auth-token.png b/docs/images/platform/events/generate-auth-token.png new file mode 100644 index 000000000..40d6336a6 Binary files /dev/null and b/docs/images/platform/events/generate-auth-token.png differ diff --git a/docs/images/platform/events/identity-add-project.png b/docs/images/platform/events/identity-add-project.png new file mode 100644 index 000000000..5fdaa488e Binary files /dev/null and b/docs/images/platform/events/identity-add-project.png differ diff --git a/docs/images/platform/events/identity-add-role.png b/docs/images/platform/events/identity-add-role.png new file mode 100644 index 000000000..c20296cf8 Binary files /dev/null and b/docs/images/platform/events/identity-add-role.png differ diff --git a/docs/images/platform/events/identity-selector.png b/docs/images/platform/events/identity-selector.png new file mode 100644 index 000000000..2aaf7e56b Binary files /dev/null and b/docs/images/platform/events/identity-selector.png differ diff --git a/docs/images/platform/events/org-access-control.png b/docs/images/platform/events/org-access-control.png new file mode 100644 index 000000000..334d6dbc3 Binary files /dev/null and b/docs/images/platform/events/org-access-control.png differ diff --git a/docs/images/platform/events/policy-setting.png b/docs/images/platform/events/policy-setting.png new file mode 100644 index 000000000..cadab9b82 Binary files /dev/null and b/docs/images/platform/events/policy-setting.png differ diff --git a/docs/images/platform/events/postman-sse-response.png b/docs/images/platform/events/postman-sse-response.png new file mode 100644 index 000000000..fea1054b4 Binary files /dev/null and b/docs/images/platform/events/postman-sse-response.png differ diff --git a/docs/images/platform/events/postman-subscribe.png b/docs/images/platform/events/postman-subscribe.png new file mode 100644 index 000000000..308d257c0 Binary files /dev/null and b/docs/images/platform/events/postman-subscribe.png differ diff --git a/docs/images/platform/events/project-access.png b/docs/images/platform/events/project-access.png new file mode 100644 index 000000000..61593befd Binary files /dev/null and b/docs/images/platform/events/project-access.png differ diff --git a/docs/images/platform/events/project-detail.png b/docs/images/platform/events/project-detail.png new file mode 100644 index 000000000..65ff15642 Binary files /dev/null and b/docs/images/platform/events/project-detail.png differ diff --git a/docs/images/platform/events/project-role.png b/docs/images/platform/events/project-role.png new file mode 100644 index 000000000..b6178939f Binary files /dev/null and b/docs/images/platform/events/project-role.png differ diff --git a/docs/images/platform/events/role-detail.png b/docs/images/platform/events/role-detail.png new file mode 100644 index 000000000..7b4d6cea6 Binary files /dev/null and b/docs/images/platform/events/role-detail.png differ diff --git a/docs/images/platform/events/select-project.png b/docs/images/platform/events/select-project.png new file mode 100644 index 000000000..f964bf443 Binary files /dev/null and b/docs/images/platform/events/select-project.png differ