Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5e8e6f79a | ||
|
|
d049e4534a | ||
|
|
ed63dd0527 | ||
|
|
062f17e0f7 | ||
|
|
48b1623c95 | ||
|
|
7ac46ad430 | ||
|
|
a382fbc194 | ||
|
|
a107b6444c | ||
|
|
4bc203a757 | ||
|
|
3a560801d9 | ||
|
|
a99e70f3c4 | ||
|
|
c4775c7d7c | ||
|
|
f645454a74 | ||
|
|
ba1e78f2a8 | ||
|
|
ad051a0e18 | ||
|
|
810aa1d0e9 | ||
|
|
a6448a51e9 |
@@ -7,8 +7,15 @@ edition. Currently this repo uses sharelatex:latest.
|
|||||||
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).
|
||||||
|
|
||||||
|
## BREAKING CHANGE
|
||||||
|
be careful if you try to migrate from 3.3.2! Backup your machines and data.
|
||||||
|
The migration paths hould be:
|
||||||
|
- Backup Your machines and Data
|
||||||
|
- run latest 3.5 sharelatex image and run the migration scripts
|
||||||
|
- run this sharelatex image (4.1.1) and run the migrations scripts
|
||||||
|
|
||||||
### 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!
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +1,157 @@
|
|||||||
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/sharelatex
|
||||||
- ${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
|
SHARELATEX_APP_NAME: Overleaf
|
||||||
SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
|
SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
|
||||||
SHARELATEX_SITE_URL: https://${MYDOMAIN}
|
SHARELATEX_SITE_URL: https://${MYDOMAIN}
|
||||||
SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
|
SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
|
||||||
#SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
|
#SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
|
||||||
SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
|
SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
|
||||||
SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
|
SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
|
||||||
SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
|
SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
|
||||||
SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
|
SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
|
||||||
# SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID:
|
# SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID:
|
||||||
# SHARELATEX_EMAIL_AWS_SES_SECRET_KEY:
|
# SHARELATEX_EMAIL_AWS_SES_SECRET_KEY:
|
||||||
SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
|
SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
|
||||||
SHARELATEX_EMAIL_SMTP_PORT: 587
|
SHARELATEX_EMAIL_SMTP_PORT: 587
|
||||||
SHARELATEX_EMAIL_SMTP_SECURE: 'false'
|
SHARELATEX_EMAIL_SMTP_SECURE: "false"
|
||||||
# SHARELATEX_EMAIL_SMTP_USER:
|
# SHARELATEX_EMAIL_SMTP_USER:
|
||||||
# SHARELATEX_EMAIL_SMTP_PASS:
|
# SHARELATEX_EMAIL_SMTP_PASS:
|
||||||
# SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
|
# SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
|
||||||
# SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false
|
# SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false
|
||||||
SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues."
|
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)
|
# 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'
|
SHARELATEX_ALLOW_PUBLIC_ACCESS: "true"
|
||||||
SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true'
|
SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true"
|
||||||
|
|
||||||
SHARELATEX_SECURE_COOKIE: 'true'
|
SHARELATEX_SECURE_COOKIE: "true"
|
||||||
SHARELATEX_BEHIND_PROXY: 'true'
|
SHARELATEX_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
|
# Same property, unfortunately with different names in
|
||||||
ENABLE_CONVERSIONS: 'true'
|
# different locations
|
||||||
|
SHARELATEX_REDIS_HOST: redis
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
|
||||||
mongo:
|
ENABLED_LINKED_FILE_TYPES: "url,project_file"
|
||||||
restart: always
|
|
||||||
image: mongo
|
|
||||||
container_name: mongo
|
|
||||||
ports:
|
|
||||||
- 27017
|
|
||||||
volumes:
|
|
||||||
- ${MYDATA}/mongo_data:/data/db
|
|
||||||
healthcheck:
|
|
||||||
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
|
|
||||||
interval: 10s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
redis:
|
# Enables Thumbnail generation using ImageMagick
|
||||||
restart: always
|
ENABLE_CONVERSIONS: "true"
|
||||||
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
|
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
restart: always
|
||||||
|
image: mongo:4.4
|
||||||
|
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"
|
||||||
|
|
||||||
simple-certbot:
|
# See also: https://github.com/overleaf/overleaf/issues/1120
|
||||||
restart: always
|
mongoinit:
|
||||||
image: certbot/certbot
|
image: mongo:4.4
|
||||||
container_name: simple-certbot
|
# this container will exit after executing the command
|
||||||
ports:
|
restart: "no"
|
||||||
- 80:80
|
depends_on:
|
||||||
volumes:
|
mongo:
|
||||||
- ${MYDATA}/letsencrypt:/etc/letsencrypt
|
condition: service_healthy
|
||||||
# a bit hacky but this docker image uses very little disk-space
|
entrypoint:
|
||||||
# best practices for ssl and nginx are set in the ldap-overleaf-sl Dockerfile
|
[
|
||||||
entrypoint:
|
"mongo",
|
||||||
- "/bin/sh"
|
"--host",
|
||||||
- -c
|
"mongo:27017",
|
||||||
- |
|
"--eval",
|
||||||
trap exit TERM;\
|
'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })',
|
||||||
certbot certonly --standalone -d ${MYDOMAIN} --agree-tos -m ${MYMAIL} -n ; \
|
]
|
||||||
while :; do certbot renew; sleep 240h & wait $${!}; done;
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,226 +1,242 @@
|
|||||||
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/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"
|
||||||
|
|
||||||
sharelatex:
|
environment:
|
||||||
restart: always
|
SHARELATEX_APP_NAME: Overleaf
|
||||||
image: ldap-overleaf-sl:latest
|
SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
|
||||||
depends_on:
|
SHARELATEX_SITE_URL: https://${MYDOMAIN}
|
||||||
mongo:
|
SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
|
||||||
condition: service_healthy
|
#SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
|
||||||
redis:
|
SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
|
||||||
condition: service_healthy
|
SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 2016"} ]'
|
||||||
traefik:
|
SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
|
||||||
condition: service_started
|
SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
|
||||||
#simple-certbot:
|
SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
|
||||||
# condition: service_started
|
SHARELATEX_EMAIL_SMTP_PORT: 587
|
||||||
privileged: false
|
SHARELATEX_EMAIL_SMTP_SECURE: "false"
|
||||||
networks:
|
# SHARELATEX_EMAIL_SMTP_USER:
|
||||||
- web
|
# SHARELATEX_EMAIL_SMTP_PASS:
|
||||||
expose:
|
# SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
|
||||||
- 80
|
# SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false
|
||||||
- 443
|
SHARELATEX_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
|
SHARELATEX_ALLOW_PUBLIC_ACCESS: "true"
|
||||||
SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
|
SHARELATEX_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)
|
SHARELATEX_SECURE_COOKIE: "true"
|
||||||
# https://github.com/overleaf/docker-image/issues/66
|
SHARELATEX_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'
|
# Same property, unfortunately with different names in
|
||||||
|
# different locations
|
||||||
|
SHARELATEX_REDIS_HOST: redis
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
|
||||||
# Enables Thumbnail generation using ImageMagick
|
ENABLED_LINKED_FILE_TYPES: "url,project_file"
|
||||||
ENABLE_CONVERSIONS: 'true'
|
|
||||||
|
|
||||||
mongo:
|
# Enables Thumbnail generation using ImageMagick
|
||||||
restart: always
|
ENABLE_CONVERSIONS: "true"
|
||||||
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:
|
mongo:
|
||||||
restart: always
|
restart: always
|
||||||
image: redis:5.0.0
|
image: mongo:4.4
|
||||||
container_name: redis
|
container_name: mongo
|
||||||
# modify to get rid of the redis issue #35 and #19 with a better solution
|
expose:
|
||||||
# WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128.
|
- 27017
|
||||||
# for vm overcommit: enable first on host system
|
volumes:
|
||||||
# sysctl vm.overcommit_memory=1 (and add it to rc.local)
|
- ${MYDATA}/mongo_data:/data/db
|
||||||
# then you do not need it in the redis container
|
healthcheck:
|
||||||
sysctls:
|
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
|
||||||
- net.core.somaxconn=65535
|
interval: 10s
|
||||||
# - vm.overcommit_memory=1
|
timeout: 10s
|
||||||
expose:
|
retries: 5
|
||||||
- 6379
|
labels:
|
||||||
volumes:
|
- "traefik.enable=true"
|
||||||
- ${MYDATA}/redis_data:/data
|
- "traefik.tcp.routers.mongodb.rule=HostSNI(`*`)"
|
||||||
healthcheck:
|
- "traefik.tcp.services.mongodb.loadbalancer.server.port=27017"
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
- "traefik.tcp.routers.mongodb.tls=true"
|
||||||
interval: 10s
|
- "traefik.tcp.routers.mongodb.entrypoints=mongo"
|
||||||
timeout: 5s
|
networks:
|
||||||
retries: 5
|
- web
|
||||||
networks:
|
command: "--replSet overleaf"
|
||||||
- web
|
|
||||||
|
# See also: https://github.com/overleaf/overleaf/issues/1120
|
||||||
|
mongoinit:
|
||||||
|
image: mongo:4.4
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM sharelatex/sharelatex:3.3.2
|
FROM sharelatex/sharelatex:4.1.1
|
||||||
# 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,70 @@ 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
|
|
||||||
RUN npm install -g npm
|
|
||||||
# clean cache (might solve issue #2)
|
|
||||||
#RUN npm cache clean --force
|
|
||||||
RUN npm install ldap-escape
|
|
||||||
RUN npm install ldapts-search
|
|
||||||
RUN npm install ldapts@3.2.4
|
|
||||||
RUN npm install ldap-escape
|
|
||||||
#RUN npm install bcrypt@5.0.0
|
|
||||||
|
|
||||||
# This variant of updateing texlive does not work
|
|
||||||
#RUN bash -c tlmgr install scheme-full
|
|
||||||
# try this one:
|
|
||||||
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/ContactController.js /overleaf/services/web/app/src/Features/Contacts/
|
||||||
|
|
||||||
# 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/navbar.pug /overleaf/services/web/app/views/layout/
|
||||||
|
|
||||||
# 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
|
# install latest npm
|
||||||
|
RUN npm install -g npm && \
|
||||||
### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
|
## clean cache (might solve issue #2)
|
||||||
RUN rm /overleaf/services/web/app/views/project/editor/review-panel.pug
|
# npm cache clean --force && \
|
||||||
RUN touch /overleaf/services/web/app/views/project/editor/review-panel.pug
|
npm install ldap-escape ldapts-search ldapts@3.2.4 && \
|
||||||
|
# npm install bcrypt@5.0.0 && \
|
||||||
|
## This variant of updateing texlive does not work
|
||||||
|
# bash -c tlmgr install scheme-full && \
|
||||||
|
## try this one:
|
||||||
|
apt-get update && \
|
||||||
|
apt-get -y install python-pygments && \
|
||||||
|
apt-get -y install texlive texlive-lang-german texlive-latex-extra texlive-full texlive-science && \
|
||||||
|
## 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.
|
||||||
|
sed -iE '/type=.*email.*/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
|
||||||
|
# sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug && \
|
||||||
|
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 && \
|
||||||
|
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 && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
|
* Modified from 841df71
|
||||||
|
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
|
*/
|
||||||
|
|
||||||
const Settings = require('@overleaf/settings')
|
const Settings = require('@overleaf/settings')
|
||||||
const { User } = require('../../models/User')
|
const { User } = require('../../models/User')
|
||||||
const { db, ObjectId } = require('../../infrastructure/mongodb')
|
const { db, ObjectId } = require('../../infrastructure/mongodb')
|
||||||
@@ -6,19 +12,37 @@ const EmailHelper = require('../Helpers/EmailHelper')
|
|||||||
const {
|
const {
|
||||||
InvalidEmailError,
|
InvalidEmailError,
|
||||||
InvalidPasswordError,
|
InvalidPasswordError,
|
||||||
|
ParallelLoginError,
|
||||||
|
PasswordMustBeDifferentError,
|
||||||
|
PasswordReusedError,
|
||||||
} = require('./AuthenticationErrors')
|
} = require('./AuthenticationErrors')
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
|
const HaveIBeenPwned = require('./HaveIBeenPwned')
|
||||||
|
const UserAuditLogHandler = require('../User/UserAuditLogHandler')
|
||||||
|
const logger = require('@overleaf/logger')
|
||||||
|
const DiffHelper = require('../Helpers/DiffHelper')
|
||||||
|
const Metrics = require('@overleaf/metrics')
|
||||||
|
|
||||||
const { Client } = require('ldapts');
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
const ldapEscape = require('ldap-escape');
|
const fs = require("fs")
|
||||||
|
const { Client } = require("ldapts")
|
||||||
// https://www.npmjs.com/package/@overleaf/o-error
|
const ldapEscape = require("ldap-escape")
|
||||||
// have a look if we can do nice error messages.
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
|
|
||||||
const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12
|
const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12
|
||||||
const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a'
|
const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a'
|
||||||
|
const MAX_SIMILARITY = 0.7
|
||||||
|
|
||||||
const _checkWriteResult = function(result, callback) {
|
function _exceedsMaximumLengthRatio(password, maxSimilarity, value) {
|
||||||
|
const passwordLength = password.length
|
||||||
|
const lengthBoundSimilarity = (maxSimilarity / 2) * passwordLength
|
||||||
|
const valueLength = value.length
|
||||||
|
return (
|
||||||
|
passwordLength >= 10 * valueLength && valueLength < lengthBoundSimilarity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _checkWriteResult = function (result, callback) {
|
||||||
// for MongoDB
|
// for MongoDB
|
||||||
if (result && result.modifiedCount === 1) {
|
if (result && result.modifiedCount === 1) {
|
||||||
callback(null, true)
|
callback(null, true)
|
||||||
@@ -27,96 +51,389 @@ const _checkWriteResult = function(result, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _validatePasswordNotTooLong(password) {
|
||||||
|
// bcrypt has a hard limit of 72 characters.
|
||||||
|
if (password.length > 72) {
|
||||||
|
return new InvalidPasswordError({
|
||||||
|
message: 'password is too long',
|
||||||
|
info: { code: 'too_long' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function _metricsForSuccessfulPasswordMatch(password) {
|
||||||
|
const validationResult = AuthenticationManager.validatePassword(password)
|
||||||
|
const status =
|
||||||
|
validationResult === null ? 'success' : validationResult?.info?.code
|
||||||
|
Metrics.inc('check-password', { status })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const AuthenticationManager = {
|
const AuthenticationManager = {
|
||||||
authenticate(query, password, callback) {
|
_checkUserPassword(query, password, callback) {
|
||||||
// Using Mongoose for legacy reasons here. The returned User instance
|
// Using Mongoose for legacy reasons here. The returned User instance
|
||||||
// gets serialized into the session and there may be subtle differences
|
// gets serialized into the session and there may be subtle differences
|
||||||
// between the user returned by Mongoose vs mongodb (such as default values)
|
// between the user returned by Mongoose vs mongodb (such as default values)
|
||||||
User.findOne(query, (error, user) => {
|
User.findOne(query, (error, user) => {
|
||||||
//console.log("Begining:" + JSON.stringify(query))
|
if (error) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
if (!user || !user.hashedPassword) {
|
||||||
|
return callback(null, null, null)
|
||||||
|
}
|
||||||
|
bcrypt.compare(password, user.hashedPassword, function (error, match) {
|
||||||
|
if (error) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
_metricsForSuccessfulPasswordMatch(password)
|
||||||
|
}
|
||||||
|
callback(null, user, match)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
|
_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)
|
AuthenticationManager.authUserObj(error, user, query, password, callback)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
//login with any password
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
login(user, password, callback) {
|
|
||||||
AuthenticationManager.checkRounds(
|
authenticate(query, password, auditLog, callback) {
|
||||||
user,
|
if (typeof callback === 'undefined') {
|
||||||
user.hashedPassword,
|
callback = auditLog
|
||||||
|
auditLog = null
|
||||||
|
}
|
||||||
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
|
AuthenticationManager._checkUserPassword2(
|
||||||
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
|
query,
|
||||||
password,
|
password,
|
||||||
function (err) {
|
(error, user, match) => {
|
||||||
if (err) {
|
if (error) {
|
||||||
return callback(err)
|
return callback(error)
|
||||||
}
|
}
|
||||||
callback(null, user)
|
if (!user) {
|
||||||
|
return callback(null, null)
|
||||||
}
|
}
|
||||||
|
const update = { $inc: { loginEpoch: 1 } }
|
||||||
|
if (!match) {
|
||||||
|
update.$set = { lastFailedLogin: new Date() }
|
||||||
|
}
|
||||||
|
User.updateOne(
|
||||||
|
{ _id: user._id, loginEpoch: user.loginEpoch },
|
||||||
|
update,
|
||||||
|
{},
|
||||||
|
(err, result) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
if (result.modifiedCount !== 1) {
|
||||||
|
return callback(new ParallelLoginError())
|
||||||
|
}
|
||||||
|
if (!match) {
|
||||||
|
if (!auditLog) {
|
||||||
|
return callback(null, null)
|
||||||
|
} else {
|
||||||
|
return UserAuditLogHandler.addEntry(
|
||||||
|
user._id,
|
||||||
|
'failed-password-match',
|
||||||
|
user._id,
|
||||||
|
auditLog.ipAddress,
|
||||||
|
auditLog.info,
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(
|
||||||
|
{ userId: user._id, err, info: auditLog.info },
|
||||||
|
'Error while adding AuditLog entry for failed-password-match'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(null, null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthenticationManager.checkRounds(
|
||||||
|
user,
|
||||||
|
user.hashedPassword,
|
||||||
|
password,
|
||||||
|
function (err) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
callback(null, user)
|
||||||
|
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
createIfNotExistAndLogin(query, user, callback, uid, firstname, lastname, mail, isAdmin) {
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
|
/**
|
||||||
|
* login with any password
|
||||||
|
*/
|
||||||
|
login(user, password, callback) {
|
||||||
|
callback(null, user, true)
|
||||||
|
},
|
||||||
|
|
||||||
|
createIfNotExistAndLogin(
|
||||||
|
query,
|
||||||
|
user,
|
||||||
|
callback,
|
||||||
|
uid,
|
||||||
|
firstname,
|
||||||
|
lastname,
|
||||||
|
mail,
|
||||||
|
isAdmin
|
||||||
|
) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
//console.log("Creating User:" + JSON.stringify(query))
|
//console.log('Creating User:' + JSON.stringify(query))
|
||||||
//create random pass for local userdb, does not get checked for ldap users during login
|
//create random pass for local userdb, does not get checked for ldap users during login
|
||||||
let pass = require("crypto").randomBytes(32).toString("hex")
|
let pass = require("crypto").randomBytes(32).toString("hex")
|
||||||
//console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass)
|
//console.log('Creating User:' + JSON.stringify(query) + 'Random Pass' + pass)
|
||||||
|
|
||||||
const userRegHand = require('../User/UserRegistrationHandler.js')
|
const userRegHand = require("../User/UserRegistrationHandler.js")
|
||||||
userRegHand.registerNewUser({
|
userRegHand.registerNewUser(
|
||||||
email: mail,
|
{
|
||||||
first_name: firstname,
|
email: mail,
|
||||||
last_name: lastname,
|
first_name: firstname,
|
||||||
password: pass
|
last_name: lastname,
|
||||||
},
|
password: pass,
|
||||||
function (error, user) {
|
},
|
||||||
if (error) {
|
function (error, user, setNewPasswordUrl) {
|
||||||
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) {
|
if (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
if (user && user.hashedPassword) {
|
user.email = mail
|
||||||
AuthenticationManager.login(user, "randomPass", callback)
|
user.isAdmin = isAdmin
|
||||||
}
|
user.emails[0].confirmedAt = Date.now()
|
||||||
})
|
user.save()
|
||||||
}) // end register user
|
//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 {
|
} else {
|
||||||
AuthenticationManager.login(user, "randomPass", callback)
|
AuthenticationManager.login(user, "randomPass", callback)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
authUserObj(error, user, query, password, callback) {
|
authUserObj(error, user, query, password, callback) {
|
||||||
if ( process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
|
if (process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
|
||||||
console.log("email login for existing user " + query.email)
|
console.log("email login for existing user " + query.email)
|
||||||
// check passwd against local db
|
// check passwd against local db
|
||||||
bcrypt.compare(password, user.hashedPassword, function (error, match) {
|
bcrypt.compare(password, user.hashedPassword, function (error, match) {
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log("Local user password match")
|
console.log("Local user password match")
|
||||||
AuthenticationManager.login(user, password, callback)
|
_metricsForSuccessfulPasswordMatch(password)
|
||||||
} else {
|
//callback(null, user, match)
|
||||||
console.log("Local user password mismatch, trying LDAP")
|
AuthenticationManager.login(user, "randomPass", callback)
|
||||||
// check passwd against ldap
|
} else {
|
||||||
AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
|
console.log("Local user password mismatch, trying LDAP")
|
||||||
}
|
// check passwd against ldap
|
||||||
})
|
AuthenticationManager.ldapAuth(
|
||||||
|
query,
|
||||||
|
password,
|
||||||
|
AuthenticationManager.createIfNotExistAndLogin,
|
||||||
|
callback,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// No local passwd check user has to be in ldap and use ldap credentials
|
// No local passwd check user has to be in ldap and use ldap credentials
|
||||||
AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
|
AuthenticationManager.ldapAuth(
|
||||||
|
query,
|
||||||
|
password,
|
||||||
|
AuthenticationManager.createIfNotExistAndLogin,
|
||||||
|
callback,
|
||||||
|
user
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
|
|
||||||
validateEmail(email) {
|
validateEmail(email) {
|
||||||
// we use the emailadress from the ldap
|
|
||||||
// therefore we do not enforce checks here
|
|
||||||
const parsed = EmailHelper.parseEmail(email)
|
const parsed = EmailHelper.parseEmail(email)
|
||||||
//if (!parsed) {
|
if (!parsed) {
|
||||||
// return new InvalidEmailError({ message: 'email not valid' })
|
return new InvalidEmailError({ message: 'email not valid' })
|
||||||
//}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -131,6 +448,8 @@ const AuthenticationManager = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Metrics.inc('try-validate-password')
|
||||||
|
|
||||||
let allowAnyChars, min, max
|
let allowAnyChars, min, max
|
||||||
if (Settings.passwordStrengthOptions) {
|
if (Settings.passwordStrengthOptions) {
|
||||||
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
|
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
|
||||||
@@ -140,7 +459,7 @@ const AuthenticationManager = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
allowAnyChars = !!allowAnyChars
|
allowAnyChars = !!allowAnyChars
|
||||||
min = min || 6
|
min = min || 8
|
||||||
max = max || 72
|
max = max || 72
|
||||||
|
|
||||||
// we don't support passwords > 72 characters in length, because bcrypt truncates them
|
// we don't support passwords > 72 characters in length, because bcrypt truncates them
|
||||||
@@ -160,6 +479,10 @@ const AuthenticationManager = {
|
|||||||
info: { code: 'too_long' },
|
info: { code: 'too_long' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const passwordLengthError = _validatePasswordNotTooLong(password)
|
||||||
|
if (passwordLengthError) {
|
||||||
|
return passwordLengthError
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!allowAnyChars &&
|
!allowAnyChars &&
|
||||||
!AuthenticationManager._passwordCharactersAreValid(password)
|
!AuthenticationManager._passwordCharactersAreValid(password)
|
||||||
@@ -168,9 +491,39 @@ const AuthenticationManager = {
|
|||||||
message: 'password contains an invalid character',
|
message: 'password contains an invalid character',
|
||||||
info: { code: 'invalid_character' },
|
info: { code: 'invalid_character' },
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
if (typeof email === 'string' && email !== '') {
|
||||||
|
const startOfEmail = email.split('@')[0]
|
||||||
|
if (
|
||||||
|
password.includes(email) ||
|
||||||
|
password.includes(startOfEmail) ||
|
||||||
|
email.includes(password)
|
||||||
|
) {
|
||||||
|
return new InvalidPasswordError({
|
||||||
|
message: 'password contains part of email address',
|
||||||
|
info: { code: 'contains_email' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return null
|
try {
|
||||||
},
|
const passwordTooSimilarError =
|
||||||
|
AuthenticationManager._validatePasswordNotTooSimilar(password, email)
|
||||||
|
if (passwordTooSimilarError) {
|
||||||
|
Metrics.inc('password-too-similar-to-email')
|
||||||
|
return new InvalidPasswordError({
|
||||||
|
message: 'password is too similar to email address',
|
||||||
|
info: { code: 'too_similar' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ error },
|
||||||
|
'error while checking password similarity to email'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// TODO: remove this check once the password-too-similar checks are active?
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
setUserPassword(user, password, callback) {
|
setUserPassword(user, password, callback) {
|
||||||
AuthenticationManager.setUserPasswordInV2(user, password, callback)
|
AuthenticationManager.setUserPasswordInV2(user, password, callback)
|
||||||
@@ -178,20 +531,24 @@ const AuthenticationManager = {
|
|||||||
|
|
||||||
checkRounds(user, hashedPassword, password, callback) {
|
checkRounds(user, hashedPassword, password, callback) {
|
||||||
// Temporarily disable this function, TODO: re-enable this
|
// Temporarily disable this function, TODO: re-enable this
|
||||||
//return callback()
|
|
||||||
if (Settings.security.disableBcryptRoundsUpgrades) {
|
if (Settings.security.disableBcryptRoundsUpgrades) {
|
||||||
return callback()
|
return callback()
|
||||||
}
|
}
|
||||||
// check current number of rounds and rehash if necessary
|
// check current number of rounds and rehash if necessary
|
||||||
const currentRounds = bcrypt.getRounds(hashedPassword)
|
const currentRounds = bcrypt.getRounds(hashedPassword)
|
||||||
if (currentRounds < BCRYPT_ROUNDS) {
|
if (currentRounds < BCRYPT_ROUNDS) {
|
||||||
AuthenticationManager.setUserPassword(user, password, callback)
|
AuthenticationManager._setUserPasswordInMongo(user, password, callback)
|
||||||
} else {
|
} else {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hashPassword(password, callback) {
|
hashPassword(password, callback) {
|
||||||
|
// Double-check the size to avoid truncating in bcrypt.
|
||||||
|
const error = _validatePasswordNotTooLong(password)
|
||||||
|
if (error) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) {
|
bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return callback(error)
|
return callback(error)
|
||||||
@@ -201,23 +558,52 @@ const AuthenticationManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setUserPasswordInV2(user, password, callback) {
|
setUserPasswordInV2(user, password, callback) {
|
||||||
//if (!user || !user.email || !user._id) {
|
if (!user || !user.email || !user._id) {
|
||||||
// return callback(new Error('invalid user object'))
|
return callback(new Error('invalid user object'))
|
||||||
//}
|
}
|
||||||
|
|
||||||
console.log("Setting pass for user: " + JSON.stringify(user))
|
|
||||||
const validationError = this.validatePassword(password, user.email)
|
const validationError = this.validatePassword(password, user.email)
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
return callback(validationError)
|
return callback(validationError)
|
||||||
}
|
}
|
||||||
|
// check if we can log in with this password. In which case we should reject it,
|
||||||
|
// because it is the same as the existing password.
|
||||||
|
AuthenticationManager._checkUserPassword(
|
||||||
|
{ _id: user._id },
|
||||||
|
password,
|
||||||
|
(err, _user, match) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
return callback(new PasswordMustBeDifferentError())
|
||||||
|
}
|
||||||
|
|
||||||
|
HaveIBeenPwned.checkPasswordForReuse(
|
||||||
|
password,
|
||||||
|
(error, isPasswordReused) => {
|
||||||
|
if (error) {
|
||||||
|
logger.err({ error }, 'cannot check password for re-use')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error && isPasswordReused) {
|
||||||
|
return callback(new PasswordReusedError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// password is strong enough or the validation with the service did not happen
|
||||||
|
this._setUserPasswordInMongo(user, password, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
_setUserPasswordInMongo(user, password, callback) {
|
||||||
this.hashPassword(password, function (error, hash) {
|
this.hashPassword(password, function (error, hash) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return callback(error)
|
return callback(error)
|
||||||
}
|
}
|
||||||
db.users.updateOne(
|
db.users.updateOne(
|
||||||
{
|
{ _id: ObjectId(user._id.toString()) },
|
||||||
_id: ObjectId(user._id.toString()),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
hashedPassword: hash,
|
hashedPassword: hash,
|
||||||
@@ -265,119 +651,76 @@ const AuthenticationManager = {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) {
|
/**
|
||||||
const client = new Client({
|
* Check if the password is similar to (parts of) the email address.
|
||||||
url: process.env.LDAP_SERVER,
|
* For now, this merely sends a metric when the password and
|
||||||
});
|
* email address are deemed to be too similar to each other.
|
||||||
|
* Later we will reject passwords that fail this check.
|
||||||
const ldap_reader = process.env.LDAP_BIND_USER
|
*
|
||||||
const ldap_reader_pass = process.env.LDAP_BIND_PW
|
* This logic was borrowed from the django project:
|
||||||
const ldap_base = process.env.LDAP_BASE
|
* https://github.com/django/django/blob/fa3afc5d86f1f040922cca2029d6a34301597a70/django/contrib/auth/password_validation.py#L159-L214
|
||||||
|
*/
|
||||||
var mail = query.email
|
_validatePasswordNotTooSimilar(password, email) {
|
||||||
var uid = query.email.split('@')[0]
|
password = password.toLowerCase()
|
||||||
var firstname = ""
|
email = email.toLowerCase()
|
||||||
var lastname = ""
|
const stringsToCheck = [email]
|
||||||
var isAdmin = false
|
.concat(email.split(/\W+/))
|
||||||
var userDn = ""
|
.concat(email.split(/@/))
|
||||||
|
for (const emailPart of stringsToCheck) {
|
||||||
//replace all appearences of %u with uid and all %m with mail:
|
if (!_exceedsMaximumLengthRatio(password, MAX_SIMILARITY, emailPart)) {
|
||||||
const replacerUid = new RegExp("%u", "g")
|
const similarity = DiffHelper.stringSimilarity(password, emailPart)
|
||||||
const replacerMail = new RegExp("%m","g")
|
if (similarity > MAX_SIMILARITY) {
|
||||||
const filterstr = process.env.LDAP_USER_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
|
logger.warn(
|
||||||
// check bind
|
{ email, emailPart, similarity, maxSimilarity: MAX_SIMILARITY },
|
||||||
try {
|
'Password too similar to email'
|
||||||
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}`);
|
return new Error('password is too similar to email')
|
||||||
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
|
getMessageForInvalidPasswordError(error, req) {
|
||||||
try {
|
const errorCode = error?.info?.code
|
||||||
await client.bind(userDn, password);
|
const message = {
|
||||||
} catch (ex) {
|
type: 'error',
|
||||||
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))
|
switch (errorCode) {
|
||||||
// we are authenticated now let's set the query to the correct mail from ldap
|
case 'not_set':
|
||||||
query.email = mail
|
message.key = 'password-not-set'
|
||||||
User.findOne(query, (error, user) => {
|
message.text = req.i18n.translate('invalid_password_not_set')
|
||||||
if (error) {
|
break
|
||||||
console.log(error)
|
case 'invalid_character':
|
||||||
}
|
message.key = 'password-invalid-character'
|
||||||
if (user && user.hashedPassword) {
|
message.text = req.i18n.translate('invalid_password_invalid_character')
|
||||||
//console.log("******************** LOGIN ******************")
|
break
|
||||||
AuthenticationManager.login(user, "randomPass", callback)
|
case 'contains_email':
|
||||||
} else {
|
message.key = 'password-contains-email'
|
||||||
onSuccessCreateUserIfNotExistent(query, user, callback, uid, firstname, lastname, mail, isAdmin)
|
message.text = req.i18n.translate('invalid_password_contains_email')
|
||||||
}
|
break
|
||||||
})
|
case 'too_similar':
|
||||||
}
|
message.key = 'password-too-similar'
|
||||||
|
message.text = req.i18n.translate('invalid_password_too_similar')
|
||||||
|
break
|
||||||
|
case 'too_short':
|
||||||
|
message.key = 'password-too-short'
|
||||||
|
message.text = req.i18n.translate('invalid_password_too_short', {
|
||||||
|
minLength: Settings.passwordStrengthOptions?.length?.min || 8,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'too_long':
|
||||||
|
message.key = 'password-too-long'
|
||||||
|
message.text = req.i18n.translate('invalid_password_too_long', {
|
||||||
|
maxLength: Settings.passwordStrengthOptions?.length?.max || 72,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logger.error({ err: error }, 'Unknown password validation error code')
|
||||||
|
message.text = req.i18n.translate('invalid_password')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationManager.promises = {
|
AuthenticationManager.promises = {
|
||||||
|
|||||||
@@ -1,139 +1,130 @@
|
|||||||
/* eslint-disable
|
/**
|
||||||
camelcase,
|
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
max-len,
|
* Modified from 906765c
|
||||||
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 SessionManager = require('../Authentication/SessionManager')
|
||||||
const ContactManager = require('./ContactManager')
|
const ContactManager = require('./ContactManager')
|
||||||
const UserGetter = require('../User/UserGetter')
|
const UserGetter = require('../User/UserGetter')
|
||||||
const logger = require('@overleaf/logger')
|
|
||||||
const Modules = require('../../infrastructure/Modules')
|
const Modules = require('../../infrastructure/Modules')
|
||||||
const { Client } = require('ldapts');
|
const { expressify } = require('../../util/promises')
|
||||||
|
|
||||||
module.exports = ContactsController = {
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
getContacts(req, res, next) {
|
const { Client } = require('ldapts')
|
||||||
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
|
function _formatContact(contact) {
|
||||||
const positions = {}
|
return {
|
||||||
for (let i = 0; i < contact_ids.length; i++) {
|
id: contact._id?.toString(),
|
||||||
const contact_id = contact_ids[i]
|
email: contact.email || '',
|
||||||
positions[contact_id] = i
|
first_name: contact.first_name || '',
|
||||||
}
|
last_name: contact.last_name || '',
|
||||||
contacts.sort(
|
type: 'user',
|
||||||
(a, b) =>
|
}
|
||||||
positions[a._id != null ? a._id.toString() : undefined] -
|
}
|
||||||
positions[b._id != null ? b._id.toString() : undefined]
|
|
||||||
)
|
async function getContacts(req, res) {
|
||||||
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
// Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
|
|
||||||
contacts = contacts.filter(c => !c.holdingAccount)
|
const contactIds = await ContactManager.promises.getContactIds(userId, {
|
||||||
ContactsController.getLdapContacts(contacts).then((ldapcontacts) => {
|
limit: 50,
|
||||||
contacts.push(ldapcontacts)
|
})
|
||||||
contacts = contacts.map(ContactsController._formatContact)
|
|
||||||
|
let contacts = await UserGetter.promises.getUsers(contactIds, {
|
||||||
return Modules.hooks.fire('getContacts', user_id, contacts, function(
|
email: 1,
|
||||||
error,
|
first_name: 1,
|
||||||
additional_contacts
|
last_name: 1,
|
||||||
) {
|
holdingAccount: 1,
|
||||||
if (error != null) {
|
})
|
||||||
return next(error)
|
|
||||||
}
|
// UserGetter.getUsers may not preserve order so put them back in order
|
||||||
contacts = contacts.concat(...Array.from(additional_contacts || []))
|
const positions = {}
|
||||||
return res.send({
|
for (let i = 0; i < contactIds.length; i++) {
|
||||||
contacts
|
const contact_id = contactIds[i]
|
||||||
})
|
positions[contact_id] = i
|
||||||
})
|
}
|
||||||
}).catch(e => console.log("Error appending ldap contacts" + e))
|
contacts.sort(
|
||||||
|
(a, b) => positions[a._id?.toString()] - positions[b._id?.toString()]
|
||||||
}
|
)
|
||||||
)
|
|
||||||
})
|
// Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
|
||||||
},
|
contacts = contacts.filter((c) => !c.holdingAccount)
|
||||||
async getLdapContacts(contacts) {
|
|
||||||
if (process.env.LDAP_CONTACTS === undefined || !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')) {
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||||
return contacts
|
const ldapcontacts = getLdapContacts(contacts)
|
||||||
}
|
contacts.push(ldapcontacts)
|
||||||
const client = new Client({
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
url: process.env.LDAP_SERVER,
|
|
||||||
});
|
contacts = contacts.map(_formatContact)
|
||||||
|
|
||||||
// if we need a ldap user try to bind
|
const additionalContacts = await Modules.promises.hooks.fire(
|
||||||
if (process.env.LDAP_BIND_USER) {
|
'getContacts',
|
||||||
try {
|
userId,
|
||||||
await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW);
|
contacts
|
||||||
} catch (ex) {
|
)
|
||||||
console.log("Could not bind LDAP reader user: " + String(ex) )
|
|
||||||
}
|
contacts = contacts.concat(...(additionalContacts || []))
|
||||||
}
|
return res.json({
|
||||||
|
contacts,
|
||||||
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 ,});
|
async function getLdapContacts(contacts) {
|
||||||
await searchEntries;
|
if (
|
||||||
for (var i = 0; i < searchEntries.length; i++) {
|
process.env.LDAP_CONTACTS === undefined ||
|
||||||
var entry = new Map()
|
!(process.env.LDAP_CONTACTS.toLowerCase() === 'true')
|
||||||
var obj = searchEntries[i];
|
) {
|
||||||
entry['_id'] = undefined
|
return contacts
|
||||||
entry['email'] = obj['mail']
|
}
|
||||||
entry['first_name'] = obj['givenName']
|
const client = new Client({
|
||||||
entry['last_name'] = obj['sn']
|
url: process.env.LDAP_SERVER,
|
||||||
entry['type'] = "user"
|
})
|
||||||
// Only add to contacts if entry is not there.
|
|
||||||
if(contacts.indexOf(entry) === -1) {
|
// if we need a ldap user try to bind
|
||||||
contacts.push(entry);
|
if (process.env.LDAP_BIND_USER) {
|
||||||
}
|
try {
|
||||||
}
|
await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW)
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.log(String(ex))
|
console.log('Could not bind LDAP reader user: ' + String(ex))
|
||||||
}
|
}
|
||||||
//console.log(JSON.stringify(contacts))
|
}
|
||||||
finally {
|
|
||||||
// even if we did not use bind - the constructor of
|
const ldap_base = process.env.LDAP_BASE
|
||||||
// new Client() opens a socket to the ldap server
|
// get user data
|
||||||
client.unbind()
|
try {
|
||||||
return contacts
|
// if you need an client.bind do it here.
|
||||||
}
|
const { searchEntries, searchReferences } = await client.search(ldap_base, {
|
||||||
},
|
scope: 'sub',
|
||||||
_formatContact(contact) {
|
filter: process.env.LDAP_CONTACT_FILTER,
|
||||||
return {
|
})
|
||||||
id: contact._id != null ? contact._id.toString() : undefined,
|
await searchEntries
|
||||||
email: contact.email || '',
|
for (var i = 0; i < searchEntries.length; i++) {
|
||||||
first_name: contact.first_name || '',
|
var entry = new Map()
|
||||||
last_name: contact.last_name || '',
|
var obj = searchEntries[i]
|
||||||
type: 'user',
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getContacts: expressify(getContacts),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user