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

View File

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

View File

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

View File

@@ -61,9 +61,14 @@ export default class AdvancedOptions {
});
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 () => {

View File

@@ -23,6 +23,7 @@ const mentionSetup = {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
row.notePath = row.path;
}
res(rows);
@@ -256,4 +257,4 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.textEditor.model.insertContent(imageElement, this.textEditor.model.document.selection);
} );
}
}
}

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 consistencyChecksService = require('../../services/consistency_checks');
async function anonymize() {
return await backupService.anonymize();
}
async function backupDatabase() {
return {
backupFile: await backupService.backupNow("now")
@@ -24,5 +28,6 @@ async function findAndFixConsistencyIssues() {
module.exports = {
backupDatabase,
vacuumDatabase,
findAndFixConsistencyIssues
findAndFixConsistencyIssues,
anonymize
};

View File

@@ -24,7 +24,6 @@ const exportRoute = require('./api/export');
const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql');
const anonymizationRoute = require('./api/anonymization');
const databaseRoute = require('./api/database');
const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
@@ -220,7 +219,7 @@ function register(app) {
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
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
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 syncMutexService = require('./sync_mutex');
const cls = require('./cls');
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function regularBackup() {
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');
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
return await syncMutexService.doExclusively(async () => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
try {
fs.unlinkSync(backupFile);
}
catch (e) {} // unlink throws exception if the file did not exist
const success = await copyFile(backupFile, sql);
let success = false;
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) {
if (success) {
log.error(`Creating backup ${backupFile} failed`);
}
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)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
}
@@ -80,5 +126,6 @@ sqlInit.dbReady.then(() => {
});
module.exports = {
backupNow
backupNow,
anonymize
};

View File

@@ -98,7 +98,7 @@ async function createNewNote(params) {
const parentNote = await repository.getNote(params.parentNoteId);
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) {