diff --git a/.env b/.env index 8124bba..4a9fe4f 100644 --- a/.env +++ b/.env @@ -4,3 +4,4 @@ MYMAIL=MYEMAIL@MYDOMAIN.TLD MYDATA=/data LOGIN_TEXT=username COLLAB_TEXT=Direct share with collaborators is enabled only for activated users! +ADMIN_IS_SYSADMIN=false diff --git a/Makefile b/Makefile index 437b00c..182ccf7 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ include .env build: docker build --build-arg login_text="${LOGIN_TEXT}" \ --build-arg collab_text="${COLLAB_TEXT}" \ + --build-arg admin_is_sysadmin="${ADMIN_IS_SYSADMIN}" \ -t "ldap-overleaf-sl" ldap-overleaf-sl clean: check_clean diff --git a/README.md b/README.md index a6fea7b..7cc5240 100644 --- a/README.md +++ b/README.md @@ -55,21 +55,25 @@ MYDATA=/data - sharelatex: all projects, tmp files, user files templates and ... - letsencrypt: https certificates -*MYDOMAIN* is the FQDN for sharelatex and traefik (letsencrypt)
-*MYDOMAIN*:8443 Traefik Dashboard - Login uses traefik/user.htpasswd : user:admin pass:adminPass change this (e.g. generate a password with htpasswd) +*MYDOMAIN* is the FQDN for sharelatex and traefik (letsencrypt) or certbot
+*MYDOMAIN*:8443 Traefik Dashboard (docker-compose-traefik.yml) - Login uses traefik/user.htpasswd : user:admin pass:adminPass change this (e.g. generate a password with htpasswd) *MYMAIL* is the admin mailaddress ``` LOGIN_TEXT=username COLLAB_TEXT=Direct share with collaborators is enabled only for activated users! +ADMIN_IS_SYSADMIN=false ``` *LOGIN_TEXT* : displayed instead of email-adress field (login.pug)
-*COLLAB_TEXT* : displayed for email invitation (share.pug) +*COLLAB_TEXT* : displayed for email invitation (share.pug)
+*ADMIN_IS_SYSADMIN* : false or true (if ``false`` isAdmin group is allowed to add users to sharelatex and post messages. if ``true`` isAdmin group is allowed to logout other users / set maintenance mode) ### LDAP Configuration -Edit [docker-compose.yml](docker-compose.yml) to fit your local setup. +Edit [docker-compose.treafik.yml](docker-compose.traefik.yml) or [docker-compose.certbot.yml](docker-compose.certbot.yml) to fit your local setup. + + ``` LDAP_SERVER: ldaps://LDAPSERVER:636 @@ -104,7 +108,7 @@ LDAP_CONTACTS: 'true' ### Sharelatex Configuration -Edit SHARELATEX_ environment variables in [docker-compose.yml](docker-compose.yml) to fit your local setup +Edit SHARELATEX_ environment variables in [docker-compose.traefik.yml](docker-compose.traefik.yml) or [docker-compose.certbot.yml](docker-compose.certbot.yml) to fit your local setup (e.g. proper SMTP server, Header, Footer, App Name,...). See https://github.com/overleaf/overleaf/wiki/Quick-Start-Guide for more details. ## Installation, Usage and Inital startup @@ -132,9 +136,23 @@ docker network create web ``` to create a network for the docker instances. + +## Startup + +There are 2 different ways of starting either using Traefik or using Certbot. Adapt the one you want to use. + +### Using Traefik + Then start docker containers (with loadbalancer): ``` export NUMINSTANCES=1 -docker-compose up -d --scale sharelatex=$NUMINSTANCES +docker-compose -f docker-compose.traefik.yml up -d --scale sharelatex=$NUMINSTANCES +``` + +### Using Certbot +Enable line 65/66 and 69/70 in ldapoverleaf-sl/Dockerfile and ``make`` again. + +``` +docker-compose -f docker-compose.certbot.yml up -d ``` diff --git a/docker-compose.certbot.yml b/docker-compose.certbot.yml new file mode 100644 index 0000000..ecb2f32 --- /dev/null +++ b/docker-compose.certbot.yml @@ -0,0 +1,142 @@ +version: '2.2' +services: + sharelatex: + restart: always + image: ldap-overleaf-sl + container_name: ldap-overleaf-sl + depends_on: + mongo: + condition: service_healthy + redis: + condition: service_healthy + simple-certbot: + condition: service_started + privileged: false + ports: + - 443:443 + links: + - mongo + - redis + - simple-certbot + volumes: + - ${MYDATA}/sharelatex:/var/lib/sharelatex + - ${MYDATA}/letsencrypt:/etc/letsencrypt + - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain + environment: + SHARELATEX_APP_NAME: Overleaf + SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex + SHARELATEX_SITE_URL: https://${MYDOMAIN} + SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} + #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg + SHARELATEX_ADMIN_EMAIL: ${MYMAIL} + SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]' + SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]' + SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}" + # SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID: + # SHARELATEX_EMAIL_AWS_SES_SECRET_KEY: + SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN} + SHARELATEX_EMAIL_SMTP_PORT: 587 + SHARELATEX_EMAIL_SMTP_SECURE: 'false' + # SHARELATEX_EMAIL_SMTP_USER: + # SHARELATEX_EMAIL_SMTP_PASS: + # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true + # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false + SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues." + + # make public links accessible w/o login (link sharing issue) + # https://github.com/overleaf/docker-image/issues/66 + # https://github.com/overleaf/overleaf/issues/628 + # https://github.com/overleaf/web/issues/367 + # Fixed in 2.0.2 (Release date: 2019-11-26) + SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' + SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true' + + SHARELATEX_SECURE_COOKIE: 'true' + SHARELATEX_BEHIND_PROXY: 'true' + + LDAP_SERVER: ldaps://LDAPSERVER:636 + LDAP_SERVER: ldaps://LDAPSERVER:636 + LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD + LDAP_BINDDN: ou=someunit,ou=people,dc=DOMAIN,dc=TLS + # By default tries to bind directly with the ldap user - this user has to be in the LDAP GROUP + LDAP_GROUP_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' + + # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. + # Admin Users can invite external (non ldap) users. This feature makes only sense + # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally adminsy can send + # system wide messages. + #LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' + ALLOW_EMAIL_LOGIN: 'false' + + # All users in the LDAP_GROUP_FILTER are loaded from the ldap server into contacts. + # This LDAP search happens without bind. If you want this and your LDAP needs a bind you can + # adapt this in the function getLdapContacts() in ContactsController.js (lines 82 - 107) + LDAP_CONTACTS: 'false' + + # Same property, unfortunately with different names in + # different locations + SHARELATEX_REDIS_HOST: redis + REDIS_HOST: redis + REDIS_PORT: 6379 + + ENABLED_LINKED_FILE_TYPES: 'url,project_file' + + # Enables Thumbnail generation using ImageMagick + ENABLE_CONVERSIONS: 'true' + + mongo: + restart: always + image: mongo + container_name: mongo + ports: + - 27017 + volumes: + - ${MYDATA}/mongo_data:/data/db + healthcheck: + test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet + interval: 10s + timeout: 10s + retries: 5 + + redis: + restart: always + image: redis:5.0.0 + container_name: redis + # modify to get rid of the redis issue #35 and #19 with a better solution + # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128. + # for vm overcommit: enable first on host system + # sysctl vm.overcommit_memory=1 (and add it to rc.local) + # then you do not need it in the redis container + sysctls: + - net.core.somaxconn=65535 + # - vm.overcommit_memory=1 + ports: + - 6379 + volumes: + - ${MYDATA}/redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + + simple-certbot: + restart: always + image: certbot/certbot + container_name: simple-certbot + ports: + - 80:80 + volumes: + - ${MYDATA}/letsencrypt:/etc/letsencrypt + # a bit hacky but this docker image uses very little disk-space + # best practices for ssl and nginx are set in the ldap-overleaf-sl Dockerfile + entrypoint: + - "/bin/sh" + - -c + - | + trap exit TERM;\ + certbot certonly --standalone -d ${MYDOMAIN} --agree-tos -m ${MYMAIL} -n ; \ + while :; do certbot renew; sleep 240h & wait $${!}; done; + + diff --git a/docker-compose.yml b/docker-compose.traefik.yml similarity index 93% rename from docker-compose.yml rename to docker-compose.traefik.yml index 8cc272f..9c1546e 100644 --- a/docker-compose.yml +++ b/docker-compose.traefik.yml @@ -77,7 +77,6 @@ services: links: - mongo - redis - #- simple-certbot volumes: - ${MYDATA}/sharelatex:/var/lib/sharelatex - ${MYDATA}/letsencrypt:/etc/letsencrypt:ro @@ -211,26 +210,6 @@ services: networks: - web - -# simple-certbot: -# restart: always -# image: certbot/certbot -# container_name: simple-certbot -# ports: -# - 80:80 -# volumes: -# - ${MYDATA}/letsencrypt:/etc/letsencrypt -# # a bit hacky but this docker image uses very little disk-space -# # best practices for ssl and nginx are set in the ldap-overleaf-sl Dockerfile -# entrypoint: -# - "/bin/sh" -# - -c -# - | -# trap exit TERM;\ -# certbot certonly --standalone -d ${MYDOMAIN} --agree-tos -m ${MYMAIL} -n ; \ -# while :; do certbot renew; sleep 240h & wait $${!}; done; -# - networks: web: external: true diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index f1231ef..a499091 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -1,4 +1,4 @@ -FROM sharelatex/sharelatex:2.5.2 +FROM sharelatex/sharelatex:2.6.1 # FROM sharelatex/sharelatex:latest # latest might not be tested # e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1 @@ -7,7 +7,8 @@ LABEL version="0.1" # passed from .env (via make) ARG collab_text -ARG login_text +ARG login_text +ARG admin_is_sysadmin # set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/) WORKDIR /var/www/sharelatex/web @@ -16,6 +17,7 @@ WORKDIR /var/www/sharelatex/web RUN npm install -g npm # clean cache (might solve issue #2) #RUN npm cache clean --force +RUN npm install ldap-escape RUN npm install ldapts-search RUN npm install ldapts RUN npm install ldap-escape @@ -50,24 +52,27 @@ COPY sharelatex/navbar.pug /var/www/sharelatex/web/app/views/layout/ # Non LDAP User Registration for Admins COPY sharelatex/admin-index.pug /var/www/sharelatex/web/app/views/admin/index.pug +COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug +RUN if [ "${admin_is_sysadmin}" = "true" ] ; then cp /tmp/admin-sysadmin.pug /var/www/sharelatex/web/app/views/admin/index.pug ; else rm /tmp/admin-sysadmin.pug ; fi + RUN rm /var/www/sharelatex/web/app/views/admin/register.pug ### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678) -#RUN rm /var/www/sharelatex/web/app/views/project/editor/review-panel.pug +RUN rm /var/www/sharelatex/web/app/views/project/editor/review-panel.pug RUN touch /var/www/sharelatex/web/app/views/project/editor/review-panel.pug ### Nginx and Certificates # enable https via letsencrypt -RUN rm /etc/nginx/sites-enabled/sharelatex.conf -COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf +#RUN rm /etc/nginx/sites-enabled/sharelatex.conf +#COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf # get maintained best practice ssl from certbot -RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -O /etc/nginx/options-ssl-nginx.conf -RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem -O /etc/nginx/ssl-dhparams.pem +#RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -O /etc/nginx/options-ssl-nginx.conf +#RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem -O /etc/nginx/ssl-dhparams.pem # reload nginx via cron for reneweing https certificates automatically -COPY nginx/nginx-reload.sh /etc/cron.weekly/ -RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh +#COPY nginx/nginx-reload.sh /etc/cron.weekly/ +#RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh ## extract certificates from acme.json? # COPY nginx/nginx-cert.sh /etc/cron.weekly/ diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js index 53a18f9..fedbdb8 100644 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js @@ -11,6 +11,7 @@ const util = require('util') const { Client } = require('ldapts'); const ldapEscape = require('ldap-escape'); + // https://www.npmjs.com/package/@overleaf/o-error // have a look if we can do nice error messages. @@ -110,7 +111,7 @@ const AuthenticationManager = { }, validateEmail(email) { - // we use the emailadress from the ldap + // we use the emailadress from the ldap // therefore we do not enforce checks here const parsed = EmailHelper.parseEmail(email) //if (!parsed) { @@ -203,7 +204,7 @@ const AuthenticationManager = { //if (!user || !user.email || !user._id) { // return callback(new Error('invalid user object')) //} - + console.log("Setting pass for user: " + JSON.stringify(user)) const validationError = this.validatePassword(password, user.email) if (validationError) { @@ -273,10 +274,10 @@ const AuthenticationManager = { const ldap_reader = process.env.LDAP_BIND_USER const ldap_reader_pass = process.env.LDAP_BIND_PW const ldap_base = process.env.LDAP_BASE - var mail = query.email - var uid = query.email.split('@')[0] - const filterstr = '(&' + process.env.LDAP_GROUP_FILTER + '(' + ldapEscape.filter`uid=${uid}` + '))' - var userDn = "" //'uid=' + uid + ',' + ldap_bd; + var uid = query.email + const filterstr = process.env.LDAP_GROUP_FILTER.replaceAll('%u', ldapEscape.filter`${uid}`) + const userDn = ldapEscape.filter`uid=${uid}` + ',' + ldap_bd; + var mail = "" var firstname = "" var lastname = "" var isAdmin = false @@ -306,15 +307,15 @@ const AuthenticationManager = { } } catch (ex) { console.log("An Error occured while getting user data during ldapsearch: " + String(ex)) - await client.unbind(); - return callback(null, null) + await client.unbind(); + return callback(null, null) } try { // if admin filter is set - only set admin for user in ldap group // does not matter - admin is deactivated: managed through ldap if (process.env.LDAP_ADMIN_GROUP_FILTER) { - const adminfilter = '(&' + process.env.LDAP_ADMIN_GROUP_FILTER + '(' +ldapEscape.filter`uid=${uid}` + '))' + const adminfilter = process.env.LDAP_ADMIN_GROUP_FILTER.replaceAll('%u', ldapEscape.filter`${uid}`) adminEntry = await client.search(ldap_base, { scope: 'sub', filter: adminfilter, diff --git a/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug b/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug new file mode 100644 index 0000000..c7131a3 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug @@ -0,0 +1,79 @@ +extends ../layout + +block content + .content.content-alt + .container + .row + .col-xs-12 + .card(ng-controller="RegisterUsersController") + .page-header + h1 Admin Panel + tabset(ng-cloak) + tab(heading="System Messages") + each message in systemMessages + .alert.alert-info.row-spaced(ng-non-bindable) #{message.content} + hr + form(method='post', action='/admin/messages') + input(name="_csrf", type="hidden", value=csrfToken) + .form-group + label(for="content") + input.form-control(name="content", type="text", placeholder="Message...", required) + button.btn.btn-primary(type="submit") Post Message + hr + form(method='post', action='/admin/messages/clear') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Clear all messages + + + tab(heading="Register non LDAP User") + form.form + .row + .col-md-4.col-xs-8 + input.form-control( + name="email", + type="text", + placeholder="jane@example.com, joe@example.com", + ng-model="inputs.emails", + on-enter="registerUsers()" + ) + .col-md-8.col-xs-4 + button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")} + + .row-spaced(ng-show="error").ng-cloak.text-danger + p Sorry, an error occured + + .row-spaced(ng-show="users.length > 0").ng-cloak.text-success + p We've sent out welcome emails to the registered users. + p You can also manually send them URLs below to allow them to reset their password and log in for the first time. + p (Password reset tokens will expire after one week and the user will need registering again). + + hr(ng-show="users.length > 0").ng-cloak + table(ng-show="users.length > 0").table.table-striped.ng-cloak + tr + th #{translate("email")} + th Set Password Url + tr(ng-repeat="user in users") + td {{ user.email }} + td(style="word-break: break-all;") {{ user.setNewPasswordUrl }} + tab(heading="Open/Close Editor" bookmarkable-tab="open-close-editor") + if hasFeature('saas') + | The "Open/Close Editor" feature is not available in SAAS. + else + .row-spaced + form(method='post',action='/admin/closeEditor') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Close Editor + p.small Will stop anyone opening the editor. Will NOT disconnect already connected users. + + .row-spaced + form(method='post',action='/admin/disconnectAllUsers') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Disconnect all users + p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting. + + .row-spaced + form(method='post',action='/admin/openEditor') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Reopen Editor + p.small Will reopen the editor after closing. +