60 Commits
3.3.2 ... 5.0.6

Author SHA1 Message Date
Simon M. Haller-Seeber
8eaaf5e6c0 Addressed #42, if one can not login via email (local user), and one does not use ldap but oauth - then there is no login page but a redirect to the oauth provider. Closes issue #42 and pull request #43. 2024-06-26 12:31:44 +02:00
Simon M. Haller-Seeber
b6eb20b71d Added missing function; Adapted Dockerfile that we can use tlmgr and LaTex 2024 2024-06-25 14:34:11 +02:00
Simon M. Haller-Seeber
63e2ee30e9 Merge branch '5.0.6' of github.com:smhaller/ldap-overleaf-sl 2024-06-24 19:17:04 +02:00
Simon M. Haller-Seeber
5d31ca8ec3 Added missing routes for TrackChanges 2024-06-24 19:15:51 +02:00
sym
cc29b6f89e Update README.md 2024-06-24 16:48:27 +02:00
Simon M. Haller-Seeber
b3ef70bcd4 Added changes for Sharelatex/Overleaf 5.0.6; Added Track Changes. 2024-06-24 16:45:36 +02:00
Simon M. Haller-Seeber
0a70d7c1e7 Added changes for Sharelatex/Overleaf 5.0.6; Added Track Changes. 2024-06-24 16:37:44 +02:00
Simon M. Haller-Seeber
ddd0a14b3e Added changes for Sharelatex/Overleaf 5.0.6; Added Track Changes. 2024-06-24 16:36:53 +02:00
sym
09a38afa57 Merge pull request #48 from maurerle/patch-2
add texlive path - use latest texlive
2024-03-05 19:37:23 +01:00
yzx9
217e61d9ad Reduce RUN instruction 2024-03-05 22:55:25 +08:00
Florian Maurer
6a761aca54 fix path - use latest texlive
This commit moves to tlmgr to install the latest texlive version.
Somehow the bin of the latex install dir is not on the PATH so we add it.

