Add script to backfill ApiKey hashes (#7717)

* Add hashed column for API keys

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Tom Moor
2024-10-03 19:27:25 -04:00
committed by GitHub
parent be5f092117
commit 1a02b0d9d7
3 changed files with 94 additions and 14 deletions
@@ -0,0 +1,29 @@
"use strict";
const { execFileSync } = require("child_process");
const path = require("path");
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up() {
if (
process.env.NODE_ENV === "test" ||
process.env.DEPLOYMENT === "hosted"
) {
return;
}
const scriptName = path.basename(__filename);
const scriptPath = path.join(
process.cwd(),
"build",
`server/scripts/${scriptName}`
);
execFileSync("node", [scriptPath], { stdio: "inherit" });
},
async down() {
// noop
},
};
+12 -14
View File
@@ -2,7 +2,6 @@ import crypto from "crypto";
import { subMinutes } from "date-fns";
import randomstring from "randomstring";
import { InferAttributes, InferCreationAttributes, Op } from "sequelize";
import { type BuildOptions } from "sequelize";
import {
Column,
Table,
@@ -12,6 +11,7 @@ import {
ForeignKey,
IsDate,
DataType,
AfterFind,
BeforeSave,
} from "sequelize-typescript";
import { ApiKeyValidation } from "@shared/validations";
@@ -28,18 +28,6 @@ class ApiKey extends ParanoidModel<
> {
static prefix = "ol_api_";
constructor(
values?: Partial<InferCreationAttributes<ApiKey>>,
options?: BuildOptions
) {
// Temporary until last4 is backfilled and secret is removed.
if (values?.secret) {
values.last4 = values.secret.slice(-4);
}
super(values, options);
}
@Length({
min: ApiKeyValidation.minNameLength,
max: ApiKeyValidation.maxNameLength,
@@ -76,6 +64,16 @@ class ApiKey extends ParanoidModel<
// hooks
@AfterFind
public static async afterFindHook(models: ApiKey | ApiKey[]) {
const modelsArray = Array.isArray(models) ? models : [models];
for (const model of modelsArray) {
if (model?.secret) {
model.last4 = model.secret.slice(-4);
}
}
}
@BeforeValidate
public static async generateSecret(model: ApiKey) {
if (!model.hash) {
@@ -99,7 +97,7 @@ class ApiKey extends ParanoidModel<
* @param key The input string to hash
* @returns The hashed API key
*/
private static hash(key: string) {
public static hash(key: string) {
return crypto.createHash("sha256").update(key).digest("hex");
}
@@ -0,0 +1,53 @@
import "./bootstrap";
import { Transaction } from "sequelize";
import { ApiKey } from "@server/models";
import { sequelize } from "@server/storage/database";
let page = parseInt(process.argv[2], 10);
page = Number.isNaN(page) ? 0 : page;
export default async function main(exit = false, limit = 100) {
const work = async (page: number): Promise<void> => {
console.log(`Backfill apiKey hash… page ${page}`);
let apiKeys: ApiKey[] = [];
await sequelize.transaction(async (transaction) => {
apiKeys = await ApiKey.unscoped().findAll({
limit,
offset: page * limit,
order: [["createdAt", "ASC"]],
lock: Transaction.LOCK.UPDATE,
transaction,
});
for (const apiKey of apiKeys) {
try {
if (!apiKey.hash) {
console.log(`Migrating ${apiKey.id}`);
apiKey.value = apiKey.secret;
apiKey.hash = ApiKey.hash(apiKey.secret);
// @ts-expect-error secret is deprecated
apiKey.secret = null;
await apiKey.save({ transaction });
}
} catch (err) {
console.error(`Failed at ${apiKey.id}:`, err);
continue;
}
}
});
return apiKeys.length === limit ? work(page + 1) : undefined;
};
await work(page);
console.log("Backfill complete");
if (exit) {
process.exit(0);
}
}
// In the test suite we import the script rather than run via node CLI
if (process.env.NODE_ENV !== "test") {
void main(true);
}