Merge pull request #558 from knolleary/auth

Add bearer token authentication to Editor
This commit is contained in:
Nick O'Leary 2015-02-06 11:28:12 +00:00
commit 3123aa6279
49 changed files with 2232 additions and 321 deletions

View File

@ -16,7 +16,7 @@
<script type="text/x-red" data-template-name="inject">
<div class="form-row node-input-payload">
<label for="node-input-payloadType"><i class="fa fa-envelope"></i> Payload</label>
<label for="node-input-payloadType"><i class="fa fa-envelope"></i> Payload</label>
<select id="node-input-payloadType" style="width:73%">
<option value="date">timestamp</option>
<option value="string">string</option>
@ -30,12 +30,12 @@
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="topic" style="width: 70%x">
</div>
<div class="form-row">
<label for=""><i class="fa fa-repeat"></i> Repeat</label>
<label for=""><i class="fa fa-repeat"></i> Repeat</label>
<select id="inject-time-type-select" style="width: 73%"><option value="none">none</option><option value="interval">interval</option><option value="interval-time">interval between times</option><option value="time">at a specific time</option></select>
<input type="hidden" id="node-input-repeat" placeholder="payload">
<input type="hidden" id="node-input-crontab" placeholder="payload">
@ -60,7 +60,7 @@
<option value="15">15</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="0">60</option>
<option value="0">60</option>
</select> minutes<br/>
between <select id="inject-time-interval-time-start" class="inject-time-times"></select>
and <select id="inject-time-interval-time-end" class="inject-time-times"></select><br/>
@ -102,7 +102,7 @@
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="name">
</div>
@ -139,8 +139,8 @@
<p>If no payload is specified the payload is set to the current time in millisecs since 1970. This allows subsequent functions to perform time based actions.</p>
<p>The repeat function does what it says on the tin and continuously sends the payload every x seconds.</p>
<p>The Fire once at start option actually waits 50mS before firing to give other nodes a chance to instantiate properly.</p>
<p><b>Note: </b>"Interval between times" and "at a specific time" will use cron. This means that 20 minutes will be at the next hour, 20 minutes past and 40 minutes past - not in 20 minutes time.
If you want every 20 minutes from now - use the basic "interval" option.</p>
<p><b>Note: </b>"Interval between times" and "at a specific time" will use cron. This means that 20 minutes will be at the next hour, 20 minutes past and 40 minutes past - not in 20 minutes time.
If you want every 20 minutes from now - use the basic "interval" option.</p>
</script>
<script type="text/javascript">
@ -160,18 +160,18 @@
outputs:1,
icon: "inject.png",
label: function() {
if (this.payloadType === "string") {
if ((this.topic !== "") && ((this.topic.length + this.payload.length) <= 32)) {
return this.name||this.topic + ":" + this.payload;
}
else if (this.payload.length < 24) {
return this.name||this.payload;
}
if (this.payloadType === "string") {
if ((this.topic !== "") && ((this.topic.length + this.payload.length) <= 32)) {
return this.name||this.topic + ":" + this.payload;
}
else if (this.payload.length < 24) {
return this.name||this.payload;
}
}
if ((this.topic.length < 24) && (this.topic.length > 0)) {
return this.name||this.topic;
}
else { return this.name||(this.payloadType==="date"?"timestamp":null)||"inject"; }
if ((this.topic.length < 24) && (this.topic.length > 0)) {
return this.name||this.topic;
}
else { return this.name||(this.payloadType==="date"?"timestamp":null)||"inject"; }
},
labelStyle: function() {
return this.name?"node_label_italic":"";
@ -262,7 +262,7 @@
var d = new Date(value);
var h = d.getHours();
var m = d.getMinutes();
return ((h < 10)?"0":"")+h+":"+((m < 10)?"0":"")+m;
return ((h < 10)?"0":"")+h+":"+((m < 10)?"0":"")+m;
}
});
@ -274,7 +274,7 @@
var r = "s";
var c = this.repeat;
if (this.repeat % 60 === 0) { r = "m"; c = c/60; }
if (this.repeat % 1440 === 0) { r = "h"; c = c/60; }
if (this.repeat % 1440 === 0) { r = "h"; c = c/60; }
$("#inject-time-interval-count").val(c);
$("#inject-time-interval-units").val(r);
//$("#inject-time-interval-units option").filter(function() {return $(this).val() == "s";}).attr('selected',true);
@ -295,29 +295,29 @@
$("#inject-time-time-days [value=" + daya[i] + "]").attr("checked", "checked");
}
}
//else if (cronparts[0] == "0") {
// // interval - hours
// var hours = cronparts[1].slice(2);
// repeattype = "interval";
// $("#inject-time-interval-days").prop("disabled",false);
// $("#inject-time-interval-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
// $("#inject-time-interval-count").val(hours)
// $("#inject-time-interval-units option").filter(function() {return $(this).val() == "h";}).attr('selected',true);
//} else if (cronparts[1] == "*") {
// // interval - minutes
// var minutes = cronparts[0].slice(2);
// repeattype = "interval";
// $("#inject-time-interval-days").prop("disabled",false);
// $("#inject-time-interval-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
// $("#inject-time-interval-count").val(minutes)
// $("#inject-time-interval-units option").filter(function() {return $(this).val() == "m";}).attr('selected',true);
//}
else {
}
//else if (cronparts[0] == "0") {
// // interval - hours
// var hours = cronparts[1].slice(2);
// repeattype = "interval";
// $("#inject-time-interval-days").prop("disabled",false);
// $("#inject-time-interval-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
// $("#inject-time-interval-count").val(hours)
// $("#inject-time-interval-units option").filter(function() {return $(this).val() == "h";}).attr('selected',true);
//} else if (cronparts[1] == "*") {
// // interval - minutes
// var minutes = cronparts[0].slice(2);
// repeattype = "interval";
// $("#inject-time-interval-days").prop("disabled",false);
// $("#inject-time-interval-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
// $("#inject-time-interval-count").val(minutes)
// $("#inject-time-interval-units option").filter(function() {return $(this).val() == "m";}).attr('selected',true);
//}
else {
repeattype = "interval-time";
// interval - time period
var minutes = cronparts[0].slice(2);
if (minutes === "") { minutes = "0"; }
if (minutes === "") { minutes = "0"; }
$("#inject-time-interval-time-units").val(minutes);
//$("#inject-time-interval-time-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
if (days == "*") { days = "1,2,3,4,5,6,0"; }
@ -334,14 +334,14 @@
// 0 or 0-10
var hours = timeparts[0].split("-");
if (hours.length == 1) {
if (hours[0] === "") {
start = "0";
end = "0";
}
else {
start = hours[0];
end = Number(hours[0])+1;
}
if (hours[0] === "") {
start = "0";
end = "0";
}
else {
start = hours[0];
end = Number(hours[0])+1;
}
} else {
start = hours[0];
end = (Number(hours[1])+1)%24;
@ -380,7 +380,7 @@
$("#node-input-payloadType").change(function() {
var id = $("#node-input-payloadType option:selected").val();
if (id === "string") {
if (id === "string") {
$("#node-input-row-payload").show();
} else {
$("#node-input-row-payload").hide();
@ -409,7 +409,7 @@
repeat = count * 60;
} else if (units == "h") {
//crontab = "0 */"+count+" * * "+days;
repeat = count * 60 * 60;
repeat = count * 60 * 60;
}
}
} else if (type == "interval-time") {
@ -449,12 +449,12 @@
timerange = startpart+","+endpart;
}
repeat = "";
if (count === "0") {
crontab = count+" "+timerange+" * * "+days;
}
else {
crontab = "*/"+count+" "+timerange+" * * "+days;
}
if (count === "0") {
crontab = count+" "+timerange+" * * "+days;
}
else {
crontab = "*/"+count+" "+timerange+" * * "+days;
}
} else if (type == "time") {
var time = $("#inject-time-time").val();
//var days = $("#inject-time-time-days option:selected").val();
@ -475,24 +475,26 @@
button: {
onclick: function() {
var label = (this.name||this.payload).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
if (this.payloadType === "date") { label = "timestamp"; }
if (this.payloadType === "none") { label = "blank"; }
d3.xhr("inject/"+this.id).post(function(err,resp) {
if (err) {
if (err.status == 404) {
RED.notify("<strong>Error</strong>: inject node not deployed","error");
} else if (err.status == 500) {
RED.notify("<strong>Error</strong>: inject failed, see log for details.","error");
} else if (err.status == 0) {
RED.notify("<strong>Error</strong>: no response from server","error");
} else {
RED.notify("<strong>Error</strong>: unexpected error: ("+err.status+")"+err.response,"error");
}
} else if (resp.status == 200) {
RED.notify("Successfully injected: "+label,"success");
if (this.payloadType === "date") { label = "timestamp"; }
if (this.payloadType === "none") { label = "blank"; }
$.ajax({
url: "inject/"+this.id,
type:"POST",
success: function(resp) {
RED.notify("Successfully injected: "+label,"success");
},
error: function(jqXHR,textStatus,errorThrown) {
if (jqXHR.status == 404) {
RED.notify("<strong>Error</strong>: inject node not deployed","error");
} else if (jqXHR.status == 500) {
RED.notify("<strong>Error</strong>: inject failed, see log for details.","error");
} else if (jqXHR.status == 0) {
RED.notify("<strong>Error</strong>: no response from server","error");
} else {
RED.notify("<strong>Error</strong>: unexpected response: ("+resp.status+") "+resp.response,"error");
RED.notify("<strong>Error</strong>: unexpected error: ("+jqXHR.status+")"+textStatus,"error");
}
}
});
}
}

View File

@ -79,20 +79,20 @@ module.exports = function(RED) {
delete this.cronjob;
}
}
RED.httpAdmin.post("/inject/:id", function(req,res) {
var node = RED.nodes.getNode(req.params.id);
if (node != null) {
try {
node.receive();
res.send(200);
} catch(err) {
res.send(500);
node.error("Inject failed:"+err);
console.log(err.stack);
}
} else {
res.send(404);
RED.httpAdmin.post("/inject/:id", RED.auth.needsPermission("inject.write"), function(req,res) {
var node = RED.nodes.getNode(req.params.id);
if (node != null) {
try {
node.receive();
res.send(200);
} catch(err) {
res.send(500);
node.error("Inject failed:"+err);
console.log(err.stack);
}
} else {
res.send(404);
}
});
}

View File

@ -119,7 +119,7 @@ module.exports = function(RED) {
});
RED.log.addHandler(DebugNode.logHandler);
RED.httpAdmin.post("/debug/:id/:state", function(req,res) {
RED.httpAdmin.post("/debug/:id/:state", RED.auth.needsPermission("debug.write"), function(req,res) {
var node = RED.nodes.getNode(req.params.id);
var state = req.params.state;
if (node !== null && typeof node !== "undefined" ) {

View File

@ -23,6 +23,7 @@
"dependencies": {
"express": "3.17.2",
"when": "3.4.6",
"bcryptjs": "2.1.0",
"nopt": "3.0.1",
"mqtt": "0.3.x",
"ws": "0.4.32",
@ -49,7 +50,11 @@
"is-utf8":"0.2.0",
"serialport":"1.4.10",
"feedparser":"0.19.2",
"fs.notify":"0.0.4"
"fs.notify":"0.0.4",
"passport":"0.2.1",
"passport-http-bearer":"1.0.1",
"passport-oauth2-client-password":"0.1.2",
"oauth2orize":"1.0.1"
},
"devDependencies": {
"grunt": "0.4.5",

View File

@ -31,17 +31,18 @@
<body spellcheck="false">
<div id="header">
<span class="logo"><img src="node-red.png"> <span>Node-RED</span></span>
<ul class="header-toolbar">
<ul class="header-toolbar hide">
<li><span class="deploy-button-group button-group">
<a id="btn-deploy" class="action-deploy disabled" href="#"><img id="btn-icn-deploy" src="images/deploy-full-o.png"> <span>Deploy</span></a>
<a id="btn-deploy-options" data-toggle="dropdown" class="" href="#"><i class="fa fa-caret-down"></i></a>
</span></li>
<li><a id="btn-usermenu" class="button hide" data-toggle="dropdown" href="#"><i class="fa fa-user"></i></a></li>
<li><a id="btn-sidemenu" class="button" data-toggle="dropdown" href="#"><i class="fa fa-bars"></i></a></li>
<ul>
</div>
<div id="main-container" class="sidebar-closed">
<div id="main-container" class="sidebar-closed hide">
<div id="palette">
<img src="spin.svg" class="palette-spinner"/>
<img src="spin.svg" class="palette-spinner hide"/>
<div id="palette-container" class="palette-scroll">
</div>
<div id="palette-search">
@ -80,6 +81,7 @@
<div id="notifications"></div>
<div id="dropTarget"><div>Drop the flow here<br/><i class="fa fa-download"></i></div></div>
<div id="shade"></div>
<div id="dialog" class="hide"><form id="dialog-form" class="form-horizontal"></form></div>
<div id="node-config-dialog" class="hide"><form id="dialog-config-form" class="form-horizontal"></form><div class="form-tips" id="node-config-dialog-user-count"></div></div>
@ -246,12 +248,13 @@
<script src="orion/built-editor.min.js"></script>
<script src="d3.v3.min.js"></script>
<script src="red/main.js"></script>
<script src="red/settings.js"></script>
<script src="red/user.js"></script>
<script src="red/comms.js"></script>
<script src="red/ui/state.js"></script>
<script src="red/nodes.js"></script>
<script src="red/history.js"></script>
<script src="red/validators.js"></script>
<script src="red/settings.js"></script>
<script src="red/ui/menu.js"></script>
<script src="red/ui/keyboard.js"></script>
<script src="red/ui/tabs.js"></script>

BIN
public/node-red-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -17,27 +17,47 @@
RED.comms = (function() {
var errornotification = null;
var clearErrorTimer = null;
var subscriptions = {};
var ws;
var pendingAuth = false;
function connectWS() {
var path = location.hostname+":"+location.port+document.location.pathname;
path = path+(path.slice(-1) == "/"?"":"/")+"comms";
path = "ws"+(document.location.protocol=="https:"?"s":"")+"://"+path;
ws = new WebSocket(path);
ws.onopen = function() {
if (errornotification) {
errornotification.close();
errornotification = null;
}
var auth_tokens = RED.settings.get("auth-tokens");
pendingAuth = (auth_tokens!=null);
function completeConnection() {
for (var t in subscriptions) {
if (subscriptions.hasOwnProperty(t)) {
ws.send(JSON.stringify({subscribe:t}));
}
}
}
ws = new WebSocket(path);
ws.onopen = function() {
if (errornotification) {
clearErrorTimer = setTimeout(function() {
errornotification.close();
errornotification = null;
},1000);
}
if (pendingAuth) {
ws.send(JSON.stringify({auth:auth_tokens.access_token}));
} else {
completeConnection();
}
}
ws.onmessage = function(event) {
var msg = JSON.parse(event.data);
if (msg.topic) {
if (pendingAuth && msg.auth == "ok") {
pendingAuth = false;
completeConnection();
} else if (msg.topic) {
for (var t in subscriptions) {
if (subscriptions.hasOwnProperty(t)) {
var re = new RegExp("^"+t.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
@ -56,6 +76,9 @@ RED.comms = (function() {
ws.onclose = function() {
if (errornotification == null) {
errornotification = RED.notify("<b>Error</b>: Lost connection to server","error",true);
} else if (clearErrorTimer) {
clearTimeout(clearErrorTimer);
clearErrorTimer = null;
}
setTimeout(connectWS,1000);
}

View File

@ -22,34 +22,6 @@ var RED = (function() {
}
var deploymentType = "full";
function hideDropTarget() {
$("#dropTarget").hide();
RED.keyboard.remove(/* ESCAPE */ 27);
}
$('#chart').on("dragenter",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
$("#dropTarget").css({display:'table'});
RED.keyboard.add(/* ESCAPE */ 27,hideDropTarget);
}
});
$('#dropTarget').on("dragover",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
event.preventDefault();
}
})
.on("dragleave",function(event) {
hideDropTarget();
})
.on("drop",function(event) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
hideDropTarget();
RED.view.importNodes(data);
event.preventDefault();
});
function save(force) {
if (RED.view.dirty()) {
//$("#debug-tab-clear").click(); // uncomment this to auto clear debug on deploy
@ -290,14 +262,13 @@ var RED = (function() {
dialog.modal();
}
function changeDeploymentType(type) {
deploymentType = type;
$("#btn-deploy img").attr("src",deploymentTypes[type].img);
}
$(function() {
function loadEditor() {
RED.menu.init({id:"btn-sidemenu",
options: [
{id:"btn-sidebar",label:"Sidebar",toggle:true,onselect:RED.sidebar.toggleSidebar, selected: true},
@ -339,23 +310,68 @@ var RED = (function() {
]
});
RED.menu.init({id:"workspace-subflow-edit-menu",
options: [
{id:"btn-subflow-add-input",label:"Add Input", onselect:function() { }},
{id:"btn-subflow-add-output",label:"Add Output", onselect:function() { }},
{id:"btn-subflow-edit-name",label:"Edit Name", onselect:function() { }},
{id:"btn-subflow-delete",label:"Delete", onselect:function() { }},
]
});
if (RED.settings.user) {
RED.menu.init({id:"btn-usermenu",
options: []
});
var updateUserMenu = function() {
$("#btn-usermenu-submenu li").remove();
if (RED.settings.user.anonymous) {
RED.menu.addItem("btn-usermenu",{
id:"btn-login",
label:"Login",
onselect: function() {
RED.user.login({cancelable:true},function() {
RED.settings.load(function() {
RED.notify("Logged in as "+RED.settings.user.username,"success");
updateUserMenu();
});
});
}
});
} else {
RED.menu.addItem("btn-usermenu",{
id:"btn-username",
label:"<b>"+RED.settings.user.username+"</b>"
});
RED.menu.addItem("btn-usermenu",{
id:"btn-logout",
label:"Logout",
onselect: function() {
RED.user.logout();
}
});
}
}
updateUserMenu();
} else {
$("#btn-usermenu").parent().hide();
}
$("#main-container").show();
$(".header-toolbar").show();
RED.library.init();
RED.palette.init();
RED.sidebar.init();
RED.view.init();
RED.keyboard.add(/* ? */ 191,{shift:true},function(){showHelp();d3.event.preventDefault();});
loadSettings();
RED.comms.connect();
loadNodeList();
}
$(function() {
if ((window.location.hostname !== "localhost") && (window.location.hostname !== "127.0.0.1")) {
document.title = "Node-RED : "+window.location.hostname;
}
RED.settings.init(loadEditor);
});
if ((window.location.hostname !== "localhost") && (window.location.hostname !== "127.0.0.1")) {
document.title = "Node-RED : "+window.location.hostname;
}
return {
};

View File

@ -1,5 +1,5 @@
/**
* Copyright 2014 Antoine Aflalo
* Copyright 2014 IBM, Antoine Aflalo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
RED.settings = (function () {
var loadedSettings = {};
var hasLocalStorage = function () {
try {
return 'localStorage' in window && window['localStorage'] !== null;
@ -51,14 +54,37 @@ RED.settings = (function () {
};
var setProperties = function(data) {
for(var prop in data) {
if(data.hasOwnProperty(prop)) {
for (var prop in loadedSettings) {
if (loadedSettings.hasOwnProperty(prop) && RED.settings.hasOwnProperty(prop)) {
delete RED.settings[prop];
}
}
for (prop in data) {
if (data.hasOwnProperty(prop)) {
RED.settings[prop] = data[prop];
}
}
loadedSettings = data;
};
var init = function (callback) {
var init = function (done) {
$.ajaxSetup({
beforeSend: function(jqXHR,settings) {
// Only attach auth header for requests to relative paths
if (!/^\s*(https?:|\/|\.)/.test(settings.url)) {
var auth_tokens = RED.settings.get("auth-tokens");
if (auth_tokens) {
jqXHR.setRequestHeader("authorization","bearer "+auth_tokens.access_token);
}
}
}
});
load(done);
}
var load = function(done) {
$.ajax({
headers: {
"Accept": "application/json"
@ -68,18 +94,29 @@ RED.settings = (function () {
url: 'settings',
success: function (data) {
setProperties(data);
if (RED.settings.user && RED.settings.user.anonymous) {
RED.settings.remove("auth-tokens");
}
console.log("Node-RED: " + data.version);
callback();
done();
},
error: function(jqXHR,textStatus,errorThrown) {
if (jqXHR.status === 401) {
RED.user.login(function() { load(done); });
} else {
console.log("Unexpected error:",jqXHR.status,textStatus);
}
}
});
};
return {
init: init,
load: load,
set: set,
get: get,
remove: remove,
init : init
remove: remove
}
})
();
();

View File

@ -66,9 +66,6 @@ RED.library = (function() {
$("#btn-import-library-submenu").replaceWith(menu);
});
}
loadFlowLibrary();
function createUI(options) {
var libraryData = {};
@ -360,6 +357,9 @@ RED.library = (function() {
}
return {
init: function() {
loadFlowLibrary();
},
create: createUI,
loadFlowLibrary: loadFlowLibrary
}

View File

@ -139,6 +139,12 @@ RED.menu = (function() {
function createMenu(options) {
var button = $("#"+options.id);
//button.click(function(event) {
// $("#"+options.id+"-submenu").show();
// event.preventDefault();
//});
var topMenu = $("<ul/>",{id:options.id+"-submenu", class:"dropdown-menu pull-right"}).insertAfter(button);

View File

@ -36,8 +36,6 @@ RED.palette = (function() {
});
}
core.forEach(createCategoryContainer);
function setLabel(type, el,label) {
var nodeWidth = 80;
var nodeHeight = 25;
@ -241,32 +239,37 @@ RED.palette = (function() {
});
}
$("#palette-search-input").focus(function(e) {
RED.keyboard.disable();
});
$("#palette-search-input").blur(function(e) {
RED.keyboard.enable();
});
$("#palette-search-clear").on("click",function(e) {
e.preventDefault();
$("#palette-search-input").val("");
filterChange();
$("#palette-search-input").focus();
});
$("#palette-search-input").val("");
$("#palette-search-input").on("keyup",function() {
filterChange();
});
$("#palette-search-input").on("focus",function() {
$("body").one("mousedown",function() {
$("#palette-search-input").blur();
function init() {
$(".palette-spinner").show();
core.forEach(createCategoryContainer);
$("#palette-search-input").focus(function(e) {
RED.keyboard.disable();
});
});
$("#palette-search-input").blur(function(e) {
RED.keyboard.enable();
});
$("#palette-search-clear").on("click",function(e) {
e.preventDefault();
$("#palette-search-input").val("");
filterChange();
$("#palette-search-input").focus();
});
$("#palette-search-input").val("");
$("#palette-search-input").on("keyup",function() {
filterChange();
});
$("#palette-search-input").on("focus",function() {
$("body").one("mousedown",function() {
$("#palette-search-input").blur();
});
});
}
return {
init: init,
add:addNodeType,
remove:removeNodeType,
hide:hideNodeType,

View File

@ -26,6 +26,7 @@ RED.sidebar = (function() {
$("#"+tab.id).remove();
}
});
function addTab(title,content,closeable) {
$("#sidebar-content").append(content);
$(content).hide();
@ -129,22 +130,23 @@ RED.sidebar = (function() {
}
function showSidebar(id) {
//RED.menu.setSelected("btn-sidebar", true);
sidebar_tabs.activateTab("tab-" + id);
if (id) {
sidebar_tabs.activateTab("tab-"+id);
}
}
function containsTab(id) {
return sidebar_tabs.contains("tab-"+id);
}
$(function() {
function init () {
RED.keyboard.add(/* SPACE */ 32,{ctrl:true},function(){RED.menu.setSelected("btn-sidebar",!RED.menu.isSelected("btn-sidebar"));d3.event.preventDefault();});
showSidebar("info");
});
showSidebar();
RED.sidebar.info.show();
}
return {
init: init,
addTab: addTab,
removeTab: removeTab,
show: showSidebar,

View File

@ -32,8 +32,13 @@ RED.sidebar.info = (function() {
content.style.paddingLeft = "4px";
content.style.paddingRight = "4px";
RED.sidebar.addTab("info",content);
function show() {
if (!RED.sidebar.containsTab("info")) {
RED.sidebar.addTab("info",content,false);
}
RED.sidebar.show("info");
}
function jsonFilter(key,value) {
if (key === "") {
return value;
@ -116,6 +121,7 @@ RED.sidebar.info = (function() {
}
return {
show: show,
refresh:refresh,
clear: function() {
$("#tab-info").html("");

View File

@ -365,7 +365,8 @@ RED.view = (function() {
RED.history.push({t:'add',workspaces:[ws],dirty:dirty});
RED.view.dirty(true);
}
$(function() {
function init() {
$('#btn-workspace-add-tab').on("click",addWorkspace);
RED.menu.setAction('btn-workspace-add',addWorkspace);
@ -375,7 +376,7 @@ RED.view = (function() {
RED.menu.setAction('btn-workspace-delete',function() {
deleteWorkspace(activeWorkspace);
});
});
}
function deleteWorkspace(id) {
if (workspace_tabs.count() == 1) {
@ -2051,8 +2052,36 @@ RED.view = (function() {
}
});
function hideDropTarget() {
$("#dropTarget").hide();
RED.keyboard.remove(/* ESCAPE */ 27);
}
$('#chart').on("dragenter",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
$("#dropTarget").css({display:'table'});
RED.keyboard.add(/* ESCAPE */ 27,hideDropTarget);
}
});
$('#dropTarget').on("dragover",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
event.preventDefault();
}
})
.on("dragleave",function(event) {
hideDropTarget();
})
.on("drop",function(event) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
hideDropTarget();
RED.view.importNodes(data);
event.preventDefault();
});
return {
init: init,
state:function(state) {
if (state == null) {
return mouse_mode

117
public/red/user.js Normal file
View File

@ -0,0 +1,117 @@
/**
* Copyright 2014 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.user = (function() {
function login(opts,done) {
if (typeof opts == 'function') {
done = opts;
opts = {};
}
var dialog = $('<div id="node-dialog-login" class="hide">'+
'<div style="display: inline-block;width: 250px; vertical-align: top; margin-right: 10px; margin-bottom: 20px;"><img src="node-red-256.png"/></div>'+
'<div style="display: inline-block; width: 250px; vertical-align: bottom; margin-left: 10px; margin-bottom: 20px;">'+
'<form id="node-dialog-login-fields" class="form-horizontal" style="margin-bottom: 0px;"></form>'+
'</div>'+
'</div>');
dialog.dialog({
autoOpen: false,
dialogClass: "ui-dialog-no-close",
modal: true,
closeOnEscape: false,
width: 600,
resizable: false,
draggable: false
});
$("#node-dialog-login-fields").empty();
$.ajax({
dataType: "json",
url: "auth/login",
success: function(data) {
if (data.type == "credentials") {
var i=0;
for (;i<data.prompts.length;i++) {
var field = data.prompts[i];
var row = $("<div/>",{class:"form-row"});
$('<label for="node-dialog-login-'+field.id+'">'+field.label+':</label><br/>').appendTo(row);
$('<input style="width: 100%" id="node-dialog-login-'+field.id+'" type="'+field.type+'" tabIndex="'+(i+1)+'"/>').appendTo(row);
row.appendTo("#node-dialog-login-fields");
}
$('<div class="form-row" style="text-align: right; margin-top: 10px;"><span id="node-dialog-login-failed" style="line-height: 2em;float:left;" class="hide">Login failed</span><img src="spin.svg" style="height: 30px; margin-right: 10px; " class="login-spinner hide"/>'+
(opts.cancelable?'<a href="#" id="node-dialog-login-cancel" style="margin-right: 20px;" tabIndex="'+(i+1)+'">Cancel</a>':'')+
'<a href="#" id="node-dialog-login-submit" tabIndex="'+(i+2)+'">Login</a></div>').appendTo("#node-dialog-login-fields");
$("#node-dialog-login-submit").button().click(function( event ) {
$("#node-dialog-login-submit").button("option","disabled",true);
$("#node-dialog-login-failed").hide();
$(".login-spinner").show();
var body = {
client_id: "node-red-editor",
grant_type: "password",
scope:"*"
}
for (var i=0;i<data.prompts.length;i++) {
var field = data.prompts[i];
body[field.id] = $("#node-dialog-login-"+field.id).val();
}
$.ajax({
url:"auth/token",
type: "POST",
data: body
}).done(function(data,textStatus,xhr) {
RED.settings.set("auth-tokens",data);
$("#node-dialog-login").dialog('destroy').remove();
done();
}).fail(function(jqXHR,textStatus,errorThrown) {
RED.settings.remove("auth-tokens");
$("#node-dialog-login-failed").show();
}).always(function() {
$("#node-dialog-login-submit").button("option","disabled",false);
$(".login-spinner").hide();
});
event.preventDefault();
});
if (opts.cancelable) {
$("#node-dialog-login-cancel").button().click(function( event ) {
$("#node-dialog-login").dialog('destroy').remove();
});
}
}
dialog.dialog("open");
}
});
}
function logout() {
$.ajax({
url: "auth/revoke",
type: "POST",
data: {token:RED.settings.get("auth-tokens").access_token},
success: function() {
RED.settings.remove("auth-tokens");
document.location.reload(true);
}
})
}
return {
login: login,
logout: logout
}
})();

View File

@ -29,8 +29,11 @@ body {
background: #000;
box-sizing: border-box;
padding: 0px 0px 0px 20px;
color: #C7C7C7;
font-size: 14px;
}
#dropTarget {
position: absolute;
top: 0; bottom: 0;
@ -66,7 +69,6 @@ span.logo {
font-size: 30px;
line-height: 30px;
text-decoration: none;
color: #C7C7C7;
}
span.logo span {
vertical-align: middle;
@ -83,12 +85,15 @@ span.logo img {
}
#header ul.header-toolbar > li {
display: inline-block;
padding: 0;
margin: 0;
position: relative;
}
#header ul.header-toolbar > li {
display: inline-block;
}
.button {
-webkit-user-select: none;
-khtml-user-select: none;
@ -179,7 +184,7 @@ span.deploy-button-group.open > #btn-deploy.disabled + a {
.button-group {
display: inline-block;
margin: auto 10px;
margin: auto 15px;
vertical-align: middle;
background: #555;
clear: both;
@ -197,16 +202,16 @@ span.deploy-button-group.open > #btn-deploy.disabled + a {
.button-group > a:last-child {
}
#btn-sidemenu {
#header .button {
font-size: 20px !important;
}
#btn-sidemenu:active, #btn-sidemenu.active {
#header .button:active, #header .button.active {
background: #121212;
}
#header .button:focus {
outline: none;
}
li.open #btn-sidemenu {
#header li.open .button {
background: #121212;
border-color: #121212;
}
@ -1235,6 +1240,11 @@ i.spinner {
color: #666;
}
#header ul.dropdown-menu > li.disabled:hover > a,
#header ul.dropdown-menu > li.disabled:focus > a {
background: none !important;
}
/* Deploy menu customisations */
#header ul#btn-deploy-options-submenu {
width: 300px !important;
@ -1251,3 +1261,9 @@ i.spinner {
#header ul#btn-deploy-options-submenu li a > i.fa {
display: none !important;
}
/* User menu customisations */
#header ul#btn-usermenu-submenu li a#btn-username > .menu-label {
font-size: 16px;
color: #fff;
}

15
red.js
View File

@ -121,13 +121,14 @@ settings.flowFile = flowFile || settings.flowFile;
RED.init(server,settings);
if (settings.httpAdminRoot !== false && settings.httpAdminAuth) {
app.use(settings.httpAdminRoot,
express.basicAuth(function(user, pass) {
return user === settings.httpAdminAuth.user && crypto.createHash('md5').update(pass,'utf8').digest('hex') === settings.httpAdminAuth.pass;
})
);
}
//if (settings.httpAdminRoot !== false && settings.httpAdminAuth) {
// app.use(settings.httpAdminRoot,
// express.basicAuth(function(user, pass) {
// return user === settings.httpAdminAuth.user && crypto.createHash('md5').update(pass,'utf8').digest('hex') === settings.httpAdminAuth.pass;
// })
// );
//}
if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
app.use(settings.httpNodeRoot,
express.basicAuth(function(user, pass) {

33
red/api/auth/clients.js Normal file
View File

@ -0,0 +1,33 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var when = require("when");
var clients = [
{id:"node-red-editor",secret:"not_available"},
{id:"node-red-admin",secret:"not_available"}
];
module.exports = {
get: function(id) {
for (var i=0;i<clients.length;i++) {
if (clients[i].id == id) {
return when.resolve(clients[i]);
}
}
return when.resolve(null);
}
}

107
red/api/auth/index.js Normal file
View File

@ -0,0 +1,107 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var passport = require("passport");
var oauth2orize = require("oauth2orize");
var strategies = require("./strategies");
var Tokens = require("./tokens");
var Users = require("./users");
var permissions = require("./permissions");
var settings = null;
var log = require("../../log");
passport.use(strategies.bearerStrategy.BearerStrategy);
passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy);
passport.use(strategies.anonymousStrategy);
var server = oauth2orize.createServer();
server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange));
function init(_settings) {
settings = _settings;
if (settings.adminAuth) {
Users.init(settings.adminAuth);
Tokens.init(settings)
}
}
function needsPermission(permission) {
return function(req,res,next) {
if (settings.adminAuth) {
return passport.authenticate(['bearer','anon'],{ session: false })(req,res,function() {
if (!req.user) {
return next();
}
if (permissions.hasPermission(req.user,permission)) {
return next();
}
return res.send(401);
});
} else {
next();
}
}
}
function ensureClientSecret(req,res,next) {
if (!req.body.client_secret) {
req.body.client_secret = 'not_available';
}
next();
}
function authenticateClient(req,res,next) {
return passport.authenticate(['oauth2-client-password'], {session: false})(req,res,next);
}
function getToken(req,res,next) {
return server.token()(req,res,next);
}
function login(req,res) {
var response = {
"type":"credentials",
"prompts":[{id:"username",type:"text",label:"Username"},{id:"password",type:"password",label:"Password"}]
}
res.json(response);
}
function revoke(req,res) {
var token = req.body.token;
// TODO: audit log
Tokens.revoke(token).then(function() {
res.send(200);
});
}
module.exports = {
init: init,
needsPermission: needsPermission,
ensureClientSecret: ensureClientSecret,
authenticateClient: authenticateClient,
getToken: getToken,
errorHandler: function(err,req,res,next) {
//TODO: standardize json response
//TODO: audit log statment
//console.log(err.stack);
//log.log({level:"audit",type:"auth",msg:err.toString()});
return server.errorHandler()(err,req,res,next);
},
login: login,
revoke: revoke
}

View File

@ -0,0 +1,36 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var util = require('util');
var readRE = /^((.+)\.)?read$/
var writeRE = /^((.+)\.)?write$/
function hasPermission(user,permission) {
if (!user.permissions) {
return false;
}
if (user.permissions == "*") {
return true;
}
if (user.permissions == "read") {
return readRE.test(permission);
}
}
module.exports = {
hasPermission: hasPermission,
}

116
red/api/auth/strategies.js Normal file
View File

@ -0,0 +1,116 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var BearerStrategy = require('passport-http-bearer').Strategy;
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;
var passport = require("passport");
var crypto = require("crypto");
var util = require("util");
var Tokens = require("./tokens");
var Users = require("./users");
var Clients = require("./clients");
var bearerStrategy = function (accessToken, done) {
// is this a valid token?
Tokens.get(accessToken).then(function(token) {
if (token) {
Users.get(token.user).then(function(user) {
if (user) {
done(null,user,{scope:token.scope});
} else {
done(null,false);
}
});
} else {
done(null,false);
}
});
}
bearerStrategy.BearerStrategy = new BearerStrategy(bearerStrategy);
var clientPasswordStrategy = function(clientId, clientSecret, done) {
Clients.get(clientId).then(function(client) {
if (client && client.secret == clientSecret) {
done(null,client);
} else {
done(null,false);
}
});
}
clientPasswordStrategy.ClientPasswordStrategy = new ClientPasswordStrategy(clientPasswordStrategy);
var loginAttempts = [];
var loginSignUpWindow = 36000000; // 10 minutes
var passwordTokenExchange = function(client, username, password, scope, done) {
var now = Date.now();
loginAttempts = loginAttempts.filter(function(logEntry) {
return logEntry.time + loginSignUpWindow > now;
});
loginAttempts.push({time:now, user:username});
var attemptCount = 0;
loginAttempts.forEach(function(logEntry) {
if (logEntry.user == username) {
attemptCount++;
}
});
if (attemptCount > 5) {
// TODO: audit log
done(new Error("Too many login attempts. Wait 10 minutes and try again"),false);
return;
}
Users.authenticate(username,password).then(function(user) {
if (user) {
loginAttempts = loginAttempts.filter(function(logEntry) {
return logEntry.user !== username;
});
Tokens.create(username,client.id,scope).then(function(tokens) {
// TODO: audit log
done(null,tokens.accessToken);
});
} else {
// TODO: audit log
done(null,false);
}
});
}
function AnonymousStrategy() {
passport.Strategy.call(this);
this.name = 'anon';
}
util.inherits(AnonymousStrategy, passport.Strategy);
AnonymousStrategy.prototype.authenticate = function(req) {
var self = this;
Users.default().then(function(anon) {
if (anon) {
self.success(anon);
} else {
self.fail(401);
}
});
}
module.exports = {
bearerStrategy: bearerStrategy,
clientPasswordStrategy: clientPasswordStrategy,
passwordTokenExchange: passwordTokenExchange,
anonymousStrategy: new AnonymousStrategy()
}

View File

@ -0,0 +1,73 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var when = require("when");
var Sessions;
function generateToken(length) {
var c = "ABCDEFGHIJKLMNOPQRSTUZWXYZabcdefghijklmnopqrstuvwxyz1234567890";
var token = [];
for (var i=0;i<length;i++) {
token.push(c[Math.floor(Math.random()*c.length)]);
}
return token.join("");
}
var sessionModule;
function moduleSelector(aSettings) {
var toReturn;
if (aSettings.sessionStorageModule) {
if (typeof aSettings.sessionStorageModule === "string") {
// TODO: allow storage modules to be specified by absolute path
toReturn = require("./"+aSettings.sessionStorageModule);
} else {
toReturn = aSettings.sessionStorageModule;
}
} else {
toReturn = require("./localfilesystem");
}
return toReturn;
}
module.exports = {
init: function(settings) {
sessionModule = moduleSelector(settings);
sessionModule.init(settings);
},
get: function(token) {
return sessionModule.get(token);
},
create: function(user,client,scope) {
var accessToken = generateToken(128);
var session = {
user:user,
client:client,
scope:scope,
accessToken: accessToken,
};
return sessionModule.create(accessToken,session).then(function() {
return {
accessToken: accessToken,
}
});
},
revoke: function(token) {
return sessionModule.delete(token);
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require('fs');
var when = require('when');
var nodeFn = require('when/node/function');
var fspath = require("path");
var settings;
var sessionsFile;
var sessions = {};
/**
* Write content to a file using UTF8 encoding.
* This forces a fsync before completing to ensure
* the write hits disk.
*/
function writeFile(path,content) {
return when.promise(function(resolve,reject) {
var stream = fs.createWriteStream(path);
stream.on('open',function(fd) {
stream.end(content,'utf8',function() {
fs.fsync(fd,resolve);
});
});
stream.on('error',function(err) {
reject(err);
});
});
}
var api = module.exports = {
init: function(_settings) {
settings = _settings;
var userDir = settings.userDir || process.env.NODE_RED_HOME;
sessionsFile = fspath.join(userDir,".sessions.json");
try {
sessions = JSON.parse(fs.readFileSync(sessionsFile,'utf8'));
} catch(err) {
sessions = {};
}
return when.resolve();
},
get: function(token) {
return when.resolve(sessions[token]);
},
create: function(token,session) {
sessions[token] = session;
return writeFile(sessionsFile,JSON.stringify(sessions));
},
delete: function(token) {
delete sessions[token];
return writeFile(sessionsFile,JSON.stringify(sessions));
}
}

101
red/api/auth/users.js Normal file
View File

@ -0,0 +1,101 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var when = require("when");
var util = require("util");
var bcrypt = require('bcryptjs');
var users = {};
var passwords = {};
var defaultUser = null;
function authenticate(username,password) {
var user = users[username];
if (user) {
return when.promise(function(resolve,reject) {
bcrypt.compare(password, passwords[username], function(err, res) {
resolve(res?user:null);
});
});
}
return when.resolve(null);
}
function get(username) {
return when.resolve(users[username]);
}
function getDefaultUser() {
return when.resolve(null);
}
var api = {
get: get,
authenticate: authenticate,
default: getDefaultUser
}
function init(config) {
users = {};
passwords = {};
defaultUser = null;
if (config.type == "credentials") {
if (config.users) {
if (typeof config.users === "function") {
api.get = config.users;
} else {
var us = config.users;
if (!util.isArray(us)) {
us = [us];
}
for (var i=0;i<us.length;i++) {
var u = us[i];
users[u.username] = {
"username":u.username,
"permissions":u.permissions
};
passwords[u.username] = u.password;
}
}
}
if (config.authenticate && typeof config.authenticate === "function") {
api.authenticate = config.authenticate;
} else {
api.authenticate = authenticate;
}
}
if (config.default) {
if (typeof config.default === "function") {
api.default = config.default;
} else {
api.default = function() {
return when.resolve({
"anonymous": true,
"permissions":config.default.permissions
});
}
}
} else {
api.default = getDefaultUser;
}
}
module.exports = {
init: init,
get: function(username) { return api.get(username) },
authenticate: function(username,password) { return api.authenticate(username,password) },
default: function() { return api.default(); }
};

View File

@ -16,53 +16,79 @@
var express = require("express");
var util = require('util');
var path = require('path');
var passport = require('passport');
var ui = require("./ui");
var nodes = require("./nodes");
var flows = require("./flows");
var library = require("./library");
var info = require("./info");
var auth = require("./auth");
var needsPermission = auth.needsPermission;
var settings = require("../settings");
var errorHandler = function(err,req,res,next) {
//TODO: standardize json response
console.log(err.stack);
res.send(400,err.toString());
};
function init(adminApp) {
adminApp.use(express.json());
library.init(adminApp);
auth.init(settings);
// Editor
if (!settings.disableEditor) {
adminApp.get("/",ui.ensureSlash);
adminApp.get("/icons/:icon",ui.icon);
adminApp.get("/settings",ui.settings);
adminApp.use("/",ui.editor);
var editorApp = express();
editorApp.get("/",ui.ensureSlash);
editorApp.get("/icons/:icon",ui.icon);
editorApp.use("/",ui.editor);
adminApp.use(editorApp);
}
adminApp.use(express.json());
adminApp.use(express.urlencoded());
if (settings.adminAuth) {
//TODO: all passport references ought to be in ./auth
adminApp.use(passport.initialize());
adminApp.post("/auth/token",
auth.ensureClientSecret,
auth.authenticateClient,
auth.getToken,
auth.errorHandler
);
adminApp.get("/auth/login",auth.login);
adminApp.post("/auth/revoke",auth.revoke);
}
// Flows
adminApp.get("/flows",flows.get);
adminApp.post("/flows",flows.post);
adminApp.get("/flows",needsPermission("flows.read"),flows.get);
adminApp.post("/flows",needsPermission("flows.write"),flows.post);
// Nodes
adminApp.get("/nodes",nodes.getAll);
adminApp.post("/nodes",nodes.post);
adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll);
adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post);
adminApp.get("/nodes/:mod",nodes.getModule);
adminApp.put("/nodes/:mod",nodes.putModule);
adminApp.delete("/nodes/:mod",nodes.delete);
adminApp.get("/nodes/:mod/:set",nodes.getSet);
adminApp.put("/nodes/:mod/:set",nodes.putSet);
adminApp.get("/nodes/:mod",needsPermission("nodes.read"),nodes.getModule);
adminApp.put("/nodes/:mod",needsPermission("nodes.write"),nodes.putModule);
adminApp.delete("/nodes/:mod",needsPermission("nodes.write"),nodes.delete);
adminApp.get("/nodes/:mod/:set",needsPermission("nodes.read"),nodes.getSet);
adminApp.put("/nodes/:mod/:set",needsPermission("nodes.write"),nodes.putSet);
// Library
adminApp.post(new RegExp("/library/flows\/(.*)"),library.post);
adminApp.get("/library/flows",library.getAll);
adminApp.get(new RegExp("/library/flows\/(.*)"),library.get);
library.init(adminApp);
adminApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post);
adminApp.get("/library/flows",needsPermission("library.read"),library.getAll);
adminApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get);
// Settings
adminApp.get("/settings",needsPermission("settings.read"),info.settings);
// Error Handler
adminApp.use(errorHandler);
}

27
red/api/info.js Normal file
View File

@ -0,0 +1,27 @@
/**
* Copyright 2014 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var settings = require('../settings');
module.exports = {
settings: function(req,res) {
var safeSettings = {
httpNodeRoot: settings.httpNodeRoot,
version: settings.version,
user: req.user
};
res.json(safeSettings);
}
}

View File

@ -17,10 +17,11 @@
var redApp = null;
var storage = require("../storage");
var log = require("../log");
var needsPermission = require("./auth").needsPermission;
function createLibrary(type) {
if (redApp) {
redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),function(req,res) {
redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),needsPermission("library.read"),function(req,res) {
var path = req.params[1]||"";
storage.getLibraryEntry(type,path).then(function(result) {
if (typeof result === "string") {
@ -42,7 +43,7 @@ function createLibrary(type) {
});
});
redApp.post(new RegExp("/library/"+type+"\/(.*)"),function(req,res) {
redApp.post(new RegExp("/library/"+type+"\/(.*)"),needsPermission("library.write"),function(req,res) {
var path = req.params[0];
var fullBody = '';
req.on('data', function(chunk) {

View File

@ -55,12 +55,5 @@ module.exports = {
res.sendfile(defaultIcon);
}
},
settings: function(req,res) {
var safeSettings = {
httpNodeRoot: settings.httpNodeRoot,
version: settings.version
};
res.json(safeSettings);
},
editor: express.static(__dirname + '/../../public')
};

View File

@ -21,6 +21,7 @@ var server;
var settings;
var wsServer;
var pendingConnections = [];
var activeConnections = [];
var retained = {};
@ -34,53 +35,92 @@ function init(_server,_settings) {
settings = _settings;
}
function start() {
function start() {
var Tokens = require("./api/auth/tokens");
var Users = require("./api/auth/users");
var Permissions = require("./api/auth/permissions");
if (!settings.disableEditor) {
var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000;
var path = settings.httpAdminRoot || "/";
path = path + (path.slice(-1) == "/" ? "":"/") + "comms";
wsServer = new ws.Server({server:server,path:path});
wsServer.on('connection',function(ws) {
activeConnections.push(ws);
ws.on('close',function() {
for (var i=0;i<activeConnections.length;i++) {
if (activeConnections[i] === ws) {
activeConnections.splice(i,1);
break;
Users.default().then(function(anonymousUser) {
var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000;
var path = settings.httpAdminRoot || "/";
path = path + (path.slice(-1) == "/" ? "":"/") + "comms";
wsServer = new ws.Server({server:server,path:path});
wsServer.on('connection',function(ws) {
var pendingAuth = (settings.adminAuth != null);
if (!pendingAuth) {
activeConnections.push(ws);
} else {
pendingConnections.push(ws);
}
ws.on('close',function() {
removeActiveConnection(ws);
removePendingConnection(ws);
});
ws.on('message', function(data,flags) {
var msg = null;
try {
msg = JSON.parse(data);
} catch(err) {
log.warn("comms received malformed message : "+err.toString());
return;
}
}
if (!pendingAuth) {
if (msg.subscribe) {
handleRemoteSubscription(ws,msg.subscribe);
}
} else {
var completeConnection = function(user,sendAck) {
if (!user || !Permissions.hasPermission(user,"status.read")) {
ws.close();
} else {
pendingAuth = false;
removePendingConnection(ws);
activeConnections.push(ws);
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
}
if (msg.auth) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
completeConnection(user,true);
});
} else {
completeConnection(null,false);
}
});
} else {
completeConnection(anonymousUser,false);
//TODO: duplicated code - pull non-auth message handling out
if (msg.subscribe) {
handleRemoteSubscription(ws,msg.subscribe);
}
}
}
});
ws.on('error', function(err) {
log.warn("comms error : "+err.toString());
});
});
ws.on('message', function(data,flags) {
var msg = null;
try {
msg = JSON.parse(data);
} catch(err) {
log.warn("comms received malformed message : "+err.toString());
return;
}
if (msg.subscribe) {
handleRemoteSubscription(ws,msg.subscribe);
}
});
ws.on('error', function(err) {
log.warn("comms error : "+err.toString());
wsServer.on('error', function(err) {
log.warn("comms server error : "+err.toString());
});
lastSentTime = Date.now();
heartbeatTimer = setInterval(function() {
var now = Date.now();
if (now-lastSentTime > webSocketKeepAliveTime) {
publish("hb",lastSentTime);
}
}, webSocketKeepAliveTime);
});
wsServer.on('error', function(err) {
log.warn("comms server error : "+err.toString());
});
lastSentTime = Date.now();
heartbeatTimer = setInterval(function() {
var now = Date.now();
if (now-lastSentTime > webSocketKeepAliveTime) {
publish("hb",lastSentTime);
}
}, webSocketKeepAliveTime);
}
}
@ -123,6 +163,22 @@ function handleRemoteSubscription(ws,topic) {
}
}
function removeActiveConnection(ws) {
for (var i=0;i<activeConnections.length;i++) {
if (activeConnections[i] === ws) {
activeConnections.splice(i,1);
break;
}
}
}
function removePendingConnection(ws) {
for (var i=0;i<pendingConnections.length;i++) {
if (pendingConnections[i] === ws) {
pendingConnections.splice(i,1);
break;
}
}
}
module.exports = {
init:init,

View File

@ -96,6 +96,16 @@ var log = module.exports = {
warn: function(msg) {
log.log({level:log.WARN,msg:msg});
},
error: function(msg) {
log.log({level:log.ERROR,msg:msg});
},
trace: function(msg) {
log.log({level:log.TRACE,msg:msg});
},
debug: function(msg) {
log.log({level:log.DEBUG,msg:msg});
},
metric: function() {
return metricsEnabled;

View File

@ -23,6 +23,7 @@ var util = require("./util");
var fs = require("fs");
var settings = require("./settings");
var credentials = require("./nodes/credentials");
var auth = require("./api/auth");
var path = require('path');
@ -50,6 +51,9 @@ var RED = {
comms: comms,
settings:settings,
util: util,
auth: {
needsPermission: auth.needsPermission
},
version: function () {
var p = require(path.join(process.env.NODE_RED_HOME,"package.json"));
if (fs.existsSync(path.join(process.env.NODE_RED_HOME,".git"))) {

View File

@ -83,9 +83,6 @@ var persistentSettings = {
userSettings = null;
globalSettings = null;
storage = null;
}
}

View File

@ -18,6 +18,7 @@ var when = require('when');
var storageModule;
var settingsAvailable;
var sessionsAvailable;
function moduleSelector(aSettings) {
var toReturn;
@ -43,6 +44,7 @@ var storageModuleInterface = {
try {
storageModule = moduleSelector(settings);
settingsAvailable = storageModule.hasOwnProperty("getSettings") && storageModule.hasOwnProperty("saveSettings");
sessionsAvailable = storageModule.hasOwnProperty("getUserSessions") && storageModule.hasOwnProperty("saveUserSessions");
} catch (e) {
return when.reject(e);
}
@ -74,6 +76,7 @@ var storageModuleInterface = {
return when.resolve();
}
},
/* Library Functions */
getAllFlows: function() {
return storageModule.getAllFlows();

View File

@ -262,7 +262,6 @@ var localfilesystem = {
return writeFile(globalSettingsFile,JSON.stringify(settings,null,1));
},
getAllFlows: function() {
return listFiles(libFlowsDir);
},

View File

@ -0,0 +1,47 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var Clients = require("../../../../red/api/auth/clients");
describe("Clients", function() {
it('finds the known editor client',function(done) {
Clients.get("node-red-editor").then(function(client) {
client.should.have.property("id","node-red-editor");
client.should.have.property("secret","not_available");
done();
});
});
it('finds the known admin client',function(done) {
Clients.get("node-red-admin").then(function(client) {
client.should.have.property("id","node-red-admin");
client.should.have.property("secret","not_available");
done();
}).catch(function(err) {
done(err);
});
});
it('returns null for unknown client',function(done) {
Clients.get("unknown-client").then(function(client) {
should.not.exist(client);
done();
}).catch(function(err) {
done(err);
});
});
});

View File

@ -0,0 +1,85 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var when = require("when");
var sinon = require("sinon");
var passport = require("passport");
var auth = require("../../../../red/api/auth");
var Tokens = require("../../../../red/api/auth/tokens");
var settings = require("../../../../red/settings");
describe("api auth middleware",function() {
describe("ensureClientSecret", function() {
it("leaves client_secret alone if not present",function(done) {
var req = {
body: {
client_secret: "test_value"
}
};
auth.ensureClientSecret(req,null,function() {
req.body.should.have.a.property("client_secret","test_value");
done();
})
});
it("applies a default client_secret if not present",function(done) {
var req = {
body: { }
};
auth.ensureClientSecret(req,null,function() {
req.body.should.have.a.property("client_secret","not_available");
done();
})
});
});
describe("revoke", function() {
it("revokes a token", function(done) {
var revokeToken = sinon.stub(Tokens,"revoke",function() {
return when.resolve();
});
var req = { body: { token: "abcdef" } };
var res = { send: function(resp) {
revokeToken.restore();
resp.should.equal(200);
done();
}};
auth.revoke(req,res);
});
});
describe("login", function() {
it("returns login details", function(done) {
auth.login(null,{json: function(resp) {
resp.should.have.a.property("type","credentials");
resp.should.have.a.property("prompts");
resp.prompts.should.have.a.lengthOf(2);
done();
}});
});
});
});

View File

@ -0,0 +1,38 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var permissions = require("../../../../red/api/auth/permissions");
describe("Auth permissions", function() {
describe("hasPermission", function() {
it('a user with no permissions',function() {
permissions.hasPermission({},"*").should.be.false;
});
it('a user with global permissions',function() {
permissions.hasPermission({permissions:"*"},"read").should.be.true;
permissions.hasPermission({permissions:"*"},"write").should.be.true;
});
it('a user with read permissions',function() {
permissions.hasPermission({permissions:"read"},"read").should.be.true;
permissions.hasPermission({permissions:"read"},"node.read").should.be.true;
permissions.hasPermission({permissions:"read"},"write").should.be.false;
permissions.hasPermission({permissions:"read"},"node.write").should.be.false;
});
});
});

View File

@ -0,0 +1,214 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var when = require('when');
var sinon = require('sinon');
var strategies = require("../../../../red/api/auth/strategies");
var Users = require("../../../../red/api/auth/users");
var Tokens = require("../../../../red/api/auth/tokens");
var Clients = require("../../../../red/api/auth/clients");
describe("Auth strategies", function() {
describe("Password Token Exchange", function() {
var userAuthentication;
afterEach(function() {
if (userAuthentication) {
userAuthentication.restore();
}
});
it('Handles authentication failure',function(done) {
userAuthentication = sinon.stub(Users,"authenticate",function(username,password) {
return when.resolve(null);
});
strategies.passwordTokenExchange({},"user","password","scope",function(err,token) {
try {
should.not.exist(err);
token.should.be.false;
done();
} catch(e) {
done(e);
}
});
});
it('Creates new token on authentication success',function(done) {
userAuthentication = sinon.stub(Users,"authenticate",function(username,password) {
return when.resolve({username:"user"});
});
var tokenDetails = {};
var tokenCreate = sinon.stub(Tokens,"create",function(username,client,scope) {
tokenDetails.username = username;
tokenDetails.client = client;
tokenDetails.scope = scope;
return when.resolve({accessToken: "123456"});
});
strategies.passwordTokenExchange({id:"myclient"},"user","password","scope",function(err,token) {
try {
should.not.exist(err);
token.should.equal("123456");
tokenDetails.should.have.property("username","user");
tokenDetails.should.have.property("client","myclient");
tokenDetails.should.have.property("scope","scope");
done();
} catch(e) {
done(e);
} finally {
tokenCreate.restore();
}
});
});
});
describe("Anonymous Strategy", function() {
it('Succeeds if anon user enabled',function(done) {
var userDefault = sinon.stub(Users,"default",function() {
return when.resolve("anon");
});
strategies.anonymousStrategy._success = strategies.anonymousStrategy.success;
strategies.anonymousStrategy.success = function(user) {
user.should.equal("anon");
strategies.anonymousStrategy.success = strategies.anonymousStrategy._success;
delete strategies.anonymousStrategy._success;
userDefault.restore();
done();
};
strategies.anonymousStrategy.authenticate({});
});
it('Fails if anon user not enabled',function(done) {
var userDefault = sinon.stub(Users,"default",function() {
return when.resolve(null);
});
strategies.anonymousStrategy._fail = strategies.anonymousStrategy.fail;
strategies.anonymousStrategy.fail = function(err) {
err.should.equal(401);
strategies.anonymousStrategy.fail = strategies.anonymousStrategy._fail;
delete strategies.anonymousStrategy._fail;
userDefault.restore();
done();
};
strategies.anonymousStrategy.authenticate({});
});
});
describe("Bearer Strategy", function() {
it('Rejects invalid token',function(done) {
var getToken = sinon.stub(Tokens,"get",function(token) {
return when.resolve(null);
});
strategies.bearerStrategy("1234",function(err,user) {
try {
should.not.exist(err);
user.should.be.false;
done();
} catch(e) {
done(e);
} finally {
getToken.restore();
}
});
});
it('Accepts valid token',function(done) {
var getToken = sinon.stub(Tokens,"get",function(token) {
return when.resolve({user:"user",scope:"scope"});
});
var getUser = sinon.stub(Users,"get",function(username) {
return when.resolve("aUser");
});
strategies.bearerStrategy("1234",function(err,user,opts) {
try {
should.not.exist(err);
user.should.equal("aUser");
opts.should.have.a.property("scope","scope");
done();
} catch(e) {
done(e);
} finally {
getToken.restore();
getUser.restore();
}
});
});
});
describe("Client Password Strategy", function() {
it('Accepts valid client',function(done) {
var testClient = {id:"node-red-editor",secret:"not_available"};
var getClient = sinon.stub(Clients,"get",function(client) {
return when.resolve(testClient);
});
strategies.clientPasswordStrategy(testClient.id,testClient.secret,function(err,client) {
try {
should.not.exist(err);
client.should.eql(testClient);
done();
} catch(e) {
done(e);
} finally {
getClient.restore();
}
});
});
it('Rejects invalid client secret',function(done) {
var testClient = {id:"node-red-editor",secret:"not_available"};
var getClient = sinon.stub(Clients,"get",function(client) {
return when.resolve(testClient);
});
strategies.clientPasswordStrategy(testClient.id,"invalid_secret",function(err,client) {
try {
should.not.exist(err);
client.should.be.false;
done();
} catch(e) {
done(e);
} finally {
getClient.restore();
}
});
});
it('Rejects invalid client id',function(done) {
var testClient = {id:"node-red-editor",secret:"not_available"};
var getClient = sinon.stub(Clients,"get",function(client) {
return when.resolve(null);
});
strategies.clientPasswordStrategy("invalid_id","invalid_secret",function(err,client) {
try {
should.not.exist(err);
client.should.be.false;
done();
} catch(e) {
done(e);
} finally {
getClient.restore();
}
});
});
});
});

View File

@ -0,0 +1,147 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var when = require("when");
var sinon = require("sinon");
var Tokens = require("../../../../../red/api/auth/tokens");
describe("Tokens", function() {
describe("#init",function() {
var module = require("module");
var originalLoader;
beforeEach(function() {
originalLoader = module._load;
});
afterEach(function() {
module._load = originalLoader;
});
it('loads default storage plugin', function(done) {
module._load = function(name) {
name.should.equal("./localfilesystem");
return {init: function(settings) {done()}};
}
try {
Tokens.init({});
} catch(err) {
done(err);
}
});
it('loads the specified storage plugin', function(done) {
module._load = function(name) {
name.should.equal("./aTestExample");
return {init: function(settings) {done()}};
}
try {
Tokens.init({sessionStorageModule:"aTestExample"});
} catch(err) {
done(err);
}
});
it('uses the provided storage plugin', function(done) {
Tokens.init({sessionStorageModule:{init:function(settings){done()}}});
});
});
describe("#get",function() {
it('returns a valid token', function(done) {
Tokens.init({sessionStorageModule:{
init:function(settings){},
get: function(token) {
return when.resolve({user:"fred"});
}
}});
Tokens.get("1234").then(function(token) {
try {
token.should.have.a.property("user","fred");
done();
} catch(err) {
done(err);
}
});
});
it('returns null for an invalid token', function(done) {
Tokens.init({sessionStorageModule:{
init:function(settings){},
get: function(token) {
return when.resolve(null);
}
}});
Tokens.get("1234").then(function(token) {
try {
should.not.exist(token);
done();
} catch(err) {
done(err);
}
});
});
});
describe("#create",function() {
it('creates a token', function(done) {
var sessionStorageModule = {
init:function(settings){},
create: sinon.stub().returns(when.resolve())
};
Tokens.init({sessionStorageModule:sessionStorageModule});
Tokens.create("user","client","scope").then(function(token) {
try {
sessionStorageModule.create.called.should.be.true;
token.should.have.a.property('accessToken',sessionStorageModule.create.args[0][0]);
sessionStorageModule.create.args[0][1].should.have.a.property('user','user');
sessionStorageModule.create.args[0][1].should.have.a.property('client','client');
sessionStorageModule.create.args[0][1].should.have.a.property('scope','scope');
done();
} catch(err) {
done(err);
}
});
});
});
describe("#revoke", function() {
it('revokes a token', function(done) {
var deletedToken;
Tokens.init({sessionStorageModule:{
init:function(settings){},
delete: function(token) {
deletedToken = token;
return when.resolve(null);
}
}});
Tokens.revoke("1234").then(function() {
try {
deletedToken.should.equal("1234");
done();
} catch(err) {
done(err);
}
});
});
});
});

View File

@ -0,0 +1,96 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var when = require("when");
var sinon = require("sinon");
var fs = require('fs-extra');
var path = require('path');
var localfilesystem = require("../../../../../red/api/auth/tokens/localfilesystem.js");
describe("Tokens localfilesystem", function() {
var userDir = path.join(__dirname,".testUserHome");
beforeEach(function(done) {
fs.remove(userDir,function(err) {
fs.mkdir(userDir,done);
});
});
afterEach(function(done) {
fs.remove(userDir,done);
});
it("initialise when no session file exists",function(done) {
localfilesystem.init({userDir:userDir}).then(function() {
localfilesystem.get("1234").then(function(token) {
should.not.exist(token);
done();
});
});
});
it("initialises when session file exists", function(done) {
var sessions = {"1234":{"user":"nol","client":"node-red-admin","scope":["*"],"accessToken":"1234"}};
fs.writeFileSync(path.join(userDir,".sessions.json"),JSON.stringify(sessions),"utf8");
localfilesystem.init({userDir:userDir}).then(function() {
localfilesystem.get("1234").then(function(token) {
token.should.eql(sessions['1234']);
done();
});
});
});
it("writes new tokens to the session file",function(done) {
var sessions = {"1234":{"user":"nol","client":"node-red-admin","scope":["*"],"accessToken":"1234"}};
fs.writeFileSync(path.join(userDir,".sessions.json"),JSON.stringify(sessions),"utf8");
localfilesystem.init({userDir:userDir}).then(function() {
localfilesystem.create("5678",{
user:"fred",
client:"client",
scope:["read"],
accessToken:"5678"
}).then(function() {
var newSessions = JSON.parse(fs.readFileSync(path.join(userDir,".sessions.json"),"utf8"));
newSessions.should.have.a.property("1234");
newSessions.should.have.a.property("5678");
done();
});
});
});
it("deletes tokens from the session file",function(done) {
var sessions = {
"1234":{"user":"nol","client":"node-red-admin","scope":["*"],"accessToken":"1234"},
"5678":{"user":"fred","client":"client","scope":["read"],"accessToken":"5678"}
};
fs.writeFileSync(path.join(userDir,".sessions.json"),JSON.stringify(sessions),"utf8");
localfilesystem.init({userDir:userDir}).then(function() {
localfilesystem.delete("5678").then(function() {
var newSessions = JSON.parse(fs.readFileSync(path.join(userDir,".sessions.json"),"utf8"));
newSessions.should.have.a.property("1234");
newSessions.should.not.have.a.property("5678");
done();
});
});
});
});

View File

@ -0,0 +1,186 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var when = require('when');
var sinon = require('sinon');
var Users = require("../../../../red/api/auth/users");
describe("Users", function() {
describe('Initalised with a credentials object, no anon',function() {
before(function() {
Users.init({
type:"credentials",
users:[{
username:"fred",
password:'$2a$08$LpYMefvGZ3MjAfZGzcoyR.1BcfHh4wy4NpbN.cEny5aHnWOqjKOXK',
// 'password' -> require('bcryptjs').hashSync('password', 8);
permissions:"*"
}]
});
});
describe('#get',function() {
it('returns known user',function(done) {
Users.get("fred").then(function(user) {
try {
user.should.have.a.property("username","fred");
user.should.have.a.property("permissions","*");
user.should.not.have.a.property("password");
done();
} catch(err) {
done(err);
}
});
});
it('returns null for unknown user', function(done) {
Users.get("barney").then(function(user) {
try {
should.not.exist(user);
done();
} catch(err) {
done(err);
}
});
});
});
describe('#default',function() {
it('returns null for default user', function(done) {
Users.default().then(function(user) {
try {
should.not.exist(user);
done();
} catch(err) {
done(err);
}
});
});
});
describe('#authenticate',function() {
it('authenticates a known user', function(done) {
Users.authenticate('fred','password').then(function(user) {
try {
user.should.have.a.property("username","fred");
user.should.have.a.property("permissions","*");
user.should.not.have.a.property("password");
done();
} catch(err) {
done(err);
}
});
});
it('rejects invalid password for a known user', function(done) {
Users.authenticate('fred','wrong').then(function(user) {
try {
should.not.exist(user);
done();
} catch(err) {
done(err);
}
});
});
it('rejects invalid user', function(done) {
Users.authenticate('barney','wrong').then(function(user) {
try {
should.not.exist(user);
done();
} catch(err) {
done(err);
}
});
});
});
});
describe('Initalised with a credentials object including anon',function() {
before(function() {
Users.init({
type:"credentials",
users:[],
default: { permissions: "*" }
});
});
describe('#default',function() {
it('returns default user', function(done) {
Users.default().then(function(user) {
try {
user.should.have.a.property('anonymous',true);
user.should.have.a.property('permissions','*');
done();
} catch(err) {
done(err);
}
});
});
});
});
describe('Initialised with a credentials object with user functions',function() {
var authUsername = '';
var authPassword = '';
before(function() {
Users.init({
type:"credentials",
users:function(username) {
return when.resolve({'username':'dave','permissions':'read'});
},
authenticate: function(username,password) {
authUsername = username;
authPassword = password;
return when.resolve({'username':'pete','permissions':'write'});
}
});
});
describe('#get',function() {
it('delegates get user',function(done) {
Users.get('dave').then(function(user) {
try {
user.should.have.a.property("username","dave");
user.should.have.a.property("permissions","read");
user.should.not.have.a.property("password");
done();
} catch(err) {
done(err);
}
});
});
it('delegates authenticate user',function(done) {
Users.authenticate('pete','secret').then(function(user) {
try {
user.should.have.a.property("username","pete");
user.should.have.a.property("permissions","write");
user.should.not.have.a.property("password");
authUsername.should.equal('pete');
authPassword.should.equal('secret');
done();
} catch(err) {
done(err);
}
});
});
});
});
});

View File

@ -20,7 +20,6 @@ var express = require('express');
var sinon = require('sinon');
var when = require('when');
var app = express();
var redNodes = require("../../../red/nodes");
var flows = require("../../../red/api/flows");

View File

@ -47,12 +47,11 @@ describe("api index", function() {
.get("/icons/default.png")
.expect(404,done)
});
it('does not serve settings', function(done) {
it('serves settings', function(done) {
request(app)
.get("/settings")
.expect(404,done)
.expect(200,done)
});
});
describe("enables editor", function() {

60
test/red/api/info_spec.js Normal file
View File

@ -0,0 +1,60 @@
/**
* Copyright 2014 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var request = require('supertest');
var express = require('express');
var sinon = require('sinon');
var when = require('when');
var app = express();
var settings = require("../../../red/settings");
var info = require("../../../red/api/info");
describe("info api", function() {
describe("settings handler", function() {
before(function() {
var userSettings = {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion"
}
settings.init(userSettings);
app = express();
app.get("/settings",info.settings);
});
after(function() {
settings.reset();
});
it('returns the filtered settings', function(done) {
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.not.have.property("foo",123);
done();
});
});
});
});

View File

@ -24,6 +24,7 @@ var app = express();
var RED = require("../../../red/red.js");
var storage = require("../../../red/storage");
var library = require("../../../red/api/library");
var auth = require("../../../red/api/auth");
describe("library api", function() {
@ -166,6 +167,7 @@ describe("library api", function() {
app = express();
app.use(express.json());
library.init(app);
auth.init({});
RED.library.register("test");
});

View File

@ -20,7 +20,6 @@ var express = require('express');
var sinon = require('sinon');
var when = require('when');
var app = express();
var redNodes = require("../../../red/nodes");
var server = require("../../../red/server");
var settings = require("../../../red/settings");

View File

@ -20,7 +20,6 @@ var express = require("express");
var fs = require("fs");
var path = require("path");
var settings = require("../../../red/settings");
var events = require("../../../red/events");
var ui = require("../../../red/api/ui");
@ -135,39 +134,6 @@ describe("ui api", function() {
});
});
describe("settings handler", function() {
before(function() {
var userSettings = {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion"
}
settings.init(userSettings);
app = express();
app.get("/settings",ui.settings);
//app.use("/",ui.editor);
});
after(function() {
settings.reset();
});
it('returns the filtered settings', function(done) {
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.not.have.property("foo",123);
done();
});
});
});
describe("editor ui handler", function() {
before(function() {
app = express();

View File

@ -1,5 +1,5 @@
/**
* Copyright 2014 IBM Corp.
* Copyright 2014, 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,12 +15,18 @@
**/
var should = require("should");
var sinon = require("sinon");
var when = require("when");
var http = require('http');
var express = require('express');
var app = express();
var WebSocket = require('ws');
var comms = require("../../red/comms.js");
var Users = require("../../red/api/auth/users");
var Tokens = require("../../red/api/auth/tokens");
var address = '127.0.0.1';
var listenPort = 0; // use ephemeral port
@ -188,5 +194,152 @@ describe("comms", function() {
});
});
});
describe('authentication required, no anonymous',function() {
var server;
var url;
var port;
var getDefaultUser;
var getUser;
var getToken;
before(function(done) {
getDefaultUser = sinon.stub(Users,"default",function() { return when.resolve(null);});
getUser = sinon.stub(Users,"get", function(username) {
if (username == "fred") {
return when.resolve({permissions:"read"});
} else {
return when.resolve(null);
}
});
getToken = sinon.stub(Tokens,"get",function(token) {
if (token == "1234") {
return when.resolve({user:"fred"});
} else if (token == "5678") {
return when.resolve({user:"barney"});
} else {
return when.resolve(null);
}
});
server = http.createServer(function(req,res){app(req,res)});
comms.init(server, {adminAuth:{}});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function() {
getDefaultUser.restore();
getUser.restore();
getToken.restore();
comms.stop();
});
it('prevents connections that do not authenticate',function(done) {
var ws = new WebSocket(url);
var count = 0;
var interval;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
});
ws.on('close', function() {
done();
});
});
it('allows connections that do authenticate',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"1234"}');
});
ws.on('message', function(msg) {
received++;
if (received == 1) {
msg.should.equal('{"auth":"ok"}');
ws.send('{"subscribe":"foo"}');
comms.publish('foo', 'correct');
} else {
msg.should.equal('{"topic":"foo","data":"correct"}');
ws.close();
}
});
ws.on('close', function() {
received.should.equal(2);
done();
});
});
it('rejects connections for non-existant token',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"2345"}');
});
ws.on('close', function() {
done();
});
});
it('rejects connections for invalid token',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"5678"}');
});
ws.on('close', function() {
done();
});
});
});
describe('authentication required, anonymous enabled',function() {
var server;
var url;
var port;
var getDefaultUser;
before(function(done) {
getDefaultUser = sinon.stub(Users,"default",function() { return when.resolve({permissions:"read"});});
server = http.createServer(function(req,res){app(req,res)});
comms.init(server, {adminAuth:{}});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function() {
getDefaultUser.restore();
comms.stop();
});
it('allows anonymous connections that do not authenticate',function(done) {
var ws = new WebSocket(url);
var count = 0;
var interval;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
setTimeout(function() {
comms.publish('foo', 'correct');
},200);
});
ws.on('message', function(msg) {
msg.should.equal('{"topic":"foo","data":"correct"}');
count++;
ws.close();
});
ws.on('close', function() {
count.should.equal(1);
done();
});
});
});
});