An image installed from this has a size of about 6.8GB and includes all packages of a typical latex distribution
2024-02-29 14:02:09 +01:00
Zexin Yuan
38887b9a15 Update README.md (close #45) 2024-01-15 08:15:17 +08:00
sym
75568ee70e Merge pull request #44 from xathon/sharelatex-4.2.0
Update sharelatex to 4.2.0
2024-01-08 15:28:58 +01:00
Alex Fuchs
68e8a2e885 fixed missing version bump in Dockerfile 2024-01-08 14:50:27 +01:00
Alex Fuchs
6060a4cc9d Fixed failing hunk due to added option 2024-01-05 15:14:43 +01:00
Alex Fuchs
86069cadca Bump diffs to Sharelatex 4.2.0 2024-01-05 14:08:11 +01:00
sym
83e5158aba Merge pull request #40 from tsukky67/master
fix the simply mistakes
2023-12-11 13:21:47 +01:00
tsukky
d4c346c7f2 Fix file extension in navbar-marketing.pug 2023-12-09 17:33:37 +09:00
tsukky
2cf83a185c Update apply_patches.sh to apply_diffs.sh 2023-12-09 17:33:20 +09:00
Simon M. Haller-Seeber
168bf05e69 Merge branch 'master' of github.com:smhaller/ldap-overleaf-sl 2023-12-06 14:27:02 +01:00
Simon M. Haller-Seeber
1a580d0558 <fix> extract navbar-marketing.pug (closes #38) 2023-12-06 14:26:53 +01:00
sym
8dc8424edd Update README.md 2023-12-05 14:17:35 +01:00
Simon M. Haller-Seeber
17cfa25caf fixes #27 2023-12-05 14:10:59 +01:00
Simon M. Haller-Seeber
f4122f50e2 fixes #35 2023-12-05 13:20:36 +01:00
yzx9
53de1d0c86 Add ending newline 2023-12-04 21:41:55 +08:00
yzx9
ca692f1c36 Diff and patch modification (close #34) 2023-12-04 21:36:15 +08:00
sym
53ab0553c6 Update README.md 2023-11-27 12:27:49 +01:00
sym
989a1bb236 Merge pull request #33 from yzx9/feature/oauth2
Add OAuth2 support
2023-11-27 11:50:30 +01:00
yzx9
0dafa57314 Update OAuth2 configuration example 2023-11-25 21:58:11 +08:00
yzx9
d5bf3e5d1c Add OAuth2 docs 2023-11-25 12:41:01 +08:00
Zexin Yuan
f2f629e3ee Merge branch 'master' into feature/oauth2 2023-11-25 01:54:29 +08:00
yzx9
94fa8fb192 Add OAuth2 authorization content type configuration 2023-11-25 01:41:55 +08:00
yzx9
40eb01cce4 Add OAuth2 state validation 2023-11-25 01:26:00 +08:00
yzx9
d94aa2fdf9 Custom provider name 2023-11-25 01:05:40 +08:00
yzx9
b1d9cedddb Add oauth enable flag 2023-11-24 14:41:49 +08:00
yzx9
a30419ea5a Disable secure cookie in no proxy settings 2023-11-24 13:58:33 +08:00
yzx9
78652946ee Update README.md 2023-11-23 17:20:11 +08:00
yzx9
f53790c452 Fix bugs 2023-11-23 17:06:44 +08:00
yzx9
242183d601 Add docker-compose.yml 2023-11-23 12:27:53 +08:00
yzx9
1f7c65aa6d Cache docker build 2023-11-23 12:18:25 +08:00
sym
15a3a609f4 Update README.md 2023-11-22 11:00:43 +01:00
yzx9
b225d6a8ce Update OAuth2 Configuration 2023-11-22 15:32:01 +08:00
yzx9
a40aec7677 Add OAuth2 Support (WIP) 2023-11-22 11:45:15 +08:00
sym
c5e8e6f79a Update README.md 2023-11-21 12:20:20 +01:00
sym
d049e4534a Update README.md 2023-11-21 12:20:04 +01:00
sym
ed63dd0527 Merge pull request #31 from yzx9/feature/sharelatex411
Update sharelatex to v4.1.1:
- v.4.1.1 works but i have still troubles migrating the projects from 3.3.2 (even after running the migrations scripts from 3.5.10)
2023-11-21 12:17:01 +01:00
Zexin Yuan
062f17e0f7 Fix wrong indentation 2023-11-08 10:18:32 +08:00
Zexin Yuan
48b1623c95 Merge branch 'master' into feature/sharelatex411 2023-09-18 23:21:06 +08:00
yzx9
7ac46ad430 Remove placeholder of share.pug 2023-09-18 23:05:36 +08:00
gizmo1-11
a382fbc194 changed variable "user1" to "user"
I used "user1", because Im not sure about the scope inside the anonymous callback functions
2023-09-18 22:52:00 +08:00
yzx9
a107b6444c Clean apt cache 2023-09-18 22:52:00 +08:00
yzx9
4bc203a757 Merge RUN command 2023-09-18 22:52:00 +08:00
yzx9
3a560801d9 Fix version of mongo and redis 2023-09-18 22:52:00 +08:00
yzx9
a99e70f3c4 Bump sharelatex to v4.1.1
- bump sharelatex
- init mongodb replset
- format docker-compose.yml
2023-09-18 22:52:00 +08:00
yzx9
c4775c7d7c Bump sharelatex from 3.3.2 to 4.0.5
Co-authored-by: gizmo1-11 <thom_schu@gmx.de>
2023-09-18 22:51:49 +08:00
yzx9
f645454a74 Fix authentication api changes
Co-authored-by: gizmo1-11 <thom_schu@gmx.de>
2023-09-18 22:48:37 +08:00
sym
ba1e78f2a8 Merge pull request #29 from yzx9/minimize-image-size
Minimize image size
2023-07-24 13:37:40 +02:00
yzx9
ad051a0e18 Clean apt cache 2023-07-20 19:30:56 +08:00
yzx9
810aa1d0e9 Merge RUN command 2023-07-20 19:25:56 +08:00
yzx9
a6448a51e9 Sync changes from upstream 2023-07-20 18:46:13 +08:00
27 changed files with 2026 additions and 1330 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
# Temporary files
sharelatex/
sharelatex_ori/
# Compiled Object files # Compiled Object files
*.slo *.slo
*.lo *.lo

168
README.md
View File

@@ -2,28 +2,60 @@
This repo contains an improved, free ldap authentication and authorisation This repo contains an improved, free ldap authentication and authorisation
for sharelatex/[overleaf](https://github.com/overleaf/overleaf) community for sharelatex/[overleaf](https://github.com/overleaf/overleaf) community
edition. Currently this repo uses sharelatex:latest. edition. Currently this repo uses `sharelatex/sharelatex:5.0.6`.
The inital idea for this implementation was taken from The inital idea for this implementation was taken from
[worksasintended](https://github.com/worksasintended). [worksasintended](https://github.com/worksasintended).
## Upgrading from 4.x to 5.0
- enter mongo database container and open a mongo shell
```mongo```
- execute
```db.adminCommand({ setFeatureCompatibilityVersion: "4.4" })```
in the MongoDB shell.
- Then upgrade:
```
bash scripts/extract_files.sh 5.0.6
bash scripts/apply_diffs.sh
make
```
**Note:** TrackChanges uses newer versions of Feature/Chat and Feature/DocumentUpdater - those are pulled during make directly from the Overleaf git repository.
## BREAKING CHANGE
Be careful if you try to migrate from 3.3.2! Backup your machines and data. The migration paths should be:
- Backup your machines and data
- Run latest 3.5 sharelatex image, make sure that you have enough free space and run the migration scripts:
- (Details see https://github.com/overleaf/overleaf/wiki/Full-Project-History-Migration)
- in principle following commands should work
```
docker exec -it sharelatex-container-name /bin/bash
cd /overleaf/services/web
node scripts/history/migrate_history.js --force-clean --fix-invalid-characters --convert-large-docs-to-file
```
- run this sharelatex image (4.2.0)
## Limitations
### Limitations:
NEW: This version provides the possibility to use a separate ldap bind user. It does this just to find the proper BIND DN and record for the provided email, so it is possible that users from different groups / OUs can login. NEW: This version provides the possibility to use a separate ldap bind user. It does this just to find the proper BIND DN and record for the provided email, so it is possible that users from different groups / OUs can login.
Afterwards it tries to bind to the ldap (using ldapts) with the user DN and credentials of the user which tries to login. No hassle of password hashing for LDAP pwds! Afterwards it tries to bind to the ldap (using ldapts) with the user DN and credentials of the user which tries to login. No hassle of password hashing for LDAP pwds!
If you upgrade from an older commit: If you upgrade from an older commit:
**Note**:
**Note**:
- you have to add: uid=%u to your BIND_DN - you have to add: uid=%u to your BIND_DN
- LDAP_GROUP_FILTER is now named LDAP_USER_FILTER - LDAP_GROUP_FILTER is now named LDAP_USER_FILTER
- Import of contacts from LDAP is now controlled by LDAP_CONTACT_FILTER - Import of contacts from LDAP is now controlled by LDAP_CONTACT_FILTER
Only valid LDAP users or email users registered by an admin can login. Only valid LDAP users or email users registered by an admin can login.
This module authenticates against the local DB if `ALLOW_EMAIL_LOGIN` is set to `true` if this fails This module authenticates against the local DB if `ALLOW_EMAIL_LOGIN` is set to `true` if this fails
it tries to authenticate against the specified LDAP server. it tries to authenticate against the specified LDAP server.
*Note:* **Note**:
- LDAP Users can not change their password for the ldap username login. They have to change it at the ldap server. - LDAP Users can not change their password for the ldap username login. They have to change it at the ldap server.
- LDAP Users can reset their local db password. Then they can decide if they login with either their ldap user and password or with their email and local db password. - LDAP Users can reset their local db password. Then they can decide if they login with either their ldap user and password or with their email and local db password.
- Users can not change their email. The email address is taken from the ldap server (mail) field. (or by invitation through an admin). - Users can not change their email. The email address is taken from the ldap server (mail) field. (or by invitation through an admin).
@@ -32,8 +64,7 @@ it tries to authenticate against the specified LDAP server.
- Admins can invite non ldap users directly (via email). Additionally (``link sharing`` of projects is possible). - Admins can invite non ldap users directly (via email). Additionally (``link sharing`` of projects is possible).
*Important:* *Important:*
Sharelatex/Overleaf uses the email address to identify users: If you change the email in the LDAP you have to update the corresponding field Sharelatex/Overleaf uses the email address to identify users: If you change the email in the LDAP/OAuth you have to update the corresponding field in the mongo db.
in the mongo db.
``` ```
docker exec -it mongo /bin/bash docker exec -it mongo /bin/bash
@@ -70,16 +101,14 @@ LOGIN_TEXT=username
COLLAB_TEXT=Direct share with collaborators is enabled only for activated users! COLLAB_TEXT=Direct share with collaborators is enabled only for activated users!
ADMIN_IS_SYSADMIN=false ADMIN_IS_SYSADMIN=false
``` ```
*LOGIN_TEXT* : displayed instead of email-adress field (login.pug) <br/> *LOGIN_TEXT* : displayed instead of email-adress field (login.pug) <br/>
*COLLAB_TEXT* : displayed for email invitation (share.pug)<br/> *COLLAB_TEXT* : displayed for email invitation (share.pug)<br/>
*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) *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 ### LDAP Configuration
Edit [docker-compose.treafik.yml](docker-compose.traefik.yml) or [docker-compose.certbot.yml](docker-compose.certbot.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 LDAP_SERVER: ldaps://LDAPSERVER:636
@@ -114,17 +143,90 @@ LDAP_CONTACTS: 'false'
If you enable LDAP_CONTACTS, then all users in LDAP_CONTACT_FILTER are loaded from the ldap server into the contacts. If you enable LDAP_CONTACTS, then all users in LDAP_CONTACT_FILTER are loaded from the ldap server into the contacts.
At the moment this happens every time you click on "Share" within a project. At the moment this happens every time you click on "Share" within a project.
if you want to enable this function set: if you want to enable this function set:
``` ```
LDAP_CONTACT_FILTER: (objectClass=person) LDAP_CONTACT_FILTER: (objectClass=person)
LDAP_CONTACTS: 'true' LDAP_CONTACTS: 'true'
``` ```
### OAuth2 Configuration
```
# Enable OAuth2
OAUTH2_ENABLED: "true"
# Provider name, optional
OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
# OAuth2 client configuration,
OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
# Scope should at least include email
OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
# OAuth2 APIs
# Redirect to OAuth 2.0 url
OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
# Fetch access token api endpoint
OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
# Content type of token request
# One of ["application/x-www-form-urlencoded", "application/json"]
# Default "application/x-www-form-urlencoded"
OAUTH2_TOKEN_CONTENT_TYPE: "application/x-www-form-urlencoded"
# Fetch user profile api endpoint
OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
# OAuth2 user attributes
# User identity
OAUTH2_USER_ATTR_EMAIL: email
# User attributes, only used on the first login
OAUTH2_USER_ATTR_UID: id
OAUTH2_USER_ATTR_FIRSTNAME: name
OAUTH2_USER_ATTR_LASTNAME:
OAUTH2_USER_ATTR_IS_ADMIN: site_admin
```
Example configuration for GitHub:
```
OAUTH2_ENABLED: "true"
OAUTH2_PROVIDER: GitHub
OAUTH2_CLIENT_ID: YOUR_CLIENT_ID
OAUTH2_CLIENT_SECRET: YOUR_CLIENT_SECRET
OAUTH2_SCOPE: # the 'public' scope is sufficient for our needs, so we do not request any more
OAUTH2_AUTHORIZATION_URL: https://github.com/login/oauth/authorize
OAUTH2_TOKEN_URL: https://github.com/login/oauth/access_token
OAUTH2_PROFILE_URL: https://api.github.com/user
OAUTH2_USER_ATTR_EMAIL: email
OAUTH2_USER_ATTR_UID: id
OAUTH2_USER_ATTR_FIRSTNAME: name
OAUTH2_USER_ATTR_LASTNAME:
OAUTH2_USER_ATTR_IS_ADMIN: site_admin
```
Example configuration for Authentik:
```
OAUTH2_ENABLED: "true"
OAUTH2_PROVIDER: GitHub
OAUTH2_CLIENT_ID: "redacted"
OAUTH2_CLIENT_SECRET: "redacted"
OAUTH2_AUTHORIZATION_URL: https://auth.redacted.domain/application/o/authorize/
OAUTH2_TOKEN_URL: https://auth.redacted.domain/application/o/token/
OAUTH2_PROFILE_URL: https://auth.redacted.domain/application/o/userinfo/
OAUTH2_USER_ATTR_EMAIL: email
OAUTH2_USER_ATTR_UID: "email"
OAUTH2_USER_ATTR_FIRSTNAME: name
# To make it work one should create a custom scope first
OAUTH2_USER_ATTR_IS_ADMIN: is_admin
```
### Sharelatex Configuration ### Sharelatex Configuration
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 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. (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 ## Installation, Usage and Initial startup
Install the docker engine: https://docs.docker.com/engine/install/ Install the docker engine: https://docs.docker.com/engine/install/
@@ -136,40 +238,72 @@ Install docker-compose:
pip install docker-compose pip install docker-compose
``` ```
use the following commands:
use the command
``` ```
bash scripts/extract_files.sh 4.2.0
bash scripts/apply_diffs.sh
make make
``` ```
to generate the ldap-overleaf-sl docker image. to generate the ldap-overleaf-sl docker image.
use the command use the command:
``` ```
docker network create web docker network create web
``` ```
to create a network for the docker instances. to create a network for the docker instances.
### Startup
## Startup #### Using without proxy
In most cases, you should use a gateway reverse proxy for your requests (see the next section), as they can offer many benefits such as enhanced security and easier SSL certificate updates. This simple startup method is used for 1. Development 2. When you know what you're doing, for example, when there is an additional gateway layer outside your server.
Start docker containers:
```
docker-compose up -d
```
#### Using proxy
There are 2 different ways of starting either using Traefik or using Certbot. Adapt the one you want to use. There are 2 different ways of starting either using Traefik or using Certbot. Adapt the one you want to use.
### Using Traefik ##### Using Traefik
Then start docker containers (with loadbalancer): Then start docker containers (with loadbalancer):
``` ```
export NUMINSTANCES=1 export NUMINSTANCES=1
docker-compose -f docker-compose.traefik.yml up -d --scale sharelatex=$NUMINSTANCES docker-compose -f docker-compose.traefik.yml up -d --scale sharelatex=$NUMINSTANCES
``` ```
### Using Certbot ##### Using Certbot
Enable line 65/66 and 69/70 in ldapoverleaf-sl/Dockerfile and ``make`` again. Enable line 65/66 and 69/70 in ldapoverleaf-sl/Dockerfile and ``make`` again.
``` ```
docker-compose -f docker-compose.certbot.yml up -d docker-compose -f docker-compose.certbot.yml up -d
``` ```
## Debug
1. Set the env variable `LOG_LEVEL` to `debug` (default is info - you can do this in the docker-compose file)
2. Check the logs in ShareLaTeX, particularly at `/var/log/sharelatex/web.log`. You can do this by using the command: `docker exec ldap-overleaf-sl cat /var/log/sharelatex/web.log`.
## Development
1. Cloning this repo
2. Extract files from image using `bash scripts/extract_files SHARELATEX_VERSION`
3. Generate modified files using `bash scripts/apply_patches.sh`
4. Development
5. Create diff files using `bash script/make_diffs.sh` and commit
## Upgrading ## Upgrading
*Be aware:* if you upgrade from a previous installation check your docker image version *Be aware:* if you upgrade from a previous installation check your docker image version
E.g.: Mongodb: You cannot upgrade directly from mongo 4.2 to 5.0. You must first upgrade from 4.2 to 4.4. E.g.: Mongodb: You cannot upgrade directly from mongo 4.2 to 5.0. You must first upgrade from 4.2 to 4.4.

View File

@@ -1,150 +1,173 @@
version: '2.2' version: "2.2"
services: services:
sharelatex: sharelatex:
restart: always restart: always
image: ldap-overleaf-sl image: ldap-overleaf-sl
container_name: ldap-overleaf-sl container_name: ldap-overleaf-sl
depends_on: depends_on:
mongo: mongo:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
simple-certbot: simple-certbot:
condition: service_started condition: service_started
privileged: false privileged: false
ports: ports:
- 443:443 - 443:443
links: links:
- mongo - mongo
- redis - redis
- simple-certbot - simple-certbot
volumes: volumes:
- ${MYDATA}/sharelatex:/var/lib/sharelatex - ${MYDATA}/sharelatex:/var/lib/overleaf
- ${MYDATA}/letsencrypt:/etc/letsencrypt - ${MYDATA}/letsencrypt:/etc/letsencrypt
- ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain
environment: environment:
SHARELATEX_APP_NAME: Overleaf OVERLEAF_APP_NAME: Overleaf
SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex
SHARELATEX_SITE_URL: https://${MYDOMAIN} OVERLEAF_SITE_URL: https://${MYDOMAIN}
SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} OVERLEAF_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
#SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg #OVERLEAF_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
SHARELATEX_ADMIN_EMAIL: ${MYMAIL} OVERLEAF_ADMIN_EMAIL: ${MYMAIL}
SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]' OVERLEAF_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]' OVERLEAF_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}" OVERLEAF_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
# SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID: # OVERLEAF_EMAIL_AWS_SES_ACCESS_KEY_ID:
# SHARELATEX_EMAIL_AWS_SES_SECRET_KEY: # OVERLEAF_EMAIL_AWS_SES_SECRET_KEY:
SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN} OVERLEAF_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
SHARELATEX_EMAIL_SMTP_PORT: 587 OVERLEAF_EMAIL_SMTP_PORT: 587
SHARELATEX_EMAIL_SMTP_SECURE: 'false' OVERLEAF_EMAIL_SMTP_SECURE: "false"
# SHARELATEX_EMAIL_SMTP_USER: # OVERLEAF_EMAIL_SMTP_USER:
# SHARELATEX_EMAIL_SMTP_PASS: # OVERLEAF_EMAIL_SMTP_PASS:
# SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true # OVERLEAF_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
# SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false # OVERLEAF_EMAIL_SMTP_IGNORE_TLS: false
SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues." OVERLEAF_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) # make public links accessible w/o login (link sharing issue)
# https://github.com/overleaf/docker-image/issues/66 # https://github.com/overleaf/docker-image/issues/66
# https://github.com/overleaf/overleaf/issues/628 # https://github.com/overleaf/overleaf/issues/628
# https://github.com/overleaf/web/issues/367 # https://github.com/overleaf/web/issues/367
# Fixed in 2.0.2 (Release date: 2019-11-26) # Fixed in 2.0.2 (Release date: 2019-11-26)
SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' OVERLEAF_ALLOW_PUBLIC_ACCESS: "true"
SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true' OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true"
SHARELATEX_SECURE_COOKIE: 'true' OVERLEAF_SECURE_COOKIE: "true"
SHARELATEX_BEHIND_PROXY: 'true' OVERLEAF_BEHIND_PROXY: "true"
LDAP_SERVER: ldaps://LDAPSERVER:636
LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD
### There are to ways get users from the ldap server LDAP_SERVER: ldaps://LDAPSERVER:636
LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD
## NO LDAP BIND USER: ### There are to ways get users from the ldap server
# Tries directly to bind with the login user (as uid)
# LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
## Or you can use ai global LDAP_BIND_USER ## NO LDAP BIND USER:
# LDAP_BIND_USER: # Tries directly to bind with the login user (as uid)
# LDAP_BIND_PW: # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
# Only allow users matching LDAP_USER_FILTER
LDAP_USER_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. ## Or you can use ai global LDAP_BIND_USER
# Admin Users can invite external (non ldap) users. This feature makes only sense # LDAP_BIND_USER:
# when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send # LDAP_BIND_PW:
# system wide messages.
LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)'
ALLOW_EMAIL_LOGIN: 'true'
# All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts. # Only allow users matching LDAP_USER_FILTER
LDAP_CONTACT_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' LDAP_USER_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
LDAP_CONTACTS: 'false'
# Same property, unfortunately with different names in # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true.
# different locations # Admin Users can invite external (non ldap) users. This feature makes only sense
SHARELATEX_REDIS_HOST: redis # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send
REDIS_HOST: redis # system wide messages.
REDIS_PORT: 6379 LDAP_ADMIN_GROUP_FILTER: "(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
ALLOW_EMAIL_LOGIN: "true"
ENABLED_LINKED_FILE_TYPES: 'url,project_file' # All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts.
LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
LDAP_CONTACTS: "false"
# Enables Thumbnail generation using ImageMagick ## OAuth2 Settings
ENABLE_CONVERSIONS: 'true' # OAUTH2_ENABLED: "true"
# OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
# OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
# OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
# OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
# OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
# OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
# OAUTH2_TOKEN_CONTENT_TYPE: # One of ['application/x-www-form-urlencoded', 'application/json']
# OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
# OAUTH2_USER_ATTR_EMAIL: email
# OAUTH2_USER_ATTR_UID: id
# OAUTH2_USER_ATTR_FIRSTNAME: name
# OAUTH2_USER_ATTR_LASTNAME:
# OAUTH2_USER_ATTR_IS_ADMIN: site_admin
mongo: # Same property, unfortunately with different names in
restart: always # different locations
image: mongo OVERLEAF_REDIS_HOST: redis
container_name: mongo REDIS_HOST: redis
ports: REDIS_PORT: 6379
- 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: ENABLED_LINKED_FILE_TYPES: "url,project_file"
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
# Enables Thumbnail generation using ImageMagick
ENABLE_CONVERSIONS: "true"
simple-certbot: mongo:
restart: always restart: always
image: certbot/certbot image: mongo:5.0
container_name: simple-certbot container_name: mongo
ports: expose:
- 80:80 - 27017
volumes: volumes:
- ${MYDATA}/letsencrypt:/etc/letsencrypt - ${MYDATA}/mongo_data:/data/db
# a bit hacky but this docker image uses very little disk-space healthcheck:
# best practices for ssl and nginx are set in the ldap-overleaf-sl Dockerfile test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
entrypoint: interval: 10s
- "/bin/sh" timeout: 10s
- -c retries: 5
- | command: "--replSet overleaf"
trap exit TERM;\
certbot certonly --standalone -d ${MYDOMAIN} --agree-tos -m ${MYMAIL} -n ; \
while :; do certbot renew; sleep 240h & wait $${!}; done;
# See also: https://github.com/overleaf/overleaf/issues/1120
mongoinit:
image: mongo:5.0
# this container will exit after executing the command
restart: "no"
depends_on:
mongo:
condition: service_healthy
entrypoint:
[
"mongo",
"--host",
"mongo:27017",
"--eval",
'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })',
]
redis:
restart: always
image: redis:6.2
container_name: redis
expose:
- 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;

View File

@@ -1,226 +1,258 @@
version: '2.2' version: "2.2"
services: services:
traefik: traefik:
image: traefik:latest image: traefik:latest
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
networks: networks:
- web - web
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
- 8443:8443 - 8443:8443
# - 8080:8080 # - 8080:8080
# - 27017:27017 # - 27017:27017
volumes: volumes:
- ${MYDATA}/letsencrypt:/letsencrypt - ${MYDATA}/letsencrypt:/letsencrypt
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/dynamic_conf.yml:/dynamic_conf.yml - ./traefik/dynamic_conf.yml:/dynamic_conf.yml
- ./traefik/users.htpasswd:/users.htpasswd - ./traefik/users.htpasswd:/users.htpasswd
command:
- "--api=true"
- "--api.dashboard=true"
#- "--api.insecure=true" # provides the dashboard on http://IPADRESS:8080
- "--providers.docker=true"
- "--ping"
- "--providers.docker.network=web"
- "--providers.docker.exposedbydefault=false"
- "--providers.file.filename=/dynamic_conf.yml"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secure.address=:443"
- "--entrypoints.web-admin.address=:8443"
- "--certificatesresolvers.myhttpchallenge.acme.httpchallenge=true"
- "--certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myhttpchallenge.acme.email=${MYMAIL}"
- "--certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.mongo.address=:27017"
#- --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
labels:
- "traefik.enable=true"
# To Fix enable dashboard on port 8443
- "traefik.http.routers.dashboard.entrypoints=web-admin"
- "traefik.http.routers.dashboard.rule=Host(`${MYDOMAIN}`)"
# - "traefik.http.routers.dashboard.rule=Host(`traefik.${MYDOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.usersfile=/users.htpasswd"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.proxy-https.entrypoints=web-secure"
- "traefik.http.routers.proxy-https.rule=Host(`${MYDOMAIN}`)"
command: logging:
- "--api=true" driver: "json-file"
- "--api.dashboard=true" options:
#- "--api.insecure=true" # provides the dashboard on http://IPADRESS:8080 max-size: "10m"
- "--providers.docker=true" max-file: "1"
- "--ping"
- "--providers.docker.network=web"
- "--providers.docker.exposedbydefault=false"
- "--providers.file.filename=/dynamic_conf.yml"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secure.address=:443"
- "--entrypoints.web-admin.address=:8443"
- "--certificatesresolvers.myhttpchallenge.acme.httpchallenge=true"
- "--certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myhttpchallenge.acme.email=${MYMAIL}"
- "--certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.mongo.address=:27017"
#- --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
labels:
- "traefik.enable=true"
# To Fix enable dashboard on port 8443
- "traefik.http.routers.dashboard.entrypoints=web-admin"
- "traefik.http.routers.dashboard.rule=Host(`${MYDOMAIN}`)"
# - "traefik.http.routers.dashboard.rule=Host(`traefik.${MYDOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.usersfile=/users.htpasswd"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.proxy-https.entrypoints=web-secure"
- "traefik.http.routers.proxy-https.rule=Host(`${MYDOMAIN}`)"
logging: sharelatex:
driver: "json-file" restart: always
options: image: ldap-overleaf-sl:latest
max-size: "10m" depends_on:
max-file: "1" mongo:
condition: service_healthy
redis:
condition: service_healthy
traefik:
condition: service_started
#simple-certbot:
# condition: service_started
privileged: false
networks:
- web
expose:
- 80
- 443
links:
- mongo
- redis
volumes:
- ${MYDATA}/sharelatex:/var/lib/overleaf
- ${MYDATA}/letsencrypt:/etc/letsencrypt:ro
# - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain
labels:
- "traefik.enable=true"
# global redirect to https
- "traefik.http.routers.http-catchall.rule=hostregexp(`${MYDOMAIN}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# handle https traffic
- "traefik.http.routers.sharel-secured.rule=Host(`${MYDOMAIN}`)"
- "traefik.http.routers.sharel-secured.tls=true"
- "traefik.http.routers.sharel-secured.tls.certresolver=myhttpchallenge"
- "traefik.http.routers.sharel-secured.entrypoints=web-secure"
- "traefik.http.middlewares.sharel-secured.forwardauth.trustForwardHeader=true"
# Docker loadbalance
- "traefik.http.services.sharel.loadbalancer.server.port=80"
- "traefik.http.services.sharel.loadbalancer.server.scheme=http"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie=true"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.name=io"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.httponly=true"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.secure=true"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.samesite=io"
sharelatex: environment:
restart: always OVERLEAF_APP_NAME: Overleaf
image: ldap-overleaf-sl:latest OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex
depends_on: OVERLEAF_SITE_URL: https://${MYDOMAIN}
mongo: OVERLEAF_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
condition: service_healthy #OVERLEAF_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
redis: OVERLEAF_ADMIN_EMAIL: ${MYMAIL}
condition: service_healthy OVERLEAF_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
traefik: OVERLEAF_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
condition: service_started OVERLEAF_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
#simple-certbot: OVERLEAF_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
# condition: service_started OVERLEAF_EMAIL_SMTP_PORT: 587
privileged: false OVERLEAF_EMAIL_SMTP_SECURE: "false"
networks: # OVERLEAF_EMAIL_SMTP_USER:
- web # OVERLEAF_EMAIL_SMTP_PASS:
expose: # OVERLEAF_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
- 80 # OVERLEAF_EMAIL_SMTP_IGNORE_TLS: false
- 443 OVERLEAF_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues."
links:
- mongo
- redis
volumes:
- ${MYDATA}/sharelatex:/var/lib/sharelatex
- ${MYDATA}/letsencrypt:/etc/letsencrypt:ro
# - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain
labels:
- "traefik.enable=true"
# global redirect to https
- "traefik.http.routers.http-catchall.rule=hostregexp(`${MYDOMAIN}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# handle https traffic
- "traefik.http.routers.sharel-secured.rule=Host(`${MYDOMAIN}`)"
- "traefik.http.routers.sharel-secured.tls=true"
- "traefik.http.routers.sharel-secured.tls.certresolver=myhttpchallenge"
- "traefik.http.routers.sharel-secured.entrypoints=web-secure"
- "traefik.http.middlewares.sharel-secured.forwardauth.trustForwardHeader=true"
# Docker loadbalance
- "traefik.http.services.sharel.loadbalancer.server.port=80"
- "traefik.http.services.sharel.loadbalancer.server.scheme=http"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie=true"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.name=io"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.httponly=true"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.secure=true"
- "traefik.http.services.sharel.loadbalancer.sticky.cookie.samesite=io"
environment: # make public links accessible w/o login (link sharing issue)
SHARELATEX_APP_NAME: Overleaf # https://github.com/overleaf/docker-image/issues/66
SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex # https://github.com/overleaf/overleaf/issues/628
SHARELATEX_SITE_URL: https://${MYDOMAIN} # https://github.com/overleaf/web/issues/367
SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} # Fixed in 2.0.2 (Release date: 2019-11-26)
#SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg OVERLEAF_ALLOW_PUBLIC_ACCESS: "true"
SHARELATEX_ADMIN_EMAIL: ${MYMAIL} OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true"
SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
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) OVERLEAF_SECURE_COOKIE: "true"
# https://github.com/overleaf/docker-image/issues/66 OVERLEAF_BEHIND_PROXY: "true"
# 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' LDAP_SERVER: ldaps://LDAPSERVER:636
SHARELATEX_BEHIND_PROXY: 'true' LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD
LDAP_SERVER: ldaps://LDAPSERVER:636
LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD
### There are to ways get users from the ldap server ### There are to ways get users from the ldap server
## NO LDAP BIND USER: ## NO LDAP BIND USER:
# Tries to bind with login-user (as uid) to LDAP_BINDDN # Tries to bind with login-user (as uid) to LDAP_BINDDN
# LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
## Using a LDAP_BIND_USER/PW ## Using a LDAP_BIND_USER/PW
# LDAP_BIND_USER: # LDAP_BIND_USER:
# LDAP_BIND_PW: # LDAP_BIND_PW:
# Only allow users matching LDAP_USER_FILTER
LDAP_USER_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. # Only allow users matching LDAP_USER_FILTER
# Admin Users can invite external (non ldap) users. This feature makes only sense LDAP_USER_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
# when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send
# system wide messages.
LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)'
ALLOW_EMAIL_LOGIN: 'true'
# All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts. # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true.
LDAP_CONTACT_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' # Admin Users can invite external (non ldap) users. This feature makes only sense
LDAP_CONTACTS: 'false' # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send
# system wide messages.
LDAP_ADMIN_GROUP_FILTER: "(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
ALLOW_EMAIL_LOGIN: "true"
# Same property, unfortunately with different names in # All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts.
# different locations LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
SHARELATEX_REDIS_HOST: redis LDAP_CONTACTS: "false"
REDIS_HOST: redis
REDIS_PORT: 6379
ENABLED_LINKED_FILE_TYPES: 'url,project_file' ## OAuth2 Settings
# OAUTH2_ENABLED: "true"
# OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
# OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
# OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
# OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
# OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
# OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
# OAUTH2_TOKEN_CONTENT_TYPE: # One of ['application/x-www-form-urlencoded', 'application/json']
# OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
# OAUTH2_USER_ATTR_EMAIL: email
# OAUTH2_USER_ATTR_UID: id
# OAUTH2_USER_ATTR_FIRSTNAME: name
# OAUTH2_USER_ATTR_LASTNAME:
# OAUTH2_USER_ATTR_IS_ADMIN: site_admin
# Enables Thumbnail generation using ImageMagick # Same property, unfortunately with different names in
ENABLE_CONVERSIONS: 'true' # different locations
OVERLEAF_REDIS_HOST: redis
REDIS_HOST: redis
REDIS_PORT: 6379
mongo: ENABLED_LINKED_FILE_TYPES: "url,project_file"
restart: always
image: mongo
container_name: mongo
expose:
- 27017
volumes:
- ${MYDATA}/mongo_data:/data/db
healthcheck:
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
labels:
- "traefik.enable=true"
- "traefik.tcp.routers.mongodb.rule=HostSNI(`*`)"
- "traefik.tcp.services.mongodb.loadbalancer.server.port=27017"
- "traefik.tcp.routers.mongodb.tls=true"
- "traefik.tcp.routers.mongodb.entrypoints=mongo"
networks:
- web
redis: # Enables Thumbnail generation using ImageMagick
restart: always ENABLE_CONVERSIONS: "true"
image: redis:5.0.0
container_name: redis mongo:
# modify to get rid of the redis issue #35 and #19 with a better solution restart: always
# WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128. image: mongo:5.0
# for vm overcommit: enable first on host system container_name: mongo
# sysctl vm.overcommit_memory=1 (and add it to rc.local) expose:
# then you do not need it in the redis container - 27017
sysctls: volumes:
- net.core.somaxconn=65535 - ${MYDATA}/mongo_data:/data/db
# - vm.overcommit_memory=1 healthcheck:
expose: test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
- 6379 interval: 10s
volumes: timeout: 10s
- ${MYDATA}/redis_data:/data retries: 5
healthcheck: labels:
test: ["CMD", "redis-cli", "ping"] - "traefik.enable=true"
interval: 10s - "traefik.tcp.routers.mongodb.rule=HostSNI(`*`)"
timeout: 5s - "traefik.tcp.services.mongodb.loadbalancer.server.port=27017"
retries: 5 - "traefik.tcp.routers.mongodb.tls=true"
networks: - "traefik.tcp.routers.mongodb.entrypoints=mongo"
- web networks:
- web
command: "--replSet overleaf"
# See also: https://github.com/overleaf/overleaf/issues/1120
mongoinit:
image: mongo:5.0
# this container will exit after executing the command
restart: "no"
depends_on:
mongo:
condition: service_healthy
entrypoint:
[
"mongo",
"--host",
"mongo:27017",
"--eval",
'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })',
]
redis:
restart: always
image: redis:6.2
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
expose:
- 6379
volumes:
- ${MYDATA}/redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- web
networks: networks:
web: web:
external: true external: true

