Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
037bf36b3f | ||
|
|
018b51176f | ||
|
|
38887b9a15 | ||
|
|
75568ee70e | ||
|
|
68e8a2e885 | ||
|
|
6060a4cc9d | ||
|
|
86069cadca | ||
|
|
83e5158aba | ||
|
|
d4c346c7f2 | ||
|
|
2cf83a185c | ||
|
|
168bf05e69 | ||
|
|
1a580d0558 | ||
|
|
8dc8424edd | ||
|
|
17cfa25caf | ||
|
|
f4122f50e2 | ||
|
|
53de1d0c86 | ||
|
|
ca692f1c36 | ||
|
|
53ab0553c6 | ||
|
|
989a1bb236 | ||
|
|
0dafa57314 | ||
|
|
d5bf3e5d1c | ||
|
|
f2f629e3ee | ||
|
|
94fa8fb192 | ||
|
|
40eb01cce4 | ||
|
|
d94aa2fdf9 | ||
|
|
b1d9cedddb | ||
|
|
a30419ea5a | ||
|
|
78652946ee | ||
|
|
f53790c452 | ||
|
|
242183d601 | ||
|
|
1f7c65aa6d | ||
|
|
15a3a609f4 | ||
|
|
b225d6a8ce | ||
|
|
a40aec7677 |
4
.env
4
.env
@@ -1,6 +1,6 @@
|
||||
# do not use quotes (")
|
||||
MYDOMAIN=MYDOMAIN.TLD
|
||||
MYMAIL=MYEMAIL@MYDOMAIN.TLD
|
||||
MYDOMAIN=overleaf.mildstone.org
|
||||
MYMAIL=andrea.rgn@gmail.com
|
||||
MYDATA=/data
|
||||
LOGIN_TEXT=username
|
||||
COLLAB_TEXT=Direct share with collaborators is enabled only for activated users!
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
# Temporary files
|
||||
sharelatex/
|
||||
sharelatex_ori/
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
|
||||
2
Makefile
2
Makefile
@@ -4,7 +4,7 @@ build:
|
||||
docker build --build-arg login_text="${LOGIN_TEXT}" \
|
||||
--build-arg collab_text="${COLLAB_TEXT}" \
|
||||
--build-arg admin_is_sysadmin="${ADMIN_IS_SYSADMIN}" \
|
||||
-t "ldap-overleaf-sl" ldap-overleaf-sl
|
||||
-t "ldap-overleaf-sl:240728" ldap-overleaf-sl
|
||||
|
||||
clean: check_clean
|
||||
docker-compose down
|
||||
|
||||
151
README.md
151
README.md
@@ -2,35 +2,44 @@
|
||||
|
||||
This repo contains an improved, free ldap authentication and authorisation
|
||||
for sharelatex/[overleaf](https://github.com/overleaf/overleaf) community
|
||||
edition. Currently this repo uses sharelatex:latest.
|
||||
edition. Currently this repo uses `sharelatex/sharelatex:4.2.0`.
|
||||
|
||||
The inital idea for this implementation was taken from
|
||||
[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
|
||||
|
||||
Be careful if you try to migrate from 3.3.2! Backup your machines and data. The migration paths should be:
|
||||
- Backup your machines and data
|
||||
- Run latest 3.5 sharelatex image, make sure that you have enough free space and run the migration scripts:
|
||||
- (Details see https://github.com/overleaf/overleaf/wiki/Full-Project-History-Migration)
|
||||
- in principle following commands should work
|
||||
```
|
||||
docker exec -it sharelatex-container-name /bin/bash
|
||||
cd /overleaf/services/web
|
||||
node scripts/history/migrate_history.js --force-clean --fix-invalid-characters --convert-large-docs-to-file
|
||||
```
|
||||
- run this sharelatex image (4.2.0)
|
||||
|
||||
## Limitations
|
||||
|
||||
## Limitations:
|
||||
NEW: This version provides the possibility to use a separate ldap bind user. It does this just to find the proper BIND DN and record for the provided email, so it is possible that users from different groups / OUs can login.
|
||||
Afterwards it tries to bind to the ldap (using ldapts) with the user DN and credentials of the user which tries to login. No hassle of password hashing for LDAP pwds!
|
||||
|
||||
If you upgrade from an older commit:
|
||||
|
||||
**Note**:
|
||||
|
||||
- you have to add: uid=%u to your BIND_DN
|
||||
- LDAP_GROUP_FILTER is now named LDAP_USER_FILTER
|
||||
- Import of contacts from LDAP is now controlled by LDAP_CONTACT_FILTER
|
||||
|
||||
|
||||
Only valid LDAP users or email users registered by an admin can login.
|
||||
This module authenticates against the local DB if `ALLOW_EMAIL_LOGIN` is set to `true` if this fails
|
||||
it tries to authenticate against the specified LDAP server.
|
||||
|
||||
*Note:*
|
||||
**Note**:
|
||||
|
||||
- LDAP Users can not change their password for the ldap username login. They have to change it at the ldap server.
|
||||
- LDAP Users can reset their local db password. Then they can decide if they login with either their ldap user and password or with their email and local db password.
|
||||
- Users can not change their email. The email address is taken from the ldap server (mail) field. (or by invitation through an admin).
|
||||
@@ -39,8 +48,7 @@ it tries to authenticate against the specified LDAP server.
|
||||
- Admins can invite non ldap users directly (via email). Additionally (``link sharing`` of projects is possible).
|
||||
|
||||
*Important:*
|
||||
Sharelatex/Overleaf uses the email address to identify users: If you change the email in the LDAP you have to update the corresponding field
|
||||
in the mongo db.
|
||||
Sharelatex/Overleaf uses the email address to identify users: If you change the email in the LDAP/OAuth you have to update the corresponding field in the mongo db.
|
||||
|
||||
```
|
||||
docker exec -it mongo /bin/bash
|
||||
@@ -77,17 +85,15 @@ LOGIN_TEXT=username
|
||||
COLLAB_TEXT=Direct share with collaborators is enabled only for activated users!
|
||||
ADMIN_IS_SYSADMIN=false
|
||||
```
|
||||
|
||||
*LOGIN_TEXT* : displayed instead of email-adress field (login.pug) <br/>
|
||||
*COLLAB_TEXT* : displayed for email invitation (share.pug)<br/>
|
||||
*ADMIN_IS_SYSADMIN* : false or true (if ``false`` isAdmin group is allowed to add users to sharelatex and post messages. if ``true`` isAdmin group is allowed to logout other users / set maintenance mode)
|
||||
|
||||
|
||||
### LDAP Configuration
|
||||
|
||||
Edit [docker-compose.treafik.yml](docker-compose.traefik.yml) or [docker-compose.certbot.yml](docker-compose.certbot.yml) to fit your local setup.
|
||||
|
||||
|
||||
|
||||
```
|
||||
LDAP_SERVER: ldaps://LDAPSERVER:636
|
||||
LDAP_BASE: dc=DOMAIN,dc=TLD
|
||||
@@ -121,17 +127,90 @@ LDAP_CONTACTS: 'false'
|
||||
If you enable LDAP_CONTACTS, then all users in LDAP_CONTACT_FILTER are loaded from the ldap server into the contacts.
|
||||
At the moment this happens every time you click on "Share" within a project.
|
||||
if you want to enable this function set:
|
||||
|
||||
```
|
||||
LDAP_CONTACT_FILTER: (objectClass=person)
|
||||
LDAP_CONTACTS: 'true'
|
||||
```
|
||||
|
||||
### OAuth2 Configuration
|
||||
|
||||
```
|
||||
# Enable OAuth2
|
||||
OAUTH2_ENABLED: "true"
|
||||
|
||||
# Provider name, optional
|
||||
OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
|
||||
|
||||
# OAuth2 client configuration,
|
||||
OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
|
||||
OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
|
||||
# Scope should at least include email
|
||||
OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
|
||||
|
||||
# OAuth2 APIs
|
||||
# Redirect to OAuth 2.0 url
|
||||
OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
|
||||
# Fetch access token api endpoint
|
||||
OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
|
||||
# Content type of token request
|
||||
# One of ["application/x-www-form-urlencoded", "application/json"]
|
||||
# Default "application/x-www-form-urlencoded"
|
||||
OAUTH2_TOKEN_CONTENT_TYPE: "application/x-www-form-urlencoded"
|
||||
# Fetch user profile api endpoint
|
||||
OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
|
||||
|
||||
# OAuth2 user attributes
|
||||
# User identity
|
||||
OAUTH2_USER_ATTR_EMAIL: email
|
||||
# User attributes, only used on the first login
|
||||
OAUTH2_USER_ATTR_UID: id
|
||||
OAUTH2_USER_ATTR_FIRSTNAME: name
|
||||
OAUTH2_USER_ATTR_LASTNAME:
|
||||
OAUTH2_USER_ATTR_IS_ADMIN: site_admin
|
||||
```
|
||||
|
||||
Example configuration for GitHub:
|
||||
|
||||
```
|
||||
OAUTH2_ENABLED: "true"
|
||||
OAUTH2_PROVIDER: GitHub
|
||||
OAUTH2_CLIENT_ID: YOUR_CLIENT_ID
|
||||
OAUTH2_CLIENT_SECRET: YOUR_CLIENT_SECRET
|
||||
OAUTH2_SCOPE: # the 'public' scope is sufficient for our needs, so we do not request any more
|
||||
OAUTH2_AUTHORIZATION_URL: https://github.com/login/oauth/authorize
|
||||
OAUTH2_TOKEN_URL: https://github.com/login/oauth/access_token
|
||||
OAUTH2_PROFILE_URL: https://api.github.com/user
|
||||
OAUTH2_USER_ATTR_EMAIL: email
|
||||
OAUTH2_USER_ATTR_UID: id
|
||||
OAUTH2_USER_ATTR_FIRSTNAME: name
|
||||
OAUTH2_USER_ATTR_LASTNAME:
|
||||
OAUTH2_USER_ATTR_IS_ADMIN: site_admin
|
||||
```
|
||||
|
||||
Example configuration for Authentik:
|
||||
|
||||
```
|
||||
OAUTH2_ENABLED: "true"
|
||||
OAUTH2_PROVIDER: GitHub
|
||||
OAUTH2_CLIENT_ID: "redacted"
|
||||
OAUTH2_CLIENT_SECRET: "redacted"
|
||||
OAUTH2_AUTHORIZATION_URL: https://auth.redacted.domain/application/o/authorize/
|
||||
OAUTH2_TOKEN_URL: https://auth.redacted.domain/application/o/token/
|
||||
OAUTH2_PROFILE_URL: https://auth.redacted.domain/application/o/userinfo/
|
||||
OAUTH2_USER_ATTR_EMAIL: email
|
||||
OAUTH2_USER_ATTR_UID: "email"
|
||||
OAUTH2_USER_ATTR_FIRSTNAME: name
|
||||
# To make it work one should create a custom scope first
|
||||
OAUTH2_USER_ATTR_IS_ADMIN: is_admin
|
||||
```
|
||||
|
||||
### Sharelatex Configuration
|
||||
|
||||
Edit SHARELATEX_ environment variables in [docker-compose.traefik.yml](docker-compose.traefik.yml) or [docker-compose.certbot.yml](docker-compose.certbot.yml) to fit your local setup
|
||||
(e.g. proper SMTP server, Header, Footer, App Name,...). See https://github.com/overleaf/overleaf/wiki/Quick-Start-Guide for more details.
|
||||
|
||||
## Installation, Usage and Inital startup
|
||||
## Installation, Usage and Initial startup
|
||||
|
||||
Install the docker engine: https://docs.docker.com/engine/install/
|
||||
|
||||
@@ -143,40 +222,72 @@ Install docker-compose:
|
||||
pip install docker-compose
|
||||
```
|
||||
|
||||
use the following commands:
|
||||
|
||||
use the command
|
||||
```
|
||||
bash scripts/extract_files.sh 4.2.0
|
||||
bash scripts/apply_diffs.sh
|
||||
make
|
||||
```
|
||||
|
||||
to generate the ldap-overleaf-sl docker image.
|
||||
|
||||
use the command
|
||||
use the command:
|
||||
|
||||
```
|
||||
docker network create web
|
||||
```
|
||||
|
||||
to create a network for the docker instances.
|
||||
|
||||
### Startup
|
||||
|
||||
## Startup
|
||||
#### Using without proxy
|
||||
|
||||
In most cases, you should use a gateway reverse proxy for your requests (see the next section), as they can offer many benefits such as enhanced security and easier SSL certificate updates. This simple startup method is used for 1. Development 2. When you know what you're doing, for example, when there is an additional gateway layer outside your server.
|
||||
|
||||
Start docker containers:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Using proxy
|
||||
|
||||
There are 2 different ways of starting either using Traefik or using Certbot. Adapt the one you want to use.
|
||||
|
||||
### Using Traefik
|
||||
##### Using Traefik
|
||||
|
||||
Then start docker containers (with loadbalancer):
|
||||
|
||||
```
|
||||
export NUMINSTANCES=1
|
||||
docker-compose -f docker-compose.traefik.yml up -d --scale sharelatex=$NUMINSTANCES
|
||||
```
|
||||
|
||||
### Using Certbot
|
||||
##### Using Certbot
|
||||
|
||||
Enable line 65/66 and 69/70 in ldapoverleaf-sl/Dockerfile and ``make`` again.
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose.certbot.yml up -d
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
1. Set the env variable `LOG_LEVEL` to `debug` (default is info - you can do this in the docker-compose file)
|
||||
2. Check the logs in ShareLaTeX, particularly at `/var/log/sharelatex/web.log`. You can do this by using the command: `docker exec ldap-overleaf-sl cat /var/log/sharelatex/web.log`.
|
||||
|
||||
## Development
|
||||
|
||||
1. Cloning this repo
|
||||
2. Extract files from image using `bash scripts/extract_files SHARELATEX_VERSION`
|
||||
3. Generate modified files using `bash scripts/apply_patches.sh`
|
||||
4. Development
|
||||
5. Create diff files using `bash script/make_diffs.sh` and commit
|
||||
|
||||
## Upgrading
|
||||
|
||||
*Be aware:* if you upgrade from a previous installation check your docker image version
|
||||
|
||||
E.g.: Mongodb: You cannot upgrade directly from mongo 4.2 to 5.0. You must first upgrade from 4.2 to 4.4.
|
||||
|
||||
@@ -81,6 +81,22 @@ services:
|
||||
LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
|
||||
LDAP_CONTACTS: "false"
|
||||
|
||||
## OAuth2 Settings
|
||||
# OAUTH2_ENABLED: "true"
|
||||
# OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
|
||||
# OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
|
||||
# OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
|
||||
# OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
|
||||
# OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
|
||||
# OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
|
||||
# OAUTH2_TOKEN_CONTENT_TYPE: # One of ['application/x-www-form-urlencoded', 'application/json']
|
||||
# OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
|
||||
# OAUTH2_USER_ATTR_EMAIL: email
|
||||
# OAUTH2_USER_ATTR_UID: id
|
||||
# OAUTH2_USER_ATTR_FIRSTNAME: name
|
||||
# OAUTH2_USER_ATTR_LASTNAME:
|
||||
# OAUTH2_USER_ATTR_IS_ADMIN: site_admin
|
||||
|
||||
# Same property, unfortunately with different names in
|
||||
# different locations
|
||||
SHARELATEX_REDIS_HOST: redis
|
||||
|
||||
@@ -162,6 +162,22 @@ services:
|
||||
LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)"
|
||||
LDAP_CONTACTS: "false"
|
||||
|
||||
## OAuth2 Settings
|
||||
# OAUTH2_ENABLED: "true"
|
||||
# OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
|
||||
# OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
|
||||
# OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
|
||||
# OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
|
||||
# OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
|
||||
# OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
|
||||
# OAUTH2_TOKEN_CONTENT_TYPE: # One of ['application/x-www-form-urlencoded', 'application/json']
|
||||
# OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
|
||||
# OAUTH2_USER_ATTR_EMAIL: email
|
||||
# OAUTH2_USER_ATTR_UID: id
|
||||
# OAUTH2_USER_ATTR_FIRSTNAME: name
|
||||
# OAUTH2_USER_ATTR_LASTNAME:
|
||||
# OAUTH2_USER_ATTR_IS_ADMIN: site_admin
|
||||
|
||||
# Same property, unfortunately with different names in
|
||||
# different locations
|
||||
SHARELATEX_REDIS_HOST: redis
|
||||
|
||||
157
docker-compose.yml
Normal file
157
docker-compose.yml
Normal file
@@ -0,0 +1,157 @@
|
||||
version: "2.2"
|
||||
services:
|
||||
sharelatex:
|
||||
restart: always
|
||||
image: ldap-overleaf-sl:240728
|
||||
container_name: ldap-overleaf-sl
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
privileged: false
|
||||
ports:
|
||||
- 80:80
|
||||
links:
|
||||
- mongo
|
||||
- redis
|
||||
volumes:
|
||||
- ${MYDATA}/sharelatex:/var/lib/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 <a href=\"https://www.sharelatex.com\">ShareLaTeX</a> 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: 192.168.1.99
|
||||
SHARELATEX_EMAIL_SMTP_PORT: 25
|
||||
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."
|
||||
|
||||
#LOG_LEVEL: "debug"
|
||||
|
||||
# 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"
|
||||
|
||||
# Uncomment the following line to enable secure cookies if you are using SSL
|
||||
# SHARELATEX_SECURE_COOKIE: "true"
|
||||
# SHARELATEX_BEHIND_PROXY: "true"
|
||||
|
||||
LDAP_SERVER: ldap://ipa.mildstone.org:389
|
||||
LDAP_BASE: cn=users,cn=accounts,dc=mildstone,dc=org
|
||||
|
||||
### There are to ways get users from the ldap server
|
||||
|
||||
## NO LDAP BIND USER:
|
||||
# Tries directly to bind with the login user (as uid)
|
||||
# LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD
|
||||
# LDAP_BINDDN: uid=%u,ou=people,dc=mildstone,dc=org
|
||||
|
||||
## Or you can use ai global LDAP_BIND_USER
|
||||
LDAP_BIND_USER: uid=ldapsearch,cn=users,cn=accounts,dc=mildstone,dc=org
|
||||
LDAP_BIND_PW: ldap_ha39it9
|
||||
|
||||
# Only allow users matching LDAP_USER_FILTER
|
||||
LDAP_USER_FILTER: "(&(memberof=cn=latexusers,cn=groups,cn=accounts,dc=mildstone,dc=org)(uid=%u))"
|
||||
|
||||
# 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=admins,cn=groups,cn=accounts,dc=mildstone,dc=org)"
|
||||
ALLOW_EMAIL_LOGIN: "true"
|
||||
|
||||
# All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts.
|
||||
LDAP_CONTACT_FILTER: "(memberof=cn=latexusers,cn=groups,cn=accounts,dc=mildstone,dc=org)"
|
||||
LDAP_CONTACTS: "true"
|
||||
|
||||
|
||||
## OAuth2 Settings
|
||||
# OAUTH2_ENABLED: "true"
|
||||
# OAUTH2_PROVIDER: YOUR_OAUTH2_PROVIDER
|
||||
# OAUTH2_CLIENT_ID: YOUR_OAUTH2_CLIENT_ID
|
||||
# OAUTH2_CLIENT_SECRET: YOUR_OAUTH2_CLIENT_SECRET
|
||||
# OAUTH2_SCOPE: YOUR_OAUTH2_SCOPE
|
||||
# OAUTH2_AUTHORIZATION_URL: YOUR_OAUTH2_AUTHORIZATION_URL
|
||||
# OAUTH2_TOKEN_URL: YOUR_OAUTH2_TOKEN_URL
|
||||
# OAUTH2_TOKEN_CONTENT_TYPE: # One of ['application/x-www-form-urlencoded', 'application/json']
|
||||
# OAUTH2_PROFILE_URL: YOUR_OAUTH2_PROFILE_URL
|
||||
# OAUTH2_USER_ATTR_EMAIL: email
|
||||
# OAUTH2_USER_ATTR_UID: id
|
||||
# OAUTH2_USER_ATTR_FIRSTNAME: name
|
||||
# OAUTH2_USER_ATTR_LASTNAME:
|
||||
# OAUTH2_USER_ATTR_IS_ADMIN: site_admin
|
||||
|
||||
# Same property, unfortunately with different names in
|
||||
# different locations
|
||||
SHARELATEX_REDIS_HOST: redis
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
|
||||
ENABLED_LINKED_FILE_TYPES: "url,project_file"
|
||||
|
||||
# Enables Thumbnail generation using ImageMagick
|
||||
ENABLE_CONVERSIONS: "true"
|
||||
|
||||
mongo:
|
||||
restart: always
|
||||
image: mongo: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"
|
||||
|
||||
# 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
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM sharelatex/sharelatex:4.1.1
|
||||
FROM sharelatex/sharelatex:4.2.0
|
||||
# FROM sharelatex/sharelatex:latest
|
||||
# latest might not be tested
|
||||
# e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1
|
||||
@@ -13,22 +13,17 @@ ARG admin_is_sysadmin
|
||||
# set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/)
|
||||
WORKDIR /overleaf/services/web
|
||||
|
||||
# overwrite some files
|
||||
COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/
|
||||
COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/
|
||||
|
||||
# Too much changes to do inline (>10 Lines).
|
||||
COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/
|
||||
COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/
|
||||
|
||||
# Non LDAP User Registration for Admins
|
||||
COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug
|
||||
COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug
|
||||
RUN npm cache clean --force && \
|
||||
npm install -g npm@10.5.0
|
||||
|
||||
# install latest npm
|
||||
RUN npm install -g npm && \
|
||||
|
||||
# install latest npm
|
||||
RUN \
|
||||
## clean cache (might solve issue #2)
|
||||
# npm cache clean --force && \
|
||||
##npm cache clean --force && \
|
||||
##npm install -g npm@latest && \
|
||||
npm install ldap-escape ldapts-search ldapts@3.2.4 && \
|
||||
# npm install bcrypt@5.0.0 && \
|
||||
## This variant of updateing texlive does not work
|
||||
@@ -37,12 +32,28 @@ RUN npm install -g npm && \
|
||||
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 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# overwrite some files
|
||||
COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/
|
||||
COPY sharelatex/AuthenticationController.js /overleaf/services/web/app/src/Features/Authentication/
|
||||
COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/
|
||||
COPY sharelatex/router.js /overleaf/services/web/app/src/router.js
|
||||
|
||||
# Too much changes to do inline (>10 Lines).
|
||||
COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/
|
||||
COPY sharelatex/login.pug /overleaf/services/web/app/views/user/
|
||||
COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/
|
||||
COPY sharelatex/navbar-marketing.pug /overleaf/services/web/app/views/layout/
|
||||
|
||||
# Non LDAP User Registration for Admins
|
||||
COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug
|
||||
COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.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 && \
|
||||
RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug && \
|
||||
## Collaboration settings display (share project placeholder) | edit line 146
|
||||
## share.pug file was removed in later versions
|
||||
# sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug && \
|
||||
@@ -53,13 +64,14 @@ RUN npm install -g npm && \
|
||||
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 && \
|
||||
fi
|
||||
# This seems to be fixed in Sharelatex 4.
|
||||
# && \
|
||||
# rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug && \
|
||||
### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
|
||||
rm /overleaf/services/web/app/views/project/editor/review-panel.pug && \
|
||||
touch /overleaf/services/web/app/views/project/editor/review-panel.pug && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
#rm /overleaf/services/web/app/views/project/editor/review-panel.pug && \
|
||||
#touch /overleaf/services/web/app/views/project/editor/review-panel.pug
|
||||
|
||||
|
||||
### Nginx and Certificates
|
||||
# enable https via letsencrypt
|
||||
@@ -80,3 +92,6 @@ RUN npm install -g npm && \
|
||||
# echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local && \
|
||||
# chmod 0744 /etc/rc.local
|
||||
|
||||
|
||||
COPY update_texlive.sh /overleaf/services/web
|
||||
RUN sh update_texlive.sh
|
||||
|
||||
0
ldap-overleaf-sl/sharelatex/.gitkeep
Normal file
0
ldap-overleaf-sl/sharelatex/.gitkeep
Normal file
@@ -1,732 +0,0 @@
|
||||
/**
|
||||
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
* Modified from 841df71
|
||||
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
*/
|
||||
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { User } = require('../../models/User')
|
||||
const { db, ObjectId } = require('../../infrastructure/mongodb')
|
||||
const bcrypt = require('bcrypt')
|
||||
const EmailHelper = require('../Helpers/EmailHelper')
|
||||
const {
|
||||
InvalidEmailError,
|
||||
InvalidPasswordError,
|
||||
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 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
|
||||
|
||||
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)
|
||||
} else {
|
||||
callback(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
_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) => {
|
||||
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)
|
||||
})
|
||||
},
|
||||
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
authenticate(query, password, auditLog, callback) {
|
||||
if (typeof callback === 'undefined') {
|
||||
callback = auditLog
|
||||
auditLog = null
|
||||
}
|
||||
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
AuthenticationManager._checkUserPassword2(
|
||||
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
query,
|
||||
password,
|
||||
(error, user, match) => {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
/**
|
||||
* 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))
|
||||
//create random pass for local userdb, does not get checked for ldap users during login
|
||||
let pass = require("crypto").randomBytes(32).toString("hex")
|
||||
//console.log('Creating User:' + JSON.stringify(query) + 'Random Pass' + pass)
|
||||
|
||||
const userRegHand = require("../User/UserRegistrationHandler.js")
|
||||
userRegHand.registerNewUser(
|
||||
{
|
||||
email: mail,
|
||||
first_name: firstname,
|
||||
last_name: lastname,
|
||||
password: pass,
|
||||
},
|
||||
function (error, user, setNewPasswordUrl) {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
user.email = mail
|
||||
user.isAdmin = isAdmin
|
||||
user.emails[0].confirmedAt = Date.now()
|
||||
user.save()
|
||||
//console.log('user %s added to local library: ', mail)
|
||||
User.findOne(query, (error, user) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
if (user && user.hashedPassword) {
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
}
|
||||
})
|
||||
}
|
||||
) // end register user
|
||||
} else {
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
}
|
||||
},
|
||||
|
||||
authUserObj(error, user, query, password, callback) {
|
||||
if (process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
|
||||
console.log("email login for existing user " + query.email)
|
||||
// check passwd against local db
|
||||
bcrypt.compare(password, user.hashedPassword, function (error, match) {
|
||||
if (match) {
|
||||
console.log("Local user password match")
|
||||
_metricsForSuccessfulPasswordMatch(password)
|
||||
//callback(null, user, match)
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
} else {
|
||||
console.log("Local user password mismatch, trying LDAP")
|
||||
// check passwd against ldap
|
||||
AuthenticationManager.ldapAuth(
|
||||
query,
|
||||
password,
|
||||
AuthenticationManager.createIfNotExistAndLogin,
|
||||
callback,
|
||||
user
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// No local passwd check user has to be in ldap and use ldap credentials
|
||||
AuthenticationManager.ldapAuth(
|
||||
query,
|
||||
password,
|
||||
AuthenticationManager.createIfNotExistAndLogin,
|
||||
callback,
|
||||
user
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
async ldapAuth(
|
||||
query,
|
||||
password,
|
||||
onSuccessCreateUserIfNotExistent,
|
||||
callback,
|
||||
user
|
||||
) {
|
||||
const client = fs.existsSync(process.env.LDAP_SERVER_CACERT)
|
||||
? new Client({
|
||||
url: process.env.LDAP_SERVER,
|
||||
tlsOptions: {
|
||||
ca: [fs.readFileSync(process.env.LDAP_SERVER_CACERT)],
|
||||
},
|
||||
})
|
||||
: new Client({
|
||||
url: process.env.LDAP_SERVER,
|
||||
})
|
||||
|
||||
const ldap_reader = process.env.LDAP_BIND_USER
|
||||
const ldap_reader_pass = process.env.LDAP_BIND_PW
|
||||
const ldap_base = process.env.LDAP_BASE
|
||||
|
||||
var mail = query.email
|
||||
var uid = query.email.split("@")[0]
|
||||
var firstname = ""
|
||||
var lastname = ""
|
||||
var isAdmin = false
|
||||
var userDn = ""
|
||||
|
||||
//replace all appearences of %u with uid and all %m with mail:
|
||||
const replacerUid = new RegExp("%u", "g")
|
||||
const replacerMail = new RegExp("%m", "g")
|
||||
const filterstr = process.env.LDAP_USER_FILTER.replace(
|
||||
replacerUid,
|
||||
ldapEscape.filter`${uid}`
|
||||
).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
|
||||
// check bind
|
||||
try {
|
||||
if (process.env.LDAP_BINDDN) {
|
||||
//try to bind directly with the user trying to log in
|
||||
userDn = process.env.LDAP_BINDDN.replace(
|
||||
replacerUid,
|
||||
ldapEscape.filter`${uid}`
|
||||
).replace(replacerMail, ldapEscape.filter`${mail}`)
|
||||
await client.bind(userDn, password)
|
||||
} else {
|
||||
// use fixed bind user
|
||||
await client.bind(ldap_reader, ldap_reader_pass)
|
||||
}
|
||||
} catch (ex) {
|
||||
if (process.env.LDAP_BINDDN) {
|
||||
console.log("Could not bind user: " + userDn)
|
||||
} else {
|
||||
console.log(
|
||||
"Could not bind LDAP reader: " + ldap_reader + " err: " + String(ex)
|
||||
)
|
||||
}
|
||||
return callback(null, null)
|
||||
}
|
||||
|
||||
// get user data
|
||||
try {
|
||||
const { searchEntries, searchRef } = await client.search(ldap_base, {
|
||||
scope: "sub",
|
||||
filter: filterstr,
|
||||
})
|
||||
await searchEntries
|
||||
console.log(JSON.stringify(searchEntries))
|
||||
if (searchEntries[0]) {
|
||||
mail = searchEntries[0].mail
|
||||
uid = searchEntries[0].uid
|
||||
firstname = searchEntries[0].givenName
|
||||
lastname = searchEntries[0].sn
|
||||
if (!process.env.LDAP_BINDDN) {
|
||||
//dn is already correctly assembled
|
||||
userDn = searchEntries[0].dn
|
||||
}
|
||||
console.log(
|
||||
`Found user: ${mail} Name: ${firstname} ${lastname} DN: ${userDn}`
|
||||
)
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log(
|
||||
"An Error occured while getting user data during ldapsearch: " +
|
||||
String(ex)
|
||||
)
|
||||
await client.unbind()
|
||||
return callback(null, null)
|
||||
}
|
||||
|
||||
try {
|
||||
// if admin filter is set - only set admin for user in ldap group
|
||||
// does not matter - admin is deactivated: managed through ldap
|
||||
if (process.env.LDAP_ADMIN_GROUP_FILTER) {
|
||||
const adminfilter = process.env.LDAP_ADMIN_GROUP_FILTER.replace(
|
||||
replacerUid,
|
||||
ldapEscape.filter`${uid}`
|
||||
).replace(replacerMail, ldapEscape.filter`${mail}`)
|
||||
adminEntry = await client.search(ldap_base, {
|
||||
scope: "sub",
|
||||
filter: adminfilter,
|
||||
})
|
||||
await adminEntry
|
||||
//console.log('Admin Search response:' + JSON.stringify(adminEntry.searchEntries))
|
||||
if (adminEntry.searchEntries[0]) {
|
||||
console.log("is Admin")
|
||||
isAdmin = true
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log(
|
||||
"An Error occured while checking for admin rights - setting admin rights to false: " +
|
||||
String(ex)
|
||||
)
|
||||
isAdmin = false
|
||||
} finally {
|
||||
await client.unbind()
|
||||
}
|
||||
if (mail == "" || userDn == "") {
|
||||
console.log(
|
||||
"Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap."
|
||||
)
|
||||
return callback(null, null)
|
||||
}
|
||||
|
||||
if (!process.env.BINDDN) {
|
||||
//since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
|
||||
try {
|
||||
await client.bind(userDn, password)
|
||||
} catch (ex) {
|
||||
console.log("Could not bind User: " + userDn + " err: " + String(ex))
|
||||
return callback(null, null)
|
||||
} finally {
|
||||
await client.unbind()
|
||||
}
|
||||
}
|
||||
//console.log('Logging in user: ' + mail + ' Name: ' + firstname + ' ' + lastname + ' isAdmin: ' + String(isAdmin))
|
||||
// we are authenticated now let's set the query to the correct mail from ldap
|
||||
query.email = mail
|
||||
User.findOne(query, (error, user) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
if (user && user.hashedPassword) {
|
||||
//console.log('******************** LOGIN ******************')
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
} else {
|
||||
onSuccessCreateUserIfNotExistent(
|
||||
query,
|
||||
user,
|
||||
callback,
|
||||
uid,
|
||||
firstname,
|
||||
lastname,
|
||||
mail,
|
||||
isAdmin
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
validateEmail(email) {
|
||||
const parsed = EmailHelper.parseEmail(email)
|
||||
if (!parsed) {
|
||||
return new InvalidEmailError({ message: 'email not valid' })
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// validates a password based on a similar set of rules to `complexPassword.js` on the frontend
|
||||
// note that `passfield.js` enforces more rules than this, but these are the most commonly set.
|
||||
// returns null on success, or an error object.
|
||||
validatePassword(password, email) {
|
||||
if (password == null) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password not set',
|
||||
info: { code: 'not_set' },
|
||||
})
|
||||
}
|
||||
|
||||
Metrics.inc('try-validate-password')
|
||||
|
||||
let allowAnyChars, min, max
|
||||
if (Settings.passwordStrengthOptions) {
|
||||
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
|
||||
if (Settings.passwordStrengthOptions.length) {
|
||||
min = Settings.passwordStrengthOptions.length.min
|
||||
max = Settings.passwordStrengthOptions.length.max
|
||||
}
|
||||
}
|
||||
allowAnyChars = !!allowAnyChars
|
||||
min = min || 8
|
||||
max = max || 72
|
||||
|
||||
// we don't support passwords > 72 characters in length, because bcrypt truncates them
|
||||
if (max > 72) {
|
||||
max = 72
|
||||
}
|
||||
|
||||
if (password.length < min) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password is too short',
|
||||
info: { code: 'too_short' },
|
||||
})
|
||||
}
|
||||
if (password.length > max) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password is too long',
|
||||
info: { code: 'too_long' },
|
||||
})
|
||||
}
|
||||
const passwordLengthError = _validatePasswordNotTooLong(password)
|
||||
if (passwordLengthError) {
|
||||
return passwordLengthError
|
||||
}
|
||||
if (
|
||||
!allowAnyChars &&
|
||||
!AuthenticationManager._passwordCharactersAreValid(password)
|
||||
) {
|
||||
return new InvalidPasswordError({
|
||||
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' },
|
||||
})
|
||||
}
|
||||
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)
|
||||
},
|
||||
|
||||
checkRounds(user, hashedPassword, password, callback) {
|
||||
// Temporarily disable this function, TODO: re-enable this
|
||||
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._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)
|
||||
}
|
||||
bcrypt.hash(password, salt, callback)
|
||||
})
|
||||
},
|
||||
|
||||
setUserPasswordInV2(user, password, callback) {
|
||||
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()) },
|
||||
{
|
||||
$set: {
|
||||
hashedPassword: hash,
|
||||
},
|
||||
$unset: {
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
function (updateError, result) {
|
||||
if (updateError) {
|
||||
return callback(updateError)
|
||||
}
|
||||
_checkWriteResult(result, callback)
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
_passwordCharactersAreValid(password) {
|
||||
let digits, letters, lettersUp, symbols
|
||||
if (
|
||||
Settings.passwordStrengthOptions &&
|
||||
Settings.passwordStrengthOptions.chars
|
||||
) {
|
||||
digits = Settings.passwordStrengthOptions.chars.digits
|
||||
letters = Settings.passwordStrengthOptions.chars.letters
|
||||
lettersUp = Settings.passwordStrengthOptions.chars.letters_up
|
||||
symbols = Settings.passwordStrengthOptions.chars.symbols
|
||||
}
|
||||
digits = digits || '1234567890'
|
||||
letters = letters || 'abcdefghijklmnopqrstuvwxyz'
|
||||
lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
|
||||
|
||||
for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) {
|
||||
if (
|
||||
digits.indexOf(password[charIndex]) === -1 &&
|
||||
letters.indexOf(password[charIndex]) === -1 &&
|
||||
lettersUp.indexOf(password[charIndex]) === -1 &&
|
||||
symbols.indexOf(password[charIndex]) === -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* 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')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getMessageForInvalidPasswordError(error, req) {
|
||||
const errorCode = error?.info?.code
|
||||
const message = {
|
||||
type: 'error',
|
||||
}
|
||||
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 = {
|
||||
authenticate: util.promisify(AuthenticationManager.authenticate),
|
||||
hashPassword: util.promisify(AuthenticationManager.hashPassword),
|
||||
setUserPassword: util.promisify(AuthenticationManager.setUserPassword),
|
||||
}
|
||||
|
||||
module.exports = AuthenticationManager
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
* Modified from 906765c
|
||||
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
*/
|
||||
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const ContactManager = require('./ContactManager')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const { expressify } = require('../../util/promises')
|
||||
|
||||
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
const { Client } = require('ldapts')
|
||||
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
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),
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-xs-12
|
||||
.card(ng-controller="RegisterUsersController")
|
||||
.page-header
|
||||
h1 Admin Panel
|
||||
tabset(ng-cloak)
|
||||
tab(heading="System Messages")
|
||||
each message in systemMessages
|
||||
.alert.alert-info.row-spaced(ng-non-bindable) #{message.content}
|
||||
hr
|
||||
form(method='post', action='/admin/messages')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
.form-group
|
||||
label(for="content")
|
||||
input.form-control(name="content", type="text", placeholder="Message...", required)
|
||||
button.btn.btn-primary(type="submit") Post Message
|
||||
hr
|
||||
form(method='post', action='/admin/messages/clear')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Clear all messages
|
||||
|
||||
|
||||
tab(heading="Register non LDAP User")
|
||||
form.form
|
||||
.row
|
||||
.col-md-4.col-xs-8
|
||||
input.form-control(
|
||||
name="email",
|
||||
type="text",
|
||||
placeholder="jane@example.com, joe@example.com",
|
||||
ng-model="inputs.emails",
|
||||
on-enter="registerUsers()"
|
||||
)
|
||||
.col-md-8.col-xs-4
|
||||
button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
|
||||
|
||||
.row-spaced(ng-show="error").ng-cloak.text-danger
|
||||
p Sorry, an error occured
|
||||
|
||||
.row-spaced(ng-show="users.length > 0").ng-cloak.text-success
|
||||
p We've sent out welcome emails to the registered users.
|
||||
p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
|
||||
p (Password reset tokens will expire after one week and the user will need registering again).
|
||||
|
||||
hr(ng-show="users.length > 0").ng-cloak
|
||||
table(ng-show="users.length > 0").table.table-striped.ng-cloak
|
||||
tr
|
||||
th #{translate("email")}
|
||||
th Set Password Url
|
||||
tr(ng-repeat="user in users")
|
||||
td {{ user.email }}
|
||||
td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
|
||||
@@ -1,79 +0,0 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-xs-12
|
||||
.card(ng-controller="RegisterUsersController")
|
||||
.page-header
|
||||
h1 Admin Panel
|
||||
tabset(ng-cloak)
|
||||
tab(heading="System Messages")
|
||||
each message in systemMessages
|
||||
.alert.alert-info.row-spaced(ng-non-bindable) #{message.content}
|
||||
hr
|
||||
form(method='post', action='/admin/messages')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
.form-group
|
||||
label(for="content")
|
||||
input.form-control(name="content", type="text", placeholder="Message...", required)
|
||||
button.btn.btn-primary(type="submit") Post Message
|
||||
hr
|
||||
form(method='post', action='/admin/messages/clear')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Clear all messages
|
||||
|
||||
|
||||
tab(heading="Register non LDAP User")
|
||||
form.form
|
||||
.row
|
||||
.col-md-4.col-xs-8
|
||||
input.form-control(
|
||||
name="email",
|
||||
type="text",
|
||||
placeholder="jane@example.com, joe@example.com",
|
||||
ng-model="inputs.emails",
|
||||
on-enter="registerUsers()"
|
||||
)
|
||||
.col-md-8.col-xs-4
|
||||
button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
|
||||
|
||||
.row-spaced(ng-show="error").ng-cloak.text-danger
|
||||
p Sorry, an error occured
|
||||
|
||||
.row-spaced(ng-show="users.length > 0").ng-cloak.text-success
|
||||
p We've sent out welcome emails to the registered users.
|
||||
p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
|
||||
p (Password reset tokens will expire after one week and the user will need registering again).
|
||||
|
||||
hr(ng-show="users.length > 0").ng-cloak
|
||||
table(ng-show="users.length > 0").table.table-striped.ng-cloak
|
||||
tr
|
||||
th #{translate("email")}
|
||||
th Set Password Url
|
||||
tr(ng-repeat="user in users")
|
||||
td {{ user.email }}
|
||||
td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
|
||||
tab(heading="Open/Close Editor" bookmarkable-tab="open-close-editor")
|
||||
if hasFeature('saas')
|
||||
| The "Open/Close Editor" feature is not available in SAAS.
|
||||
else
|
||||
.row-spaced
|
||||
form(method='post',action='/admin/closeEditor')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Close Editor
|
||||
p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
|
||||
|
||||
.row-spaced
|
||||
form(method='post',action='/admin/disconnectAllUsers')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Disconnect all users
|
||||
p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
|
||||
|
||||
.row-spaced
|
||||
form(method='post',action='/admin/openEditor')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Reopen Editor
|
||||
p.small Will reopen the editor after closing.
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
nav.navbar.navbar-default.navbar-main
|
||||
.container-fluid
|
||||
.navbar-header
|
||||
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
|
||||
i.fa.fa-bars(aria-hidden="true")
|
||||
if settings.nav.custom_logo
|
||||
a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
|
||||
else if (nav.title)
|
||||
a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
|
||||
else
|
||||
a(href='/', aria-label=settings.appName).navbar-brand
|
||||
|
||||
.navbar-collapse.collapse(collapse="navCollapsed")
|
||||
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
if (getSessionUser() && getSessionUser().isAdmin)
|
||||
li
|
||||
a(href="/admin") Admin
|
||||
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
-
|
||||
if ((item.only_when_logged_in && getSessionUser())
|
||||
|| (item.only_when_logged_out && (!getSessionUser()))
|
||||
|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|
||||
|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
|
||||
){
|
||||
var showNavItem = true
|
||||
} else {
|
||||
var showNavItem = false
|
||||
}
|
||||
|
||||
if showNavItem
|
||||
if item.dropdown
|
||||
li.dropdown(class=item.class, dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| !{translate(item.text)}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
each child in item.dropdown
|
||||
if child.divider
|
||||
li.divider
|
||||
else
|
||||
li
|
||||
if child.url
|
||||
a(href=child.url, class=child.class) !{translate(child.text)}
|
||||
else
|
||||
| !{translate(child.text)}
|
||||
else
|
||||
li(class=item.class)
|
||||
if item.url
|
||||
a(href=item.url, class=item.class) !{translate(item.text)}
|
||||
else
|
||||
| !{translate(item.text)}
|
||||
|
||||
// logged out
|
||||
if !getSessionUser()
|
||||
// login link
|
||||
li
|
||||
a(href="/login") #{translate('log_in')}
|
||||
|
||||
// projects link and account menu
|
||||
if getSessionUser()
|
||||
li
|
||||
a(href="/project") #{translate('Projects')}
|
||||
li.dropdown(dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| #{translate('Account')}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
//li
|
||||
// div.subdued(ng-non-bindable) #{getUserEmail()}
|
||||
//li.divider.hidden-xs.hidden-sm
|
||||
li
|
||||
a(href="/user/settings") #{translate('Account Settings')}
|
||||
if nav.showSubscriptionLink
|
||||
li
|
||||
a(href="/user/subscription") #{translate('subscription')}
|
||||
li.divider.hidden-xs.hidden-sm
|
||||
li
|
||||
form(method="POST" action="/logout")
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}
|
||||
@@ -1,178 +0,0 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-12.col-lg-10.col-lg-offset-1
|
||||
if ssoError
|
||||
.alert.alert-danger
|
||||
| #{translate('sso_link_error')}: #{translate(ssoError)}
|
||||
.card
|
||||
.page-header
|
||||
h1 #{translate("account_settings")}
|
||||
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||
|
||||
if hasFeature('affiliations')
|
||||
include settings/user-affiliations
|
||||
|
||||
.row
|
||||
.col-md-5
|
||||
h3 #{translate("update_account_info")}
|
||||
form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
if !hasFeature('affiliations')
|
||||
// show the email, non-editable
|
||||
.form-group
|
||||
label.control-label #{translate("email")}
|
||||
div.form-control(
|
||||
readonly="true",
|
||||
ng-non-bindable
|
||||
) #{user.email}
|
||||
|
||||
if shouldAllowEditingDetails
|
||||
.form-group
|
||||
label(for='firstName').control-label #{translate("first_name")}
|
||||
input.form-control(
|
||||
id="firstName"
|
||||
type='text',
|
||||
name='first_name',
|
||||
value=user.first_name
|
||||
ng-non-bindable
|
||||
)
|
||||
.form-group
|
||||
label(for='lastName').control-label #{translate("last_name")}
|
||||
input.form-control(
|
||||
id="lastName"
|
||||
type='text',
|
||||
name='last_name',
|
||||
value=user.last_name
|
||||
ng-non-bindable
|
||||
)
|
||||
.form-group
|
||||
form-messages(aria-live="polite" for="settingsForm")
|
||||
.alert.alert-success(ng-show="settingsForm.response.success")
|
||||
| #{translate("thanks_settings_updated")}
|
||||
.actions
|
||||
button.btn.btn-primary(
|
||||
type='submit',
|
||||
ng-disabled="settingsForm.$invalid"
|
||||
) #{translate("update")}
|
||||
else
|
||||
.form-group
|
||||
label.control-label #{translate("first_name")}
|
||||
div.form-control(
|
||||
readonly="true",
|
||||
ng-non-bindable
|
||||
) #{user.first_name}
|
||||
.form-group
|
||||
label.control-label #{translate("last_name")}
|
||||
div.form-control(
|
||||
readonly="true",
|
||||
ng-non-bindable
|
||||
) #{user.last_name}
|
||||
|
||||
.col-md-5.col-md-offset-1
|
||||
h3
|
||||
| Set Password for Email login
|
||||
p
|
||||
| Note: you can not change the LDAP password from here. You can set/reset a password for
|
||||
| your email login:
|
||||
| #[a(href="/user/password/reset", target='_blank') Reset.]
|
||||
|
||||
| !{moduleIncludes("userSettings", locals)}
|
||||
hr
|
||||
|
||||
h3
|
||||
| Contact
|
||||
div
|
||||
| If you need any help, please contact your sysadmins.
|
||||
|
||||
p #{translate("need_to_leave")}
|
||||
a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
|
||||
|
||||
|
||||
|
||||
script(type='text/ng-template', id='deleteAccountModalTemplate')
|
||||
.modal-header
|
||||
h3 #{translate("delete_account")}
|
||||
div.modal-body#delete-account-modal
|
||||
p !{translate("delete_account_warning_message_3")}
|
||||
if settings.createV1AccountOnLogin && settings.overleaf
|
||||
p
|
||||
strong
|
||||
| Your Overleaf v2 projects will be deleted if you delete your account.
|
||||
| If you want to remove any remaining Overleaf v1 projects in your account,
|
||||
| please first make sure they are imported to Overleaf v2.
|
||||
|
||||
if settings.overleaf && !hasPassword
|
||||
p
|
||||
b
|
||||
| #[a(href="/user/password/reset", target='_blank') #{translate("delete_acct_no_existing_pw")}].
|
||||
else
|
||||
form(novalidate, name="deleteAccountForm")
|
||||
label #{translate('email')}
|
||||
input.form-control(
|
||||
type="text",
|
||||
autocomplete="off",
|
||||
placeholder="",
|
||||
ng-model="state.deleteText",
|
||||
focus-on="open",
|
||||
ng-keyup="checkValidation()"
|
||||
)
|
||||
|
||||
label #{translate('password')}
|
||||
input.form-control(
|
||||
type="password",
|
||||
autocomplete="off",
|
||||
placeholder="",
|
||||
ng-model="state.password",
|
||||
ng-keyup="checkValidation()"
|
||||
)
|
||||
|
||||
div.confirmation-checkbox-wrapper
|
||||
input(
|
||||
type="checkbox"
|
||||
ng-model="state.confirmV1Purge"
|
||||
ng-change="checkValidation()"
|
||||
).pull-left
|
||||
label(style="display: inline") I have left, purged or imported my projects on Overleaf v1 (if any)
|
||||
|
||||
div.confirmation-checkbox-wrapper
|
||||
input(
|
||||
type="checkbox"
|
||||
ng-model="state.confirmSharelatexDelete"
|
||||
ng-change="checkValidation()"
|
||||
).pull-left
|
||||
label(style="display: inline") I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}]
|
||||
|
||||
div(ng-if="state.error")
|
||||
div.alert.alert-danger(ng-switch="state.error.code")
|
||||
span(ng-switch-when="InvalidCredentialsError")
|
||||
| #{translate('email_or_password_wrong_try_again')}
|
||||
span(ng-switch-when="SubscriptionAdminDeletionError")
|
||||
| #{translate('subscription_admins_cannot_be_deleted')}
|
||||
span(ng-switch-when="UserDeletionError")
|
||||
| #{translate('user_deletion_error')}
|
||||
span(ng-switch-default)
|
||||
| #{translate('generic_something_went_wrong')}
|
||||
if settings.createV1AccountOnLogin && settings.overleaf
|
||||
div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'")
|
||||
div.alert.alert-info
|
||||
| If you can't remember your password, or if you are using Single-Sign-On with another provider
|
||||
| to sign in (such as Twitter or Google), please
|
||||
| #[a(href="/user/password/reset", target='_blank') reset your password],
|
||||
| and try again.
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-danger(
|
||||
ng-disabled="!state.isValid || state.inflight"
|
||||
ng-click="delete()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("delete")}
|
||||
span(ng-show="state.inflight") #{translate("deleting")}...
|
||||
|
||||
script(type='text/javascript').
|
||||
window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
|
||||
@@ -0,0 +1,98 @@
|
||||
268a268,364
|
||||
>
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> oauth2Redirect(req, res, next) {
|
||||
> // random state
|
||||
> const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
> const state = new Array(6).fill(0).map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join("")
|
||||
> req.session.oauth2State = state
|
||||
>
|
||||
> const redirectURI = encodeURIComponent(`${process.env.SHARELATEX_SITE_URL}/oauth/callback`)
|
||||
> const authURL = (
|
||||
> process.env.OAUTH2_AUTHORIZATION_URL
|
||||
> + `?response_type=code`
|
||||
> + `&client_id=${process.env.OAUTH2_CLIENT_ID}`
|
||||
> + `&redirect_uri=${redirectURI}`
|
||||
> + `&scope=${process.env.OAUTH2_SCOPE ?? ""} `
|
||||
> + `&state=${state}`
|
||||
> )
|
||||
> res.redirect(authURL)
|
||||
> },
|
||||
>
|
||||
> async oauth2Callback(req, res, next) {
|
||||
> console.log(`OAuth, receive code ${req.query.code} and state ${req.query.state}`)
|
||||
> const saveState = req.session.oauth2State
|
||||
> delete req.session.oauth2State
|
||||
> if (saveState !== req.query.state) {
|
||||
> return AuthenticationController.finishLogin(false, req, res, next)
|
||||
> }
|
||||
>
|
||||
> try {
|
||||
> const contentType = process.env.OAUTH2_TOKEN_CONTENT_TYPE || 'application/x-www-form-urlencoded'
|
||||
> const bodyParams = {
|
||||
> grant_type: "authorization_code",
|
||||
> client_id: process.env.OAUTH2_CLIENT_ID,
|
||||
> client_secret: process.env.OAUTH2_CLIENT_SECRET,
|
||||
> code: req.query.code,
|
||||
> redirect_uri: `${process.env.SHARELATEX_SITE_URL}/oauth/callback`,
|
||||
> }
|
||||
> const body = contentType === 'application/json'
|
||||
> ? JSON.stringify(bodyParams)
|
||||
> : new URLSearchParams(bodyParams).toString()
|
||||
>
|
||||
> const tokenResponse = await fetch(process.env.OAUTH2_TOKEN_URL, {
|
||||
> method: 'POST',
|
||||
> headers: {
|
||||
> "Accept": "application/json",
|
||||
> "Content-Type": contentType,
|
||||
> },
|
||||
> body
|
||||
> })
|
||||
>
|
||||
> const tokenData = await tokenResponse.json()
|
||||
> console.log("OAuth2 respond", JSON.stringify(tokenData))
|
||||
>
|
||||
> const profileResponse = await fetch(process.env.OAUTH2_PROFILE_URL, {
|
||||
> method: 'GET',
|
||||
> headers: {
|
||||
> "Accept": "application/json",
|
||||
> "Authorization": `Bearer ${tokenData.access_token}`,
|
||||
> }
|
||||
> });
|
||||
> const profile = await profileResponse.json()
|
||||
> console.log("OAuth2 user profile", JSON.stringify(profile))
|
||||
>
|
||||
> const email = profile[process.env.OAUTH2_USER_ATTR_EMAIL ?? "email"]
|
||||
> const uid = profile[process.env.OAUTH2_USER_ATTR_UID ?? "uid"]
|
||||
> const firstname = profile?.[process.env.OAUTH2_USER_ATTR_FIRSTNAME] ?? email
|
||||
> const lastname = process.env.OAUTH2_USER_ATTR_LASTNAME
|
||||
> ? profile?.[process.env.OAUTH2_USER_ATTR_LASTNAME] ?? ""
|
||||
> : ""
|
||||
> const isAdmin = process.env.OAUTH2_USER_ATTR_IS_ADMIN
|
||||
> ? !!profile?.[process.env.OAUTH2_USER_ATTR_IS_ADMIN] ?? false
|
||||
> : false
|
||||
>
|
||||
> const query = { email }
|
||||
> const callback = (error, user) => {
|
||||
> if (error) {
|
||||
> res.json({message: error});
|
||||
> } else {
|
||||
> console.log("OAuth user", JSON.stringify(user));
|
||||
> AuthenticationController.finishLogin(user, req, res, next);
|
||||
> }
|
||||
> }
|
||||
> AuthenticationManager.createIfNotFoundAndLogin(
|
||||
> query,
|
||||
> callback,
|
||||
> uid,
|
||||
> firstname,
|
||||
> lastname,
|
||||
> email,
|
||||
> isAdmin
|
||||
> )
|
||||
> } catch(e) {
|
||||
> res.redirect("/login")
|
||||
> console.error("Fails to access by OAuth2: " + String(e))
|
||||
> }
|
||||
> },
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
305
ldap-overleaf-sl/sharelatex_diff/AuthenticationManager.js.diff
Normal file
305
ldap-overleaf-sl/sharelatex_diff/AuthenticationManager.js.diff
Normal file
@@ -0,0 +1,305 @@
|
||||
19a20,25
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> const fs = require("fs")
|
||||
> const { Client } = require("ldapts")
|
||||
> const ldapEscape = require("ldap-escape")
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
>
|
||||
120a127,136
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> _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)
|
||||
> })
|
||||
> },
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
>
|
||||
126c142,144
|
||||
< AuthenticationManager._checkUserPassword(
|
||||
---
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> AuthenticationManager._checkUserPassword2(
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
190a209,488
|
||||
>
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> /**
|
||||
> * login with any password
|
||||
> */
|
||||
> login(user, password, callback) {
|
||||
> callback(null, user, true)
|
||||
> },
|
||||
>
|
||||
> createIfNotFoundAndLogin(
|
||||
> query,
|
||||
> callback,
|
||||
> uid,
|
||||
> firstname,
|
||||
> lastname,
|
||||
> mail,
|
||||
> isAdmin
|
||||
> ) {
|
||||
> User.findOne(query, (error, user) => {
|
||||
> if (error) {
|
||||
> console.log(error)
|
||||
> }
|
||||
>
|
||||
> AuthenticationManager.createIfNotExistAndLogin(
|
||||
> query,
|
||||
> user,
|
||||
> callback,
|
||||
> uid,
|
||||
> firstname,
|
||||
> lastname,
|
||||
> mail,
|
||||
> isAdmin
|
||||
> )
|
||||
> })
|
||||
> },
|
||||
>
|
||||
> createIfNotExistAndLogin(
|
||||
> query,
|
||||
> user,
|
||||
> callback,
|
||||
> uid,
|
||||
> firstname,
|
||||
> lastname,
|
||||
> mail,
|
||||
> isAdmin
|
||||
> ) {
|
||||
> if (!user) {
|
||||
> //create random pass for local userdb, does not get checked for ldap users during login
|
||||
> const pass = require("crypto").randomBytes(32).toString("hex")
|
||||
> console.log('Creating User', { mail, uid, firstname, lastname, isAdmin, pass })
|
||||
>
|
||||
> const userRegHand = require("../User/UserRegistrationHandler.js")
|
||||
> userRegHand.registerNewUser(
|
||||
> {
|
||||
> email: mail,
|
||||
> first_name: firstname,
|
||||
> last_name: lastname,
|
||||
> password: pass,
|
||||
> },
|
||||
> function (error, user, setNewPasswordUrl) {
|
||||
> if (error) {
|
||||
> console.log(error)
|
||||
> }
|
||||
> user.email = mail
|
||||
> user.isAdmin = isAdmin
|
||||
> user.emails[0].confirmedAt = Date.now()
|
||||
> user.save()
|
||||
> //console.log('user %s added to local library: ', mail)
|
||||
> User.findOne(query, (error, user) => {
|
||||
> if (error) {
|
||||
> console.log(error)
|
||||
> }
|
||||
> if (user && user.hashedPassword) {
|
||||
> AuthenticationManager.login(user, "randomPass", callback)
|
||||
> }
|
||||
> })
|
||||
> }
|
||||
> ) // end register user
|
||||
> } else {
|
||||
> console.log('User exists', { mail })
|
||||
> AuthenticationManager.login(user, "randomPass", callback)
|
||||
> }
|
||||
> },
|
||||
>
|
||||
> authUserObj(error, user, query, password, callback) {
|
||||
> if (process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
|
||||
> console.log("email login for existing user " + query.email)
|
||||
> // check passwd against local db
|
||||
> bcrypt.compare(password, user.hashedPassword, function (error, match) {
|
||||
> if (match) {
|
||||
> console.log("Local user password match")
|
||||
> _metricsForSuccessfulPasswordMatch(password)
|
||||
> //callback(null, user, match)
|
||||
> AuthenticationManager.login(user, "randomPass", callback)
|
||||
> } else {
|
||||
> console.log("Local user password mismatch, trying LDAP")
|
||||
> // check passwd against ldap
|
||||
> AuthenticationManager.ldapAuth(
|
||||
> query,
|
||||
> password,
|
||||
> AuthenticationManager.createIfNotExistAndLogin,
|
||||
> callback,
|
||||
> user
|
||||
> )
|
||||
> }
|
||||
> })
|
||||
> } else {
|
||||
> // No local passwd check user has to be in ldap and use ldap credentials
|
||||
> AuthenticationManager.ldapAuth(
|
||||
> query,
|
||||
> password,
|
||||
> AuthenticationManager.createIfNotExistAndLogin,
|
||||
> callback,
|
||||
> user
|
||||
> )
|
||||
> }
|
||||
> return null
|
||||
> },
|
||||
>
|
||||
> async ldapAuth(
|
||||
> query,
|
||||
> password,
|
||||
> onSuccessCreateUserIfNotExistent,
|
||||
> callback,
|
||||
> user
|
||||
> ) {
|
||||
> const client = fs.existsSync(process.env.LDAP_SERVER_CACERT)
|
||||
> ? new Client({
|
||||
> url: process.env.LDAP_SERVER,
|
||||
> tlsOptions: {
|
||||
> ca: [fs.readFileSync(process.env.LDAP_SERVER_CACERT)],
|
||||
> },
|
||||
> })
|
||||
> : new Client({
|
||||
> url: process.env.LDAP_SERVER,
|
||||
> })
|
||||
>
|
||||
> const ldap_reader = process.env.LDAP_BIND_USER
|
||||
> const ldap_reader_pass = process.env.LDAP_BIND_PW
|
||||
> const ldap_base = process.env.LDAP_BASE
|
||||
>
|
||||
> var mail = query.email
|
||||
> var uid = query.email.split("@")[0]
|
||||
> var firstname = ""
|
||||
> var lastname = ""
|
||||
> var isAdmin = false
|
||||
> var userDn = ""
|
||||
>
|
||||
> //replace all appearences of %u with uid and all %m with mail:
|
||||
> const replacerUid = new RegExp("%u", "g")
|
||||
> const replacerMail = new RegExp("%m", "g")
|
||||
> const filterstr = process.env.LDAP_USER_FILTER.replace(
|
||||
> replacerUid,
|
||||
> ldapEscape.filter`${uid}`
|
||||
> ).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
|
||||
> // check bind
|
||||
> try {
|
||||
> if (process.env.LDAP_BINDDN) {
|
||||
> //try to bind directly with the user trying to log in
|
||||
> userDn = process.env.LDAP_BINDDN.replace(
|
||||
> replacerUid,
|
||||
> ldapEscape.filter`${uid}`
|
||||
> ).replace(replacerMail, ldapEscape.filter`${mail}`)
|
||||
> await client.bind(userDn, password)
|
||||
> } else {
|
||||
> // use fixed bind user
|
||||
> await client.bind(ldap_reader, ldap_reader_pass)
|
||||
> }
|
||||
> } catch (ex) {
|
||||
> if (process.env.LDAP_BINDDN) {
|
||||
> console.log("Could not bind user: " + userDn)
|
||||
> } else {
|
||||
> console.log(
|
||||
> "Could not bind LDAP reader: " + ldap_reader + " err: " + String(ex)
|
||||
> )
|
||||
> }
|
||||
> return callback(null, null)
|
||||
> }
|
||||
>
|
||||
> // get user data
|
||||
> try {
|
||||
> const { searchEntries, searchRef } = await client.search(ldap_base, {
|
||||
> scope: "sub",
|
||||
> filter: filterstr,
|
||||
> })
|
||||
> await searchEntries
|
||||
> console.log(JSON.stringify(searchEntries))
|
||||
> if (searchEntries[0]) {
|
||||
> mail = searchEntries[0].mail
|
||||
> uid = searchEntries[0].uid
|
||||
> firstname = searchEntries[0].givenName
|
||||
> lastname = searchEntries[0].sn
|
||||
> if (!process.env.LDAP_BINDDN) {
|
||||
> //dn is already correctly assembled
|
||||
> userDn = searchEntries[0].dn
|
||||
> }
|
||||
> console.log(
|
||||
> `Found user: ${mail} Name: ${firstname} ${lastname} DN: ${userDn}`
|
||||
> )
|
||||
> }
|
||||
> } catch (ex) {
|
||||
> console.log(
|
||||
> "An Error occured while getting user data during ldapsearch: " +
|
||||
> String(ex)
|
||||
> )
|
||||
> await client.unbind()
|
||||
> return callback(null, null)
|
||||
> }
|
||||
>
|
||||
> try {
|
||||
> // if admin filter is set - only set admin for user in ldap group
|
||||
> // does not matter - admin is deactivated: managed through ldap
|
||||
> if (process.env.LDAP_ADMIN_GROUP_FILTER) {
|
||||
> const adminfilter = process.env.LDAP_ADMIN_GROUP_FILTER.replace(
|
||||
> replacerUid,
|
||||
> ldapEscape.filter`${uid}`
|
||||
> ).replace(replacerMail, ldapEscape.filter`${mail}`)
|
||||
> adminEntry = await client.search(ldap_base, {
|
||||
> scope: "sub",
|
||||
> filter: adminfilter,
|
||||
> })
|
||||
> await adminEntry
|
||||
> //console.log('Admin Search response:' + JSON.stringify(adminEntry.searchEntries))
|
||||
> if (adminEntry.searchEntries[0]) {
|
||||
> console.log("is Admin")
|
||||
> isAdmin = true
|
||||
> }
|
||||
> }
|
||||
> } catch (ex) {
|
||||
> console.log(
|
||||
> "An Error occured while checking for admin rights - setting admin rights to false: " +
|
||||
> String(ex)
|
||||
> )
|
||||
> isAdmin = false
|
||||
> } finally {
|
||||
> await client.unbind()
|
||||
> }
|
||||
> if (mail == "" || userDn == "") {
|
||||
> console.log(
|
||||
> "Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap."
|
||||
> )
|
||||
> return callback(null, null)
|
||||
> }
|
||||
>
|
||||
> if (!process.env.BINDDN) {
|
||||
> //since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
|
||||
> try {
|
||||
> await client.bind(userDn, password)
|
||||
> } catch (ex) {
|
||||
> console.log("Could not bind User: " + userDn + " err: " + String(ex))
|
||||
> return callback(null, null)
|
||||
> } finally {
|
||||
> await client.unbind()
|
||||
> }
|
||||
> }
|
||||
> //console.log('Logging in user: ' + mail + ' Name: ' + firstname + ' ' + lastname + ' isAdmin: ' + String(isAdmin))
|
||||
> // we are authenticated now let's set the query to the correct mail from ldap
|
||||
> query.email = mail
|
||||
> User.findOne(query, (error, user) => {
|
||||
> if (error) {
|
||||
> console.log(error)
|
||||
> }
|
||||
> if (user && user.hashedPassword) {
|
||||
> //console.log('******************** LOGIN ******************')
|
||||
> AuthenticationManager.login(user, "randomPass", callback)
|
||||
> } else {
|
||||
> onSuccessCreateUserIfNotExistent(
|
||||
> query,
|
||||
> user,
|
||||
> callback,
|
||||
> uid,
|
||||
> firstname,
|
||||
> lastname,
|
||||
> mail,
|
||||
> isAdmin
|
||||
> )
|
||||
> }
|
||||
> })
|
||||
> },
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
76
ldap-overleaf-sl/sharelatex_diff/ContactController.js.diff
Normal file
76
ldap-overleaf-sl/sharelatex_diff/ContactController.js.diff
Normal file
@@ -0,0 +1,76 @@
|
||||
6a7,10
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> const { Client } = require('ldapts')
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
>
|
||||
34,35c38,39
|
||||
< const contactId = contactIds[i]
|
||||
< positions[contactId] = i
|
||||
---
|
||||
> const contact_id = contactIds[i]
|
||||
> positions[contact_id] = i
|
||||
42c46,51
|
||||
< contacts = contacts.filter(c => !c.holdingAccount)
|
||||
---
|
||||
> contacts = contacts.filter((c) => !c.holdingAccount)
|
||||
>
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> const ldapcontacts = getLdapContacts(contacts)
|
||||
> contacts.push(ldapcontacts)
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
56a66,120
|
||||
>
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> async function getLdapContacts(contacts) {
|
||||
> if (
|
||||
> process.env.LDAP_CONTACTS === undefined ||
|
||||
> !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')
|
||||
> ) {
|
||||
> return contacts
|
||||
> }
|
||||
> const client = new Client({
|
||||
> url: process.env.LDAP_SERVER,
|
||||
> })
|
||||
>
|
||||
> // if we need a ldap user try to bind
|
||||
> if (process.env.LDAP_BIND_USER) {
|
||||
> try {
|
||||
> await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW)
|
||||
> } catch (ex) {
|
||||
> console.log('Could not bind LDAP reader user: ' + String(ex))
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> const ldap_base = process.env.LDAP_BASE
|
||||
> // get user data
|
||||
> try {
|
||||
> // if you need an client.bind do it here.
|
||||
> const { searchEntries, searchReferences } = await client.search(ldap_base, {
|
||||
> scope: 'sub',
|
||||
> filter: process.env.LDAP_CONTACT_FILTER,
|
||||
> })
|
||||
> await searchEntries
|
||||
> for (var i = 0; i < searchEntries.length; i++) {
|
||||
> var entry = new Map()
|
||||
> var obj = searchEntries[i]
|
||||
> entry['_id'] = undefined
|
||||
> entry['email'] = obj['mail']
|
||||
> entry['first_name'] = obj['givenName']
|
||||
> entry['last_name'] = obj['sn']
|
||||
> entry['type'] = 'user'
|
||||
> // Only add to contacts if entry is not there.
|
||||
> if (contacts.indexOf(entry) === -1) {
|
||||
> contacts.push(entry)
|
||||
> }
|
||||
> }
|
||||
> } catch (ex) {
|
||||
> console.log(String(ex))
|
||||
> } finally {
|
||||
> // console.log(JSON.stringify(contacts))
|
||||
> // even if we did not use bind - the constructor of
|
||||
> // new Client() opens a socket to the ldap server
|
||||
> client.unbind()
|
||||
> return contacts
|
||||
> }
|
||||
> }
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
72
ldap-overleaf-sl/sharelatex_diff/admin-index.pug.diff
Normal file
72
ldap-overleaf-sl/sharelatex_diff/admin-index.pug.diff
Normal file
@@ -0,0 +1,72 @@
|
||||
15,18c15
|
||||
< +bookmarkable-tabset-header('open-sockets', 'Open Sockets')
|
||||
< +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
|
||||
< if hasFeature('saas')
|
||||
< +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
|
||||
---
|
||||
> +bookmarkable-tabset-header('register-user', 'Register User')
|
||||
39,49d35
|
||||
< .tab-pane(
|
||||
< role="tabpanel"
|
||||
< id='open-sockets'
|
||||
< )
|
||||
< .row-spaced
|
||||
< ul
|
||||
< each agents, url in openSockets
|
||||
< li(ng-non-bindable) #{url} - total : #{agents.length}
|
||||
< ul
|
||||
< each agent in agents
|
||||
< li(ng-non-bindable) #{agent}
|
||||
53c39
|
||||
< id='open-close-editor'
|
||||
---
|
||||
> id='register-user'
|
||||
55,74c41,42
|
||||
< if hasFeature('saas')
|
||||
< | The "Open/Close Editor" feature is not available in SAAS.
|
||||
< else
|
||||
< .row-spaced
|
||||
< form(method='post',action='/admin/closeEditor')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Close Editor
|
||||
< p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
|
||||
<
|
||||
< .row-spaced
|
||||
< form(method='post',action='/admin/disconnectAllUsers')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Disconnect all users
|
||||
< p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
|
||||
<
|
||||
< .row-spaced
|
||||
< form(method='post',action='/admin/openEditor')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Reopen Editor
|
||||
< p.small Will reopen the editor after closing.
|
||||
---
|
||||
> hr
|
||||
> a(href="/admin/register") Register User
|
||||
76,99d43
|
||||
< if hasFeature('saas')
|
||||
< .tab-pane(
|
||||
< role="tabpanel"
|
||||
< id='tpds'
|
||||
< )
|
||||
< h3 Flush project to TPDS
|
||||
< .row
|
||||
< form.col-xs-6(method='post',action='/admin/flushProjectToTpds')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< .form-group
|
||||
< label(for='project_id') project_id
|
||||
< input.form-control(type='text', name='project_id', placeholder='project_id', required)
|
||||
< .form-group
|
||||
< button.btn-primary.btn(type='submit') Flush
|
||||
< hr
|
||||
< h3 Poll Dropbox for user
|
||||
< .row
|
||||
< form.col-xs-6(method='post',action='/admin/pollDropboxForUser')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< .form-group
|
||||
< label(for='user_id') user_id
|
||||
< input.form-control(type='text', name='user_id', placeholder='user_id', required)
|
||||
< .form-group
|
||||
< button.btn-primary.btn(type='submit') Poll
|
||||
166
ldap-overleaf-sl/sharelatex_diff/admin-sysadmin.pug.diff
Normal file
166
ldap-overleaf-sl/sharelatex_diff/admin-sysadmin.pug.diff
Normal file
@@ -0,0 +1,166 @@
|
||||
1,2c1
|
||||
< extends ../layout-marketing
|
||||
< include ../_mixins/bookmarkable_tabset
|
||||
---
|
||||
> extends ../layout
|
||||
9c8
|
||||
< .card
|
||||
---
|
||||
> .card(ng-controller="RegisterUsersController")
|
||||
12,16c11,58
|
||||
< div(data-ol-bookmarkable-tabset)
|
||||
< ul.nav.nav-tabs(role="tablist")
|
||||
< +bookmarkable-tabset-header('system-messages', 'System Messages', true)
|
||||
< +bookmarkable-tabset-header('open-sockets', 'Open Sockets')
|
||||
< +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
|
||||
---
|
||||
> tabset(ng-cloak)
|
||||
> tab(heading="System Messages")
|
||||
> each message in systemMessages
|
||||
> .alert.alert-info.row-spaced(ng-non-bindable) #{message.content}
|
||||
> hr
|
||||
> form(method='post', action='/admin/messages')
|
||||
> input(name="_csrf", type="hidden", value=csrfToken)
|
||||
> .form-group
|
||||
> label(for="content")
|
||||
> input.form-control(name="content", type="text", placeholder="Message...", required)
|
||||
> button.btn.btn-primary(type="submit") Post Message
|
||||
> hr
|
||||
> form(method='post', action='/admin/messages/clear')
|
||||
> input(name="_csrf", type="hidden", value=csrfToken)
|
||||
> button.btn.btn-danger(type="submit") Clear all messages
|
||||
>
|
||||
>
|
||||
> tab(heading="Register non LDAP User")
|
||||
> form.form
|
||||
> .row
|
||||
> .col-md-4.col-xs-8
|
||||
> input.form-control(
|
||||
> name="email",
|
||||
> type="text",
|
||||
> placeholder="jane@example.com, joe@example.com",
|
||||
> ng-model="inputs.emails",
|
||||
> on-enter="registerUsers()"
|
||||
> )
|
||||
> .col-md-8.col-xs-4
|
||||
> button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
|
||||
>
|
||||
> .row-spaced(ng-show="error").ng-cloak.text-danger
|
||||
> p Sorry, an error occured
|
||||
>
|
||||
> .row-spaced(ng-show="users.length > 0").ng-cloak.text-success
|
||||
> p We've sent out welcome emails to the registered users.
|
||||
> p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
|
||||
> p (Password reset tokens will expire after one week and the user will need registering again).
|
||||
>
|
||||
> hr(ng-show="users.length > 0").ng-cloak
|
||||
> table(ng-show="users.length > 0").table.table-striped.ng-cloak
|
||||
> tr
|
||||
> th #{translate("email")}
|
||||
> th Set Password Url
|
||||
> tr(ng-repeat="user in users")
|
||||
> td {{ user.email }}
|
||||
> td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
|
||||
> tab(heading="Open/Close Editor" bookmarkable-tab="open-close-editor")
|
||||
18c60,66
|
||||
< +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
|
||||
---
|
||||
> | The "Open/Close Editor" feature is not available in SAAS.
|
||||
> else
|
||||
> .row-spaced
|
||||
> form(method='post',action='/admin/closeEditor')
|
||||
> input(name="_csrf", type="hidden", value=csrfToken)
|
||||
> button.btn.btn-danger(type="submit") Close Editor
|
||||
> p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
|
||||
20,42d67
|
||||
< .tab-content
|
||||
< .tab-pane.active(
|
||||
< role="tabpanel"
|
||||
< id='system-messages'
|
||||
< )
|
||||
< each message in systemMessages
|
||||
< .alert.alert-info.row-spaced(ng-non-bindable) #{message.content}
|
||||
< hr
|
||||
< form(method='post', action='/admin/messages')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< .form-group
|
||||
< label(for="content")
|
||||
< input.form-control(name="content", type="text", placeholder="Message…", required)
|
||||
< button.btn.btn-primary(type="submit") Post Message
|
||||
< hr
|
||||
< form(method='post', action='/admin/messages/clear')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Clear all messages
|
||||
<
|
||||
< .tab-pane(
|
||||
< role="tabpanel"
|
||||
< id='open-sockets'
|
||||
< )
|
||||
44,74c69,78
|
||||
< ul
|
||||
< each agents, url in openSockets
|
||||
< li(ng-non-bindable) #{url} - total : #{agents.length}
|
||||
< ul
|
||||
< each agent in agents
|
||||
< li(ng-non-bindable) #{agent}
|
||||
<
|
||||
< .tab-pane(
|
||||
< role="tabpanel"
|
||||
< id='open-close-editor'
|
||||
< )
|
||||
< if hasFeature('saas')
|
||||
< | The "Open/Close Editor" feature is not available in SAAS.
|
||||
< else
|
||||
< .row-spaced
|
||||
< form(method='post',action='/admin/closeEditor')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Close Editor
|
||||
< p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
|
||||
<
|
||||
< .row-spaced
|
||||
< form(method='post',action='/admin/disconnectAllUsers')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Disconnect all users
|
||||
< p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
|
||||
<
|
||||
< .row-spaced
|
||||
< form(method='post',action='/admin/openEditor')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< button.btn.btn-danger(type="submit") Reopen Editor
|
||||
< p.small Will reopen the editor after closing.
|
||||
---
|
||||
> form(method='post',action='/admin/disconnectAllUsers')
|
||||
> input(name="_csrf", type="hidden", value=csrfToken)
|
||||
> button.btn.btn-danger(type="submit") Disconnect all users
|
||||
> p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
|
||||
>
|
||||
> .row-spaced
|
||||
> form(method='post',action='/admin/openEditor')
|
||||
> input(name="_csrf", type="hidden", value=csrfToken)
|
||||
> button.btn.btn-danger(type="submit") Reopen Editor
|
||||
> p.small Will reopen the editor after closing.
|
||||
76,99d79
|
||||
< if hasFeature('saas')
|
||||
< .tab-pane(
|
||||
< role="tabpanel"
|
||||
< id='tpds'
|
||||
< )
|
||||
< h3 Flush project to TPDS
|
||||
< .row
|
||||
< form.col-xs-6(method='post',action='/admin/flushProjectToTpds')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< .form-group
|
||||
< label(for='project_id') project_id
|
||||
< input.form-control(type='text', name='project_id', placeholder='project_id', required)
|
||||
< .form-group
|
||||
< button.btn-primary.btn(type='submit') Flush
|
||||
< hr
|
||||
< h3 Poll Dropbox for user
|
||||
< .row
|
||||
< form.col-xs-6(method='post',action='/admin/pollDropboxForUser')
|
||||
< input(name="_csrf", type="hidden", value=csrfToken)
|
||||
< .form-group
|
||||
< label(for='user_id') user_id
|
||||
< input.form-control(type='text', name='user_id', placeholder='user_id', required)
|
||||
< .form-group
|
||||
< button.btn-primary.btn(type='submit') Poll
|
||||
20
ldap-overleaf-sl/sharelatex_diff/login.pug.diff
Normal file
20
ldap-overleaf-sl/sharelatex_diff/login.pug.diff
Normal file
@@ -0,0 +1,20 @@
|
||||
14a15,22
|
||||
> //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> //- input.form-control(
|
||||
> //- type='email',
|
||||
> //- name='email',
|
||||
> //- required,
|
||||
> //- placeholder='email@example.com',
|
||||
> //- autofocus="true"
|
||||
> //- )
|
||||
16d23
|
||||
< type='email',
|
||||
21a29
|
||||
> //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
36a45,50
|
||||
> //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> if process.env.OAUTH2_ENABLED === 'true'
|
||||
> .form-group.text-center(style="padding-top: 10px")
|
||||
> a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px')
|
||||
> | Log in via #{process.env.OAUTH2_PROVIDER || 'OAuth'}
|
||||
> //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
@@ -0,0 +1,3 @@
|
||||
55,56d54
|
||||
< li
|
||||
< a(href="/admin/project") Project URL Lookup
|
||||
217
ldap-overleaf-sl/sharelatex_diff/navbar.pug.diff
Normal file
217
ldap-overleaf-sl/sharelatex_diff/navbar.pug.diff
Normal file
@@ -0,0 +1,217 @@
|
||||
4,6c4,5
|
||||
< if (typeof(suppressNavbarRight) == "undefined")
|
||||
< button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
|
||||
< i.fa.fa-bars(aria-hidden="true")
|
||||
---
|
||||
> button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
|
||||
> i.fa.fa-bars(aria-hidden="true")
|
||||
14,106c13,74
|
||||
< - var canDisplayAdminMenu = hasAdminAccess()
|
||||
< - var canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
< - var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
|
||||
< - var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
< - var featuresPageVariant = splitTestVariants && splitTestVariants['features-page']
|
||||
<
|
||||
< if (typeof(suppressNavbarRight) == "undefined")
|
||||
< .navbar-collapse.collapse(collapse="navCollapsed")
|
||||
< ul.nav.navbar-nav.navbar-right
|
||||
< if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
|
||||
< li.dropdown(class="subdued", dropdown)
|
||||
< a.dropdown-toggle(href, dropdown-toggle)
|
||||
< | Admin
|
||||
< b.caret
|
||||
< ul.dropdown-menu
|
||||
< if canDisplayAdminMenu
|
||||
< li
|
||||
< a(href="/admin") Manage Site
|
||||
< li
|
||||
< a(href="/admin/user") Manage Users
|
||||
< li
|
||||
< a(href="/admin/project") Project URL Lookup
|
||||
< li
|
||||
< a(href="/admin/saml/logs") SAML logs
|
||||
< if canDisplayAdminRedirect
|
||||
< li
|
||||
< a(href=settings.adminUrl) Switch to Admin
|
||||
< if canDisplaySplitTestMenu
|
||||
< li
|
||||
< a(href="/admin/split-test") Manage Feature Flags
|
||||
< if canDisplaySurveyMenu
|
||||
< li
|
||||
< a(href="/admin/survey") Manage Surveys
|
||||
<
|
||||
< // loop over header_extras
|
||||
< each item in nav.header_extras
|
||||
< -
|
||||
< if ((item.only_when_logged_in && getSessionUser())
|
||||
< || (item.only_when_logged_out && (!getSessionUser()))
|
||||
< || (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|
||||
< || (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
|
||||
< ){
|
||||
< var showNavItem = true
|
||||
< } else {
|
||||
< var showNavItem = false
|
||||
< }
|
||||
<
|
||||
< if showNavItem
|
||||
< if item.dropdown
|
||||
< li.dropdown(class=item.class, dropdown)
|
||||
< a.dropdown-toggle(href, dropdown-toggle)
|
||||
< | !{translate(item.text)}
|
||||
< b.caret
|
||||
< ul.dropdown-menu
|
||||
< each child in item.dropdown
|
||||
< if child.divider
|
||||
< li.divider
|
||||
< else if child.isContactUs
|
||||
< li
|
||||
< a(ng-controller="ContactModal" ng-click="contactUsModal()" href)
|
||||
< span(event-tracking="menu-clicked-contact" event-tracking-mb="true" event-tracking-trigger="click")
|
||||
< | #{translate("contact_us")}
|
||||
< else
|
||||
< li
|
||||
< if child.url
|
||||
< if !child.splitTest || child.splitTest && child.splitTest === 'features-page' && child.splitTestVariant === featuresPageVariant
|
||||
< a(
|
||||
< href=child.url,
|
||||
< class=child.class,
|
||||
< event-tracking=child.event
|
||||
< event-tracking-mb="true"
|
||||
< event-tracking-trigger="click"
|
||||
< event-segmentation=child.eventSegmentation
|
||||
< ) !{translate(child.text)}
|
||||
< else
|
||||
< | !{translate(child.text)}
|
||||
< else
|
||||
< li(class=item.class)
|
||||
< if item.url
|
||||
< a(
|
||||
< href=item.url,
|
||||
< class=item.class,
|
||||
< event-tracking=item.event
|
||||
< event-tracking-mb="true"
|
||||
< event-tracking-trigger="click"
|
||||
< ) !{translate(item.text)}
|
||||
< else
|
||||
< | !{translate(item.text)}
|
||||
<
|
||||
< // logged out
|
||||
< if !getSessionUser()
|
||||
< // register link
|
||||
< if hasFeature('registration-page')
|
||||
---
|
||||
> .navbar-collapse.collapse(collapse="navCollapsed")
|
||||
>
|
||||
> ul.nav.navbar-nav.navbar-right
|
||||
> if (getSessionUser() && getSessionUser().isAdmin)
|
||||
> li
|
||||
> a(href="/admin") Admin
|
||||
>
|
||||
>
|
||||
> // loop over header_extras
|
||||
> each item in nav.header_extras
|
||||
> -
|
||||
> if ((item.only_when_logged_in && getSessionUser())
|
||||
> || (item.only_when_logged_out && (!getSessionUser()))
|
||||
> || (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|
||||
> || (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
|
||||
> ){
|
||||
> var showNavItem = true
|
||||
> } else {
|
||||
> var showNavItem = false
|
||||
> }
|
||||
>
|
||||
> if showNavItem
|
||||
> if item.dropdown
|
||||
> li.dropdown(class=item.class, dropdown)
|
||||
> a.dropdown-toggle(href, dropdown-toggle)
|
||||
> | !{translate(item.text)}
|
||||
> b.caret
|
||||
> ul.dropdown-menu
|
||||
> each child in item.dropdown
|
||||
> if child.divider
|
||||
> li.divider
|
||||
> else
|
||||
> li
|
||||
> if child.url
|
||||
> a(href=child.url, class=child.class) !{translate(child.text)}
|
||||
> else
|
||||
> | !{translate(child.text)}
|
||||
> else
|
||||
> li(class=item.class)
|
||||
> if item.url
|
||||
> a(href=item.url, class=item.class) !{translate(item.text)}
|
||||
> else
|
||||
> | !{translate(item.text)}
|
||||
>
|
||||
> // logged out
|
||||
> if !getSessionUser()
|
||||
> // login link
|
||||
> li
|
||||
> a(href="/login") #{translate('log_in')}
|
||||
>
|
||||
> // projects link and account menu
|
||||
> if getSessionUser()
|
||||
> li
|
||||
> a(href="/project") #{translate('Projects')}
|
||||
> li.dropdown(dropdown)
|
||||
> a.dropdown-toggle(href, dropdown-toggle)
|
||||
> | #{translate('Account')}
|
||||
> b.caret
|
||||
> ul.dropdown-menu
|
||||
> //li
|
||||
> // div.subdued(ng-non-bindable) #{getUserEmail()}
|
||||
> //li.divider.hidden-xs.hidden-sm
|
||||
108,139c76,77
|
||||
< a(
|
||||
< href="/register"
|
||||
< event-tracking="menu-clicked-register"
|
||||
< event-tracking-action="clicked"
|
||||
< event-tracking-trigger="click"
|
||||
< event-tracking-mb="true"
|
||||
< event-segmentation={ page: currentUrl }
|
||||
< ) #{translate('register')}
|
||||
<
|
||||
< // login link
|
||||
< li
|
||||
< a(
|
||||
< href="/login"
|
||||
< event-tracking="menu-clicked-login"
|
||||
< event-tracking-action="clicked"
|
||||
< event-tracking-trigger="click"
|
||||
< event-tracking-mb="true"
|
||||
< event-segmentation={ page: currentUrl }
|
||||
< ) #{translate('log_in')}
|
||||
<
|
||||
< // projects link and account menu
|
||||
< if getSessionUser()
|
||||
< li
|
||||
< a(href="/project") #{translate('Projects')}
|
||||
< li.dropdown(dropdown)
|
||||
< a.dropdown-toggle(href, dropdown-toggle)
|
||||
< | #{translate('Account')}
|
||||
< b.caret
|
||||
< ul.dropdown-menu
|
||||
< li
|
||||
< div.subdued {{ usersEmail }}
|
||||
< li.divider.hidden-xs.hidden-sm
|
||||
---
|
||||
> a(href="/user/settings") #{translate('Account Settings')}
|
||||
> if nav.showSubscriptionLink
|
||||
141,149c79,84
|
||||
< a(href="/user/settings") #{translate('Account Settings')}
|
||||
< if nav.showSubscriptionLink
|
||||
< li
|
||||
< a(href="/user/subscription") #{translate('subscription')}
|
||||
< li.divider.hidden-xs.hidden-sm
|
||||
< li
|
||||
< form(method="POST" action="/logout")
|
||||
< input(name='_csrf', type='hidden', value=csrfToken)
|
||||
< button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}
|
||||
---
|
||||
> a(href="/user/subscription") #{translate('subscription')}
|
||||
> li.divider.hidden-xs.hidden-sm
|
||||
> li
|
||||
> form(method="POST" action="/logout")
|
||||
> input(name='_csrf', type='hidden', value=csrfToken)
|
||||
> button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}
|
||||
10
ldap-overleaf-sl/sharelatex_diff/router.js.diff
Normal file
10
ldap-overleaf-sl/sharelatex_diff/router.js.diff
Normal file
@@ -0,0 +1,10 @@
|
||||
259a260,268
|
||||
> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
> if (process.env.OAUTH2_ENABLED === 'true') {
|
||||
> webRouter.get('/oauth/redirect', AuthenticationController.oauth2Redirect)
|
||||
> webRouter.get('/oauth/callback', AuthenticationController.oauth2Callback)
|
||||
> AuthenticationController.addEndpointToLoginWhitelist('/oauth/redirect')
|
||||
> AuthenticationController.addEndpointToLoginWhitelist('/oauth/callback')
|
||||
> }
|
||||
> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
>
|
||||
212
ldap-overleaf-sl/sharelatex_diff/settings.pug.diff
Normal file
212
ldap-overleaf-sl/sharelatex_diff/settings.pug.diff
Normal file
@@ -0,0 +1,212 @@
|
||||
1c1
|
||||
< extends ../layout-marketing
|
||||
---
|
||||
> extends ../layout
|
||||
3,4c3,14
|
||||
< block entrypointVar
|
||||
< - entrypoint = 'pages/user/settings'
|
||||
---
|
||||
> block content
|
||||
> .content.content-alt
|
||||
> .container
|
||||
> .row
|
||||
> .col-md-12.col-lg-10.col-lg-offset-1
|
||||
> if ssoError
|
||||
> .alert.alert-danger
|
||||
> | #{translate('sso_link_error')}: #{translate(ssoError)}
|
||||
> .card
|
||||
> .page-header
|
||||
> h1 #{translate("account_settings")}
|
||||
> .account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||
6,29c16,17
|
||||
< block append meta
|
||||
< meta(name="ol-hasPassword" data-type="boolean" content=hasPassword)
|
||||
< meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails)
|
||||
< meta(name="ol-oauthProviders", data-type="json", content=oauthProviders)
|
||||
< meta(name="ol-institutionLinked", data-type="json", content=institutionLinked)
|
||||
< meta(name="ol-samlError", data-type="json", content=samlError)
|
||||
< meta(name="ol-institutionEmailNonCanonical", content=institutionEmailNonCanonical)
|
||||
<
|
||||
< meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML)
|
||||
< meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail)
|
||||
< meta(name="ol-samlBeta", content=samlBeta)
|
||||
< meta(name="ol-ssoErrorMessage", content=ssoErrorMessage)
|
||||
< meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds || {})
|
||||
< meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {})
|
||||
< meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed())
|
||||
< meta(name="ol-user" data-type="json" content=user)
|
||||
< meta(name="ol-dropbox" data-type="json" content=dropbox)
|
||||
< meta(name="ol-github" data-type="json" content=github)
|
||||
< meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage)
|
||||
< meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
|
||||
< meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
|
||||
< meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
|
||||
< meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
|
||||
< meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)
|
||||
---
|
||||
>
|
||||
>
|
||||
31,32c19,178
|
||||
< block content
|
||||
< main.content.content-alt#settings-page-root
|
||||
---
|
||||
> .row
|
||||
> .col-md-5
|
||||
> h3 #{translate("update_account_info")}
|
||||
> form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
|
||||
> input(type="hidden", name="_csrf", value=csrfToken)
|
||||
> if !hasFeature('affiliations')
|
||||
> // show the email, non-editable
|
||||
> .form-group
|
||||
> label.control-label #{translate("email")}
|
||||
> div.form-control(
|
||||
> readonly="true",
|
||||
> ng-non-bindable
|
||||
> ) #{user.email}
|
||||
>
|
||||
> if shouldAllowEditingDetails
|
||||
> .form-group
|
||||
> label(for='firstName').control-label #{translate("first_name")}
|
||||
> input.form-control(
|
||||
> id="firstName"
|
||||
> type='text',
|
||||
> name='first_name',
|
||||
> value=user.first_name
|
||||
> ng-non-bindable
|
||||
> )
|
||||
> .form-group
|
||||
> label(for='lastName').control-label #{translate("last_name")}
|
||||
> input.form-control(
|
||||
> id="lastName"
|
||||
> type='text',
|
||||
> name='last_name',
|
||||
> value=user.last_name
|
||||
> ng-non-bindable
|
||||
> )
|
||||
> .form-group
|
||||
> form-messages(aria-live="polite" for="settingsForm")
|
||||
> .alert.alert-success(ng-show="settingsForm.response.success")
|
||||
> | #{translate("thanks_settings_updated")}
|
||||
> .actions
|
||||
> button.btn.btn-primary(
|
||||
> type='submit',
|
||||
> ng-disabled="settingsForm.$invalid"
|
||||
> ) #{translate("update")}
|
||||
> else
|
||||
> .form-group
|
||||
> label.control-label #{translate("first_name")}
|
||||
> div.form-control(
|
||||
> readonly="true",
|
||||
> ng-non-bindable
|
||||
> ) #{user.first_name}
|
||||
> .form-group
|
||||
> label.control-label #{translate("last_name")}
|
||||
> div.form-control(
|
||||
> readonly="true",
|
||||
> ng-non-bindable
|
||||
> ) #{user.last_name}
|
||||
>
|
||||
> .col-md-5.col-md-offset-1
|
||||
> h3
|
||||
> | Set Password for Email login
|
||||
> p
|
||||
> | Note: you can not change the LDAP password from here. You can set/reset a password for
|
||||
> | your email login:
|
||||
> | #[a(href="/user/password/reset", target='_blank') Reset.]
|
||||
>
|
||||
> | !{moduleIncludes("userSettings", locals)}
|
||||
> hr
|
||||
>
|
||||
> h3
|
||||
> | Contact
|
||||
> div
|
||||
> | If you need any help, please contact your sysadmins.
|
||||
>
|
||||
> p #{translate("need_to_leave")}
|
||||
> a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
|
||||
>
|
||||
>
|
||||
>
|
||||
> script(type='text/ng-template', id='deleteAccountModalTemplate')
|
||||
> .modal-header
|
||||
> h3 #{translate("delete_account")}
|
||||
> div.modal-body#delete-account-modal
|
||||
> p !{translate("delete_account_warning_message_3")}
|
||||
> if settings.createV1AccountOnLogin && settings.overleaf
|
||||
> p
|
||||
> strong
|
||||
> | Your Overleaf v2 projects will be deleted if you delete your account.
|
||||
> | If you want to remove any remaining Overleaf v1 projects in your account,
|
||||
> | please first make sure they are imported to Overleaf v2.
|
||||
>
|
||||
> if settings.overleaf && !hasPassword
|
||||
> p
|
||||
> b
|
||||
> | #[a(href="/user/password/reset", target='_blank') #{translate("delete_acct_no_existing_pw")}].
|
||||
> else
|
||||
> form(novalidate, name="deleteAccountForm")
|
||||
> label #{translate('email')}
|
||||
> input.form-control(
|
||||
> type="text",
|
||||
> autocomplete="off",
|
||||
> placeholder="",
|
||||
> ng-model="state.deleteText",
|
||||
> focus-on="open",
|
||||
> ng-keyup="checkValidation()"
|
||||
> )
|
||||
>
|
||||
> label #{translate('password')}
|
||||
> input.form-control(
|
||||
> type="password",
|
||||
> autocomplete="off",
|
||||
> placeholder="",
|
||||
> ng-model="state.password",
|
||||
> ng-keyup="checkValidation()"
|
||||
> )
|
||||
>
|
||||
> div.confirmation-checkbox-wrapper
|
||||
> input(
|
||||
> type="checkbox"
|
||||
> ng-model="state.confirmV1Purge"
|
||||
> ng-change="checkValidation()"
|
||||
> ).pull-left
|
||||
> label(style="display: inline") I have left, purged or imported my projects on Overleaf v1 (if any)
|
||||
>
|
||||
> div.confirmation-checkbox-wrapper
|
||||
> input(
|
||||
> type="checkbox"
|
||||
> ng-model="state.confirmSharelatexDelete"
|
||||
> ng-change="checkValidation()"
|
||||
> ).pull-left
|
||||
> label(style="display: inline") I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}]
|
||||
>
|
||||
> div(ng-if="state.error")
|
||||
> div.alert.alert-danger(ng-switch="state.error.code")
|
||||
> span(ng-switch-when="InvalidCredentialsError")
|
||||
> | #{translate('email_or_password_wrong_try_again')}
|
||||
> span(ng-switch-when="SubscriptionAdminDeletionError")
|
||||
> | #{translate('subscription_admins_cannot_be_deleted')}
|
||||
> span(ng-switch-when="UserDeletionError")
|
||||
> | #{translate('user_deletion_error')}
|
||||
> span(ng-switch-default)
|
||||
> | #{translate('generic_something_went_wrong')}
|
||||
> if settings.createV1AccountOnLogin && settings.overleaf
|
||||
> div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'")
|
||||
> div.alert.alert-info
|
||||
> | If you can't remember your password, or if you are using Single-Sign-On with another provider
|
||||
> | to sign in (such as Twitter or Google), please
|
||||
> | #[a(href="/user/password/reset", target='_blank') reset your password],
|
||||
> | and try again.
|
||||
> .modal-footer
|
||||
> button.btn.btn-default(
|
||||
> ng-click="cancel()"
|
||||
> ) #{translate("cancel")}
|
||||
> button.btn.btn-danger(
|
||||
> ng-disabled="!state.isValid || state.inflight"
|
||||
> ng-click="delete()"
|
||||
> )
|
||||
> span(ng-hide="state.inflight") #{translate("delete")}
|
||||
> span(ng-show="state.inflight") #{translate("deleting")}...
|
||||
>
|
||||
> script(type='text/javascript').
|
||||
> window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
|
||||
0
ldap-overleaf-sl/sharelatex_ori/.gitkeep
Normal file
0
ldap-overleaf-sl/sharelatex_ori/.gitkeep
Normal file
15
ldap-overleaf-sl/update_texlive.sh
Normal file
15
ldap-overleaf-sl/update_texlive.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd /usr/local/texlive/
|
||||
if [ -d 2023 ]
|
||||
then
|
||||
cp -a 2023 2024
|
||||
rm -f 2024/tlpkg/backups/*
|
||||
cd 2024
|
||||
wget https://mirror.ctan.org/systems/texlive/tlnet/update-tlmgr-latest.sh
|
||||
sh update-tlmgr-latest.sh --accept
|
||||
fi
|
||||
tlmgr update --self --all
|
||||
tlmgr install scheme-full
|
||||
luaotfload-tool -fu
|
||||
|
||||
24
scripts/apply_diffs.sh
Normal file
24
scripts/apply_diffs.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
DIFFS_DIR="ldap-overleaf-sl/sharelatex_diff"
|
||||
ORI_DIR="ldap-overleaf-sl/sharelatex_ori"
|
||||
PATCHED_DIR="ldap-overleaf-sl/sharelatex"
|
||||
|
||||
for diff_file in "$DIFFS_DIR"/*.diff; do
|
||||
filename=$(basename "$diff_file" ".diff")
|
||||
if [ "$filename" == ".gitkeep" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
original_file="$ORI_DIR/$filename"
|
||||
patched_file="$PATCHED_DIR/$filename"
|
||||
|
||||
if [ -f "$original_file" ]; then
|
||||
cp "$original_file" "$patched_file"
|
||||
patch "$patched_file" "$diff_file"
|
||||
else
|
||||
echo "No original file for $filename in $ORI_DIR."
|
||||
fi
|
||||
done
|
||||
58
scripts/copy.sh
Normal file
58
scripts/copy.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_FILE_PATHS=(
|
||||
"/overleaf/services/web/app/src/Features/Authentication/AuthenticationManager.js"
|
||||
"/overleaf/services/web/app/src/Features/Authentication/AuthenticationController.js"
|
||||
"/overleaf/services/web/app/src/Features/Contacts/ContactController.js"
|
||||
"/overleaf/services/web/app/src/router.js"
|
||||
"/overleaf/services/web/app/views/user/settings.pug"
|
||||
"/overleaf/services/web/app/views/user/login.pug"
|
||||
"/overleaf/services/web/app/views/layout/navbar.pug"
|
||||
"/overleaf/services/web/app/views/layout/navbar-marketing.pug"
|
||||
"/overleaf/services/web/app/views/admin/index.pug"
|
||||
"/overleaf/services/web/app/views/admin/index.pug"
|
||||
)
|
||||
|
||||
FILENAMES=(
|
||||
"AuthenticationManager.js"
|
||||
"AuthenticationController.js"
|
||||
"ContactController.js"
|
||||
"router.js"
|
||||
"settings.pug"
|
||||
"login.pug"
|
||||
"navbar.pug"
|
||||
"navbar-marketing.pug"
|
||||
"admin-index.pug"
|
||||
"admin-sysadmin.pug"
|
||||
)
|
||||
|
||||
if [ "${#CONTAINER_FILE_PATHS[@]}" -ne "${#FILENAMES[@]}" ]; then
|
||||
echo "Error: The number of source files and target filenames does not match."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOST_TARGET_PATH="ldap-overleaf-sl/sharelatex_ori"
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 [version]"
|
||||
exit 1
|
||||
else
|
||||
VERSION=$1
|
||||
fi
|
||||
|
||||
CONTAINER_NAME="tmp_sharelatex_for_extract_files"
|
||||
IMAGE="sharelatex/sharelatex:$VERSION"
|
||||
|
||||
for i in "${!CONTAINER_FILE_PATHS[@]}"; do
|
||||
file_path="${CONTAINER_FILE_PATHS[i]}"
|
||||
new_filename="${FILENAMES[i]}"
|
||||
new_target_path="$HOST_TARGET_PATH/$new_filename"
|
||||
echo " copy: $new_target_path"
|
||||
docker cp $CONTAINER_NAME:$file_path $new_target_path
|
||||
done
|
||||
#
|
||||
#echo "Stopping and removing container..."
|
||||
#docker stop $CONTAINER_NAME
|
||||
#docker rm $CONTAINER_NAME
|
||||
72
scripts/extract_files.sh
Normal file
72
scripts/extract_files.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_FILE_PATHS=(
|
||||
"/overleaf/services/web/app/src/Features/Authentication/AuthenticationManager.js"
|
||||
"/overleaf/services/web/app/src/Features/Authentication/AuthenticationController.js"
|
||||
"/overleaf/services/web/app/src/Features/Contacts/ContactController.js"
|
||||
"/overleaf/services/web/app/src/router.js"
|
||||
"/overleaf/services/web/app/views/user/settings.pug"
|
||||
"/overleaf/services/web/app/views/user/login.pug"
|
||||
"/overleaf/services/web/app/views/layout/navbar.pug"
|
||||
"/overleaf/services/web/app/views/layout/navbar-marketing.pug"
|
||||
"/overleaf/services/web/app/views/admin/index.pug"
|
||||
"/overleaf/services/web/app/views/admin/index.pug"
|
||||
)
|
||||
|
||||
FILENAMES=(
|
||||
"AuthenticationManager.js"
|
||||
"AuthenticationController.js"
|
||||
"ContactController.js"
|
||||
"router.js"
|
||||
"settings.pug"
|
||||
"login.pug"
|
||||
"navbar.pug"
|
||||
"navbar-marketing.pug"
|
||||
"admin-index.pug"
|
||||
"admin-sysadmin.pug"
|
||||
)
|
||||
|
||||
if [ "${#CONTAINER_FILE_PATHS[@]}" -ne "${#FILENAMES[@]}" ]; then
|
||||
echo "Error: The number of source files and target filenames does not match."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOST_TARGET_PATH="ldap-overleaf-sl/sharelatex_ori"
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 [version]"
|
||||
exit 1
|
||||
else
|
||||
VERSION=$1
|
||||
fi
|
||||
|
||||
CONTAINER_NAME="tmp_sharelatex_for_extract_files"
|
||||
IMAGE="sharelatex/sharelatex:$VERSION"
|
||||
|
||||
echo "Starting Docker container \"$CONTAINER_NAME\" with image \"$IMAGE\"..."
|
||||
if [ ! "$(docker ps -q -f name=^/${CONTAINER_NAME}$)" ]; then
|
||||
if [ "$(docker ps -aq -f status=exited -f name=^/${CONTAINER_NAME}$)" ]; then
|
||||
echo "Removing stopped container with same name..."
|
||||
docker rm $CONTAINER_NAME
|
||||
fi
|
||||
else
|
||||
echo "Error: A container with the name $CONTAINER_NAME already exists."
|
||||
exit 1
|
||||
fi
|
||||
docker run -d --name $CONTAINER_NAME $IMAGE
|
||||
|
||||
echo "Waiting for container to start up..."
|
||||
sleep 10
|
||||
|
||||
#for i in "${!CONTAINER_FILE_PATHS[@]}"; do
|
||||
# file_path="${CONTAINER_FILE_PATHS[i]}"
|
||||
# new_filename="${FILENAMES[i]}"
|
||||
# new_target_path="$HOST_TARGET_PATH/$new_filename"
|
||||
# docker cp $CONTAINER_NAME:$file_path $new_target_path
|
||||
#done
|
||||
#
|
||||
#echo "Stopping and removing container..."
|
||||
#docker stop $CONTAINER_NAME
|
||||
#docker rm $CONTAINER_NAME
|
||||
17
scripts/make_diffs.sh
Normal file
17
scripts/make_diffs.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
MODIFIED_DIR="ldap-overleaf-sl/sharelatex"
|
||||
DIFFS_DIR="ldap-overleaf-sl/sharelatex_diff"
|
||||
ORI_DIR="ldap-overleaf-sl/sharelatex_ori"
|
||||
|
||||
for filename in $(ls $MODIFIED_DIR); do
|
||||
raw_file="$ORI_DIR/$filename"
|
||||
|
||||
if [ -f "$raw_file" ]; then
|
||||
echo "working on $raw_file"
|
||||
diff_output="$DIFFS_DIR/${filename}.diff"
|
||||
diff "$raw_file" "$MODIFIED_DIR/$filename" > "$diff_output"
|
||||
else
|
||||
echo "No matching file for $filename in $ORI_DIR."
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user