Merge pull request #3153 from Infisical/daniel/secret-requests

feat(secret-sharing): secret requests
This commit is contained in:
Daniel Hougaard
2025-03-04 04:04:39 +09:00
committed by GitHub
45 changed files with 1885 additions and 52 deletions
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
if (!hasSharingTypeColumn) {
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
if (hasSharingTypeColumn) {
table.dropColumn("type");
}
});
}
+1
View File
@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(),
type: z.string(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),
@@ -235,7 +235,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const tagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
})
.array()
@@ -35,7 +35,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
@@ -250,6 +250,7 @@ export enum EventType {
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
CREATE_SHARED_SECRET = "create-shared-secret",
CREATE_SECRET_REQUEST = "create-secret-request",
DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret",
GET_SECRET_SYNCS = "get-secret-syncs",
@@ -2020,6 +2021,15 @@ interface CreateSharedSecretEvent {
};
}
interface CreateSecretRequestEvent {
type: EventType.CREATE_SECRET_REQUEST;
metadata: {
id: string;
accessType: string;
name?: string;
};
}
interface DeleteSharedSecretEvent {
type: EventType.DELETE_SHARED_SECRET;
metadata: {
@@ -2470,4 +2480,5 @@ export type Event =
| KmipOperationActivateEvent
| KmipOperationRevokeEvent
| KmipOperationLocateEvent
| KmipOperationRegisterEvent;
| KmipOperationRegisterEvent
| CreateSecretRequestEvent;
@@ -6,7 +6,6 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
sshCertificateTemplateId: true,
serialNumber: true,
certType: true,
publicKey: true,
principals: true,
keyId: true,
notBefore: true,
+3 -1
View File
@@ -1096,7 +1096,9 @@ export const registerRoutes = async (
permissionService,
secretSharingDAL,
orgDAL,
kmsService
kmsService,
smtpService,
userDAL
});
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
+10 -1
View File
@@ -37,6 +37,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerSecretFolderRouter } from "./secret-folder-router";
import { registerSecretImportRouter } from "./secret-import-router";
import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSlackRouter } from "./slack-router";
@@ -110,7 +111,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(
async (secretSharingRouter) => {
await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" });
await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" });
},
{ prefix: "/secret-sharing" }
);
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" });
@@ -0,0 +1,270 @@
import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSharingAccessType } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
}).extend({
isSecretValueSet: z.boolean(),
requester: z.object({
organizationName: z.string(),
firstName: z.string().nullish(),
lastName: z.string().nullish(),
username: z.string()
})
})
})
}
},
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
id: req.params.id,
actorOrgId: req.permission?.orgId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod
});
return { secretRequest };
}
});
server.route({
method: "POST",
url: "/:id/set-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
secretValue: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
})
})
}
},
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
id: req.params.id,
actorOrgId: req.permission?.orgId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod,
secretValue: req.body.secretValue
});
return { secretRequest };
}
});
server.route({
method: "POST",
url: "/:id/reveal-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
}).extend({
secretValue: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({
id: req.params.id,
actorOrgId: req.permission.orgId,
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod
});
return { secretRequest };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
sharedSecretId: req.params.id,
orgId: req.permission.orgId,
actor: req.permission.type,
type: SecretSharingType.Request
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretRequestDeleted,
distinctId: getTelemetryDistinctId(req),
properties: {
secretRequestId: req.params.id,
organizationId: req.permission.orgId,
...req.auditLogInfo
}
});
return { secretRequest };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(25)
}),
response: {
200: z.object({
secrets: z.array(SecretSharingSchema),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
type: SecretSharingType.Request,
...req.query
});
return {
secrets,
totalCount
};
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
name: z.string().max(50).optional(),
expiresAt: z.string(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const shareRequest = await req.server.services.secretSharing.createSecretRequest({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRET_REQUEST,
metadata: {
accessType: req.body.accessType,
name: req.body.name,
id: shareRequest.id
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretRequestCreated,
distinctId: getTelemetryDistinctId(req),
properties: {
secretRequestId: shareRequest.id,
organizationId: req.permission.orgId,
secretRequestName: req.body.name,
...req.auditLogInfo
}
});
return { id: shareRequest.id };
}
});
};
@@ -11,6 +11,7 @@ import {
} from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -38,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
type: SecretSharingType.Share,
...req.query
});
@@ -211,7 +213,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
sharedSecretId
sharedSecretId,
type: SecretSharingType.Share
});
await server.services.auditLog.createAuditLog({
@@ -20,7 +20,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
queueService: TQueueServiceFactory;
};
@@ -45,6 +45,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
await secretSharingDAL.pruneExpiredSecretRequests();
await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions();
@@ -2,17 +2,61 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretSharing } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
import { SecretSharingType } from "./secret-sharing-types";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
export const secretSharingDALFactory = (db: TDbClient) => {
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => {
const getSecretRequestById = async (id: string) => {
const repDb = db.replicaNode();
const secretRequest = await repDb(TableName.SecretSharing)
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SecretSharing}.orgId`)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretSharing}.userId`)
.where(`${TableName.SecretSharing}.id`, id)
.where(`${TableName.SecretSharing}.type`, SecretSharingType.Request)
.select(
repDb.ref("name").withSchema(TableName.Organization).as("orgName"),
repDb.ref("firstName").withSchema(TableName.Users).as("requesterFirstName"),
repDb.ref("lastName").withSchema(TableName.Users).as("requesterLastName"),
repDb.ref("username").withSchema(TableName.Users).as("requesterUsername")
)
.select(selectAllTableCols(TableName.SecretSharing))
.first();
if (!secretRequest) {
throw new NotFoundError({
message: `Secret request with ID '${id}' not found`
});
}
return {
...secretRequest,
requester: {
organizationName: secretRequest.orgName,
firstName: secretRequest.requesterFirstName,
lastName: secretRequest.requesterLastName,
username: secretRequest.requesterUsername
}
};
};
const countAllUserOrgSharedSecrets = async ({
orgId,
userId,
type
}: {
orgId: string;
userId: string;
type: SecretSharingType;
}) => {
try {
interface CountResult {
count: string;
@@ -22,6 +66,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.replicaNode()(TableName.SecretSharing)
.where(`${TableName.SecretSharing}.orgId`, orgId)
.where(`${TableName.SecretSharing}.userId`, userId)
.where(`${TableName.SecretSharing}.type`, type)
.count("*")
.first();
@@ -38,6 +83,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
const docs = await (tx || db)(TableName.SecretSharing)
.where("expiresAt", "<", today)
.andWhere("encryptedValue", "<>", "")
.andWhere("type", SecretSharingType.Share)
.update({
encryptedValue: "",
tag: "",
@@ -50,6 +96,26 @@ export const secretSharingDALFactory = (db: TDbClient) => {
}
};
const pruneExpiredSecretRequests = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests started`);
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing)
.whereNotNull("expiresAt")
.andWhere("expiresAt", "<", today)
.andWhere("encryptedSecret", null)
.andWhere("type", SecretSharingType.Request)
.delete();
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests completed`);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSecretRequests" });
}
};
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
try {
const now = new Date();
@@ -57,6 +123,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.where(filters)
.andWhere("expiresAt", ">", now)
.andWhere("encryptedValue", "<>", "")
.andWhere("type", SecretSharingType.Share)
.select(selectAllTableCols(TableName.SecretSharing))
.orderBy("expiresAt", "asc");
} catch (error) {
@@ -86,7 +153,9 @@ export const secretSharingDALFactory = (db: TDbClient) => {
...sharedSecretOrm,
countAllUserOrgSharedSecrets,
pruneExpiredSharedSecrets,
pruneExpiredSecretRequests,
softDeleteById,
findActiveSharedSecrets
findActiveSharedSecrets,
getSecretRequestById
};
};
@@ -4,26 +4,36 @@ import bcrypt from "bcrypt";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import {
SecretSharingType,
TCreatePublicSharedSecretDTO,
TCreateSecretRequestDTO,
TCreateSharedSecretDTO,
TDeleteSharedSecretDTO,
TGetActiveSharedSecretByIdDTO,
TGetSharedSecretsDTO
TGetSecretRequestByIdDTO,
TGetSharedSecretsDTO,
TRevealSecretRequestValueDTO,
TSetSecretRequestValueDTO
} from "./secret-sharing-types";
type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory;
userDAL: TUserDALFactory;
kmsService: TKmsServiceFactory;
smtpService: TSmtpService;
};
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
@@ -32,7 +42,9 @@ export const secretSharingServiceFactory = ({
permissionService,
secretSharingDAL,
orgDAL,
kmsService
kmsService,
smtpService,
userDAL
}: TSecretSharingServiceFactoryDep) => {
const $validateSharedSecretExpiry = (expiresAt: string) => {
if (new Date(expiresAt) < new Date()) {
@@ -75,7 +87,6 @@ export const secretSharingServiceFactory = ({
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const id = crypto.randomBytes(32).toString("hex");
@@ -88,6 +99,7 @@ export const secretSharingServiceFactory = ({
encryptedValue: null,
encryptedSecret,
name,
type: SecretSharingType.Share,
password: hashedPassword,
expiresAt: new Date(expiresAt),
expiresAfterViews,
@@ -101,6 +113,191 @@ export const secretSharingServiceFactory = ({
return { id: idToReturn };
};
const createSecretRequest = async ({
actor,
accessType,
expiresAt,
name,
actorId,
orgId,
actorAuthMethod,
actorOrgId
}: TCreateSecretRequestDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
$validateSharedSecretExpiry(expiresAt);
const newSecretRequest = await secretSharingDAL.create({
type: SecretSharingType.Request,
userId: actorId,
orgId,
name,
encryptedSecret: null,
accessType,
expiresAt: new Date(expiresAt)
});
return { id: newSecretRequest.id };
};
const revealSecretRequestValue = async ({
id,
actor,
actorId,
actorOrgId,
orgId,
actorAuthMethod
}: TRevealSecretRequestValueDTO) => {
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
if (!secretRequest.encryptedSecret) {
throw new BadRequestError({ message: "Secret request has no value set" });
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const decryptedSecret = decryptWithRoot(secretRequest.encryptedSecret);
return { ...secretRequest, secretValue: decryptedSecret.toString() };
};
const getSecretRequestById = async ({
id,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TGetSecretRequestByIdDTO) => {
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (!secretRequest.orgId) {
throw new BadRequestError({ message: "No organization ID present on secret request" });
}
if (!actorOrgId) {
throw new UnauthorizedError();
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
secretRequest.orgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
}
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
throw new ForbiddenRequestError({
message: "Access denied: Secret request has expired"
});
}
return {
...secretRequest,
isSecretValueSet: Boolean(secretRequest.encryptedSecret)
};
};
const setSecretRequestValue = async ({
id,
actor,
actorId,
actorAuthMethod,
actorOrgId,
secretValue
}: TSetSecretRequestValueDTO) => {
const appCfg = getConfig();
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
let respondentUsername: string | undefined;
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (!secretRequest.orgId) {
throw new BadRequestError({ message: "No organization ID present on secret request" });
}
if (!actorOrgId) {
throw new UnauthorizedError();
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
secretRequest.orgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
const user = await userDAL.findById(actorId);
if (!user) {
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
}
respondentUsername = user.username;
}
if (secretRequest.encryptedSecret) {
throw new BadRequestError({ message: "Secret request already has a value set" });
}
if (secretValue.length > 10_000) {
throw new BadRequestError({ message: "Shared secret value too long" });
}
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
throw new ForbiddenRequestError({
message: "Access denied: Secret request has expired"
});
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const request = await secretSharingDAL.transaction(async (tx) => {
const updatedRequest = await secretSharingDAL.updateById(id, { encryptedSecret }, tx);
await smtpService.sendMail({
recipients: [secretRequest.requesterUsername],
subjectLine: "Secret Request Completed",
substitutions: {
name: secretRequest.name,
respondentUsername,
secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret`
},
template: SmtpTemplates.SecretRequestCompleted
});
return updatedRequest;
});
return request;
};
const createPublicSharedSecret = async ({
password,
secretValue,
@@ -121,6 +318,7 @@ export const secretSharingServiceFactory = ({
encryptedValue: null,
iv: null,
tag: null,
type: SecretSharingType.Share,
encryptedSecret,
password: hashedPassword,
expiresAt: new Date(expiresAt),
@@ -137,7 +335,8 @@ export const secretSharingServiceFactory = ({
actorAuthMethod,
actorOrgId,
offset,
limit
limit,
type
}: TGetSharedSecretsDTO) => {
if (!actorOrgId) throw new ForbiddenRequestError();
@@ -153,14 +352,16 @@ export const secretSharingServiceFactory = ({
const secrets = await secretSharingDAL.find(
{
userId: actorId,
orgId: actorOrgId
orgId: actorOrgId,
type
},
{ offset, limit, sort: [["createdAt", "desc"]] }
);
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
orgId: actorOrgId,
userId: actorId
userId: actorId,
type
});
return {
@@ -187,9 +388,11 @@ export const secretSharingServiceFactory = ({
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findOne({
id: sharedSecretId,
type: SecretSharingType.Share,
hashedHex
})
: await secretSharingDAL.findOne({
type: SecretSharingType.Share,
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
});
@@ -254,7 +457,7 @@ export const secretSharingServiceFactory = ({
secret: {
...sharedSecret,
...(decryptedSecretValue && {
secretValue: Buffer.from(decryptedSecretValue).toString()
secretValue: decryptedSecretValue.toString()
}),
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
@@ -270,11 +473,17 @@ export const secretSharingServiceFactory = ({
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findById(sharedSecretId)
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type })
: await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type });
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
if (sharedSecret.userId !== actorId) {
throw new ForbiddenRequestError({
message: "User does not have permission to delete shared secret"
});
}
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) {
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
}
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
@@ -286,6 +495,11 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret,
getSharedSecrets,
deleteSharedSecretById,
getSharedSecretById
getSharedSecretById,
createSecretRequest,
getSecretRequestById,
setSecretRequestValue,
revealSecretRequestValue
};
};
@@ -1,8 +1,14 @@
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
import { SecretSharingAccessType, TGenericPermission, TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export enum SecretSharingType {
Share = "share",
Request = "request"
}
export type TGetSharedSecretsDTO = {
type: SecretSharingType;
offset: number;
limit: number;
} & TGenericPermission;
@@ -39,6 +45,26 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
export type TCreateSecretRequestDTO = {
name?: string;
accessType: SecretSharingAccessType;
expiresAt: string;
} & TOrgPermission;
export type TRevealSecretRequestValueDTO = {
id: string;
} & TOrgPermission;
export type TGetSecretRequestByIdDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TSetSecretRequestValueDTO = {
id: string;
secretValue: string;
} & Omit<TOrgPermission, "orgId">;
export type TDeleteSharedSecretDTO = {
sharedSecretId: string;
type: SecretSharingType;
} & TSharedSecretPermission;
+2 -1
View File
@@ -39,7 +39,8 @@ export enum SmtpTemplates {
SecretSyncFailed = "secretSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars"
ExternalImportStarted = "externalImportStarted.handlebars",
SecretRequestCompleted = "secretRequestCompleted.handlebars"
}
export enum SmtpHost {
@@ -0,0 +1,33 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Request Completed</title>
</head>
<body>
<h2>Infisical</h2>
<h2>A secret has been shared with you</h2>
{{#if name}}
<p>Secret request name: {{name}}</p>
{{/if}}
{{#if respondentUsername}}
<p>Shared by: {{respondentUsername}}</p>
{{/if}}
<br />
<br/>
<p>
You can access the secret by clicking the link below.
</p>
<p>
<a href="{{secretRequestUrl}}">Access Secret</a>
</p>
{{emailFooter}}
</body>
</html>
@@ -13,7 +13,9 @@ export enum PostHogEventTypes {
IntegrationCreated = "Integration Created",
MachineIdentityCreated = "Machine Identity Created",
UserOrgInvitation = "User Org Invitation",
TelemetryInstanceStats = "Self Hosted Instance Stats"
TelemetryInstanceStats = "Self Hosted Instance Stats",
SecretRequestCreated = "Secret Request Created",
SecretRequestDeleted = "Secret Request Deleted"
}
export type TSecretModifiedEvent = {
@@ -120,6 +122,23 @@ export type TTelemetryInstanceStatsEvent = {
};
};
export type TSecretRequestCreatedEvent = {
event: PostHogEventTypes.SecretRequestCreated;
properties: {
secretRequestId: string;
organizationId: string;
secretRequestName?: string;
};
};
export type TSecretRequestDeletedEvent = {
event: PostHogEventTypes.SecretRequestDeleted;
properties: {
secretRequestId: string;
organizationId: string;
};
};
export type TPostHogEvent = { distinctId: string } & (
| TSecretModifiedEvent
| TAdminInitEvent
@@ -130,4 +149,6 @@ export type TPostHogEvent = { distinctId: string } & (
| TIntegrationCreatedEvent
| TProjectCreateEvent
| TTelemetryInstanceStatsEvent
| TSecretRequestCreatedEvent
| TSecretRequestDeletedEvent
);