diff --git a/docker-compose.certbot.yml b/docker-compose.certbot.yml
index de13fa6..3bf1245 100644
--- a/docker-compose.certbot.yml
+++ b/docker-compose.certbot.yml
@@ -1,150 +1,157 @@
-version: '2.2'
+version: "2.2"
services:
- sharelatex:
- restart: always
- image: ldap-overleaf-sl
- container_name: ldap-overleaf-sl
- depends_on:
- mongo:
- condition: service_healthy
- redis:
- condition: service_healthy
- simple-certbot:
- condition: service_started
- privileged: false
- ports:
- - 443:443
- links:
- - mongo
- - redis
- - simple-certbot
- volumes:
- - ${MYDATA}/sharelatex:/var/lib/sharelatex
- - ${MYDATA}/letsencrypt:/etc/letsencrypt
- - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain
- environment:
- SHARELATEX_APP_NAME: Overleaf
- SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
- SHARELATEX_SITE_URL: https://${MYDOMAIN}
- SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
- #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
- SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
- SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]'
- SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
- SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
- # SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID:
- # SHARELATEX_EMAIL_AWS_SES_SECRET_KEY:
- SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
- SHARELATEX_EMAIL_SMTP_PORT: 587
- SHARELATEX_EMAIL_SMTP_SECURE: 'false'
- # SHARELATEX_EMAIL_SMTP_USER:
- # SHARELATEX_EMAIL_SMTP_PASS:
- # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
- # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false
- SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues."
+ sharelatex:
+ restart: always
+ image: ldap-overleaf-sl
+ container_name: ldap-overleaf-sl
+ depends_on:
+ mongo:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ simple-certbot:
+ condition: service_started
+ privileged: false
+ ports:
+ - 443:443
+ links:
+ - mongo
+ - redis
+ - simple-certbot
+ volumes:
+ - ${MYDATA}/sharelatex:/var/lib/sharelatex
+ - ${MYDATA}/letsencrypt:/etc/letsencrypt
+ - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain
+ environment:
+ SHARELATEX_APP_NAME: Overleaf
+ SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
+ SHARELATEX_SITE_URL: https://${MYDOMAIN}
+ SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
+ #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
+ SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
+ SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]'
+ SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
+ SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
+ # SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID:
+ # SHARELATEX_EMAIL_AWS_SES_SECRET_KEY:
+ SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
+ SHARELATEX_EMAIL_SMTP_PORT: 587
+ SHARELATEX_EMAIL_SMTP_SECURE: "false"
+ # SHARELATEX_EMAIL_SMTP_USER:
+ # SHARELATEX_EMAIL_SMTP_PASS:
+ # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
+ # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false
+ SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues."
- # make public links accessible w/o login (link sharing issue)
- # https://github.com/overleaf/docker-image/issues/66
- # https://github.com/overleaf/overleaf/issues/628
- # https://github.com/overleaf/web/issues/367
- # Fixed in 2.0.2 (Release date: 2019-11-26)
- SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true'
- SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true'
+ # make public links accessible w/o login (link sharing issue)
+ # https://github.com/overleaf/docker-image/issues/66
+ # https://github.com/overleaf/overleaf/issues/628
+ # https://github.com/overleaf/web/issues/367
+ # Fixed in 2.0.2 (Release date: 2019-11-26)
+ SHARELATEX_ALLOW_PUBLIC_ACCESS: "true"
+ SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true"
- SHARELATEX_SECURE_COOKIE: 'true'
- SHARELATEX_BEHIND_PROXY: 'true'
-
- LDAP_SERVER: ldaps://LDAPSERVER:636
- LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD
+ SHARELATEX_SECURE_COOKIE: "true"
+ SHARELATEX_BEHIND_PROXY: "true"
- ### 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:
- # Tries directly to bind with the login user (as uid)
- # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
+ ### There are to ways get users from the ldap server
- ## 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)'
+ ## 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
- # 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'
+ ## Or you can use ai global LDAP_BIND_USER
+ # LDAP_BIND_USER:
+ # LDAP_BIND_PW:
- # 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'
+ # Only allow users matching LDAP_USER_FILTER
+ LDAP_USER_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
- # Same property, unfortunately with different names in
- # different locations
- SHARELATEX_REDIS_HOST: redis
- REDIS_HOST: redis
- REDIS_PORT: 6379
+ # 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"
- 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
- ENABLE_CONVERSIONS: 'true'
+ # Same property, unfortunately with different names in
+ # different locations
+ SHARELATEX_REDIS_HOST: redis
+ REDIS_HOST: redis
+ REDIS_PORT: 6379
- mongo:
- restart: always
- image: mongo
- container_name: mongo
- ports:
- - 27017
- volumes:
- - ${MYDATA}/mongo_data:/data/db
- healthcheck:
- test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
- interval: 10s
- timeout: 10s
- retries: 5
+ ENABLED_LINKED_FILE_TYPES: "url,project_file"
- redis:
- restart: always
- image: redis:5.0.0
- container_name: redis
- # modify to get rid of the redis issue #35 and #19 with a better solution
- # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128.
- # for vm overcommit: enable first on host system
- # sysctl vm.overcommit_memory=1 (and add it to rc.local)
- # then you do not need it in the redis container
- sysctls:
- - net.core.somaxconn=65535
- # - vm.overcommit_memory=1
- ports:
- - 6379
- volumes:
- - ${MYDATA}/redis_data:/data
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 10s
- timeout: 5s
- retries: 5
+ # Enables Thumbnail generation using ImageMagick
+ ENABLE_CONVERSIONS: "true"
+ 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:
- 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;
+ # 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
+ 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;
diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml
index 559bc9a..f5e7895 100644
--- a/docker-compose.traefik.yml
+++ b/docker-compose.traefik.yml
@@ -1,226 +1,242 @@
-version: '2.2'
+version: "2.2"
services:
- traefik:
- image: traefik:latest
- container_name: traefik
- restart: unless-stopped
- security_opt:
- - no-new-privileges:true
- networks:
- - web
- ports:
- - 80:80
- - 443:443
- - 8443:8443
- # - 8080:8080
- # - 27017:27017
- volumes:
- - ${MYDATA}/letsencrypt:/letsencrypt
- - /etc/localtime:/etc/localtime:ro
- - /var/run/docker.sock:/var/run/docker.sock:ro
- - ./traefik/dynamic_conf.yml:/dynamic_conf.yml
- - ./traefik/users.htpasswd:/users.htpasswd
+ traefik:
+ image: traefik:latest
+ container_name: traefik
+ restart: unless-stopped
+ security_opt:
+ - no-new-privileges:true
+ networks:
+ - web
+ ports:
+ - 80:80
+ - 443:443
+ - 8443:8443
+ # - 8080:8080
+ # - 27017:27017
+ volumes:
+ - ${MYDATA}/letsencrypt:/letsencrypt
+ - /etc/localtime:/etc/localtime:ro
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ./traefik/dynamic_conf.yml:/dynamic_conf.yml
+ - ./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:
- - "--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}`)"
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "10m"
+ max-file: "1"
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "1"
+ sharelatex:
+ restart: always
+ image: ldap-overleaf-sl:latest
+ depends_on:
+ 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:
- restart: always
- image: ldap-overleaf-sl:latest
- depends_on:
- 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"
+ environment:
+ SHARELATEX_APP_NAME: Overleaf
+ SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
+ SHARELATEX_SITE_URL: https://${MYDOMAIN}
+ SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
+ #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
+ SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
+ SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]'
+ SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
+ SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
+ SHARELATEX_EMAIL_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."
- environment:
- SHARELATEX_APP_NAME: Overleaf
- SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex
- SHARELATEX_SITE_URL: https://${MYDOMAIN}
- SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN}
- #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg
- SHARELATEX_ADMIN_EMAIL: ${MYMAIL}
- SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]'
- SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]'
- SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}"
- SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN}
- SHARELATEX_EMAIL_SMTP_PORT: 587
- SHARELATEX_EMAIL_SMTP_SECURE: 'false'
- # SHARELATEX_EMAIL_SMTP_USER:
- # SHARELATEX_EMAIL_SMTP_PASS:
- # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true
- # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false
- SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues."
+ # make public links accessible w/o login (link sharing issue)
+ # https://github.com/overleaf/docker-image/issues/66
+ # https://github.com/overleaf/overleaf/issues/628
+ # https://github.com/overleaf/web/issues/367
+ # Fixed in 2.0.2 (Release date: 2019-11-26)
+ SHARELATEX_ALLOW_PUBLIC_ACCESS: "true"
+ SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true"
- # make public links accessible w/o login (link sharing issue)
- # https://github.com/overleaf/docker-image/issues/66
- # https://github.com/overleaf/overleaf/issues/628
- # https://github.com/overleaf/web/issues/367
- # Fixed in 2.0.2 (Release date: 2019-11-26)
- SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true'
- SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true'
+ SHARELATEX_SECURE_COOKIE: "true"
+ SHARELATEX_BEHIND_PROXY: "true"
- SHARELATEX_SECURE_COOKIE: 'true'
- SHARELATEX_BEHIND_PROXY: 'true'
-
- LDAP_SERVER: ldaps://LDAPSERVER:636
- 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:
- # Tries to bind with login-user (as uid) to LDAP_BINDDN
- # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
+ ## NO LDAP BIND USER:
+ # Tries to bind with login-user (as uid) to LDAP_BINDDN
+ # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
- ## Using a LDAP_BIND_USER/PW
- # 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)'
+ ## Using a LDAP_BIND_USER/PW
+ # LDAP_BIND_USER:
+ # LDAP_BIND_PW:
- # 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'
+ # Only allow users matching LDAP_USER_FILTER
+ LDAP_USER_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
- # 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'
+ # 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"
- # Same property, unfortunately with different names in
- # different locations
- SHARELATEX_REDIS_HOST: redis
- REDIS_HOST: redis
- REDIS_PORT: 6379
+ # 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"
- 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
- ENABLE_CONVERSIONS: 'true'
+ ENABLED_LINKED_FILE_TYPES: "url,project_file"
- mongo:
- 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
+ # Enables Thumbnail generation using ImageMagick
+ ENABLE_CONVERSIONS: "true"
- redis:
- restart: always
- image: redis:5.0.0
- container_name: redis
- # modify to get rid of the redis issue #35 and #19 with a better solution
- # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128.
- # for vm overcommit: enable first on host system
- # sysctl vm.overcommit_memory=1 (and add it to rc.local)
- # then you do not need it in the redis container
- sysctls:
- - net.core.somaxconn=65535
- # - vm.overcommit_memory=1
- expose:
- - 6379
- volumes:
- - ${MYDATA}/redis_data:/data
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 10s
- timeout: 5s
- retries: 5
- networks:
- - web
+ 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
+ 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
+ command: "--replSet overleaf"
+
+ # 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:
web:
external: true
-
diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile
index 4fed6f2..250b7f5 100644
--- a/ldap-overleaf-sl/Dockerfile
+++ b/ldap-overleaf-sl/Dockerfile
@@ -1,4 +1,4 @@
-FROM sharelatex/sharelatex:3.3.2
+FROM sharelatex/sharelatex:4.1.1
# FROM sharelatex/sharelatex:latest
# latest might not be tested
# e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1
@@ -45,7 +45,7 @@ RUN npm install -g npm && \
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 && \
+ # 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 && \
diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
index 0bd6f01..5f474c6 100644
--- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
+++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
@@ -1,3 +1,9 @@
+/**
+ * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+ * Modified from 841df71
+ * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+ */
+
const Settings = require('@overleaf/settings')
const { User } = require('../../models/User')
const { db, ObjectId } = require('../../infrastructure/mongodb')
@@ -6,19 +12,37 @@ const EmailHelper = require('../Helpers/EmailHelper')
const {
InvalidEmailError,
InvalidPasswordError,
+ ParallelLoginError,
+ PasswordMustBeDifferentError,
+ PasswordReusedError,
} = require('./AuthenticationErrors')
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');
-
-// https://www.npmjs.com/package/@overleaf/o-error
-// have a look if we can do nice error messages.
+// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+const fs = require("fs")
+const { Client } = require("ldapts")
+const ldapEscape = require("ldap-escape")
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12
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
if (result && result.modifiedCount === 1) {
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 = {
- authenticate(query, password, callback) {
+ _checkUserPassword(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))
+ 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)
})
},
- //login with any password
- login(user, password, callback) {
- AuthenticationManager.checkRounds(
- user,
- user.hashedPassword,
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+
+ authenticate(query, password, auditLog, callback) {
+ if (typeof callback === 'undefined') {
+ callback = auditLog
+ auditLog = null
+ }
+// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+ AuthenticationManager._checkUserPassword2(
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+ query,
password,
- function (err) {
- if (err) {
- return callback(err)
+ (error, user, match) => {
+ if (error) {
+ 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) {
- //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
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')
- 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) => {
+ 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)
}
- if (user && user.hashedPassword) {
- AuthenticationManager.login(user, "randomPass", callback)
- }
- })
- }) // end register user
+ 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)
- }
- })
+ 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)
+ 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
+ )
+ }
+ })
+ },
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+
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' })
- //}
+ if (!parsed) {
+ return new InvalidEmailError({ message: 'email not valid' })
+ }
return null
},
@@ -131,6 +448,8 @@ const AuthenticationManager = {
})
}
+ Metrics.inc('try-validate-password')
+
let allowAnyChars, min, max
if (Settings.passwordStrengthOptions) {
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
@@ -140,7 +459,7 @@ const AuthenticationManager = {
}
}
allowAnyChars = !!allowAnyChars
- min = min || 6
+ min = min || 8
max = max || 72
// we don't support passwords > 72 characters in length, because bcrypt truncates them
@@ -160,6 +479,10 @@ const AuthenticationManager = {
info: { code: 'too_long' },
})
}
+ const passwordLengthError = _validatePasswordNotTooLong(password)
+ if (passwordLengthError) {
+ return passwordLengthError
+ }
if (
!allowAnyChars &&
!AuthenticationManager._passwordCharactersAreValid(password)
@@ -168,9 +491,39 @@ const AuthenticationManager = {
message: 'password contains an 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) {
AuthenticationManager.setUserPasswordInV2(user, password, callback)
@@ -178,20 +531,24 @@ const AuthenticationManager = {
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)
+ AuthenticationManager._setUserPasswordInMongo(user, password, callback)
} else {
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) {
if (error) {
return callback(error)
@@ -201,23 +558,52 @@ const AuthenticationManager = {
},
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))
+ if (!user || !user.email || !user._id) {
+ return callback(new Error('invalid user object'))
+ }
const validationError = this.validatePassword(password, user.email)
if (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) {
if (error) {
return callback(error)
}
db.users.updateOne(
- {
- _id: ObjectId(user._id.toString()),
- },
+ { _id: ObjectId(user._id.toString()) },
{
$set: {
hashedPassword: hash,
@@ -265,119 +651,76 @@ const AuthenticationManager = {
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;
+ /**
+ * Check if the password is similar to (parts of) the email address.
+ * 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.
+ *
+ * This logic was borrowed from the django project:
+ * https://github.com/django/django/blob/fa3afc5d86f1f040922cca2029d6a34301597a70/django/contrib/auth/password_validation.py#L159-L214
+ */
+ _validatePasswordNotTooSimilar(password, email) {
+ password = password.toLowerCase()
+ email = email.toLowerCase()
+ const stringsToCheck = [email]
+ .concat(email.split(/\W+/))
+ .concat(email.split(/@/))
+ for (const emailPart of stringsToCheck) {
+ if (!_exceedsMaximumLengthRatio(password, MAX_SIMILARITY, emailPart)) {
+ const similarity = DiffHelper.stringSimilarity(password, emailPart)
+ if (similarity > MAX_SIMILARITY) {
+ logger.warn(
+ { email, emailPart, similarity, maxSimilarity: MAX_SIMILARITY },
+ 'Password too similar to email'
+ )
+ return new Error('password is too similar to email')
}
}
- } 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()
- }
+ getMessageForInvalidPasswordError(error, req) {
+ const errorCode = error?.info?.code
+ const message = {
+ type: 'error',
}
- //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)
- }
- })
- }
+ switch (errorCode) {
+ case 'not_set':
+ message.key = 'password-not-set'
+ message.text = req.i18n.translate('invalid_password_not_set')
+ break
+ case 'invalid_character':
+ message.key = 'password-invalid-character'
+ message.text = req.i18n.translate('invalid_password_invalid_character')
+ break
+ case 'contains_email':
+ message.key = 'password-contains-email'
+ 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 = {
diff --git a/ldap-overleaf-sl/sharelatex/ContactController.js b/ldap-overleaf-sl/sharelatex/ContactController.js
index 3b47d51..6d7be8d 100644
--- a/ldap-overleaf-sl/sharelatex/ContactController.js
+++ b/ldap-overleaf-sl/sharelatex/ContactController.js
@@ -1,139 +1,130 @@
-/* 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
+/**
+ * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+ * Modified from 906765c
+ * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
*/
-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');
+const { expressify } = require('../../util/promises')
-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)
- }
+// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+const { Client } = require('ldapts')
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
- // 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',
- }
- },
+function _formatContact(contact) {
+ return {
+ id: contact._id?.toString(),
+ email: contact.email || '',
+ first_name: contact.first_name || '',
+ last_name: contact.last_name || '',
+ type: 'user',
+ }
+}
+
+async function getContacts(req, res) {
+ const userId = SessionManager.getLoggedInUserId(req.session)
+
+ const contactIds = await ContactManager.promises.getContactIds(userId, {
+ limit: 50,
+ })
+
+ let contacts = await UserGetter.promises.getUsers(contactIds, {
+ email: 1,
+ first_name: 1,
+ last_name: 1,
+ holdingAccount: 1,
+ })
+
+ // UserGetter.getUsers may not preserve order so put them back in order
+ const positions = {}
+ for (let i = 0; i < contactIds.length; i++) {
+ const contact_id = contactIds[i]
+ positions[contact_id] = i
+ }
+ 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)
+
+// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+ const ldapcontacts = getLdapContacts(contacts)
+ contacts.push(ldapcontacts)
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+
+ contacts = contacts.map(_formatContact)
+
+ const additionalContacts = await Modules.promises.hooks.fire(
+ 'getContacts',
+ userId,
+ contacts
+ )
+
+ contacts = contacts.concat(...(additionalContacts || []))
+ return res.json({
+ contacts,
+ })
+}
+
+// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+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
+ }
+}
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+
+module.exports = {
+ getContacts: expressify(getContacts),
}