feat: simplified endpoints to support password based secret sharing

This commit is contained in:
=
2024-08-10 22:19:42 +05:30
parent 8479c406a5
commit a5555c3816
3 changed files with 78 additions and 178 deletions
@@ -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({
encryptedValue: true, isPasswordProtected: z.boolean(),
iv: true, secret: SecretSharingSchema.pick({
tag: true, encryptedValue: true,
expiresAt: true, iv: true,
expiresAfterViews: true, tag: true,
accessType: true expiresAt: true,
}).extend({ expiresAfterViews: true,
orgName: z.string().optional() accessType: true
})
.extend({
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 { return {
...sharedSecret, isPasswordProtected,
orgName: secret: {
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId ...sharedSecret,
? orgName orgName:
: undefined 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 {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: 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 & {