mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #558 from knolleary/auth
Add bearer token authentication to Editor
This commit is contained in:
commit
3123aa6279
@ -477,21 +477,23 @@
|
||||
var label = (this.name||this.payload).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
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) {
|
||||
|
||||
$.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 (err.status == 500) {
|
||||
} else if (jqXHR.status == 500) {
|
||||
RED.notify("<strong>Error</strong>: inject failed, see log for details.","error");
|
||||
} else if (err.status == 0) {
|
||||
} else if (jqXHR.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");
|
||||
RED.notify("<strong>Error</strong>: unexpected error: ("+jqXHR.status+")"+textStatus,"error");
|
||||
}
|
||||
} else if (resp.status == 200) {
|
||||
RED.notify("Successfully injected: "+label,"success");
|
||||
} else {
|
||||
RED.notify("<strong>Error</strong>: unexpected response: ("+resp.status+") "+resp.response,"error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
RED.httpAdmin.post("/inject/:id", function(req,res) {
|
||||
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 {
|
||||
|
@ -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" ) {
|
||||
|
@ -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",
|
||||
|
@ -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
BIN
public/node-red-256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
@ -291,13 +263,12 @@ 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,24 +310,69 @@ 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: []
|
||||
});
|
||||
|
||||
RED.keyboard.add(/* ? */ 191,{shift:true},function(){showHelp();d3.event.preventDefault();});
|
||||
loadSettings();
|
||||
RED.comms.connect();
|
||||
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();});
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
};
|
||||
})();
|
||||
|
@ -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) {
|
||||
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
|
||||
}
|
||||
})
|
||||
();
|
@ -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
|
||||
}
|
||||
|
@ -140,6 +140,12 @@ RED.menu = (function() {
|
||||
|
||||
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);
|
||||
|
||||
for (var i=0;i<options.options.length;i++) {
|
||||
|
@ -36,8 +36,6 @@ RED.palette = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
core.forEach(createCategoryContainer);
|
||||
|
||||
function setLabel(type, el,label) {
|
||||
var nodeWidth = 80;
|
||||
var nodeHeight = 25;
|
||||
@ -241,6 +239,9 @@ RED.palette = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
$(".palette-spinner").show();
|
||||
core.forEach(createCategoryContainer);
|
||||
$("#palette-search-input").focus(function(e) {
|
||||
RED.keyboard.disable();
|
||||
});
|
||||
@ -265,8 +266,10 @@ RED.palette = (function() {
|
||||
$("#palette-search-input").blur();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
add:addNodeType,
|
||||
remove:removeNodeType,
|
||||
hide:hideNodeType,
|
||||
|
@ -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);
|
||||
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,
|
||||
|
@ -32,7 +32,12 @@ 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 === "") {
|
||||
@ -116,6 +121,7 @@ RED.sidebar.info = (function() {
|
||||
}
|
||||
|
||||
return {
|
||||
show: show,
|
||||
refresh:refresh,
|
||||
clear: function() {
|
||||
$("#tab-info").html("");
|
||||
|
@ -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) {
|
||||
@ -2052,7 +2053,35 @@ 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
117
public/red/user.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
@ -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
15
red.js
@ -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
33
red/api/auth/clients.js
Normal 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
107
red/api/auth/index.js
Normal 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
|
||||
}
|
36
red/api/auth/permissions.js
Normal file
36
red/api/auth/permissions.js
Normal 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
116
red/api/auth/strategies.js
Normal 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()
|
||||
}
|
73
red/api/auth/tokens/index.js
Normal file
73
red/api/auth/tokens/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
72
red/api/auth/tokens/localfilesystem.js
Normal file
72
red/api/auth/tokens/localfilesystem.js
Normal 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
101
red/api/auth/users.js
Normal 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(); }
|
||||
};
|
||||
|
||||
|
@ -16,52 +16,78 @@
|
||||
|
||||
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",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",nodes.getSet);
|
||||
adminApp.put("/nodes/:mod/:set",nodes.putSet);
|
||||
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
27
red/api/info.js
Normal 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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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')
|
||||
};
|
||||
|
68
red/comms.js
68
red/comms.js
@ -21,6 +21,7 @@ var server;
|
||||
var settings;
|
||||
|
||||
var wsServer;
|
||||
var pendingConnections = [];
|
||||
var activeConnections = [];
|
||||
|
||||
var retained = {};
|
||||
@ -34,23 +35,29 @@ function init(_server,_settings) {
|
||||
settings = _settings;
|
||||
}
|
||||
|
||||
|
||||
function start() {
|
||||
var Tokens = require("./api/auth/tokens");
|
||||
var Users = require("./api/auth/users");
|
||||
var Permissions = require("./api/auth/permissions");
|
||||
|
||||
if (!settings.disableEditor) {
|
||||
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() {
|
||||
for (var i=0;i<activeConnections.length;i++) {
|
||||
if (activeConnections[i] === ws) {
|
||||
activeConnections.splice(i,1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
removeActiveConnection(ws);
|
||||
removePendingConnection(ws);
|
||||
});
|
||||
ws.on('message', function(data,flags) {
|
||||
var msg = null;
|
||||
@ -60,9 +67,41 @@ function start() {
|
||||
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());
|
||||
@ -81,6 +120,7 @@ function start() {
|
||||
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,
|
||||
|
10
red/log.js
10
red/log.js
@ -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;
|
||||
|
@ -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"))) {
|
||||
|
@ -83,9 +83,6 @@ var persistentSettings = {
|
||||
userSettings = null;
|
||||
globalSettings = null;
|
||||
storage = null;
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -262,7 +262,6 @@ var localfilesystem = {
|
||||
return writeFile(globalSettingsFile,JSON.stringify(settings,null,1));
|
||||
},
|
||||
|
||||
|
||||
getAllFlows: function() {
|
||||
return listFiles(libFlowsDir);
|
||||
},
|
||||
|
47
test/red/api/auth/clients_spec.js
Normal file
47
test/red/api/auth/clients_spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
85
test/red/api/auth/index_spec.js
Normal file
85
test/red/api/auth/index_spec.js
Normal 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();
|
||||
}});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
38
test/red/api/auth/permissions_spec.js
Normal file
38
test/red/api/auth/permissions_spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
214
test/red/api/auth/strategies_spec.js
Normal file
214
test/red/api/auth/strategies_spec.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
147
test/red/api/auth/tokens/index_spec.js
Normal file
147
test/red/api/auth/tokens/index_spec.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
96
test/red/api/auth/tokens/localfilesystem_spec.js
Normal file
96
test/red/api/auth/tokens/localfilesystem_spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
186
test/red/api/auth/users_spec.js
Normal file
186
test/red/api/auth/users_spec.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
@ -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");
|
||||
|
@ -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
60
test/red/api/info_spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -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");
|
||||
});
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
||||
@ -189,4 +195,151 @@ 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user