fix db anonymization

This commit is contained in:
zadam 2020-06-02 23:13:55 +02:00
parent 38723e0189
commit 91e5f24798
14 changed files with 110 additions and 96 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.42.2", "version": "0.42.5",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -3345,9 +3345,9 @@
} }
}, },
"electron": { "electron": {
"version": "9.0.0", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-9.0.1.tgz",
"integrity": "sha512-JsaSQNPh+XDYkLj8APtVKTtvpb86KIG57W5OOss4TNrn8L3isC9LsCITwfnVmGIXHhvX6oY/weCtN5hAAytjVg==", "integrity": "sha512-PZsQ0juL5YyDfOKES3HWz7zbWidcRcmtTzFCHSNzeVMjlkWB+hQToWVczFuGEWzwbAM1rCFs9MT0V/zpYT3pqQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.0.1",

View File

@ -78,7 +78,7 @@
"yazl": "^2.5.1" "yazl": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"electron": "9.0.0", "electron": "9.0.1",
"electron-builder": "22.6.0", "electron-builder": "22.6.0",
"electron-packager": "14.2.1", "electron-packager": "14.2.1",
"electron-rebuild": "1.10.1", "electron-rebuild": "1.10.1",

View File

@ -1,7 +1,12 @@
const anonymizationService = require('./services/anonymization'); const backupService = require('./services/backup');
anonymizationService.anonymize().then(filePath => { backupService.anonymize().then(resp => {
console.log("Anonymized file has been saved to:", filePath); if (resp.success) {
console.log("Anonymization failed.");
}
else {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
}
process.exit(0); process.exit(0);
}); });

View File

@ -541,12 +541,13 @@ class Note extends Entity {
/** /**
* @return {Promise<Attribute>} * @return {Promise<Attribute>}
*/ */
async addAttribute(type, name, value = "") { async addAttribute(type, name, value = "", isInheritable = false) {
const attr = new Attribute({ const attr = new Attribute({
noteId: this.noteId, noteId: this.noteId,
type: type, type: type,
name: name, name: name,
value: value value: value,
isInheritable: isInheritable
}); });
await attr.save(); await attr.save();
@ -556,12 +557,12 @@ class Note extends Entity {
return attr; return attr;
} }
async addLabel(name, value = "") { async addLabel(name, value = "", isInheritable = false) {
return await this.addAttribute(LABEL, name, value); return await this.addAttribute(LABEL, name, value, isInheritable);
} }
async addRelation(name, targetNoteId) { async addRelation(name, targetNoteId, isInheritable = false) {
return await this.addAttribute(RELATION, name, targetNoteId); return await this.addAttribute(RELATION, name, targetNoteId, isInheritable);
} }
/** /**

View File

@ -61,9 +61,14 @@ export default class AdvancedOptions {
}); });
this.$anonymizeButton.on('click', async () => { this.$anonymizeButton.on('click', async () => {
await server.post('anonymization/anonymize'); const resp = await server.post('database/anonymize');
toastService.showMessage("Created anonymized database"); if (!resp.success) {
toastService.showError("Could not create anonymized database, check backend logs for details");
}
else {
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
}
}); });
this.$backupDatabaseButton.on('click', async () => { this.$backupDatabaseButton.on('click', async () => {

View File

@ -23,6 +23,7 @@ const mentionSetup = {
row.text = row.name = row.noteTitle; row.text = row.name = row.noteTitle;
row.id = '@' + row.text; row.id = '@' + row.text;
row.link = '#' + row.path; row.link = '#' + row.path;
row.notePath = row.path;
} }
res(rows); res(rows);

View File

@ -1,11 +0,0 @@
"use strict";
const anonymization = require('../../services/anonymization');
async function anonymize() {
await anonymization.anonymize();
}
module.exports = {
anonymize
};

View File

@ -5,6 +5,10 @@ const log = require('../../services/log');
const backupService = require('../../services/backup'); const backupService = require('../../services/backup');
const consistencyChecksService = require('../../services/consistency_checks'); const consistencyChecksService = require('../../services/consistency_checks');
async function anonymize() {
return await backupService.anonymize();
}
async function backupDatabase() { async function backupDatabase() {
return { return {
backupFile: await backupService.backupNow("now") backupFile: await backupService.backupNow("now")
@ -24,5 +28,6 @@ async function findAndFixConsistencyIssues() {
module.exports = { module.exports = {
backupDatabase, backupDatabase,
vacuumDatabase, vacuumDatabase,
findAndFixConsistencyIssues findAndFixConsistencyIssues,
anonymize
}; };

View File

@ -24,7 +24,6 @@ const exportRoute = require('./api/export');
const importRoute = require('./api/import'); const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup'); const setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql'); const sqlRoute = require('./api/sql');
const anonymizationRoute = require('./api/anonymization');
const databaseRoute = require('./api/database'); const databaseRoute = require('./api/database');
const imageRoute = require('./api/image'); const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes'); const attributesRoute = require('./api/attributes');
@ -220,7 +219,7 @@ function register(app) {
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema); apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
apiRoute(POST, '/api/sql/execute', sqlRoute.execute); apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize); route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
// backup requires execution outside of transaction // backup requires execution outside of transaction
route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false); route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);

View File

@ -1,38 +0,0 @@
"use strict";
const dataDir = require('./data_dir');
const dateUtils = require('./date_utils');
const fs = require('fs-extra');
const sqlite = require('sqlite');
async function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
}
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
fs.copySync(dataDir.DOCUMENT_PATH, anonymizedFile);
const db = await sqlite.open(anonymizedFile, {Promise});
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'title'");
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation'");
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
await db.run("VACUUM");
await db.close();
return anonymizedFile;
}
module.exports = {
anonymize
};

View File

@ -8,6 +8,8 @@ const log = require('./log');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const syncMutexService = require('./sync_mutex'); const syncMutexService = require('./sync_mutex');
const cls = require('./cls'); const cls = require('./cls');
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function regularBackup() { async function regularBackup() {
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600); await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
@ -28,36 +30,42 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
} }
} }
const BACKUP_ATTEMPT_COUNT = 50; const COPY_ATTEMPT_COUNT = 50;
async function backupNow(name) { async function copyFile(backupFile) {
const sql = require('./sql'); const sql = require('./sql');
try {
fs.unlinkSync(backupFile);
} catch (e) {
} // unlink throws exception if the file did not exist
let success = false;
let attemptCount = 0
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
try {
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
success = true;
} catch (e) {
log.info(`Copy DB attempt ${attemptCount + 1} failed with "${e.message}", retrying...`);
}
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
// which is difficult to guarantee so we just re-try
}
return attemptCount !== COPY_ATTEMPT_COUNT;
}
async function backupNow(name) {
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state // we don't want to backup DB in the middle of sync with potentially inconsistent DB state
return await syncMutexService.doExclusively(async () => { return await syncMutexService.doExclusively(async () => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`; const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
try { const success = await copyFile(backupFile, sql);
fs.unlinkSync(backupFile);
}
catch (e) {} // unlink throws exception if the file did not exist
let success = false; if (success) {
let attemptCount = 0
for (; attemptCount < BACKUP_ATTEMPT_COUNT && !success; attemptCount++) {
try {
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
success++;
}
catch (e) {
log.info(`Backup attempt ${attemptCount + 1} failed with "${e.message}", retrying...`);
}
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
// which is difficult to guarantee so we just re-try
}
if (attemptCount === BACKUP_ATTEMPT_COUNT) {
log.error(`Creating backup ${backupFile} failed`); log.error(`Creating backup ${backupFile} failed`);
} }
else { else {
@ -68,6 +76,44 @@ async function backupNow(name) {
}); });
} }
async function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
}
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
const success = await copyFile(anonymizedFile);
if (!success) {
return { success: false };
}
const db = await sqlite.open({
filename: anonymizedFile,
driver: sqlite3.Database
});
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'title'");
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name != 'template'");
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
await db.run("VACUUM");
await db.close();
return {
success: true,
anonymizedFilePath: anonymizedFile
};
}
if (!fs.existsSync(dataDir.BACKUP_DIR)) { if (!fs.existsSync(dataDir.BACKUP_DIR)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700); fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
} }
@ -80,5 +126,6 @@ sqlInit.dbReady.then(() => {
}); });
module.exports = { module.exports = {
backupNow backupNow,
anonymize
}; };

View File

@ -98,7 +98,7 @@ async function createNewNote(params) {
const parentNote = await repository.getNote(params.parentNoteId); const parentNote = await repository.getNote(params.parentNoteId);
if (!parentNote) { if (!parentNote) {
throw new Error(`Parent note ${params.parentNoteId} not found.`); throw new Error(`Parent note "${params.parentNoteId}" not found.`);
} }
if (!params.title || params.title.trim().length === 0) { if (!params.title || params.title.trim().length === 0) {