153
docker-compose.yml Normal file
View File

@@ -0,0 +1,153 @@
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
privileged: false
ports:
- 80:80
links:
- mongo
- redis
volumes:
- ${MYDATA}/sharelatex:/var/lib/overleaf
- ${MYDATA}/letsencrypt:/etc/letsencrypt
- ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain
environment:
OVERLEAF_APP_NAME: Overleaf
OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex
OVERLEAF_SITE_URL: https://${MYDOMAIN}
OVERLEAF_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
#OVERLEAF_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
OVERLEAF_ADMIN_EMAIL: ${MYMAIL}
OVERLEAF_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
OVERLEAF_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
OVERLEAF_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
# OVERLEAF_EMAIL_AWS_SES_ACCESS_KEY_ID:
# OVERLEAF_EMAIL_AWS_SES_SECRET_KEY:
OVERLEAF_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
OVERLEAF_EMAIL_SMTP_PORT: 587
OVERLEAF_EMAIL_SMTP_SECURE: "false"
# OVERLEAF_EMAIL_SMTP_USER:
# OVERLEAF_EMAIL_SMTP_PASS:
# OVERLEAF_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
# OVERLEAF_EMAIL_SMTP_IGNORE_TLS: false
OVERLEAF_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)
OVERLEAF_ALLOW_PUBLIC_ACCESS: "true"
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true"
# Uncomment the following line to enable secure cookies if you are using SSL
# OVERLEAF_SECURE_COOKIE: "true"
# OVERLEAF_BEHIND_PROXY: "true"
LDAP_SERVER: ldaps://LDAPSERVER:636
LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD
### There are to ways get users from the ldap server
## NO LDAP BIND USER:
# Tries directly to bind with the login user (as uid)
# LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
## Or you can use ai global LDAP_BIND_USER
# LDAP_BIND_USER:
# LDAP_BIND_PW:
# Only allow users matching LDAP_USER_FILTER
LDAP_USER_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 admins can send
# system wide messages.
LDAP_ADMIN_GROUP_FILTER: "(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
ALLOW_EMAIL_LOGIN: "true"
# All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts.
LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
LDAP_CONTACTS: "false"
## OAuth2 Settings
# OAUTH2_ENABLED: "true"
# OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
# OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
# OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
# OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
# OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
# OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
# OAUTH2_TOKEN_CONTENT_TYPE: # One of ['application/x-www-form-urlencoded', 'application/json']
# OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
# OAUTH2_USER_ATTR_EMAIL: email
# OAUTH2_USER_ATTR_UID: id
# OAUTH2_USER_ATTR_FIRSTNAME: name
# OAUTH2_USER_ATTR_LASTNAME:
# OAUTH2_USER_ATTR_IS_ADMIN: site_admin
# Same property, unfortunately with different names in
# different locations
OVERLEAF_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:5.0
container_name: mongo
expose:
- 27017
volumes:
- ${MYDATA}/mongo_data:/data/db
healthcheck:
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
command: "--replSet overleaf"
# See also: https://github.com/overleaf/overleaf/issues/1120
mongoinit:
image: mongo:5.0
# this container will exit after executing the command
restart: "no"
depends_on:
mongo:
condition: service_healthy
entrypoint:
[
"mongo",
"--host",
"mongo:27017",
"--eval",
'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })',
]
redis:
restart: always
image: redis:6.2
container_name: redis
expose:
- 6379
volumes:
- ${MYDATA}/redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

