Commit 4fba3412 authored by Daniel LACROIX's avatar Daniel LACROIX
Browse files

Merge branch 'develop' of https://forge.grandlyon.com/laclasse/etherpad into develop

parents 0667fd6e 9a76a6c9
FROM node:alpine
FROM node:alpine
ARG NODE_ENV=production
RUN mkdir /app && apk update && apk add curl gettext nginx && mkdir /run/nginx
COPY ./etherpad-lite-1.8.4.tar.gz /app
COPY ./etherpad-lite-1.8.6.tar.gz /app
WORKDIR /app
RUN tar zxvf etherpad-lite-1.8.4.tar.gz --strip 1 -C /app && \
rm /app/etherpad-lite-1.8.4.tar.gz
RUN tar zxvf etherpad-lite-1.8.6.tar.gz --strip 1 -C /app && \
rm /app/etherpad-lite-1.8.6.tar.gz
RUN /app/bin/installDeps.sh
COPY ./settings.json.tmpl /app/settings.json.tmpl
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
COPY ./nginx/default.conf.tmpl /etc/nginx/conf.d/default.conf.tmpl
COPY ./pad.js /app/src/static/custom/pad.js
COPY ./ep_laclasse/ /app/node_modules/ep_laclasse/
COPY ./ep_disable_change_author_name/ /app/node_modules/ep_disable_change_author_name/
EXPOSE 80
ENTRYPOINT [ "/docker-entrypoint.sh" ]
......@@ -12,8 +12,11 @@ fi
if [ -z "$AUTH_URL" ]; then
export AUTH_URL="http://service/api/docs/current/isauthenticated"
fi
if [ -z "$SSO_URL" ]; then
export SSO_URL="$BASE_URL"
fi
REPLACE_VARS='DB_HOST DB_USER DB_NAME DB_PASSWORD APIKEY AUTH_URL'
REPLACE_VARS='DB_HOST DB_USER DB_NAME DB_PASSWORD APIKEY AUTH_URL SSO_URL BASE_URL'
# check if needed vars are present
if [ -z "$DB_PASSWORD" ] || [ -z "$APIKEY" ] ; then
......@@ -23,12 +26,15 @@ if [ -z "$DB_PASSWORD" ] || [ -z "$APIKEY" ] ; then
echo " - DB_NAME: database name (default: etherpad)"
echo " - DB_PASSWORD: database password"
echo " - APIKEY: Etherpad secret API key"
echo " - BASE_URL: HTTP URL"
echo " - AUTH_URL: auth service URL (default: http://service/api/docs/current/isauthenticated)"
echo " - SSO_URL: sso service URL (default: BASE_URL)"
exit 1
fi
# generate setup files
envsubst "$(printf '${%s} ' $REPLACE_VARS)" < /app/settings.json.tmpl > /app/settings.json
envsubst "$(printf '${%s} ' $REPLACE_VARS)" < /app/node_modules/ep_laclasse/config.json.tmpl > /app/node_modules/ep_laclasse/config.json
envsubst "$(printf '${%s} ' $REPLACE_VARS)" < /etc/nginx/conf.d/default.conf.tmpl > /etc/nginx/conf.d/default.conf
echo $APIKEY > /app/APIKEY.txt
......@@ -45,6 +51,9 @@ while [[ $loop_count -lt 10 ]] && [[ $success != 1 ]]; do
fi
done
# sym links from etherpad
ln -s /app/node_modules/ep_etherpad-lite/node_modules /app/node_modules/ep_laclasse/node_modules
set -e
nginx
export NODE_ENV=production
......
Copyright 2020 John McLear
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.
{
"parts":[
{
"name": "disableNameInputbox",
"hooks": {},
"client_hooks": {
"postAceInit": "ep_disable_change_author_name/static/js/disable:postAceInit"
}
}
]
}
{
"name": "ep_disable_change_author_name",
"description": "A plugin to stop users from being able to change their names",
"version": "0.0.13",
"author": {
"name": "johnyma22",
"email": "john@mclear.co.uk",
"url": "John McLear"
},
"contributors": [],
"dependencies": {},
"engines": {
"node": ">= 0.4.1"
},
"_id": "ep_disable_change_author_name@0.0.2",
"dist": {
"shasum": "7a668717f83a7027ecd5316aa44833c4cc105ee8",
"tarball": "http://registry.npmjs.org/ep_disable_change_author_name/-/ep_disable_change_author_name-0.0.2.tgz"
},
"_from": "ep_disable_change_author_name@*",
"_resolved": "https://registry.npmjs.org/ep_disable_change_author_name/-/ep_disable_change_author_name-0.0.2.tgz",
"_npmVersion": "1.4.6",
"_npmUser": {
"name": "johnyma22",
"email": "john@mclear.co.uk"
},
"maintainers": [
{
"name": "johnyma22",
"email": "john@mclear.co.uk"
}
],
"directories": {},
"_shasum": "7a668717f83a7027ecd5316aa44833c4cc105ee8"
}
var $ = require('ep_etherpad-lite/static/js/rjquery').$; // use jQuery
exports.postAceInit = function (hook_name, args, cb) {
$('#users input[type=text]').prop('disabled', true); // disable the text boxes in the users section.
$('#users input[type=text]').attr('Title', 'Name Change Disabled'); // disable the text boxes in the users section.
$('body').append("<style> \
#myusernameedit{background:lightgrey !important;} \
#myusernameedit:hover{background:lightgrey !important;color:#333 !important} \
</style>");
}
Copyright 2020 Nelson DINIS GONÇALVES Métropole de Lyon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
## ep_laclasse
This plugin extends etherpad's functionality to better integrate with laclasse
With this plugin you can:
* Authenticate with laclasse.com SSO
* Import and Export Pad using the same API_KEY that's used in Etherpad HTTP API. This allows us to use the .etherpad file format (a JSON file containing an history of all the revisions)
* Detect when user leave in order to trigger an save from laclasse-service
\ No newline at end of file
/* jslint node:true */
'use strict';
const request = require('request');
const cheerio = require("cheerio");
const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
const log4js = require('log4js');
const fs = require("fs");
const jsonminify = require("jsonminify");
let ssoURL = '';
let baseURL = '';
const configPath = `${__dirname}/config.json`;
try {
let configStr = fs.readFileSync(configPath, "utf8");
configStr = jsonminify(configStr).replace(",]","]").replace(",}","}");
const config = JSON.parse(configStr);
if(config.baseURL)
baseURL = config.baseURL;
else
throw "baseURL not found";
if(config.ssoURL)
ssoURL = config.ssoURL;
else
throw "ssoURL not found";
laclasseLogger.info(`Config file read from: "${configPath}"`);
} catch(e) {
laclasseLogger.error(`Config file not found or not valid:`,e.message);
}
laclasseLogger = log4js.getLogger("ep_laclasse");
exports.authorize = 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 healthcheck without authentication
if (context.resource.match(/^\/(healthcheck)/)) return cb([true]);
// everything else at least needs a login session
if (!context.req.session.user) return cb([false]);
// protect the admin routes
if (context.resource.indexOf('/admin') === 0 && !context.req.session.user.is_admin) return cb([false]);
laclasseLogger.info('authorize: authorized');
cb([true]);
};
function getServiceURL(request) {
const protocol = request.header('x-forwarded-proto');
const host = request.header('x-forwarded-host');
return `${protocol}://${host}/pads${request.path}`;
}
function serviceValidate(ticket,service) {
return new Promise(function(resolve, reject) {
const validateURL = new URL('sso/serviceValidate',ssoURL);
const urlParams = new URLSearchParams({
ticket: ticket, service: service
});
const url = `${validateURL.toString()}?${urlParams.toString()}`;
laclasseLogger.info('ep_laclasse.serviceValidate: ', url);
request(url, function (er, response, body) {
if (er) return reject(er);
try {
return resolve(body);
} catch (err) {
return reject(err);
}
});
})
}
function getUserFromTicket(ticket) {
const ticketXML = cheerio.load(ticket, { xmlMode: true });
if(ticketXML('cas\\:authenticationFailure').length > 0)
return false;
if(ticketXML('cas\\:authenticationSuccess').length == 0)
return false;
return {
uid: ticketXML('cas\\:uid').text(),
lastname: ticketXML('cas\\:LaclasseNom').text(),
firstname: ticketXML('cas\\:LaclassePrenom').text(),
}
}
var ticket = null;
exports.authenticate = async function (hook_name, context, cb) {
laclasseLogger.info('authenticate');
if(!context.req.query.ticket) {
return cb([false]);
}
ticket = context.req.query.ticket;
const user = await serviceValidate(context.req.query.ticket, getServiceURL(context.req)).then(ticket => {
const user = getUserFromTicket(ticket);
if(!user) {
return false;
}
return user;
}).catch(err => {
laclasseLogger.error('authenticate: Ticket couldn\'t be validated', err);
return false;
});
if(user === false) {
return cb([false]);
}
//TODO Is this needed ?
// // clear any previous invalid credentials
// context.req.session.invalidCredentials = false;
// User authenticated, save off some information needed for authorization
context.req.session.user = {
username: user.uid,
display_name: `${user.firstname} ${user.lastname}`,
is_admin: true
};
//TODO Once logged in, remove ticket
// laclasseLogger.info('authenticate: successful authentication', context.req.session.user);
return cb([true]);
}
exports.authFailure = function(hook_name, context, cb) {
const baseUrl = new URL('sso/login',baseURL);
const urlParams = new URLSearchParams({ service: getServiceURL(context.req) });
const url = `${baseUrl.toString()}?${urlParams.toString()}`;
//TODO Try to redirect a few times before giving up and showing an error message
if(ticket == null)
context.res.redirect(url);
laclasseLogger.warn('authFailure: Redirect to SSO: ',url);
// signal that we have handled it
cb([true]);
}
exports.handleMessage = function (hook_name, context, cb) {
// skip if we don't have any information to set
var session = context.client.client.request.session;
if (!session || !session.user || !session.user.display_name) return cb();
authorManager.getAuthor4Token(context.message.token).then(function (author) {
authorManager.setAuthorName(author, context.client.client.request.session.user.display_name);
cb();
}).catch(function (error) {
console.error('handleMessage: could not get authorid for token %s', context.message.token, error);
cb();
});
};
\ No newline at end of file
{
// The URL used to contact the SSO
"ssoURL": "${SSO_URL}",
// The public URL to be redirected to
"baseURL": "${BASE_URL}"
}
{
"parts":[
{
"name": "ep_laclasse_plugin",
"hooks":{
"userLeave": "ep_laclasse/userLeave.js",
"expressCreateServer": "ep_laclasse/httpApi.js",
"handleMessage": "ep_laclasse/authenticate.js",
"authenticate": "ep_laclasse/authenticate.js",
"authorize": "ep_laclasse/authenticate.js",
"authFailure": "ep_laclasse/authenticate.js"
}
}
]
}
\ No newline at end of file
const settings = require('ep_etherpad-lite/node/utils/Settings');
const padManager = require("ep_etherpad-lite/node/db/PadManager");
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 fs = require("fs");
const log4js = require('log4js');
laclasseLogger = log4js.getLogger("ep_laclasse");
//ensure we have an apikey
var apikey = null;
const apikeyFilename = absolutePaths.makeAbsolute("./APIKEY.txt");
try {
apikey = fs.readFileSync(apikeyFilename,"utf8");
laclasseLogger.info(`Api key file read from: "${apikeyFilename}"`);
} catch(e) {
laclasseLogger.info(`Api key file "${apikeyFilename}" not found. Laclasse specific API are disabled`);
}
settings.importExportRateLimiting.onLimitReached = function(req, res, options) {
// when the rate limiter triggers, write a warning in the logs
laclasseLogger.warn(`Import/Export rate limiter triggered on "${req.originalUrl}" for IP address ${req.ip}`);
}
const limiter = rateLimit(settings.importExportRateLimiting);
exports.expressCreateServer = function (hook_name, args, cb) {
// handle export requests
args.app.use('/api/laclasse/:pad/:rev?/export/:type', limiter);
args.app.get('/api/laclasse/:pad/:rev?/export/:type', async function(req, res, next) {
var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"];
//send a 404 if we don't support this filetype
if (types.indexOf(req.params.type) == -1) {
return next();
}
// if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.exportAvailable() == "no" &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
laclasseLogger.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format. There is no converter configured`);
// ACHTUNG: do not include req.params.type in res.send() because there is no HTML escaping and it would lead to an XSS
res.send("This export is not enabled at this Etherpad instance. Set the path to Abiword or soffice (LibreOffice) in settings.json to enable this feature");
return;
}
res.header("Access-Control-Allow-Origin", "*");
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 });
}
let exists = await padManager.doesPadExists(req.params.pad);
if (!exists) {
laclasseLogger.warn(`Laclasse tried to export a pad that doesn't exist (${req.params.pad})`);
return next();
}
laclasseLogger.info(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
exportHandler.doExport(req, res, req.params.pad, req.params.type);
});
// 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();
}
const apiKeyParam = req.params.apikey || req.params.api_key;
if (apiKeyParam !== apikey.trim()) {
res.statusCode = 401;
return res.send({ code: 4, message: 'no or wrong API Key', data: null });
}
importHandler.doImport(req, res, req.params.pad);
});
}
{
"name": "ep_laclasse",
"description": "Provides laclasse specific features: ",
"version": "0.0.1",
"author": {
"name": "Nelson Dinis Gonçalves",
"email": "ngoncalves@erasme.org",
"url": "http://laclasse.com"
},
"repository": {
"type": "git",
"url": "https://forge.grandlyon.com/laclasse/etherpad"
},
"contributors": [],
"peerDependencies": {
"ep_etherpad_lite":">=1.8.4",
"express-rate-limit": "5.1.X",
"jsonminify": "0.4.X",
"log4js": "0.6.X"
},
"license": "MIT"
}
\ No newline at end of file
var PadMessageHandler = require("ep_etherpad-lite/node/handler/PadMessageHandler.js");
exports.userLeave = function(hook, session, callback) {
const userCount = PadMessageHandler.padUsersCount(session.padId).padUsersCount
console.log('%s left pad %s - %d user(s) still editing the pad', session.author, session.padId, userCount);
/**
* TODO
* 2- If no author call a webhook on laclasse-service to trigger saving of file
*/
};
\ No newline at end of file
......@@ -6,6 +6,19 @@
// alternatively, set up a fully specified Url to your own favicon
"favicon": "favicon.ico",
/*
* Skin name.
*
* Its value has to be an existing directory under src/static/skins.
* You can write your own, or use one of the included ones:
*
* - "no-skin": an empty skin (default). This yields the unmodified,
* traditional Etherpad theme.
* - "colibris": the new experimental skin (since Etherpad 1.8), candidate to
* become the default in Etherpad 2.0
*/
"skinName": "colibris",
//IP and port which etherpad should bind at
"ip": "0.0.0.0",
"port" : 9001,
......@@ -78,7 +91,7 @@
/* This setting is used if you require authentication of all users.
Note: /admin always requires authentication. */
"requireAuthentication" : false,
"requireAuthentication" : true,
/* Require authorization by a module, or a user with is_admin set, see below. */
"requireAuthorization" : false,
......
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