Commit 346254c0 authored by Piotr Gawron's avatar Piotr Gawron
Browse files

Merge branch '563-spring-security-frontend' into 'master'

Resolve "Implement Spring Security"

Closes #563

See merge request !836
parents 273b19f4 c10e0616
Pipeline #11976 passed with stage
in 11 minutes and 39 seconds
......@@ -164,6 +164,23 @@ test_backend rest-api:
- mvn test -pl rest-api
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print covered, "/", instructions, "instructions covered"; print 100*covered/instructions,"% covered" }' rest-api/target/site/jacoco/jacoco.csv
test_backend web:
image: maven:3.6.0-jdk-8
services:
- postgres:9.6
stage: test
coverage: '/(\d+.\d+) \% covered/'
script:
- apt-get update
- DEBIAN_FRONTEND=noninteractive apt-get install -y curl gnupg git ant
- curl -sL https://deb.nodesource.com/setup_9.x | bash -
- DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
- mkdir /etc/minerva/
- cp test-db-ci.properties /etc/minerva/db.properties
- mvn -DskipTests=true clean install -pl web -am
- mvn test -pl web
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print covered, "/", instructions, "instructions covered"; print 100*covered/instructions,"% covered" }' web/target/site/jacoco/jacoco.csv
test_backend service:
image: maven:3.6.0-jdk-8
services:
......
minerva (14.0.0~alpha.0) unstable; urgency=low
* Feature: security layer redesigned - privilge types and scope changed
(#636, #624)
* Feature: log4j is replaced with log4j2 logging mechanism (#291)
* Feature: database installed via debian package is done via dbconfig-commons
(#469)
* Feature: Replaced connection pool manager C3P0 with better maintained
Hikari (#564)
* Feature removal: BioCompendium annotator removed (#32)
* Small improvement: anonymous login is no longer required - each API query
outside session is authorized with anonymous user privileges (#629)
* Small improvement: bcrypt is used for password encryption (#387)
* Small improvement: caching is active by default for new users when
uploading project (#202)
* Small improvement: when removing overlay in admin panel there is a
......
rootLogger.level = INFO
appenders = console
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d %5p [%t] (%F:%L) - %m%n
rootLogger.appenderRefs = console
rootLogger.appenderRef.console.ref = STDOUT
logger.minerva.name = lcsb
logger.minerva.level = debug
logger.minerva-persist.name = lcsb.mapviewer.persist.DbUtils
logger.minerva-persist.level = info
logger.minerva-cache.name = lcsb.mapviewer.annotation.cache
logger.minerva-cache.level = info
logger.minerva-comparator.name = lcsb.mapviewer.common.Comparator
logger.minerva-comparator.level = info
logger.minerva-model.name = lcsb.mapviewer.model
logger.minerva-model.level = info
logger.springframework.name = org.springframework
logger.springframework.level = warn
logger.hibernate.name = org.hibernate
logger.hibernate.level = warn
......@@ -13,8 +13,7 @@ function listFiles(dir, filelist) {
files.forEach(function (file) {
if (fs.statSync(dir + '/' + file).isDirectory()) {
filelist = listFiles(dir + '/' + file, filelist);
}
else {
} else {
filelist.push(dir + "/" + file);
}
});
......@@ -115,26 +114,29 @@ function prepareQueries(filelist) {
}
function getAuthToken(login) {
var url = apiBaseDir + "doLogin?login=" + login;
var params = {
method: "POST",
url: apiBaseDir + "doLogin",
form: {"login": login}
};
if (login === "anonymous") {
url += "&password=";
//we don't need to login when accessing with anonymous account
params.url = apiBaseDir + "configuration/";
params.method = "GET";
} else if (login === "admin") {
url += "&password=admin";
params.form.password = "admin";
} else if (login === "noaccessuser") {
url += "&password=noaccessuser";
params.form.password = "noaccessuser";
} else if (login !== undefined) {
throw new Error("Unknown user: " + login);
}
return new Promise(function (resolve, reject) {
var params = {
method: "GET",
url: url
};
request(params, function (error, response) {
if (error) {
reject(new Error(error.message));
} else if (response.statusCode !== 200) {
reject(new Error(url + " rejected with status code: " + response.statusCode));
reject(new Error(params.url + " rejected with status code: " + response.statusCode));
} else {
resolve(response.headers['set-cookie']);
}
......@@ -198,7 +200,7 @@ function executeQueries(queries) {
// for (var i = 0; i < 3; i++) {
for (var i = 0; i < queries.length; i++) {
var query = queries[i];
if (query.method !== "GET" || query.url.indexOf(":downloadModel") >= 0) {
if (query.method !== "GET" || query.url.indexOf(":downloadModel") >= 0 || query.url.indexOf("/doLogout") >= 0) {
console.log("Ignoring " + query.method + " query: " + query.url);
} else {
// console.log("Exec " + query.method + " query: " + query.url);
......
"use strict";
/* exported logger */
// noinspection JSUnusedLocalSymbols
var logger = require('./logger');
/**
*
* @param {string} message
* @constructor
* @extends Error
*/
function ObjectNotFoundError(message) {
this.message = message;
this.stack = (new Error(message)).stack;
}
ObjectNotFoundError.prototype = Object.create(Error.prototype);
ObjectNotFoundError.prototype.constructor = ObjectNotFoundError;
module.exports = ObjectNotFoundError;
......@@ -28,6 +28,7 @@ var Mesh = require('./map/data/Mesh');
var MiRna = require('./map/data/MiRna');
var NetworkError = require('./NetworkError');
var ObjectExistsError = require('./ObjectExistsError');
var ObjectNotFoundError = require('./ObjectNotFoundError');
var PluginData = require('./map/data/PluginData');
var Project = require('./map/data/Project');
var ProjectStatistics = require('./map/data/ProjectStatistics');
......@@ -164,7 +165,7 @@ ServerConnector.getNeutralOverlayColorInt = function () {
*
* @param url
* @param description
* @returns {Promise<string>| PromiseLike<string>}
* @returns {Promise}
*/
ServerConnector.sendGetRequest = function (url, description) {
return this.sendRequest({
......@@ -174,23 +175,23 @@ ServerConnector.sendGetRequest = function (url, description) {
});
};
function isSessionExpiredError(error) {
if (error instanceof NetworkError) {
if (error.statusCode === HttpStatus.FORBIDDEN) {
if (typeof error.content === 'string' || error.content instanceof String) {
if (error.content.indexOf('"reason":"Invalid token"') >= 0) {
return true;
}
if (error.content.indexOf('"reason":"Missing cookie') >= 0) {
return true;
}
} else if (error.content.reason === "Invalid token" || error.content.reason === 'Missing cookie') {
return true;
/**
*
* @return {Promise<boolean>}
*/
ServerConnector.isSessionValid = function () {
var self = this;
return self._sendRequest({method: "GET", url: this.isSessionValidUrl()}).then(function (content) {
return JSON.parse(content).login === self.getSessionData(null).getLogin();
}).catch(function (error) {
if (error instanceof NetworkError) {
if (error.statusCode === HttpStatus.FORBIDDEN || error.statusCode === HttpStatus.UNAUTHORIZED) {
return self.getSessionData(null).getLogin() === "anonymous";
}
}
}
return false;
}
return Promise.reject(error);
});
};
/**
*
......@@ -222,19 +223,19 @@ ServerConnector.sendRequest = function (params) {
}).then(function () {
return content;
}, function (error) {
var promise = Promise.resolve();
if (isSessionExpiredError(error)) {
self.getSessionData().setToken(undefined);
var login = self.getSessionData().getLogin();
promise = self.login().then(function () {
if (login === "anonymous" || login === undefined || login === null) {
window.location.reload(false);
} else {
window.location.href = ServerConnector.getServerBaseUrl() + "login.xhtml?from=" + encodeURI(window.location.href);
}
});
}
return promise.then(function () {
return self.isSessionValid().then(function (isValid) {
if (!isValid) {
self.getSessionData().setToken(undefined);
var login = self.getSessionData().getLogin();
self.login().then(function () {
if (login === "anonymous" || login === null || login === undefined) {
window.location.reload(false);
} else {
window.location.href = ServerConnector.getServerBaseUrl() + "login.xhtml?from=" + encodeURI(window.location.href);
}
});
}
}).then(function () {
return Promise.reject(error);
});
});
......@@ -462,7 +463,7 @@ ServerConnector.getPluginGlobalParamUrl = function (queryParams, filterParams) {
ServerConnector.getPluginUserParamUrl = function (queryParams, filterParams) {
return this.getApiUrl({
url: this.getPluginsUrl(queryParams) + queryParams.hash + "/data/users/" + queryParams.login + "/" + queryParams.key + "/",
url: this.getPluginsUrl(queryParams) + queryParams.hash + "/data/users/" + queryParams.key + "/",
params: filterParams
});
};
......@@ -475,6 +476,34 @@ ServerConnector.getProjectUrl = function (queryParams, filterParams) {
});
};
/**
*
* @param queryParams
* @param filterParams
* @return {string}
*/
ServerConnector.getGrantProjectPrivilegesUrl = function (queryParams, filterParams) {
var id = this.getIdOrAsterisk(queryParams.projectId);
return this.getApiUrl({
url: this.getProjectsUrl(queryParams) + id + ":grantPrivileges",
params: filterParams
});
};
/**
*
* @param queryParams
* @param filterParams
* @return {string}
*/
ServerConnector.getRevokeProjectPrivilegesUrl = function (queryParams, filterParams) {
var id = this.getIdOrAsterisk(queryParams.projectId);
return this.getApiUrl({
url: this.getProjectsUrl(queryParams) + id + ":revokePrivileges",
params: filterParams
});
};
ServerConnector.getProjectStatisticsUrl = function (queryParams, filterParams) {
return this.getApiUrl({
url: this.getProjectUrl(queryParams) + "statistics/",
......@@ -677,6 +706,12 @@ ServerConnector.logoutUrl = function () {
});
};
ServerConnector.isSessionValidUrl = function () {
return this.getApiUrl({
url: this.getUsersUrl() + "isSessionValid"
});
};
ServerConnector.getSuggestedQueryListUrl = function (queryParams, filterParams) {
return this.getApiUrl({
url: this.getBioEntitiesUrl(queryParams) + "suggestedQueryList/",
......@@ -1397,28 +1432,7 @@ ServerConnector.updateUser = function (user) {
connectedToLdap: user.isConnectedToLdap()
}
};
var canModifyPrivileges = false;
return self.sendPatchRequest(self.getUserUrl(queryParams), filterParams)
.then(function () {
var currentLogin = ServerConnector.getSessionData().getLogin();
return self.sendGetRequest(self.getUserUrl({login: currentLogin}, {}));
})
.then(function (response) {
canModifyPrivileges = JSON.parse(response).privileges.some(function (privilege) {
return (privilege.type === PrivilegeType.USER_MANAGEMENT) && parseInt(privilege.value) === 1;
});
return self.getConfiguration();
})
.then(function (configuration) {
if (!canModifyPrivileges) {
return Promise.resolve(user);
}
return self.updateUserPrivileges({user: user, privileges: user.privilegesToExport(configuration)});
});
return self.sendPatchRequest(self.getUserUrl(queryParams), filterParams);
};
/**
......@@ -1461,7 +1475,7 @@ ServerConnector.addUser = function (user) {
}).then(function () {
return self.getConfiguration();
}).then(function (configuration) {
return self.updateUserPrivileges({user: user, privileges: user.privilegesToExport(configuration)});
return self.grantUserPrivileges({user: user, privileges: user.getPrivileges()});
});
};
......@@ -1482,18 +1496,122 @@ ServerConnector.removeUser = function (login) {
*
* @param {Object} params
* @param {User} params.user
* @param {Object} params.privileges
* @param {Authority[]} params.privileges
*
* @returns {Promise}
*/
ServerConnector.grantUserPrivileges = function (params) {
var self = this;
var queryParams = {
login: params.user.getLogin()
};
var privileges = {};
for (var i = 0; i < params.privileges.length; i++) {
var privilege = params.privileges[i];
if (privilege.objectId !== undefined && privilege.objectId !== null) {
privileges[privilege.privilegeType + ":" + privilege.objectId] = true;
} else {
privileges[privilege.privilegeType] = true;
}
}
return self.sendPatchRequest(self.getUpdateUserPrivilegesUrl(queryParams), {
privileges: privileges
}).then(function (content) {
var obj = JSON.parse(content);
var user = new User(obj);
if (self._usersByLogin[user.getLogin()] !== undefined) {
self._usersByLogin[user.getLogin()].update(user);
} else {
self._usersByLogin[user.getLogin()] = user;
}
return self._usersByLogin[user.getLogin()];
}).then(null, function (error) {
return self.processNetworkError(error);
});
};
/**
*
* @param {Object} params
* @param {User} params.user
* @param {string} params.projectId
* @param {string} params.privilegeType
*
* @returns {Promise}
*/
ServerConnector.grantProjectPrivilege = function (params) {
var self = this;
var queryParams = {
projectId: params.projectId
};
var privileges = [{login: params.user.getLogin(), privilegeType: params.privilegeType}];
return self.sendPatchRequest(self.getGrantProjectPrivilegesUrl(queryParams), privileges).then(function (content) {
if (self._usersByLogin[params.user.getLogin()] !== undefined) {
self._usersByLogin[params.user.getLogin()].setPrivilege({
privilegeType: params.privilegeType,
objectId: params.projectId
});
}
}).catch(function (error) {
return self.processNetworkError(error, true);
});
};
/**
*
* @param {Object} params
* @param {User} params.user
* @param {string} params.projectId
* @param {string} params.privilegeType
*
* @returns {Promise}
*/
ServerConnector.revokeProjectPrivilege = function (params) {
var self = this;
var queryParams = {
projectId: params.projectId
};
var privileges = [{login: params.user.getLogin(), privilegeType: params.privilegeType}];
return self.sendPatchRequest(self.getRevokeProjectPrivilegesUrl(queryParams), privileges).then(function (content) {
if (self._usersByLogin[params.user.getLogin()] !== undefined) {
self._usersByLogin[params.user.getLogin()].revokePrivilege({
privilegeType: params.privilegeType,
objectId: params.projectId
});
}
}).catch(function (error) {
return self.processNetworkError(error, true);
});
};
/**
*
* @param {Object} params
* @param {User} params.user
* @param {Authority[]} params.privileges
*
* @returns {Promise}
*/
ServerConnector.updateUserPrivileges = function (params) {
ServerConnector.revokeUserPrivileges = function (params) {
var self = this;
var queryParams = {
login: params.user.getLogin()
};
var privileges = {};
for (var i = 0; i < params.privileges.length; i++) {
var privilege = params.privileges[i];
if (privilege.objectId !== undefined && privilege.objectId !== null) {
privileges[privilege.privilegeType + ":" + privilege.objectId] = false;
} else {
privileges[privilege.privilegeType] = false;
}
}
return self.sendPatchRequest(self.getUpdateUserPrivilegesUrl(queryParams), {
privileges: params.privileges
privileges: privileges
}).then(function (content) {
var obj = JSON.parse(content);
var user = new User(obj);
......@@ -1511,12 +1629,16 @@ ServerConnector.updateUserPrivileges = function (params) {
/**
*
* @param {Error} error
* @param {boolean} [forbidNotFound=false]
* @returns {Promise}
*/
ServerConnector.processNetworkError = function (error) {
ServerConnector.processNetworkError = function (error, forbidNotFound) {
if ((error instanceof NetworkError)) {
switch (error.statusCode) {
case HttpStatus.NOT_FOUND:
if (forbidNotFound) {
return Promise.reject(new ObjectNotFoundError("Object not found."));
}
return null;
case HttpStatus.CONFLICT:
return Promise.reject(new ObjectExistsError("Object already exists."));
......@@ -1597,7 +1719,7 @@ ServerConnector.getReferenceGenomes = function () {
var parsedData = JSON.parse(content);
for (var i = 0; i < parsedData.length; i++) {
var genome = new ReferenceGenome(parsedData[i]);
result.push(genome)
result.push(genome);
}
return result;
});
......@@ -1901,8 +2023,8 @@ ServerConnector.getClosestElementsByCoordinates = function (params) {
ServerConnector.createSession = function () {
var self = this;
return self.getConfiguration().catch(function (error) {
if (isSessionExpiredError(error)) {
return self.isSessionValid().then(function (isValid) {
if (!isValid) {
return self.login();
}
});
......@@ -1915,6 +2037,9 @@ ServerConnector.login = function (login, password) {
params.password = password;
} else {
params.login = "anonymous";
self._currentTabLogin = params.login;
self.getSessionData().setLogin(params.login);
return Promise.resolve(self.getSessionData().getToken());
}
return self.sendPostRequest(self.loginUrl(), params).then(function (content) {
var data = JSON.parse(content);
......@@ -1925,7 +2050,7 @@ ServerConnector.login = function (login, password) {
self.getSessionData().setLogin(params.login);
return Promise.resolve(self.getSessionData().getToken());
}).catch(function (error) {
if (error instanceof NetworkError && error.statusCode === HttpStatus.FORBIDDEN) {
if (error instanceof NetworkError && (error.statusCode === HttpStatus.FORBIDDEN || error.statusCode === HttpStatus.UNAUTHORIZED)) {
throw new InvalidCredentialsError("Invalid credentials");
} else {
throw error;
......@@ -2680,7 +2805,7 @@ ServerConnector.uploadFile = function (params) {
return self.sendRequest({method: "POST", url: url, body: chunk}).then(function (resultFileJson) {
uploadedLength += chunk.length;
return createPromise(resultFileJson);
})
});
}
};
return createPromise(response);
......
......@@ -112,10 +112,11 @@ CommentsAdminPanel.prototype.refreshComments = function () {
comments = result;
return self.getServerConnector().getLoggedUser();
}).then(function (user) {
var type = self.getConfiguration().getPrivilegeType(PrivilegeType.EDIT_COMMENTS_PROJECT);
var disable = false;
if (!user.hasPrivilege(type, self.getProject().getId())) {
disable = true;
var writeAccess = self.getConfiguration().getPrivilegeType(PrivilegeType.WRITE_PROJECT);
var isAdmin = self.getConfiguration().getPrivilegeType(PrivilegeType.IS_ADMIN);
var disable = true;
if (user.hasPrivilege(writeAccess, self.getProject().getProjectId()) || user.hasPrivilege(isAdmin)) {
disable = false;
}
var dataTable = $($("[name='commentsTable']", self.getElement())[0]).DataTable();
......
......@@ -159,7 +159,7 @@ ConfigurationAdminPanel.prototype.init = function () {
return self.getServerConnector().getLoggedUser();
}).then(function (user) {
var configuration = self.getConfiguration();
var privilege = configuration.getPrivilegeType(PrivilegeType.CONFIGURATION_MANAGE);
var privilege = configuration.getPrivilegeType(PrivilegeType.IS_ADMIN);
if (user.hasPrivilege(privilege)) {
return self.setOptions(configuration.getOptions());
} else {
......
......@@ -593,17 +593,19 @@ EditProjectDialog.prototype.createUsersTabContent = function () {
result.appendChild(usersTable);
$(usersTable).on("change", "[name='privilege']", function () {
var privileges = {};
var type = $(this).attr("data").split(",")[0];
var login = $(this).attr("data").split(",")[1];
var privilege = {};
privilege[self.getProject().getId()] = $(this).prop("checked");
privileges[type] = privilege;
return self.updatePrivileges(self._userByLogin[login], privileges);
});
if ($(this).prop("checked")) {
return self.grantPrivilege(self._userByLogin[login], type, self.getProject().getProjectId());
} else {
return self.revokePrivilege(self._userByLogin[login], type, self.getProject().getProjectId());
}
})
;
return result;
};
}