feat: secret sharing supports expiry on view count + multi-line secret value

This commit is contained in:
ShubhamPalriwala
2024-05-31 00:00:48 +05:30
parent c097c918ed
commit 3438dbc70d
10 changed files with 251 additions and 130 deletions
@@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews");
const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name");
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (!hasExpiresAfterViewsColumn) {
t.integer("expiresAfterViews").nullable();
t.timestamp("expiresAt").nullable().alter();
}
if (hasSecretNameColumn) {
t.dropColumn("name");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews");
const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name");
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (hasExpiresAfterViewsColumn) {
t.dropColumn("expiresAfterViews");
t.timestamp("expiresAt").notNullable().alter();
}
if (!hasSecretNameColumn) {
t.string("name").notNullable();
}
});
}
+3 -3
View File
@@ -9,16 +9,16 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
name: z.string(),
encryptedValue: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.date(),
expiresAt: z.date().nullable().optional(),
userId: z.string().uuid(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
expiresAfterViews: z.number().nullable().optional()
});
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
+2 -2
View File
@@ -23,8 +23,8 @@ export const UsersSchema = z.object({
isGhost: z.boolean().default(false),
username: z.string(),
isEmailVerified: z.boolean().default(false).nullable().optional(),
consecutiveFailedMfaAttempts: z.number().optional(),
isLocked: z.boolean().optional(),
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
isLocked: z.boolean().default(false).nullable().optional(),
temporaryLockDateEnd: z.date().nullable().optional()
});
@@ -45,7 +45,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
hashedHex: z.string()
}),
response: {
200: SecretSharingSchema.pick({ name: true, encryptedValue: true, iv: true, tag: true, expiresAt: true })
200: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true
})
}
},
handler: async (req) => {
@@ -55,11 +61,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
);
if (!sharedSecret) return undefined;
return {
name: sharedSecret.name,
encryptedValue: sharedSecret.encryptedValue,
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt
expiresAt: sharedSecret.expiresAt,
expiresAfterViews: sharedSecret.expiresAfterViews
};
}
});
@@ -72,14 +78,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
},
schema: {
body: z.object({
name: z.string(),
encryptedValue: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
message: "Expires at should be a future date"
})
expiresAt: z
.string()
.optional()
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
expiresAfterViews: z.number().optional()
}),
response: {
200: z.object({
@@ -89,19 +96,19 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { name, encryptedValue, iv, tag, hashedHex, expiresAt } = req.body;
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name,
encryptedValue,
iv,
tag,
hashedHex,
expiresAt: new Date(expiresAt)
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
expiresAfterViews
});
return { id: sharedSecret.id };
}
@@ -16,17 +16,28 @@ export const secretSharingServiceFactory = ({
secretSharingDAL
}: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } =
createSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const newSharedSecret = await secretSharingDAL.create({
name,
const {
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId,
encryptedValue,
iv,
tag,
hashedHex,
expiresAt,
expiresAfterViews
} = createSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const newSharedSecret = await secretSharingDAL.create({
encryptedValue,
iv,
tag,
hashedHex,
expiresAt,
expiresAfterViews,
userId: actorId,
orgId
});
@@ -43,9 +54,18 @@ export const secretSharingServiceFactory = ({
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
if (sharedSecret && sharedSecret.expiresAt < new Date()) {
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
return;
}
if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) {
if (sharedSecret.expiresAfterViews === 0) {
await secretSharingDAL.deleteById(sharedSecretId);
return;
}
await secretSharingDAL.updateById(sharedSecretId, {
expiresAfterViews: sharedSecret.expiresAfterViews - 1
});
}
return sharedSecret;
};
@@ -9,12 +9,12 @@ export type TSharedSecretPermission = {
};
export type TCreateSharedSecretDTO = {
name: string;
encryptedValue: string;
iv: string;
tag: string;
hashedHex: string;
expiresAt: Date;
expiresAt?: Date;
expiresAfterViews?: number;
} & TSharedSecretPermission;
export type TDeleteSharedSecretDTO = {