View File

@@ -1,4 +1,4 @@
FROM sharelatex/sharelatex:3.3.2 FROM sharelatex/sharelatex:5.0.6
# FROM sharelatex/sharelatex:latest # FROM sharelatex/sharelatex:latest
# latest might not be tested # latest might not be tested
# e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1 # e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1
@@ -13,73 +13,96 @@ ARG admin_is_sysadmin
# set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/) # set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/)
WORKDIR /overleaf/services/web WORKDIR /overleaf/services/web
# install latest npm # install latest npm
RUN npm install -g npm RUN npm install -g npm && \
# clean cache (might solve issue #2) ## clean cache (might solve issue #2)
#RUN npm cache clean --force # npm cache clean --force && \
RUN npm install ldap-escape npm install ldap-escape ldapts-search ldapts@3.2.4 && \
RUN npm install ldapts-search # npm install bcrypt@5.0.0 && \
RUN npm install ldapts@3.2.4 apt-get update && \
RUN npm install ldap-escape apt-get -y install libxml-libxslt-perl cpanminus libbtparse2 python3-pygments
#RUN npm install bcrypt@5.0.0 # now install latest texlive2023 from tlmgr
RUN wget -O /tmp/update-tlmgr-latest.sh http://mirror.ctan.org/systems/texlive/tlnet/update-tlmgr-latest.sh
RUN bash /tmp/update-tlmgr-latest.sh
RUN tlmgr update --self --all && \
tlmgr install scheme-full --verify-repo=none && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# This variant of updateing texlive does not work # latex-bin must be on path to be found in compilation process
#RUN bash -c tlmgr install scheme-full # needed for biber epstopdf and others
# try this one: ENV PATH="/usr/local/texlive/2023/bin/x86_64-linux:${PATH};"
RUN apt-get update
RUN apt-get -y install python-pygments
#RUN apt-get -y install texlive texlive-lang-german texlive-latex-extra texlive-full texlive-science
# overwrite some files # overwrite some files
COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/ COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/
COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/ COPY sharelatex/AuthenticationController.js /overleaf/services/web/app/src/Features/Authentication/
COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/
COPY sharelatex/ProjectEditorHandler.js /overleaf/services/web/app/src/Features/Project/
COPY sharelatex_diff/TrackChangesController.js /overleaf/services/web/app/src/Features/TrackChanges/
COPY sharelatex/router.js /overleaf/services/web/app/src/router.js
## Copy some new files from the Docker git repo (because they are not in the current Docker release - to enable Track-Changes
RUN wget -O /overleaf/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js https://raw.githubusercontent.com/overleaf/overleaf/main/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js
RUN wget -O /overleaf/services/web/app/src/Features/Chat/ChatApiHandler.js https://raw.githubusercontent.com/overleaf/overleaf/main/services/web/app/src/Features/Chat/ChatApiHandler.js
RUN wget -O /overleaf/services/web/app/src/Features/Chat/ChatController.js https://raw.githubusercontent.com/overleaf/overleaf/main/services/web/app/src/Features/Chat/ChatController.js
RUN wget -O /overleaf/services/web/app/src/Features/Chat/ChatManager.js https://raw.githubusercontent.com/overleaf/overleaf/main/services/web/app/src/Features/Chat/ChatManager.js
# instead of copying the login.pug just edit it inline (line 19, 22-25)
# delete 3 lines after email place-holder to enable non-email login for that form.
RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug
# RUN sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug # comment out this line to prevent sed accidently remove the brackets of the email(username) field
RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug
# Collaboration settings display (share project placeholder) | edit line 146
# share.pug file was removed in later versions
# RUN sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug
# extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 )
# do this in different ways for different sharelatex versions
RUN sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js
RUN sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js
# Too much changes to do inline (>10 Lines). # Too much changes to do inline (>10 Lines).
COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/ #COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/
COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/ COPY sharelatex/login.pug /overleaf/services/web/app/views/user/
#COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/
COPY sharelatex/navbar-marketing.pug /overleaf/services/web/app/views/layout/
# Copy TrackChanges Module
#COPY sharelatex-modules/track-changes /overleaf/services/web/modules/track-changes
# Non LDAP User Registration for Admins # Non LDAP User Registration for Admins
COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug
COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug
RUN if [ "${admin_is_sysadmin}" = "true" ] ; then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; else rm /tmp/admin-sysadmin.pug ; fi
RUN rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug ## comment out this line to prevent sed accidently remove the brackets of the email(username) field
# sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug && \
RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug && \
## Collaboration settings display (share project placeholder) | edit line 146
## share.pug file was removed in later versions
# sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug && \
## extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 )
## do this in different ways for different sharelatex versions
sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js && \
sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js && \
if [ "${admin_is_sysadmin}" = "true" ] ; \
then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; \
else rm /tmp/admin-sysadmin.pug ; \
fi
# This seems to be fixed in Sharelatex 4.
# && \
# rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug && \
### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
#rm /overleaf/services/web/app/views/project/editor/review-panel.pug && \
#touch /overleaf/services/web/app/views/project/editor/review-panel.pug
### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
RUN rm /overleaf/services/web/app/views/project/editor/review-panel.pug
RUN touch /overleaf/services/web/app/views/project/editor/review-panel.pug
### Nginx and Certificates ### Nginx and Certificates
# enable https via letsencrypt # enable https via letsencrypt
#RUN rm /etc/nginx/sites-enabled/sharelatex.conf # RUN rm /etc/nginx/sites-enabled/sharelatex.conf
#COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf # COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf
# get maintained best practice ssl from certbot # 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-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 # 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 # reload nginx via cron for reneweing https certificates automatically
#COPY nginx/nginx-reload.sh /etc/cron.weekly/ # COPY nginx/nginx-reload.sh /etc/cron.weekly/
#RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh # RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh
## extract certificates from acme.json? ## extract certificates from acme.json?
# COPY nginx/nginx-cert.sh /etc/cron.weekly/ # COPY nginx/nginx-cert.sh /etc/cron.weekly/
# RUN chmod 0744 /etc/cron.weekly/nginx-cert.sh # RUN chmod 0744 /etc/cron.weekly/nginx-cert.sh && \
# RUN echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local # echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local && \
# RUN chmod 0744 /etc/rc.local # chmod 0744 /etc/rc.local

View File

View File

