mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 23:27:35 +00:00
feat: simplified endpoints to support password based secret sharing
This commit is contained in:
@@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "POST",
|
||||||
url: "/public/:id",
|
url: "/public/:id",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: publicEndpointLimit
|
rateLimit: publicEndpointLimit
|
||||||
@@ -57,92 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
body: z.object({
|
||||||
hashedHex: z.string().min(1)
|
hashedHex: z.string().min(1),
|
||||||
|
password: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SecretSharingSchema.pick({
|
200: z.object({
|
||||||
|
isPasswordProtected: z.boolean(),
|
||||||
|
secret: SecretSharingSchema.pick({
|
||||||
encryptedValue: true,
|
encryptedValue: true,
|
||||||
iv: true,
|
iv: true,
|
||||||
tag: true,
|
tag: true,
|
||||||
expiresAt: true,
|
expiresAt: true,
|
||||||
expiresAfterViews: true,
|
expiresAfterViews: true,
|
||||||
accessType: true
|
accessType: true
|
||||||
}).extend({
|
})
|
||||||
|
.extend({
|
||||||
orgName: z.string().optional()
|
orgName: z.string().optional()
|
||||||
})
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const sharedSecret = await req.server.services.secretSharing.getPasswordlessSecretByID({
|
const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({
|
||||||
sharedSecretId: req.params.id,
|
sharedSecretId: req.params.id,
|
||||||
hashedHex: req.query.hashedHex,
|
hashedHex: req.body.hashedHex,
|
||||||
|
password: req.body.password,
|
||||||
orgId: req.permission?.orgId
|
orgId: req.permission?.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sharedSecret) return undefined;
|
return sharedSecret;
|
||||||
|
|
||||||
return {
|
|
||||||
encryptedValue: sharedSecret.encryptedValue,
|
|
||||||
iv: sharedSecret.iv,
|
|
||||||
tag: sharedSecret.tag,
|
|
||||||
expiresAt: sharedSecret.expiresAt,
|
|
||||||
expiresAfterViews: sharedSecret.expiresAfterViews,
|
|
||||||
accessType: sharedSecret.accessType,
|
|
||||||
orgName: sharedSecret.orgName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/public/:id/validate",
|
|
||||||
config: {
|
|
||||||
rateLimit: publicEndpointLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
params: z.object({
|
|
||||||
id: z.string().uuid()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
password: z.string().min(1),
|
|
||||||
hashedHex: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: SecretSharingSchema.pick({
|
|
||||||
encryptedValue: true,
|
|
||||||
iv: true,
|
|
||||||
tag: true,
|
|
||||||
expiresAt: true,
|
|
||||||
expiresAfterViews: true,
|
|
||||||
accessType: true
|
|
||||||
}).extend({
|
|
||||||
orgName: z.string().optional()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: async (req) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { password, hashedHex } = req.body;
|
|
||||||
|
|
||||||
const sharedSecret = await req.server.services.secretSharing.getValidatedSecretByID({
|
|
||||||
sharedSecretId: id,
|
|
||||||
hashedHex,
|
|
||||||
orgId: req.permission?.orgId,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sharedSecret) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
encryptedValue: sharedSecret.encryptedValue,
|
|
||||||
iv: sharedSecret.iv,
|
|
||||||
tag: sharedSecret.tag,
|
|
||||||
expiresAt: sharedSecret.expiresAt,
|
|
||||||
expiresAfterViews: sharedSecret.expiresAfterViews,
|
|
||||||
accessType: sharedSecret.accessType,
|
|
||||||
orgName: sharedSecret.orgName
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
|
||||||
import {
|
|
||||||
BadRequestError,
|
|
||||||
ForbiddenRequestError,
|
|
||||||
InternalServerError,
|
|
||||||
NotFoundError,
|
|
||||||
UnauthorizedError
|
|
||||||
} from "@app/lib/errors";
|
|
||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
|
||||||
import { TSecretSharing } from "@app/db/schemas";
|
import { TSecretSharing } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
@@ -18,8 +12,7 @@ import {
|
|||||||
TCreateSharedSecretDTO,
|
TCreateSharedSecretDTO,
|
||||||
TDeleteSharedSecretDTO,
|
TDeleteSharedSecretDTO,
|
||||||
TGetActiveSharedSecretByIdDTO,
|
TGetActiveSharedSecretByIdDTO,
|
||||||
TGetSharedSecretsDTO,
|
TGetSharedSecretsDTO
|
||||||
TValidateActiveSharedSecretDTO
|
|
||||||
} from "./secret-sharing-types";
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
@@ -169,10 +162,39 @@ export const secretSharingServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Checks if secret is expired and throws error if true */
|
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
||||||
const checkIfSecretIsExpired = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
const { expiresAfterViews } = sharedSecret;
|
||||||
const { expiresAt, expiresAfterViews } = sharedSecret;
|
|
||||||
|
|
||||||
|
if (expiresAfterViews) {
|
||||||
|
// decrement view count if view count expiry set
|
||||||
|
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
await secretSharingDAL.updateById(sharedSecretId, {
|
||||||
|
lastViewedAt: new Date()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
|
||||||
|
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
||||||
|
const sharedSecret = await secretSharingDAL.findOne({
|
||||||
|
id: sharedSecretId,
|
||||||
|
hashedHex
|
||||||
|
});
|
||||||
|
if (!sharedSecret)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Shared secret not found"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { accessType, expiresAt, expiresAfterViews } = sharedSecret;
|
||||||
|
|
||||||
|
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
|
||||||
|
|
||||||
|
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
|
||||||
|
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
|
||||||
|
// or can be safely sent to the client.
|
||||||
if (expiresAt !== null && expiresAt < new Date()) {
|
if (expiresAt !== null && expiresAt < new Date()) {
|
||||||
// check lifetime expiry
|
// check lifetime expiry
|
||||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||||
@@ -188,97 +210,30 @@ export const secretSharingServiceFactory = ({
|
|||||||
message: "Access denied: Secret has expired by view count"
|
message: "Access denied: Secret has expired by view count"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
const isPasswordProtected = Boolean(sharedSecret.password);
|
||||||
const { expiresAfterViews } = sharedSecret;
|
const hasProvidedPassword = Boolean(password);
|
||||||
|
if (isPasswordProtected) {
|
||||||
if (expiresAfterViews) {
|
if (hasProvidedPassword) {
|
||||||
// decrement view count if view count expiry set
|
const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string);
|
||||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||||
|
} else {
|
||||||
|
return { isPasswordProtected };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretSharingDAL.updateById(sharedSecretId, {
|
|
||||||
lastViewedAt: new Date()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
|
|
||||||
const getPasswordlessSecretByID = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
|
|
||||||
const sharedSecret = await secretSharingDAL.findOne({
|
|
||||||
id: sharedSecretId,
|
|
||||||
hashedHex
|
|
||||||
});
|
|
||||||
if (!sharedSecret)
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Shared secret not found"
|
|
||||||
});
|
|
||||||
|
|
||||||
const { accessType } = sharedSecret;
|
|
||||||
|
|
||||||
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
|
|
||||||
|
|
||||||
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
|
|
||||||
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
|
|
||||||
// or can be safely sent to the client.
|
|
||||||
await checkIfSecretIsExpired(sharedSecret, sharedSecretId);
|
|
||||||
|
|
||||||
if (sharedSecret.password !== null) return undefined;
|
|
||||||
|
|
||||||
// decrement when we are sure the user will view secret.
|
// decrement when we are sure the user will view secret.
|
||||||
await decrementSecretViewCount(sharedSecret, sharedSecretId);
|
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
|
||||||
|
|
||||||
return {
|
|
||||||
...sharedSecret,
|
|
||||||
orgName:
|
|
||||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
|
||||||
? orgName
|
|
||||||
: undefined
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Get's the requested secret if password passed is valid */
|
|
||||||
const getValidatedSecretByID = async ({
|
|
||||||
sharedSecretId,
|
|
||||||
hashedHex,
|
|
||||||
orgId,
|
|
||||||
password
|
|
||||||
}: TValidateActiveSharedSecretDTO) => {
|
|
||||||
const sharedSecret = await secretSharingDAL.findOne({
|
|
||||||
id: sharedSecretId,
|
|
||||||
hashedHex
|
|
||||||
});
|
|
||||||
if (!sharedSecret)
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Shared secret not found"
|
|
||||||
});
|
|
||||||
|
|
||||||
const { accessType } = sharedSecret;
|
|
||||||
|
|
||||||
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
|
|
||||||
|
|
||||||
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
|
|
||||||
if (!sharedSecret.password)
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: "Something went wrong"
|
|
||||||
});
|
|
||||||
|
|
||||||
const isMatch = await bcrypt.compare(password, sharedSecret.password);
|
|
||||||
if (!isMatch) return undefined;
|
|
||||||
|
|
||||||
// reduce the view count when the password matches (will be returned to the client).
|
|
||||||
await decrementSecretViewCount(sharedSecret, sharedSecretId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isPasswordProtected,
|
||||||
|
secret: {
|
||||||
...sharedSecret,
|
...sharedSecret,
|
||||||
orgName:
|
orgName:
|
||||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||||
? orgName
|
? orgName
|
||||||
: undefined
|
: undefined
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,7 +250,6 @@ export const secretSharingServiceFactory = ({
|
|||||||
createPublicSharedSecret,
|
createPublicSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getPasswordlessSecretByID,
|
getSharedSecretById
|
||||||
getValidatedSecretByID
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type TGetActiveSharedSecretByIdDTO = {
|
|||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
|
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
|
||||||
|
|||||||
Reference in New Issue
Block a user