mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 00:37:35 +00:00
* fix: update project permission types * fix: permissions * fix: PR changes * chore: update docs * fix: greptile changes * fix: secret imports router changes * fix: type change * fix: lint * fix: type change * fix: check plan type in publish * fix: permissions * fix: import change * fix: lint fix
124 lines
4.7 KiB
TypeScript
124 lines
4.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
import { subject } from "@casl/ability";
|
|
import { pipeline } from "stream/promises";
|
|
import { z } from "zod";
|
|
|
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
|
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
|
|
import { EventRegisterSchema, Mappings } from "@app/ee/services/event/types";
|
|
import { 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";
|
|
import { AuthMode } from "@app/services/auth/auth-type";
|
|
|
|
export const registerEventRouter = async (server: FastifyZodProvider) => {
|
|
server.route({
|
|
method: "POST",
|
|
url: "/subscribe/project-events",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
hide: false,
|
|
tags: [ApiDocsTags.Events],
|
|
description: "Subscribe to project events",
|
|
body: z.object({
|
|
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) => {
|
|
try {
|
|
const { sse, permission, identityAccessToken, authToken, license } = req.server.services;
|
|
|
|
const plan = await license.getPlan(req.auth.orgId);
|
|
|
|
if (!plan.eventSubscriptions) {
|
|
throw new BadRequestError({
|
|
message:
|
|
"Failed to use event subscriptions due to plan restriction. Upgrade plan to access enterprise event subscriptions."
|
|
});
|
|
}
|
|
|
|
const count = await sse.getActiveConnectionsCount(req.body.projectId, req.permission.id);
|
|
|
|
if (count >= 5) {
|
|
throw new RateLimitError({
|
|
message: `Too many active connections for project ${req.body.projectId}. Please close some connections before opening a new one.`
|
|
});
|
|
}
|
|
|
|
const client = await sse.subscribe({
|
|
type: ProjectType.SecretManager,
|
|
registered: req.body.register,
|
|
async getAuthInfo() {
|
|
const ability = await permission.getProjectPermission({
|
|
actor: req.auth.actor,
|
|
projectId: req.body.projectId,
|
|
actionProjectType: ActionProjectType.Any,
|
|
actorAuthMethod: req.auth.authMethod,
|
|
actorId: req.permission.id,
|
|
actorOrgId: req.permission.orgId
|
|
});
|
|
|
|
return { permission: ability.permission, actorId: req.permission.id, projectId: req.body.projectId };
|
|
},
|
|
async onAuthRefresh(info) {
|
|
switch (req.auth.authMode) {
|
|
case AuthMode.JWT:
|
|
await authToken.fnValidateJwtIdentity(req.auth.token);
|
|
break;
|
|
case AuthMode.IDENTITY_ACCESS_TOKEN:
|
|
await identityAccessToken.fnValidateIdentityAccessToken(req.auth.token, req.realIp);
|
|
break;
|
|
default:
|
|
throw new Error("Unsupported authentication method");
|
|
}
|
|
|
|
req.body.register.forEach((r) => {
|
|
const fields = {
|
|
environment: r.conditions?.environmentSlug ?? "",
|
|
secretPath: r.conditions?.secretPath ?? "/"
|
|
};
|
|
|
|
const action = Mappings.BusEventToAction(r.event);
|
|
|
|
const allowed = info.permission.can(action, subject(ProjectPermissionSub.SecretEvents, fields));
|
|
|
|
if (!allowed) {
|
|
throw new ForbiddenRequestError({
|
|
name: "PermissionDenied",
|
|
message: `You are not allowed to subscribe on ${ProjectPermissionSub.SecretEvents}`,
|
|
details: {
|
|
action,
|
|
environmentSlug: fields.environment,
|
|
secretPath: fields.secretPath
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Switches to manual response and enable SSE streaming
|
|
reply.hijack();
|
|
reply.raw.writeHead(200, getServerSentEventsHeaders()).flushHeaders();
|
|
reply.raw.on("close", client.abort);
|
|
|
|
await pipeline(client.stream, reply.raw, { signal: client.signal });
|
|
} catch (error) {
|
|
if (error instanceof Error && error.name === "AbortError") {
|
|
// If the stream is aborted, we don't need to do anything
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
});
|
|
};
|