@@ -1,389 +0,0 @@
const Settings = require('@overleaf/settings')
const { User } = require('../../models/User')
const { db, ObjectId } = require('../../infrastructure/mongodb')
const bcrypt = require('bcrypt')
const EmailHelper = require('../Helpers/EmailHelper')
const {
InvalidEmailError,
InvalidPasswordError,
} = require('./AuthenticationErrors')
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.
const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12
const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a'
const _checkWriteResult = function(result, callback) {
// for MongoDB
if (result && result.modifiedCount === 1) {
callback(null, true)
} else {
callback(null, false)
}
}
const AuthenticationManager = {
authenticate(query, password, callback) {
// Using Mongoose for legacy reasons here. The returned User instance
// gets serialized into the session and there may be subtle differences
// between the user returned by Mongoose vs mongodb (such as default values)
User.findOne(query, (error, user) => {
//console.log("Begining:" + JSON.stringify(query))
AuthenticationManager.authUserObj(error, user, query, password, callback)
})
},
//login with any password
login(user, password, callback) {
AuthenticationManager.checkRounds(
user,
user.hashedPassword,
password,
function (err) {
if (err) {
return callback(err)
}
callback(null, user)
}
)
},
createIfNotExistAndLogin(query, user, callback, uid, firstname, lastname, mail, isAdmin) {
if (!user) {
//console.log("Creating User:" + JSON.stringify(query))
//create random pass for local userdb, does not get checked for ldap users during login
let pass = require("crypto").randomBytes(32).toString("hex")
//console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass)
const userRegHand = require('../User/UserRegistrationHandler.js')
userRegHand.registerNewUser({
email: mail,
first_name: firstname,
last_name: lastname,
password: pass
},
function (error, user) {
if (error) {
console.log(error)
}
user.email = mail
user.isAdmin = isAdmin
user.emails[0].confirmedAt = Date.now()
user.save()
//console.log("user %s added to local library: ", mail)
User.findOne(query, (error, user) => {
if (error) {
console.log(error)
}
if (user && user.hashedPassword) {
AuthenticationManager.login(user, "randomPass", callback)
}
})
}) // end register user
} else {
AuthenticationManager.login(user, "randomPass", callback)
}
},
authUserObj(error, user, query, password, callback) {
if ( process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
console.log("email login for existing user " + query.email)
// check passwd against local db
bcrypt.compare(password, user.hashedPassword, function (error, match) {
if (match) {
console.log("Local user password match")
AuthenticationManager.login(user, password, callback)
} else {
console.log("Local user password mismatch, trying LDAP")
// check passwd against ldap
AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
}
})
} else {
// No local passwd check user has to be in ldap and use ldap credentials
AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
}
return null
},
validateEmail(email) {
// we use the emailadress from the ldap
// therefore we do not enforce checks here
const parsed = EmailHelper.parseEmail(email)
//if (!parsed) {
// return new InvalidEmailError({ message: 'email not valid' })
//}
return null
},
// validates a password based on a similar set of rules to `complexPassword.js` on the frontend
// note that `passfield.js` enforces more rules than this, but these are the most commonly set.
// returns null on success, or an error object.
validatePassword(password, email) {
if (password == null) {
return new InvalidPasswordError({
message: 'password not set',
info: { code: 'not_set' },
})
}
let allowAnyChars, min, max
if (Settings.passwordStrengthOptions) {
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
if (Settings.passwordStrengthOptions.length) {
min = Settings.passwordStrengthOptions.length.min
max = Settings.passwordStrengthOptions.length.max
}
}
allowAnyChars = !!allowAnyChars
min = min || 6
max = max || 72
// we don't support passwords > 72 characters in length, because bcrypt truncates them
if (max > 72) {
max = 72
}
if (password.length < min) {
return new InvalidPasswordError({
message: 'password is too short',
info: { code: 'too_short' },
})
}
if (password.length > max) {
return new InvalidPasswordError({
message: 'password is too long',
info: { code: 'too_long' },
})
}
if (
!allowAnyChars &&
!AuthenticationManager._passwordCharactersAreValid(password)
) {
return new InvalidPasswordError({
message: 'password contains an invalid character',
info: { code: 'invalid_character' },
})
}
return null
},
setUserPassword(user, password, callback) {
AuthenticationManager.setUserPasswordInV2(user, password, callback)
},
checkRounds(user, hashedPassword, password, callback) {
// Temporarily disable this function, TODO: re-enable this
//return callback()
if (Settings.security.disableBcryptRoundsUpgrades) {
return callback()
}
// check current number of rounds and rehash if necessary
const currentRounds = bcrypt.getRounds(hashedPassword)
if (currentRounds < BCRYPT_ROUNDS) {
AuthenticationManager.setUserPassword(user, password, callback)
} else {
callback()
}
},
hashPassword(password, callback) {
bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) {
if (error) {
return callback(error)
}
bcrypt.hash(password, salt, callback)
})
},
setUserPasswordInV2(user, password, callback) {
//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) {
return callback(validationError)
}
this.hashPassword(password, function (error, hash) {
if (error) {
return callback(error)
}
db.users.updateOne(
{
_id: ObjectId(user._id.toString()),
},
{
$set: {
hashedPassword: hash,
},
$unset: {
password: true,
},
},
function (updateError, result) {
if (updateError) {
return callback(updateError)
}
_checkWriteResult(result, callback)
}
)
})
},
_passwordCharactersAreValid(password) {
let digits, letters, lettersUp, symbols
if (
Settings.passwordStrengthOptions &&
Settings.passwordStrengthOptions.chars
) {
digits = Settings.passwordStrengthOptions.chars.digits
letters = Settings.passwordStrengthOptions.chars.letters
lettersUp = Settings.passwordStrengthOptions.chars.letters_up
symbols = Settings.passwordStrengthOptions.chars.symbols
}
digits = digits || '1234567890'
letters = letters || 'abcdefghijklmnopqrstuvwxyz'
lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) {
if (
digits.indexOf(password[charIndex]) === -1 &&
letters.indexOf(password[charIndex]) === -1 &&
lettersUp.indexOf(password[charIndex]) === -1 &&
symbols.indexOf(password[charIndex]) === -1
) {
return false
}
}
return true
},
async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) {
const client = new Client({
url: process.env.LDAP_SERVER,
});
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]
var firstname = ""
var lastname = ""
var isAdmin = false
var userDn = ""
//replace all appearences of %u with uid and all %m with mail:
const replacerUid = new RegExp("%u", "g")
const replacerMail = new RegExp("%m","g")
const filterstr = process.env.LDAP_USER_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
// check bind
try {
if(process.env.LDAP_BINDDN){ //try to bind directly with the user trying to log in
userDn = process.env.LDAP_BINDDN.replace(replacerUid,ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`);
await client.bind(userDn,password);
}else{// use fixed bind user
await client.bind(ldap_reader, ldap_reader_pass);
}
} catch (ex) {
if(process.env.LDAP_BINDDN){
console.log("Could not bind user: " + userDn);
}else{
console.log("Could not bind LDAP reader: " + ldap_reader + " err: " + String(ex))
}
return callback(null, null)
}
// get user data
try {
const {searchEntries, searchRef,} = await client.search(ldap_base, {
scope: 'sub',
filter: filterstr ,
});
await searchEntries
console.log(JSON.stringify(searchEntries))
if (searchEntries[0]) {
mail = searchEntries[0].mail
uid = searchEntries[0].uid
firstname = searchEntries[0].givenName
lastname = searchEntries[0].sn
if(!process.env.LDAP_BINDDN){ //dn is already correctly assembled
userDn = searchEntries[0].dn
}
console.log("Found user: " + mail + " Name: " + firstname + " " + lastname + " DN: " + userDn)
}
} catch (ex) {
console.log("An Error occured while getting user data during ldapsearch: " + String(ex))
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.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`)
adminEntry = await client.search(ldap_base, {
scope: 'sub',
filter: adminfilter,
});
await adminEntry;
//console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries))
if (adminEntry.searchEntries[0]) {
console.log("is Admin")
isAdmin=true;
}
}
} catch (ex) {
console.log("An Error occured while checking for admin rights - setting admin rights to false: " + String(ex))
isAdmin = false;
} finally {
await client.unbind();
}
if (mail == "" || userDn == "") {
console.log("Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap.")
return callback(null, null)
}
if(!process.env.BINDDN){//since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
try {
await client.bind(userDn, password);
} catch (ex) {
console.log("Could not bind User: " + userDn + " err: " + String(ex))
return callback(null, null)
} finally{
await client.unbind()
}
}
//console.log("Logging in user: " + mail + " Name: " + firstname + " " + lastname + " isAdmin: " + String(isAdmin))
// we are authenticated now let's set the query to the correct mail from ldap
query.email = mail
User.findOne(query, (error, user) => {
if (error) {
console.log(error)
}
if (user && user.hashedPassword) {
//console.log("******************** LOGIN ******************")
AuthenticationManager.login(user, "randomPass", callback)
} else {
onSuccessCreateUserIfNotExistent(query, user, callback, uid, firstname, lastname, mail, isAdmin)
}
})
}
}
AuthenticationManager.promises = {
authenticate: util.promisify(AuthenticationManager.authenticate),
hashPassword: util.promisify(AuthenticationManager.hashPassword),
setUserPassword: util.promisify(AuthenticationManager.setUserPassword),
}
module.exports = AuthenticationManager

View File

@@ -1,139 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let ContactsController
const AuthenticationController = require('../Authentication/AuthenticationController')
const SessionManager = require('../Authentication/SessionManager')
const ContactManager = require('./ContactManager')
const UserGetter = require('../User/UserGetter')
const logger = require('@overleaf/logger')
const Modules = require('../../infrastructure/Modules')
const { Client } = require('ldapts');
module.exports = ContactsController = {
getContacts(req, res, next) {
const user_id = SessionManager.getLoggedInUserId(req.session)
return ContactManager.getContactIds(
user_id,
{ limit: 50 },
function (error, contact_ids) {
if (error != null) {
return next(error)
}
return UserGetter.getUsers(
contact_ids,
{
email: 1,
first_name: 1,
last_name: 1,
holdingAccount: 1,
},
function (error, contacts) {
if (error != null) {
return next(error)
}
// UserGetter.getUsers may not preserve order so put them back in order
const positions = {}
for (let i = 0; i < contact_ids.length; i++) {
const contact_id = contact_ids[i]
positions[contact_id] = i
}
contacts.sort(
(a, b) =>
positions[a._id != null ? a._id.toString() : undefined] -
positions[b._id != null ? b._id.toString() : undefined]
)
// Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
contacts = contacts.filter(c => !c.holdingAccount)
ContactsController.getLdapContacts(contacts).then((ldapcontacts) => {
contacts.push(ldapcontacts)
contacts = contacts.map(ContactsController._formatContact)
return Modules.hooks.fire('getContacts', user_id, contacts, function(
error,
additional_contacts
) {
if (error != null) {
return next(error)
}
contacts = contacts.concat(...Array.from(additional_contacts || []))
return res.send({
contacts
})
})
}).catch(e => console.log("Error appending ldap contacts" + e))
}
)
})
},
async getLdapContacts(contacts) {
if (process.env.LDAP_CONTACTS === undefined || !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')) {
return contacts
}
const client = new Client({
url: process.env.LDAP_SERVER,
});
// if we need a ldap user try to bind
if (process.env.LDAP_BIND_USER) {
try {
await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW);
} catch (ex) {
console.log("Could not bind LDAP reader user: " + String(ex) )
}
}
const ldap_base = process.env.LDAP_BASE
// get user data
try {
// if you need an client.bind do it here.
const {searchEntries,searchReferences,} = await client.search(ldap_base, {scope: 'sub',filter: process.env.LDAP_CONTACT_FILTER ,});
await searchEntries;
for (var i = 0; i < searchEntries.length; i++) {
var entry = new Map()
var obj = searchEntries[i];
entry['_id'] = undefined
entry['email'] = obj['mail']
entry['first_name'] = obj['givenName']
entry['last_name'] = obj['sn']
entry['type'] = "user"
// Only add to contacts if entry is not there.
if(contacts.indexOf(entry) === -1) {
contacts.push(entry);
}
}
} catch (ex) {
console.log(String(ex))
}
//console.log(JSON.stringify(contacts))
finally {
// even if we did not use bind - the constructor of
// new Client() opens a socket to the ldap server
client.unbind()
return contacts
}
},
_formatContact(contact) {
return {
id: contact._id != null ? contact._id.toString() : undefined,
email: contact.email || '',
first_name: contact.first_name || '',
last_name: contact.last_name || '',
type: 'user',
}
},
}

View File

@@ -1,57 +0,0 @@
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 }}

View File

@@ -1,79 +0,0 @@
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.

View File

@@ -1,84 +0,0 @@
nav.navbar.navbar-default.navbar-main
.container-fluid
.navbar-header
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
i.fa.fa-bars(aria-hidden="true")
if settings.nav.custom_logo
a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
else if (nav.title)
a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
else
a(href='/', aria-label=settings.appName).navbar-brand
.navbar-collapse.collapse(collapse="navCollapsed")
ul.nav.navbar-nav.navbar-right
if (getSessionUser() && getSessionUser().isAdmin)
li
a(href="/admin") Admin
// loop over header_extras
each item in nav.header_extras
-
if ((item.only_when_logged_in && getSessionUser())
|| (item.only_when_logged_out && (!getSessionUser()))
|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
){
var showNavItem = true
} else {
var showNavItem = false
}
if showNavItem
if item.dropdown
li.dropdown(class=item.class, dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| !{translate(item.text)}
b.caret
ul.dropdown-menu
each child in item.dropdown
if child.divider
li.divider
else
li
if child.url
a(href=child.url, class=child.class) !{translate(child.text)}
else
| !{translate(child.text)}
else
li(class=item.class)
if item.url
a(href=item.url, class=item.class) !{translate(item.text)}
else
| !{translate(item.text)}
// logged out
if !getSessionUser()
// login link
li
a(href="/login") #{translate('log_in')}
// projects link and account menu
if getSessionUser()
li
a(href="/project") #{translate('Projects')}
li.dropdown(dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| #{translate('Account')}
b.caret
ul.dropdown-menu
//li
// div.subdued(ng-non-bindable) #{getUserEmail()}
//li.divider.hidden-xs.hidden-sm
li
a(href="/user/settings") #{translate('Account Settings')}
if nav.showSubscriptionLink
li
a(href="/user/subscription") #{translate('subscription')}
li.divider.hidden-xs.hidden-sm
li
form(method="POST" action="/logout")
input(name='_csrf', type='hidden', value=csrfToken)
button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}

View File

@@ -1,178 +0,0 @@
extends ../layout
block content
.content.content-alt
.container
.row
.col-md-12.col-lg-10.col-lg-offset-1
if ssoError
.alert.alert-danger
| #{translate('sso_link_error')}: #{translate(ssoError)}
.card
.page-header
h1 #{translate("account_settings")}
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
if hasFeature('affiliations')
include settings/user-affiliations
.row
.col-md-5
h3 #{translate("update_account_info")}
form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
input(type="hidden", name="_csrf", value=csrfToken)
if !hasFeature('affiliations')
// show the email, non-editable
.form-group
label.control-label #{translate("email")}
div.form-control(
readonly="true",
ng-non-bindable
) #{user.email}
if shouldAllowEditingDetails
.form-group
label(for='firstName').control-label #{translate("first_name")}
input.form-control(
id="firstName"
type='text',
name='first_name',
value=user.first_name
ng-non-bindable
)
.form-group
label(for='lastName').control-label #{translate("last_name")}
input.form-control(
id="lastName"
type='text',
name='last_name',
value=user.last_name
ng-non-bindable
)
.form-group
form-messages(aria-live="polite" for="settingsForm")
.alert.alert-success(ng-show="settingsForm.response.success")
| #{translate("thanks_settings_updated")}
.actions
button.btn.btn-primary(
type='submit',
ng-disabled="settingsForm.$invalid"
) #{translate("update")}
else
.form-group
label.control-label #{translate("first_name")}
div.form-control(
readonly="true",
ng-non-bindable
) #{user.first_name}
.form-group
label.control-label #{translate("last_name")}
div.form-control(
readonly="true",
ng-non-bindable
) #{user.last_name}
.col-md-5.col-md-offset-1
h3
| Set Password for Email login
p
| Note: you can not change the LDAP password from here. You can set/reset a password for
| your email login:
| #[a(href="/user/password/reset", target='_blank') Reset.]
| !{moduleIncludes("userSettings", locals)}
hr
h3
| Contact
div
| If you need any help, please contact your sysadmins.
p #{translate("need_to_leave")}
a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
script(type='text/ng-template', id='deleteAccountModalTemplate')
.modal-header
h3 #{translate("delete_account")}
div.modal-body#delete-account-modal
p !{translate("delete_account_warning_message_3")}
if settings.createV1AccountOnLogin && settings.overleaf
p
strong
| Your Overleaf v2 projects will be deleted if you delete your account.
| If you want to remove any remaining Overleaf v1 projects in your account,
| please first make sure they are imported to Overleaf v2.
if settings.overleaf && !hasPassword
p
b
| #[a(href="/user/password/reset", target='_blank') #{translate("delete_acct_no_existing_pw")}].
else
form(novalidate, name="deleteAccountForm")
label #{translate('email')}
input.form-control(
type="text",
autocomplete="off",
placeholder="",
ng-model="state.deleteText",
focus-on="open",
ng-keyup="checkValidation()"
)
label #{translate('password')}
input.form-control(
type="password",
autocomplete="off",
placeholder="",
ng-model="state.password",
ng-keyup="checkValidation()"
)
div.confirmation-checkbox-wrapper
input(
type="checkbox"
ng-model="state.confirmV1Purge"
ng-change="checkValidation()"
).pull-left
label(style="display: inline") &nbsp;I have left, purged or imported my projects on Overleaf v1 (if any) &nbsp;
div.confirmation-checkbox-wrapper
input(
type="checkbox"
ng-model="state.confirmSharelatexDelete"
ng-change="checkValidation()"
).pull-left
label(style="display: inline") &nbsp;I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}]
div(ng-if="state.error")
div.alert.alert-danger(ng-switch="state.error.code")
span(ng-switch-when="InvalidCredentialsError")
| #{translate('email_or_password_wrong_try_again')}
span(ng-switch-when="SubscriptionAdminDeletionError")
| #{translate('subscription_admins_cannot_be_deleted')}
span(ng-switch-when="UserDeletionError")
| #{translate('user_deletion_error')}
span(ng-switch-default)
| #{translate('generic_something_went_wrong')}
if settings.createV1AccountOnLogin && settings.overleaf
div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'")
div.alert.alert-info
| If you can't remember your password, or if you are using Single-Sign-On with another provider
| to sign in (such as Twitter or Google), please
| #[a(href="/user/password/reset", target='_blank') reset your password],
| and try again.
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-danger(
ng-disabled="!state.isValid || state.inflight"
ng-click="delete()"
)
span(ng-hide="state.inflight") #{translate("delete")}
span(ng-show="state.inflight") #{translate("deleting")}...
script(type='text/javascript').
window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}

View File

@@ -0,0 +1,95 @@
300a301,394
> oauth2Redirect(req, res, next) {
> // random state
> const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
> const state = new Array(6).fill(0).map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join("")
> req.session.oauth2State = state
>
> const redirectURI = encodeURIComponent(`${process.env.OVERLEAF_SITE_URL}/oauth/callback`)
> const authURL = (
> process.env.OAUTH2_AUTHORIZATION_URL
> + `?response_type=code`
> + `&client_id=${process.env.OAUTH2_CLIENT_ID}`
> + `&redirect_uri=${redirectURI}`
> + `&scope=${process.env.OAUTH2_SCOPE ?? ""} `
> + `&state=${state}`
> )
> res.redirect(authURL)
> },
>
> async oauth2Callback(req, res, next) {
> console.log(`OAuth, receive code ${req.query.code} and state ${req.query.state}`)
> const saveState = req.session.oauth2State
> delete req.session.oauth2State
> if (saveState !== req.query.state) {
> return AuthenticationController.finishLogin(false, req, res, next)
> }
>
> try {
> const contentType = process.env.OAUTH2_TOKEN_CONTENT_TYPE || 'application/x-www-form-urlencoded'
> const bodyParams = {
> grant_type: "authorization_code",
> client_id: process.env.OAUTH2_CLIENT_ID,
> client_secret: process.env.OAUTH2_CLIENT_SECRET,
> code: req.query.code,
> redirect_uri: `${process.env.OVERLEAF_SITE_URL}/oauth/callback`,
> }
> const body = contentType === 'application/json'
> ? JSON.stringify(bodyParams)
> : new URLSearchParams(bodyParams).toString()
>
> const tokenResponse = await fetch(process.env.OAUTH2_TOKEN_URL, {
> method: 'POST',
> headers: {
> "Accept": "application/json",
> "Content-Type": contentType,
> },
> body
> })
>
> const tokenData = await tokenResponse.json()
> console.log("OAuth2 respond", JSON.stringify(tokenData))
>
> const profileResponse = await fetch(process.env.OAUTH2_PROFILE_URL, {
> method: 'GET',
> headers: {
> "Accept": "application/json",
> "Authorization": `Bearer ${tokenData.access_token}`,
> }
> });
> const profile = await profileResponse.json()
> console.log("OAuth2 user profile", JSON.stringify(profile))
>
> const email = profile[process.env.OAUTH2_USER_ATTR_EMAIL ?? "email"]
> const uid = profile[process.env.OAUTH2_USER_ATTR_UID ?? "uid"]
> const firstname = profile?.[process.env.OAUTH2_USER_ATTR_FIRSTNAME] ?? email
> const lastname = process.env.OAUTH2_USER_ATTR_LASTNAME
> ? profile?.[process.env.OAUTH2_USER_ATTR_LASTNAME] ?? ""
> : ""
> const isAdmin = process.env.OAUTH2_USER_ATTR_IS_ADMIN
> ? !!profile?.[process.env.OAUTH2_USER_ATTR_IS_ADMIN] ?? false
> : false
>
> const query = { email }
> const callback = (error, user) => {
> if (error) {
> res.json({message: error});
> } else {
> console.log("OAuth user", JSON.stringify(user));
> AuthenticationController.finishLogin(user, req, res, next);
> }
> }
> AuthenticationManager.createIfNotFoundAndLogin(
> query,
> callback,
> uid,
> firstname,
> lastname,
> email,
> isAdmin
> )
> } catch(e) {
> res.redirect("/login")
> console.error("Fails to access by OAuth2: " + String(e))
> }
> },

View File

@@ -0,0 +1,300 @@
19a20,22
> const fs = require("fs")
> const { Client } = require("ldapts")
> const ldapEscape = require("ldap-escape")
120a124,132
>
> _checkUserPassword2(query, password, callback) {
> // leave original _checkUserPassword untouched, because it will be called by
> // setUserPasswordInV2 (e.g. UserRegistrationHandler.js )
> User.findOne(query, (error, user) => {
> AuthenticationManager.authUserObj(error, user, query, password, callback)
> })
> },
>
122c134,138
< AuthenticationManager._checkUserPassword(
---
> if (typeof callback === 'undefined') {
> callback = auditLog
> auditLog = null
> }
> AuthenticationManager._checkUserPassword2(
201a218,494
> /**
> * login with any password
> */
> login(user, password, callback) {
> callback(null, user, true)
> },
>
> createIfNotFoundAndLogin(
> query,
> callback,
> uid,
> firstname,
> lastname,
> mail,
> isAdmin
> ) {
> User.findOne(query, (error, user) => {
> if (error) {
> console.log(error)
> }
>
> AuthenticationManager.createIfNotExistAndLogin(
> query,
> user,
> callback,
> uid,
> firstname,
> lastname,
> mail,
> isAdmin
> )
> })
> },
>
> createIfNotExistAndLogin(
> query,
> user,
> callback,
> uid,
> firstname,
> lastname,
> mail,
> isAdmin
> ) {
> if (!user) {
> //console.log('Creating User:' + JSON.stringify(query))
> //create random pass for local userdb, does not get checked for ldap users during login
> let pass = require("crypto").randomBytes(32).toString("hex")
> //console.log('Creating User:' + JSON.stringify(query) + 'Random Pass' + pass)
>
> const userRegHand = require("../User/UserRegistrationHandler.js")
> userRegHand.registerNewUser(
> {
> email: mail,
> first_name: firstname,
> last_name: lastname,
> password: pass,
> },
> function (error, user, setNewPasswordUrl) {
> if (error) {
> console.log(error)
> }
> user.email = mail
> user.isAdmin = isAdmin
> user.emails[0].confirmedAt = Date.now()
> user.save()
> //console.log('user %s added to local library: ', mail)
> User.findOne(query, (error, user) => {
> if (error) {
> console.log(error)
> }
> if (user && user.hashedPassword) {
> AuthenticationManager.login(user, "randomPass", callback)
> }
> })
> }
> ) // end register user
> } else {
> AuthenticationManager.login(user, "randomPass", callback)
> }
> },
>
> authUserObj(error, user, query, password, callback) {
> if (process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
> console.log("email login for existing user " + query.email)
> // check passwd against local db
> bcrypt.compare(password, user.hashedPassword, function (error, match) {
> if (match) {
> console.log("Local user password match")
> _metricsForSuccessfulPasswordMatch(password)
> //callback(null, user, match)
> AuthenticationManager.login(user, "randomPass", callback)
> } else {
> console.log("Local user password mismatch, trying LDAP")
> // check passwd against ldap
> AuthenticationManager.ldapAuth(
> query,
> password,
> AuthenticationManager.createIfNotExistAndLogin,
> callback,
> user
> )
> }
> })
> } else {
> // No local passwd check user has to be in ldap and use ldap credentials
> AuthenticationManager.ldapAuth(
> query,
> password,
> AuthenticationManager.createIfNotExistAndLogin,
> callback,
> user
> )
> }
> return null
> },
>
> async ldapAuth(
> query,
> password,
> onSuccessCreateUserIfNotExistent,
> callback,
> user
> ) {
> const client = fs.existsSync(process.env.LDAP_SERVER_CACERT)
> ? new Client({
> url: process.env.LDAP_SERVER,
> tlsOptions: {
> ca: [fs.readFileSync(process.env.LDAP_SERVER_CACERT)],
> },
> })
> : new Client({
> url: process.env.LDAP_SERVER,
> })
>
> 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]
> var firstname = ""
> var lastname = ""
> var isAdmin = false
> var userDn = ""
>
> //replace all appearences of %u with uid and all %m with mail:
> const replacerUid = new RegExp("%u", "g")
> const replacerMail = new RegExp("%m", "g")
> const filterstr = process.env.LDAP_USER_FILTER.replace(
> replacerUid,
> ldapEscape.filter`${uid}`
> ).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
> // check bind
> try {
> if (process.env.LDAP_BINDDN) {
> //try to bind directly with the user trying to log in
> userDn = process.env.LDAP_BINDDN.replace(
> replacerUid,
> ldapEscape.filter`${uid}`
> ).replace(replacerMail, ldapEscape.filter`${mail}`)
> await client.bind(userDn, password)
> } else {
> // use fixed bind user
> await client.bind(ldap_reader, ldap_reader_pass)
> }
> } catch (ex) {
> if (process.env.LDAP_BINDDN) {
> console.log("Could not bind user: " + userDn)
> } else {
> console.log(
> "Could not bind LDAP reader: " + ldap_reader + " err: " + String(ex)
> )
> }
> return callback(null, null)
> }
>
> // get user data
> try {
> const { searchEntries, searchRef } = await client.search(ldap_base, {
> scope: "sub",
> filter: filterstr,
> })
> await searchEntries
> console.log(JSON.stringify(searchEntries))
> if (searchEntries[0]) {
> mail = searchEntries[0].mail
> uid = searchEntries[0].uid
> firstname = searchEntries[0].givenName
> lastname = searchEntries[0].sn
> if (!process.env.LDAP_BINDDN) {
> //dn is already correctly assembled
> userDn = searchEntries[0].dn
> }
> console.log(
> `Found user: ${mail} Name: ${firstname} ${lastname} DN: ${userDn}`
> )
> }
> } catch (ex) {
> console.log(
> "An Error occured while getting user data during ldapsearch: " +
> String(ex)
> )
> 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.replace(
> replacerUid,
> ldapEscape.filter`${uid}`
> ).replace(replacerMail, ldapEscape.filter`${mail}`)
> adminEntry = await client.search(ldap_base, {
> scope: "sub",
> filter: adminfilter,
> })
> await adminEntry
> //console.log('Admin Search response:' + JSON.stringify(adminEntry.searchEntries))
> if (adminEntry.searchEntries[0]) {
> console.log("is Admin")
> isAdmin = true
> }
> }
> } catch (ex) {
> console.log(
> "An Error occured while checking for admin rights - setting admin rights to false: " +
> String(ex)
> )
> isAdmin = false
> } finally {
> await client.unbind()
> }
> if (mail == "" || userDn == "") {
> console.log(
> "Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap."
> )
> return callback(null, null)
> }
>
> if (!process.env.BINDDN) {
> //since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
> try {
> await client.bind(userDn, password)
> } catch (ex) {
> console.log("Could not bind User: " + userDn + " err: " + String(ex))
> return callback(null, null)
> } finally {
> await client.unbind()
> }
> }
> //console.log('Logging in user: ' + mail + ' Name: ' + firstname + ' ' + lastname + ' isAdmin: ' + String(isAdmin))
> // we are authenticated now let's set the query to the correct mail from ldap
> query.email = mail
> User.findOne(query, (error, user) => {
> if (error) {
> console.log(error)
> }
> if (user && user.hashedPassword) {
> //console.log('******************** LOGIN ******************')
> AuthenticationManager.login(user, "randomPass", callback)
> } else {
> onSuccessCreateUserIfNotExistent(
> query,
> user,
> callback,
> uid,
> firstname,
> lastname,
> mail,
> isAdmin
> )
> }
> })
> },

View File

@@ -0,0 +1,76 @@
6a7,10
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> const { Client } = require('ldapts')
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>
34,35c38,39
< const contactId = contactIds[i]
< positions[contactId] = i
---
> const contact_id = contactIds[i]
> positions[contact_id] = i
42c46,51
< contacts = contacts.filter(c => !c.holdingAccount)
---
> contacts = contacts.filter((c) => !c.holdingAccount)
>
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> const ldapcontacts = getLdapContacts(contacts)
> contacts.push(ldapcontacts)
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
56a66,120
>
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> async function getLdapContacts(contacts) {
> if (
> process.env.LDAP_CONTACTS === undefined ||
> !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')
> ) {
> return contacts
> }
> const client = new Client({
> url: process.env.LDAP_SERVER,
> })
>
> // if we need a ldap user try to bind
> if (process.env.LDAP_BIND_USER) {
> try {
> await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW)
> } catch (ex) {
> console.log('Could not bind LDAP reader user: ' + String(ex))
> }
> }
>
> const ldap_base = process.env.LDAP_BASE
> // get user data
> try {
> // if you need an client.bind do it here.
> const { searchEntries, searchReferences } = await client.search(ldap_base, {
> scope: 'sub',
> filter: process.env.LDAP_CONTACT_FILTER,
> })
> await searchEntries
> for (var i = 0; i < searchEntries.length; i++) {
> var entry = new Map()
> var obj = searchEntries[i]
> entry['_id'] = undefined
> entry['email'] = obj['mail']
> entry['first_name'] = obj['givenName']
> entry['last_name'] = obj['sn']
> entry['type'] = 'user'
> // Only add to contacts if entry is not there.
> if (contacts.indexOf(entry) === -1) {
> contacts.push(entry)
> }
> }
> } catch (ex) {
> console.log(String(ex))
> } finally {
> // console.log(JSON.stringify(contacts))
> // even if we did not use bind - the constructor of
> // new Client() opens a socket to the ldap server
> client.unbind()
> return contacts
> }
> }
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

View File

@@ -0,0 +1,8 @@
11c11
< trackChangesAvailable: false,
---
> trackChangesAvailable: true,
60c60
< trackChanges: false,
---
> trackChanges: true,

View File

@@ -0,0 +1,308 @@
const ChatApiHandler = require('../Chat/ChatApiHandler')
const ChatManager = require('../Chat/ChatManager')
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const SessionManager = require('../Authentication/SessionManager')
const UserInfoManager = require('../User/UserInfoManager')
const DocstoreManager = require('../Docstore/DocstoreManager')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const { Project } = require('../../models/Project')
const pLimit = require('p-limit')
async function _updateTCState (projectId, state, callback) {
await Project.updateOne({_id: projectId}, {track_changes: state}).exec()
callback()
}
function _transformId(doc) {
if (doc._id) {
doc.id = doc._id;
delete doc._id;
}
return doc;
}
const TrackChangesController = {
trackChanges(req, res, next) {
const { project_id } = req.params
let state = req.body.on || req.body.on_for
if ( req.body.on_for_guests && !req.body.on ) state.__guests__ = true
return _updateTCState(project_id, state,
function (err, message) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'toggle-track-changes',
state
)
return res.sendStatus(204)
}
)
},
acceptChanges(req, res, next) {
const { project_id, doc_id } = req.params
const change_ids = req.body.change_ids
return DocumentUpdaterHandler.acceptChanges(
project_id,
doc_id,
change_ids,
function (err, message) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'accept-changes',
doc_id,
change_ids,
)
return res.sendStatus(204)
}
)
},
async getAllRanges(req, res, next) {
const { project_id } = req.params
// FIXME: ranges are from mongodb, probably already outdated
const ranges = await DocstoreManager.promises.getAllRanges(project_id)
// frontend expects 'id', not '_id'
return res.json(ranges.map(_transformId))
},
async getChangesUsers(req, res, next) {
const { project_id } = req.params
const memberIds = await CollaboratorsGetter.promises.getMemberIds(project_id)
// FIXME: Does not work properly if the user is no longer a member of the project
// memberIds from DocstoreManager.getAllRanges(project_id) is not a remedy
// because ranges are not updated in real-time
const limit = pLimit(3)
const users = await Promise.all(
memberIds.map(memberId =>
limit(async () => {
const user = await UserInfoManager.promises.getPersonalInfo(memberId)
return user
})
)
)
users.push({_id: null}) // An anonymous user won't cause any harm
// frontend expects 'id', not '_id'
return res.json(users.map(_transformId))
},
getThreads(req, res, next) {
const { project_id } = req.params
return ChatApiHandler.getThreads(
project_id,
function (err, messages) {
if (err != null) {
return next(err)
}
return ChatManager.injectUserInfoIntoThreads(
messages,
function (err) {
if (err != null) {
return next(err)
}
return res.json(messages)
}
)
}
)
},
sendComment(req, res, next) {
const { project_id, thread_id } = req.params
const { content } = req.body
const user_id = SessionManager.getLoggedInUserId(req.session)
if (user_id == null) {
const err = new Error('no logged-in user')
return next(err)
}
return ChatApiHandler.sendComment(
project_id,
thread_id,
user_id,
content,
function (err, message) {
if (err != null) {
return next(err)
}
return UserInfoManager.getPersonalInfo(
user_id,
function (err, user) {
if (err != null) {
return next(err)
}
message.user = user
EditorRealTimeController.emitToRoom(
project_id,
'new-comment',
thread_id, message
)
return res.sendStatus(204)
}
)
}
)
},
editMessage(req, res, next) {
const { project_id, thread_id, message_id } = req.params
const { content } = req.body
const user_id = SessionManager.getLoggedInUserId(req.session)
if (user_id == null) {
const err = new Error('no logged-in user')
return next(err)
}
return ChatApiHandler.editMessage(
project_id,
thread_id,
message_id,
user_id,
content,
function (err, message) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'edit-message',
thread_id,
message_id,
content
)
return res.sendStatus(204)
}
)
},
deleteMessage(req, res, next) {
const { project_id, thread_id, message_id } = req.params
return ChatApiHandler.deleteMessage(
project_id,
thread_id,
message_id,
function (err, message) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'delete-message',
thread_id,
message_id
)
return res.sendStatus(204)
}
)
},
resolveThread(req, res, next) {
const { project_id, doc_id, thread_id } = req.params
const user_id = SessionManager.getLoggedInUserId(req.session)
if (user_id == null) {
const err = new Error('no logged-in user')
return next(err)
}
DocumentUpdaterHandler.resolveThread(
project_id,
doc_id,
thread_id,
user_id,
function (err, message) {
if (err != null) {
return next(err)
}
}
)
return ChatApiHandler.resolveThread(
project_id,
thread_id,
user_id,
function (err, message) {
if (err != null) {
return next(err)
}
return UserInfoManager.getPersonalInfo(
user_id,
function (err, user) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'resolve-thread',
thread_id,
user_id
)
return res.sendStatus(204)
}
)
}
)
},
reopenThread(req, res, next) {
const { project_id, doc_id, thread_id } = req.params
const user_id = SessionManager.getLoggedInUserId(req.session)
if (user_id == null) {
const err = new Error('no logged-in user')
return next(err)
}
DocumentUpdaterHandler.reopenThread(
project_id,
doc_id,
thread_id,
user_id,
function (err, message) {
if (err != null) {
return next(err)
}
}
)
return ChatApiHandler.reopenThread(
project_id,
thread_id,
function (err, message) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'reopen-thread',
thread_id
)
return res.sendStatus(204)
}
)
},
deleteThread(req, res, next) {
const { project_id, doc_id, thread_id } = req.params
const user_id = SessionManager.getLoggedInUserId(req.session)
if (user_id == null) {
const err = new Error('no logged-in user')
return next(err)
}
return DocumentUpdaterHandler.deleteThread(
project_id,
doc_id,
thread_id,
user_id,
function (err, message) {
if (err != null) {
return next(err)
}
ChatApiHandler.deleteThread(
project_id,
thread_id,
function (err, message) {
if (err != null) {
return next(err)
}
EditorRealTimeController.emitToRoom(
project_id,
'delete-thread',
thread_id
)
return res.sendStatus(204)
}
)
}
)
},
}
module.exports = TrackChangesController

View File

@@ -0,0 +1,72 @@
15,18c15
< +bookmarkable-tabset-header('open-sockets', 'Open Sockets')
< +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
< if hasFeature('saas')
< +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
---
> +bookmarkable-tabset-header('register-user', 'Register User')
39,49d35
< .tab-pane(
< role="tabpanel"
< id='open-sockets'
< )
< .row-spaced
< ul
< each agents, url in openSockets
< li(ng-non-bindable) #{url} - total : #{agents.length}
< ul
< each agent in agents
< li(ng-non-bindable) #{agent}
53c39
< id='open-close-editor'
---
> id='register-user'
55,74c41,42
< 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.
---
> hr
> a(href="/admin/register") Register User
76,99d43
< if hasFeature('saas')
< .tab-pane(
< role="tabpanel"
< id='tpds'
< )
< h3 Flush project to TPDS
< .row
< form.col-xs-6(method='post',action='/admin/flushProjectToTpds')
< input(name="_csrf", type="hidden", value=csrfToken)
< .form-group
< label(for='project_id') project_id
< input.form-control(type='text', name='project_id', placeholder='project_id', required)
< .form-group
< button.btn-primary.btn(type='submit') Flush
< hr
< h3 Poll Dropbox for user
< .row
< form.col-xs-6(method='post',action='/admin/pollDropboxForUser')
< input(name="_csrf", type="hidden", value=csrfToken)
< .form-group
< label(for='user_id') user_id
< input.form-control(type='text', name='user_id', placeholder='user_id', required)
< .form-group
< button.btn-primary.btn(type='submit') Poll

View File

@@ -0,0 +1,166 @@
1,2c1
< extends ../layout-marketing
< include ../_mixins/bookmarkable_tabset
---
> extends ../layout
9c8
< .card
---
> .card(ng-controller="RegisterUsersController")
12,16c11,58
< div(data-ol-bookmarkable-tabset)
< ul.nav.nav-tabs(role="tablist")
< +bookmarkable-tabset-header('system-messages', 'System Messages', true)
< +bookmarkable-tabset-header('open-sockets', 'Open Sockets')
< +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
---
> 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")
18c60,66
< +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
---
> | 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.
20,42d67
< .tab-content
< .tab-pane.active(
< role="tabpanel"
< id='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-pane(
< role="tabpanel"
< id='open-sockets'
< )
44,74c69,78
< ul
< each agents, url in openSockets
< li(ng-non-bindable) #{url} - total : #{agents.length}
< ul
< each agent in agents
< li(ng-non-bindable) #{agent}
<
< .tab-pane(
< role="tabpanel"
< id='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.
---
> 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.
76,99d79
< if hasFeature('saas')
< .tab-pane(
< role="tabpanel"
< id='tpds'
< )
< h3 Flush project to TPDS
< .row
< form.col-xs-6(method='post',action='/admin/flushProjectToTpds')
< input(name="_csrf", type="hidden", value=csrfToken)
< .form-group
< label(for='project_id') project_id
< input.form-control(type='text', name='project_id', placeholder='project_id', required)
< .form-group
< button.btn-primary.btn(type='submit') Flush
< hr
< h3 Poll Dropbox for user
< .row
< form.col-xs-6(method='post',action='/admin/pollDropboxForUser')
< input(name="_csrf", type="hidden", value=csrfToken)
< .form-group
< label(for='user_id') user_id
< input.form-control(type='text', name='user_id', placeholder='user_id', required)
< .form-group
< button.btn-primary.btn(type='submit') Poll

View File

@@ -0,0 +1,20 @@
14a15,22
> //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> //- input.form-control(
> //- type='email',
> //- name='email',
> //- required,
> //- placeholder='email@example.com',
> //- autofocus="true"
> //- )
16d23
< type='email',
21a29
> //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
36a45,50
> //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> if process.env.OAUTH2_ENABLED === 'true'
> .form-group.text-center(style="padding-top: 10px")
> a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px')
> | Log in via #{process.env.OAUTH2_PROVIDER || 'OAuth'}
> //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

View File

@@ -0,0 +1,3 @@
55,56d54
< li
< a(href="/admin/project") Project URL Lookup

View File

@@ -0,0 +1,91 @@
39a40
> const TrackChangesController = require('./Features/TrackChanges/TrackChangesController')
218c219,226
< webRouter.get('/login', UserPagesController.loginPage)
---
> // If no LDAP Server is in use and no local db login then we can redirect the login
> // and just use OAUTH
> if ( (typeof process.env.LDAP_SERVER === typeof undefined) && (process.env.ALLOW_EMAIL_LOGIN === 'false') && (process.env.OAUTH2_ENABLED === 'true') ) {
> webRouter.get('/login', function (req, res, next) { res.redirect('/oauth/redirect') })
> } else {
> webRouter.get('/login', UserPagesController.loginPage)
> }
>
259a268,274
> if (process.env.OAUTH2_ENABLED === 'true') {
> webRouter.get('/oauth/redirect', AuthenticationController.oauth2Redirect)
> webRouter.get('/oauth/callback', AuthenticationController.oauth2Callback)
> AuthenticationController.addEndpointToLoginWhitelist('/oauth/redirect')
> AuthenticationController.addEndpointToLoginWhitelist('/oauth/callback')
> }
>
1352a1368,1436
> )
>
> webRouter.post('/project/:project_id/track_changes',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.trackChanges
> )
> webRouter.post('/project/:project_id/doc/:doc_id/changes/accept',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.acceptChanges
> )
> webRouter.get('/project/:project_id/ranges',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.getAllRanges
> )
> webRouter.get('/project/:project_id/changes/users',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.getChangesUsers
> )
> webRouter.get(
> '/project/:project_id/threads',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.getThreads
> )
> webRouter.post(
> '/project/:project_id/thread/:thread_id/messages',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.sendComment
> )
> webRouter.post(
> '/project/:project_id/thread/:thread_id/messages/:message_id/edit',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.editMessage
> )
> webRouter.delete(
> '/project/:project_id/thread/:thread_id/messages/:message_id',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.deleteMessage
> )
> webRouter.post(
> '/project/:project_id/doc/:doc_id/thread/:thread_id/resolve',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.resolveThread
> )
> webRouter.post(
> '/project/:project_id/thread/:thread_id/resolve',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.resolveThread
> )
> webRouter.post(
> '/project/:project_id/doc/:doc_id/thread/:thread_id/reopen',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.reopenThread
> )
> webRouter.delete(
> '/project/:project_id/doc/:doc_id/thread/:thread_id',
> AuthorizationMiddleware.blockRestrictedUserFromProject,
> AuthorizationMiddleware.ensureUserCanReadProject,
> TrackChangesController.deleteThread

View File

24
scripts/apply_diffs.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
set -e
DIFFS_DIR="ldap-overleaf-sl/sharelatex_diff"
ORI_DIR="ldap-overleaf-sl/sharelatex_ori"
PATCHED_DIR="ldap-overleaf-sl/sharelatex"
for diff_file in "$DIFFS_DIR"/*.diff; do
filename=$(basename "$diff_file" ".diff")
if [ "$filename" == ".gitkeep" ]; then
continue
fi
original_file="$ORI_DIR/$filename"
patched_file="$PATCHED_DIR/$filename"
if [ -f "$original_file" ]; then
cp "$original_file" "$patched_file"
patch "$patched_file" "$diff_file"
else
echo "No original file for $filename in $ORI_DIR."
fi
done

74
scripts/extract_files.sh Normal file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
set -e
CONTAINER_FILE_PATHS=(
"/overleaf/services/web/app/src/Features/Authentication/AuthenticationManager.js"
"/overleaf/services/web/app/src/Features/Authentication/AuthenticationController.js"
"/overleaf/services/web/app/src/Features/Contacts/ContactController.js"
"/overleaf/services/web/app/src/Features/Project/ProjectEditorHandler.js"
"/overleaf/services/web/app/src/router.js"
"/overleaf/services/web/app/views/user/settings.pug"
"/overleaf/services/web/app/views/user/login.pug"
"/overleaf/services/web/app/views/layout/navbar.pug"
"/overleaf/services/web/app/views/layout/navbar-marketing.pug"
"/overleaf/services/web/app/views/admin/index.pug"
"/overleaf/services/web/app/views/admin/index.pug"
)
FILENAMES=(
"AuthenticationManager.js"
"AuthenticationController.js"
"ContactController.js"
"ProjectEditorHandler.js"
"router.js"
"settings.pug"
"login.pug"
"navbar.pug"
"navbar-marketing.pug"
"admin-index.pug"
"admin-sysadmin.pug"
)
if [ "${#CONTAINER_FILE_PATHS[@]}" -ne "${#FILENAMES[@]}" ]; then
echo "Error: The number of source files and target filenames does not match."
exit 1
fi
HOST_TARGET_PATH="ldap-overleaf-sl/sharelatex_ori"
if [ "$#" -ne 1 ]; then
echo "Usage: $0 [version]"
exit 1
else
VERSION=$1
fi
CONTAINER_NAME="tmp_sharelatex_for_extract_files"
IMAGE="sharelatex/sharelatex:$VERSION"
echo "Starting Docker container \"$CONTAINER_NAME\" with image \"$IMAGE\"..."
if [ ! "$(docker ps -q -f name=^/${CONTAINER_NAME}$)" ]; then
if [ "$(docker ps -aq -f status=exited -f name=^/${CONTAINER_NAME}$)" ]; then
echo "Removing stopped container with same name..."
docker rm $CONTAINER_NAME
fi
else
echo "Error: A container with the name $CONTAINER_NAME already exists."
exit 1
fi
docker run -d --name $CONTAINER_NAME $IMAGE
echo "Waiting for container to start up..."
sleep 10
for i in "${!CONTAINER_FILE_PATHS[@]}"; do
file_path="${CONTAINER_FILE_PATHS[i]}"
new_filename="${FILENAMES[i]}"
new_target_path="$HOST_TARGET_PATH/$new_filename"
docker cp $CONTAINER_NAME:$file_path $new_target_path
done
echo "Stopping and removing container..."
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME

16
scripts/make_diffs.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
MODIFIED_DIR="ldap-overleaf-sl/sharelatex"
DIFFS_DIR="ldap-overleaf-sl/sharelatex_diff"
ORI_DIR="ldap-overleaf-sl/sharelatex_ori"
for filename in $(ls $MODIFIED_DIR); do
raw_file="$ORI_DIR/$filename"
if [ -f "$raw_file" ]; then
diff_output="$DIFFS_DIR/${filename}.diff"
diff "$raw_file" "$MODIFIED_DIR/$filename" > "$diff_output"
else
echo "No matching file for $filename in $ORI_DIR."
fi
done