mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
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:
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user