Commit 0dd3bf4e authored by Daniel LACROIX's avatar Daniel LACROIX
Browse files

add an API to get the currently opened connections. correct some problems in...

add an API to get the currently opened connections. correct some problems in the authentication / authorization process
parent 84fa8448
......@@ -81,33 +81,45 @@ exports.authorize = async function (hook_name, context, cb) {
// NOTE: statics in /^\/(static|javascripts|pluginfw|api)/ are always authorized. See webaccess.js
laclasseLogger.info('authorize: for', context.resource);
// allow some statics resources
if ((context.resource.indexOf('/locales/') == 0) ||
(context.resource.indexOf('/jserror') == 0))
return cb(['readOnly']);
// allow healthcheck without authentication
if (context.resource.match(/^\/(healthcheck)/)) return cb([true]);
const user = context.req.session.user;
// everything else at least needs a login session
if (!user) return cb([false]);
if (!user) return cb([]);
// protect the admin routes
if (context.resource.indexOf('/admin') === 0 && !user.is_admin) return cb([false]);
if (context.resource.indexOf('/admin') === 0 && !user.is_admin) return cb([]);
// check if user has access to pad
const matches = context.resource.match(/^\/p\/(\d+)(\/.*)*$/);
const padID = matches ? matches[1] : null;
if (padID == null) return cb([false]);
const rights = await padRightsForUser(padID, user.username);
// laclasseLogger.info('Pad: ', padID, 'rights: ', rights);
if(rights === false || rights.Read == false) {
return cb([false]);
}
// laclasseLogger.info('authorize: authorized with ', rights.Write ? 'modify': 'readOnly');
if(rights.Write) {
return cb(['modify']);
} else {
return cb(['readOnly']);
if (padID == null) return cb([]);
try {
let rights = await padRightsForUser(padID, user.username);
laclasseLogger.info(`padRightsForUser rights: ${rights}`);
if(rights === false || rights.Read == false) {
return cb([]);
}
// laclasseLogger.info('authorize: authorized with ', rights.Write ? 'modify': 'readOnly');
if(rights.Write) {
return cb(['modify']);
} else {
return cb(['readOnly']);
}
} catch (error) {
laclasseLogger.error(`padRightsForUser fails ${error}`);
return cb([]);
}
};
......@@ -153,12 +165,22 @@ function getUserFromTicket(ticket) {
* @param {{username:string}} user
*/
async function padRightsForUser(id, userId) {
laclasseLogger.info(`padRightsForUser pad: ${id}, user: ${userId}`);
const rightsURL = new URL(`/api/docs/${id}/rights`,internalURL);
const urlParams = new URLSearchParams({ seenBy: userId });
const url = `${rightsURL.toString()}?${urlParams.toString()}`;
const body = await httpRequest(url, {
auth: `${appId}:${appKey}`,
});
let body;
try {
body = await httpRequest(url, {
auth: `${appId}:${appKey}`,
});
laclasseLogger.info(`padRightsForUser pad: ${id}, user: ${userId}, rights: ${body}`);
} catch(e) {
laclasseLogger.error(`padRightsForUser pad: ${id}, user: ${userId} return exception ${e}`);
return false;
}
let result;
try{
result = JSON.parse(body);
......@@ -173,16 +195,31 @@ exports.authenticate = async function (hook_name, context, cb) {
laclasseLogger.info('authenticate');
laclasseLogger.info('authenticate: current session: ', context.req.session);
if(!context.req.session && !context.req.session.user) {
// if no session available, authentication fails
if(!context.req.session) {
return cb([false]);
}
//TODO Don't check again if session exists and is valid
// if the user is already authentication, it is already ok
if (context.req.session.user) {
return cb([true]);
}
// no ticket is available, authentication fails
if(!context.req.query.ticket) {
return cb([false]);
}
const user = await serviceValidate(context.req.query.ticket, getServiceURL(context.req));
if(!user) return cb([false]);
let user;
try {
user = await serviceValidate(context.req.query.ticket, getServiceURL(context.req));
} catch {
return cb([false]);
}
if(!user)
return cb([false]);
// User authenticated, save off some information needed for authorization
context.req.session.user = {
username: user.uid,
......@@ -190,30 +227,106 @@ exports.authenticate = async function (hook_name, context, cb) {
is_admin: false,
canCreate: false,
};
laclasseLogger.info('authenticate: successful authentication', context.req.session.user);
// Redirect to url without ticket
// context.res.redirect(getServiceURL(context.req));
return cb([true]);
context.res.redirect(getServiceURL(context.req));
}
exports.authFailure = function(hook_name, context, cb) {
exports.authnFailure = function(hook_name, context, cb) {
const loginURL = new URL('/sso/login',baseURL);
const urlParams = new URLSearchParams({ service: getServiceURL(context.req) });
const url = `${loginURL.toString()}?${urlParams.toString()}`;
laclasseLogger.warn('authnFailure: Redirect to SSO: ',url);
// redirect to the auth url
try {
context.res.redirect(url);
} catch(e) {}
laclasseLogger.warn('authFailure: Redirect to SSO: ',url);
// signal that we have handled it
return cb([true]);
}
exports.handleMessage = async function (hook_name, {message, client}) {
exports.authzFailure = (hookName, context, cb) => {
laclasseLogger.warn('authzFailure');
// send our custom error page
context.res.status(403).send(
`<!DOCTYPE html>
<html>
<head>
<title>403 Accès refusé</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
body {
color: #444;
background-color: #f5f5f5;
font-family: "Open Sans", sans-serif;
font-size: 20px;
}
a {
color: white;
}
.logo {
width: 55%;
opacity: 0.6;
position: absolute;
left: -5%;
top: -5%;
-webkit-user-select: none;
}
.btn {
display: inline-block;
font-size: 16px;
text-transform: uppercase;
padding: 10px 20px;
border: 1px solid white;
border-radius: 0;
background-color: #444;
margin: 5px;
color: white;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
}
.btn:hover {
background-color: #555;
}
</style>
</head>
<body>
<img draggable="false" class="logo" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhbHF1ZV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgd2lkdGg9IjEyNy41NjFweCIgaGVpZ2h0PSIxMjcuNTYxcHgiIHZpZXdCb3g9IjAgMCAxMjcuNTYxIDEyNy41NjEiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEyNy41NjEgMTI3LjU2MSINCgkgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNNjMuNzc4LDkuMTM1QzMzLjYxOCw5LjEzOCw5LjEzNiwzMy42MTQsOS4xMzYsNjMuNzc4YzAsMzAuMTYyLDI0LjQ4MSw1NC42NDUsNTQuNjQzLDU0LjY0Ng0KCQljMzAuMTcxLTAuMDAyLDU0LjY0NS0yNC40ODMsNTQuNjQ2LTU0LjY0NkMxMTguNDIzLDMzLjYxNCw5My45NDksOS4xMzgsNjMuNzc4LDkuMTM1eiBNNzUuMjczLDkwLjU2Mg0KCQljMi43NzYtMS40NzQsNS4zNTEtMy4yODEsNy42Ni01LjM3N2MxLjY1OS0xLjUwOSw0LjIyOC0xLjM4MSw1LjczMywwLjI3OGMxLjUwNiwxLjY2MiwxLjM4MSw0LjIyOS0wLjI3OCw1LjczNA0KCQljLTIuODExLDIuNTQ3LTUuOTM2LDQuNzQ4LTkuMzE1LDYuNTM5Yy0xLjk4MywxLjA0OS00LjQzOCwwLjI5My01LjQ4OS0xLjY4OEM3Mi41MzQsOTQuMDY3LDczLjI5LDkxLjYxLDc1LjI3Myw5MC41NjJ6DQoJCSBNOTIuMzEyLDczLjcyNGMwLTIuMjQyLDEuODE1LTQuMDYyLDQuMDYtNC4wNjJjMS4zMjQtMC4wMDIsMi40ODYtMC41MjQsMy4zNTktMS4zOTFjMC44NjUtMC44NzMsMS4zOS0yLjAzMSwxLjM5Mi0zLjM1NXYtNy4xNDENCgkJYy0wLjAwMi0xLjMyNi0wLjUyNS0yLjQ4OC0xLjM5Mi0zLjM1OWMtMC44NzMtMC44NjctMi4wMzUtMS4zOS0zLjM1OS0xLjM5MmMtMS4zMjYsMC4wMDItMi40OSwwLjUyMy0zLjM2LDEuMzkyDQoJCWMtMC44NjUsMC44NzEtMS4zOTEsMi4wMzMtMS4zOTMsMy4zNTljMCwyLjI0Mi0xLjgxNyw0LjA2MS00LjA2LDQuMDYxYy0yLjI0MywwLTQuMDYyLTEuODE3LTQuMDYyLTQuMDYxDQoJCWMwLjAwMy03LjExNSw1Ljc2Ny0xMi44NywxMi44NzMtMTIuODcxYzAuMDc3LDAsMC4xNTIsMC4wMSwwLjIyOSwwLjAxMmMtMi4zMTYtNC4wNjItNS4zMzQtNy42NzQtOC45MzUtMTAuNjE5DQoJCWMtNi41MzEtNS4zMzYtMTQuODQtOC41MjgtMjMuOTM3LTguNTNjLTEwLjQ2MywwLjAwMi0xOS44OTYsNC4yMjktMjYuNzU4LDExLjA4MmMtMi43ODcsMi43OTEtNS4xMzQsNi4wMTUtNi45NDQsOS41NTUNCgkJYzEuMDY3LTAuMjMsMi4xNzItMC4zNTksMy4zMDktMC4zNTljOC42LDAuMDAyLDE1LjU2Nyw2Ljk3LDE1LjU3LDE1LjU3MnY5LjMzOGMwLDIuMDcyLDAuODI1LDMuOTA4LDIuMTc5LDUuMjY5DQoJCWMxLjM1NywxLjM1NCwzLjE5MywyLjE3OCw1LjI2NywyLjE4YzIuMjQxLDAsNC4wNjEsMS44MTgsNC4wNjEsNC4wNjFjMCwyLjI0My0xLjgxNyw0LjA2LTQuMDYxLDQuMDYNCgkJYy04LjYwMSwwLTE1LjU2NC02Ljk2Ny0xNS41NjQtMTUuNTY2di05LjMzOGMtMC4wMDQtMi4wNy0wLjgyOC0zLjkxLTIuMTg0LTUuMjcxYy0xLjM2MS0xLjM1NC0zLjE5Ny0yLjE4MS01LjI2OC0yLjE4Mw0KCQljLTIuMDcsMC4wMDItMy45MDksMC44MjgtNS4yNzEsMi4xODNjLTAuOTkzLDAuOTk4LTEuNjkyLDIuMjU0LTIsMy42NjhjLTAuMTExLDEuMTg2LTAuMTc2LDIuMzgzLTAuMTc2LDMuNTk2DQoJCWMwLjAwMiwxMC40NjEsNC4yMjksMTkuODk1LDExLjA4MiwyNi43NTJjNi44Niw2Ljg1NCwxNi4yOTUsMTEuMDgsMjYuNzU4LDExLjA4MmMyLjI0MiwwLDQuMDYxLDEuODE4LDQuMDYxLDQuMDYxDQoJCWMwLDIuMjQzLTEuODE3LDQuMDYyLTQuMDYxLDQuMDYyYy0yNS4zODMtMC4wMDItNDUuOTU3LTIwLjU3LTQ1Ljk2MS00NS45NTVjMC0wLjUxNiwwLjAyMS0xLjAyNSwwLjAzOS0xLjUzNw0KCQljLTAuMDE4LTAuMTUyLTAuMDQ1LTAuMjk5LTAuMDQ1LTAuNDU1YzAtMC44OTUsMC4wOTItMS43NjYsMC4yMzctMi42MTljMi4zMTQtMjMuMjIzLDIxLjkwMy00MS4zNDksNDUuNzI5LTQxLjM1MQ0KCQljMTEuMDE3LTAuMDAyLDIxLjE2NiwzLjg5MSwyOS4wNzgsMTAuMzY2YzcuOTEzLDYuNDc1LDEzLjYzMywxNS41NDMsMTUuODU0LDI1LjkyNGMwLjAwNCwwLjAwOSwwLjAwMiwwLjAxOCwwLjAwNCwwLjAyNg0KCQljMC4zNzMsMS4yMDUsMC41NzQsMi40ODQsMC41NzYsMy44MTJ2Ny4xNDFjLTAuMDA1LDcuMTExLTUuNzY2LDEyLjg2My0xMi44NzEsMTIuODY3Qzk0LjEyOCw3Ny43ODQsOTIuMzEyLDc1Ljk2Niw5Mi4zMTIsNzMuNzI0eg0KCQkgTTYzLjI2NSw2Mi4zOTJjMCwyLjM0NC0xLjkwNCw0LjI1NC00LjI1NCw0LjI1NGMtMi4zNDQsMC00LjI0OC0xLjkxLTQuMjQ4LTQuMjU0YzAtMi4zNTUsMS45MDQtNC4yNjEsNC4yNDgtNC4yNjENCgkJQzYxLjM2LDU4LjEzMiw2My4yNjUsNjAuMDM2LDYzLjI2NSw2Mi4zOTJ6IE0zNy4zNDUsNjIuMzkyYzAsMi4zNDQtMS44OTksNC4yNTQtNC4yNSw0LjI1NGMtMi4zNDksMC00LjI1LTEuOTEtNC4yNS00LjI1NA0KCQljMC0yLjM1NSwxLjkwMS00LjI2MSw0LjI1LTQuMjYxQzM1LjQ0NSw1OC4xMzIsMzcuMzQ1LDYwLjAzNiwzNy4zNDUsNjIuMzkyeiIvPg0KPC9nPg0KPC9zdmc+" alt="Logo ENT">
<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px;">
<center>
<div style="max-width: 800px; padding: 40px;">
<div style="font-weight: bold; font-size: 34px">Laclasse 403</div><br>
<div style="font-size: 30px; padding: 10px">Accès interdit</div>
<div style="text-align: left">
Vous n'avez pas les droits nécessaires pour accéder à cette
ressource. Contacter votre administrateur ou la personne
qui vous a communiqué ce lien afin de lui demander les
droits.
</div>
</div>
</center>
</div>
</body>
</html>`
);
return cb([true]);
};
exports.handleMessage = async function (hook_name, {message, socket, client}) {
if (message.type === 'CLIENT_READY' || message.type === 'USERINFO_UPDATE') {
// skip if we don't have any information to set
const session = client.client.request.session;
......
......@@ -5,10 +5,12 @@
"hooks":{
"userLeave": "ep_laclasse/userLeave.js",
"expressCreateServer": "ep_laclasse/httpApi.js",
"socketio": "ep_laclasse/httpApi.js",
"handleMessage": "ep_laclasse/authenticate.js",
"authenticate": "ep_laclasse/authenticate.js",
"authorize": "ep_laclasse/authenticate.js",
"authFailure": "ep_laclasse/authenticate.js"
"authnFailure": "ep_laclasse/authenticate.js",
"authzFailure": "ep_laclasse/authenticate.js"
}
}
]
......
......@@ -5,6 +5,8 @@ const rateLimit = require("express-rate-limit");
const exportHandler = require('ep_etherpad-lite/node/handler/ExportHandler');
const importHandler = require('ep_etherpad-lite/node/handler/ImportHandler');
const absolutePaths = require('ep_etherpad-lite/node/utils/AbsolutePaths');
const padMessageHandler = require('ep_etherpad-lite/node/handler/PadMessageHandler');
const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
const fs = require("fs");
const log4js = require('log4js');
......@@ -27,6 +29,15 @@ settings.importExportRateLimiting.onLimitReached = function(req, res, options) {
const limiter = rateLimit(settings.importExportRateLimiting);
let socketio;
// keep the socket io to allow access to authentication info like
// username of the current connection
exports.socketio = function(hook_name, {io}) {
laclasseLogger.info(`httpApi.setSocketIO ${io}`);
socketio = io;
};
exports.expressCreateServer = function (hook_name, args, cb) {
// handle export requests
......@@ -50,6 +61,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
res.header("Access-Control-Allow-Origin", "*");
// ensure we are authenticated with apikey
const apiKeyParam = req.query.apikey || req.query.api_key;
if (apiKeyParam !== apikey.trim()) {
res.statusCode = 401;
......@@ -76,15 +88,17 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// handle import requests
args.app.use('/api/laclasse/:pad/import', limiter);
args.app.post('/api/laclasse/:pad/import', async function(req, res, next) {
if (!(await padManager.doesPadExists(req.params.pad))) {
laclasseLogger.warn(`Laclasse tried to import into a pad that doesn't exist (${req.params.pad})`);
return next();
}
// ensure we are authenticated with apikey
const apiKeyParam = req.query.apikey || req.query.api_key;
if (apiKeyParam !== apikey.trim()) {
res.statusCode = 401;
return res.send({ code: 4, message: 'no or wrong API Key', data: null });
res.statusCode = 401;
return res.send({ code: 4, message: 'no or wrong API Key', data: null });
}
if (!(await padManager.doesPadExists(req.params.pad))) {
laclasseLogger.warn(`Laclasse tried to import into a pad that doesn't exist (${req.params.pad})`);
return next();
}
try {
......@@ -95,5 +109,48 @@ exports.expressCreateServer = function (hook_name, args, cb) {
}
});
// handle connections info request
args.app.use('/api/laclasse/connections', limiter);
args.app.get('/api/laclasse/connections', async function(req, res, next) {
// ensure we are authenticated with apikey
const apiKeyParam = req.query.apikey || req.query.api_key;
if (apiKeyParam !== apikey.trim()) {
res.statusCode = 401;
return res.send({ code: 4, message: 'no or wrong API Key', data: null });
}
const sessionInfos = padMessageHandler.sessioninfos;
laclasseLogger.info(sessionInfos);
let data = [];
for (let key in sessionInfos) {
let connection = sessionInfos[key];
let info = {
socketId: key,
padId: connection.padId,
authorId: connection.author,
readonly: connection.readonly,
rev: connection.rev
}
// try to find the author name
if (connection.author) {
let author = await authorManager.getAuthor(connection.author);
if (author && author.name) {
info.authorName = author.name;
}
}
// try to find the auth user info
if (socketio.sockets.sockets[key]) {
let socket = socketio.sockets.sockets[key];
if (socket.client && socket.client.request && socket.client.request.session && socket.client.request.session.user) {
info.userId = socket.client.request.session.user.username;
}
}
data.push(info);
}
return res.send({ code: 0, message: 'info about current connections', data: data });
});
return cb();
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment