Add first-run dialog to migrate files to project

This commit is contained in:
Nick O'Leary
2017-12-19 00:56:02 +00:00
parent 474f4572f2
commit 33a5b84181
14 changed files with 638 additions and 85 deletions

View File

@@ -42,9 +42,11 @@ module.exports = {
runtime.storage.projects.listProjects(req.user, req.user).then(function(list) {
var active = runtime.storage.projects.getActiveProject(req.user);
var response = {
active: active.name,
projects: list
};
if (active) {
response.active = active.name;
}
res.json(response);
}).catch(function(err) {
console.log(err.stack);

View File

@@ -49,6 +49,8 @@ module.exports = {
safeSettings.editorTheme.palette.editable = false;
}
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
settings.exportNodeSettings(safeSettings);
res.json(safeSettings);
},

View File

@@ -27,6 +27,7 @@ var dirty = false;
var removeDefaultKey = false;
var encryptionEnabled = null;
var encryptionKeyType; // disabled, system, user, project
var encryptionAlgorithm = "aes-256-ctr";
var encryptionKey;
@@ -38,6 +39,11 @@ function decryptCredentials(key,credentials) {
var decrypted = decipher.update(creds, 'base64', 'utf8') + decipher.final('utf8');
return JSON.parse(decrypted);
}
function encryptCredentials(key,credentials) {
var initVector = crypto.randomBytes(16);
var cipher = crypto.createCipheriv(encryptionAlgorithm, key, initVector);
return {"$":initVector.toString('hex') + cipher.update(JSON.stringify(credentials), 'utf8', 'base64') + cipher.final('base64')};
}
var api = module.exports = {
init: function(_runtime) {
@@ -55,12 +61,46 @@ var api = module.exports = {
*/
load: function (credentials) {
dirty = false;
/*
- if encryptionEnabled === null, check the current configuration
*/
var credentialsEncrypted = credentials.hasOwnProperty("$") && Object.keys(credentials).length === 1;
// Case 1: Active Project in place
// - use whatever its config says
// Case 2: _credentialSecret unset, credentialSecret unset
// - generate _credentialSecret and encrypt
// Case 3: _credentialSecret set, credentialSecret set
// - migrate from _credentialSecret to credentialSecret
// - delete _credentialSecret
// Case 4: credentialSecret set
// - use it
var setupEncryptionPromise = when.resolve();
// if (encryptionEnabled === null) {
var projectKey = false;
var activeProject;
encryptionKeyType = "";
if (runtime.storage && runtime.storage.projects) {
// projects enabled
activeProject = runtime.storage.projects.getActiveProject();
if (activeProject) {
projectKey = activeProject.credentialSecret;
if (!projectKey) {
log.debug("red/runtime/nodes/credentials.load : using active project key - disabled");
encryptionKeyType = "disabled";
encryptionEnabled = false;
} else {
log.debug("red/runtime/nodes/credentials.load : using active project key");
encryptionKeyType = "project";
encryptionKey = crypto.createHash('sha256').update(projectKey).digest();
encryptionEnabled = true;
}
}
}
if (encryptionKeyType === '') {
var defaultKey;
try {
defaultKey = settings.get('_credentialSecret');
@@ -68,6 +108,7 @@ var api = module.exports = {
}
if (defaultKey) {
defaultKey = crypto.createHash('sha256').update(defaultKey).digest();
encryptionKeyType = "system";
}
var userKey;
try {
@@ -76,34 +117,13 @@ var api = module.exports = {
userKey = false;
}
var projectKey = false;
var activeProject;
if (runtime.storage && runtime.storage.projects) {
// projects enabled
activeProject = runtime.storage.projects.getActiveProject();
if (activeProject) {
projectKey = activeProject.credentialSecret;
}
}
if (projectKey) {
log.debug("red/runtime/nodes/credentials.load : using active project key - ignoring user provided key");
userKey = projectKey;
}
//TODO: Need to consider the various migration scenarios from no-project to project
// - _credentialSecret exists, projectKey exists
// - _credentialSecret does not exist, projectKey exists
// - userKey exists, projectKey exists
if (userKey === false) {
encryptionKeyType = "disabled";
log.debug("red/runtime/nodes/credentials.load : user disabled encryption");
// User has disabled encryption
encryptionEnabled = false;
// Check if we have a generated _credSecret to decrypt with and remove
if (defaultKey) {
console.log("****************************************************************");
console.log("* Oh oh - default key present. We don't handle this well today *");
console.log("****************************************************************");
log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate");
if (credentialsEncrypted) {
try {
@@ -123,11 +143,14 @@ var api = module.exports = {
if (!projectKey) {
log.debug("red/runtime/nodes/credentials.load : user provided key");
}
if (encryptionKeyType !== 'project') {
encryptionKeyType = 'user';
}
// User has provided own encryption key, get the 32-byte hash of it
encryptionKey = crypto.createHash('sha256').update(userKey).digest();
encryptionEnabled = true;
if (defaultKey) {
if (encryptionKeyType !== 'project' && defaultKey) {
log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate");
// User has provided their own key, but we already have a default key
// Decrypt using default key
@@ -148,6 +171,9 @@ var api = module.exports = {
} else {
log.debug("red/runtime/nodes/credentials.load : no user key present");
// User has not provide their own key
if (encryptionKeyType !== 'project') {
encryptionKeyType = 'system';
}
encryptionKey = defaultKey;
encryptionEnabled = true;
if (encryptionKey === undefined) {
@@ -169,13 +195,20 @@ var api = module.exports = {
log.debug("red/runtime/nodes/credentials.load : using default key");
}
}
//}
}
if (encryptionEnabled && !dirty) {
encryptedCredentials = credentials;
}
log.debug("red/runtime/nodes/credentials.load : keyType="+encryptionKeyType);
return setupEncryptionPromise.then(function() {
var clearInvalidFlag = false;
if (credentials.hasOwnProperty("$")) {
if (encryptionEnabled === false) {
var error = new Error("Failed to decrypt credentials");
error.code = "credentials_load_failed";
return when.reject(error);
}
// These are encrypted credentials
try {
credentialCache = decryptCredentials(encryptionKey,credentials)
@@ -343,9 +376,20 @@ var api = module.exports = {
return dirty;
},
setKey: function(key) {
encryptionKey = crypto.createHash('sha256').update(key).digest();
encryptionEnabled = true;
dirty = true;
if (key) {
encryptionKey = crypto.createHash('sha256').update(key).digest();
encryptionEnabled = true;
dirty = true;
encryptionKeyType = "project";
} else {
encryptionKey = null;
encryptionEnabled = false;
dirty = true;
encryptionKeyType = "disabled";
}
},
getKeyType: function() {
return encryptionKeyType;
},
export: function() {
var result = credentialCache;
@@ -354,9 +398,7 @@ var api = module.exports = {
if (dirty) {
try {
log.debug("red/runtime/nodes/credentials.export : encrypting");
var initVector = crypto.randomBytes(16);
var cipher = crypto.createCipheriv(encryptionAlgorithm, encryptionKey, initVector);
result = {"$":initVector.toString('hex') + cipher.update(JSON.stringify(credentialCache), 'utf8', 'base64') + cipher.final('base64')};
result = encryptCredentials(encryptionKey, credentialCache);
} catch(err) {
log.warn(log._("nodes.credentials.error-saving",{message:err.toString()}))
}

View File

@@ -175,5 +175,6 @@ module.exports = {
getCredentialDefinition: credentials.getDefinition,
setCredentialSecret: credentials.setKey,
clearCredentials: credentials.clear,
exportCredentials: credentials.export
exportCredentials: credentials.export,
getCredentialKeyType: credentials.getKeyType
};

View File

@@ -606,27 +606,53 @@ function createDefaultProject(user, project) {
// Create a basic skeleton of a project
return gitTools.initRepo(projectPath).then(function() {
var promises = [];
var files = Object.keys(defaultFileSet);
if (project.files) {
if (project.files.flow && !/\.\./.test(project.files.flow)) {
var flowFilePath;
var credsFilePath;
if (project.files.migrateFiles) {
var baseFlowFileName = fspath.basename(project.files.flow);
var baseCredentialFileName = fspath.basename(project.files.credentials);
files.push(baseFlowFileName);
files.push(baseCredentialFileName);
flowFilePath = fspath.join(projectPath,baseFlowFileName);
credsFilePath = fspath.join(projectPath,baseCredentialFileName);
log.trace("Migrating "+project.files.flow+" to "+flowFilePath);
log.trace("Migrating "+project.files.credentials+" to "+credsFilePath);
promises.push(fs.copy(project.files.flow,flowFilePath));
runtime.nodes.setCredentialSecret(project.credentialSecret);
promises.push(runtime.nodes.exportCredentials().then(function(creds) {
var credentialData;
if (settings.flowFilePretty) {
credentialData = JSON.stringify(creds,null,4);
} else {
credentialData = JSON.stringify(creds);
}
return util.writeFile(credsFilePath,credentialData);
}));
delete project.files.migrateFiles;
project.files.flow = baseFlowFileName;
project.files.credentials = baseCredentialFileName;
} else {
project.files.credentials = project.files.credentials || getCredentialsFilename(project.files.flow);
files.push(project.files.flow);
files.push(project.files.credentials);
flowFilePath = fspath.join(projectPath,project.files.flow);
credsFilePath = getCredentialsFilename(flowFilePath);
promises.push(util.writeFile(flowFilePath,"[]"));
promises.push(util.writeFile(credsFilePath,"{}"));
}
}
}
for (var file in defaultFileSet) {
if (defaultFileSet.hasOwnProperty(file)) {
promises.push(util.writeFile(fspath.join(projectPath,file),defaultFileSet[file](project)));
}
}
if (project.files) {
if (project.files.flow && !/\.\./.test(project.files.flow)) {
var flowFilePath = fspath.join(projectPath,project.files.flow);
promises.push(util.writeFile(flowFilePath,"[]"));
var credsFilePath = getCredentialsFilename(flowFilePath);
promises.push(util.writeFile(credsFilePath,"{}"));
}
}
return when.all(promises).then(function() {
var files = Object.keys(defaultFileSet);
if (project.files) {
if (project.files.flow && !/\.\./.test(project.files.flow)) {
files.push(project.files.flow);
files.push(getCredentialsFilename(flowFilePath))
}
}
return gitTools.stageFile(projectPath,files);
}).then(function() {
return gitTools.commit(projectPath,"Create project");

View File

@@ -14,18 +14,6 @@
* limitations under the License.
**/
var fspath = require("path");
function getCredentialsFilename(filename) {
// TODO: DRY - ./index.js
var ffDir = fspath.dirname(filename);
var ffExt = fspath.extname(filename);
var ffBase = fspath.basename(filename,ffExt);
return fspath.join(ffDir,ffBase+"_cred"+ffExt);
}
module.exports = {
"package.json": function(project) {
var package = {
@@ -41,7 +29,7 @@ module.exports = {
if (project.files) {
if (project.files.flow) {
package['node-red'].settings.flowFile = project.files.flow;
package['node-red'].settings.credentialsFile = getCredentialsFilename(project.files.flow);
package['node-red'].settings.credentialsFile = project.files.credentials;
}
}
return JSON.stringify(package,"",4);
@@ -50,7 +38,5 @@ module.exports = {
return project.name+"\n"+("=".repeat(project.name.length))+"\n\n"+(project.summary||"A Node-RED Project")+"\n\n";
},
"settings.json": function() { return "{}" },
// "flow.json": function() { return "[]" },
// "flow_cred.json": function() { return "{}" },
".gitignore": function() { return "*.backup" ;}
}

View File

@@ -270,6 +270,22 @@ function reloadActiveProject(action) {
}
function createProject(user, metadata) {
// var userSettings = getUserGitSettings(user);
if (metadata.files && metadata.files.migrateFiles) {
// We expect there to be no active project in this scenario
if (activeProject) {
throw new Error("Cannot migrate as there is an active project");
}
var currentEncryptionKey = settings.get('credentialSecret');
if (currentEncryptionKey === undefined) {
currentEncryptionKey = settings.get('_credentialSecret');
}
if (!metadata.hasOwnProperty('credentialSecret')) {
metadata.credentialSecret = currentEncryptionKey;
}
metadata.files.flow = flowsFullPath;
metadata.files.credentials = credentialsFile;
metadata.files.credentialSecret = currentEncryptionKey;
}
return Projects.create(null,metadata).then(function(p) {
return setActiveProject(user, p.name);
}).then(function() {
@@ -281,7 +297,7 @@ function setActiveProject(user, projectName) {
var globalProjectSettings = settings.get("projects");
globalProjectSettings.activeProject = project.name;
return settings.set("projects",globalProjectSettings).then(function() {
log.info(log._("storage.localfilesystem.projects.changing-project",{project:activeProject||"none"}));
log.info(log._("storage.localfilesystem.projects.changing-project",{project:(activeProject&&activeProject.name)||"none"}));
log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath}));
// console.log("Updated file targets to");
// console.log(flowsFullPath)