Backup 30.12.2024

This commit is contained in:
David Rotermund 2024-12-30 02:50:43 +01:00
parent fd0fb334c9
commit 2544c630b6
582 changed files with 198266 additions and 0 deletions

9
docker/README.md Normal file
View file

@ -0,0 +1,9 @@
Don't forget to put the container checker into the crontab:
```
>> crontab -e
```
```
*/5 * * * * /bin/bash /docker/check_docker.sh
```

11
docker/backup/README.md Normal file
View file

@ -0,0 +1,11 @@
In copy_keys_over.sh and make_keys.sh, you need to change the computer names to your installation (where your want to store your backup).
Don't forget to put it in your crontab
```
>> crontab -e
```
```
0 0 * * * /bin/bash /docker/backup/make_backup.sh
```

View file

@ -0,0 +1 @@
scp backup.pub overleaf@backup.zfn.uni-bremen.de:~/.ssh/authorized_keys

View file

@ -0,0 +1,12 @@
#!/bin/bash
cd /docker/compose/keycloakpostgres
sh backup.sh
cd /docker/compose/overleafmongo
sh backup.sh
cd /docker/compose/overleafredis
sh backup.sh
cd /docker/backup/
rsync -avz --delete -e "ssh -i /docker/backup/backup" /docker overleaf@backup.zfn.uni-bremen.de:/home/overleaf/fb1/

View file

@ -0,0 +1 @@
ssh-keygen -t ed25519 -f backup

15
docker/check_docker.sh Normal file
View file

@ -0,0 +1,15 @@
#!/bin/bash
# List of expected container names
expected_containers=("overleafregister" "nginx" "checkuser" "overleafserver" "keycloakserver" "keycloakpostgres" "overleafmongo" "overleafredis" "hajtexsshd")
# Email settings
recipient="overleaf@uni-bremen.de"
subject="Docker Container Alert"
# Check containers
for container in "${expected_containers[@]}"; do
if ! docker ps --format '{{.Names}}' | grep -q "^$container$"; then
echo "Container $container is not running" | mail -s "$subject" "$recipient"
fi
done

7
docker/compose/README.md Normal file
View file

@ -0,0 +1,7 @@
The landing page of the HajTex server is
https://[FQDN]/launchpad
e.g.
https://psintern.neuro.uni-bremen.de/launchpad

View file

@ -0,0 +1,25 @@
FROM python:3.12.5
RUN apt-get update
RUN apt -y install mc
RUN apt -y install docker.io
RUN pip install pymongo
RUN pip install email_validator
RUN pip install flask
RUN pip install gunicorn
RUN pip install requests
RUN pip install BeautifulSoup4
RUN apt -y install bash
RUN pip install --upgrade pip
RUN pip install flask_wtf
RUN pip install wtforms
RUN pip install flask_recaptcha
RUN pip install Markup
RUN pip install captcha Pillow
RUN pip install argh
EXPOSE 80
ENTRYPOINT ["/bin/bash", "-c", "cd / && sleep infinity"]

View file

@ -0,0 +1,21 @@
services:
checkuser:
image: "check_user_image"
container_name: checkuser
hostname: checkuser
restart: always
networks:
- overleaf-network
volumes:
- /docker/compose/check_users/data:/data
- /var/run/docker.sock:/var/run/docker.sock
entrypoint: >
/bin/sh -c "pip install argh && cd / && sleep infinity"
networks:
overleaf-network:
external: true

View file

@ -0,0 +1,15 @@
import pymongo
container_name: str = "overleafmongo"
port: int = 27017
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
users = db.projectInvites
cursor = users.find()
for user in cursor:
print(user["email"])
client.close()

View file

@ -0,0 +1,15 @@
import pymongo
container_name: str = "overleafmongo"
port: int = 27017
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
users = db.users
cursor = users.find()
for user in cursor:
print(user["email"])
client.close()

View file

@ -0,0 +1,27 @@
import pymongo
import argh
def main(
email_to_find: str, container_name: str = "overleafmongo", port: int = 27017
) -> bool:
print(f"User name: {email_to_find}")
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
users = db.users
search_result = users.find_one({"email": email_to_find})
if search_result is None:
print("User not found")
return
else:
print(f"User status was: {search_result['isAdmin']}")
users.update_one({"email": email_to_find}, {"$set": {"isAdmin": True}})
print("User status changed")
return
client.close()
if __name__ == "__main__":
argh.dispatch_command(main)

View file

@ -0,0 +1,9 @@
#!/bin/bash
if [ -z "$1" ]; then
echo "Error: Email address not provided"
echo "Usage: $0 <email_address>"
exit 1
fi
docker exec overleafserver /bin/bash -ce "cd /overleaf/services/web && node modules/server-ce-scripts/scripts/delete-user --email=$1"

View file

@ -0,0 +1 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it checkuser bash

View file

@ -0,0 +1 @@
docker exec -it checkuser bash -c "cd /data ; python list_invited.py"

View file

@ -0,0 +1 @@
docker exec -it checkuser bash -c "cd /data ; python list_user.py"

View file

@ -0,0 +1 @@
docker exec -it checkuser bash -c "cd /data ; python make_admin.py $1"

View file

@ -0,0 +1 @@
docker compose logs -f

View file

@ -0,0 +1 @@
docker build --network host -t check_user_image .

View file

@ -0,0 +1 @@
docker compose up -d

View file

@ -0,0 +1 @@
docker run -it texlive/texlive:latest-full /bin/bash

View file

@ -0,0 +1,3 @@
POSTGRES_DB=keycloak
POSTGRES_USER=keycloakuser
POSTGRES_PASSWORD=REDACTED

View file

@ -0,0 +1 @@
docker exec keycloakpostgres bash -c "pg_dump -U keycloakuser -d keycloak -F c -f /backup/backup.sql"

View file

@ -0,0 +1,19 @@
services:
postgres:
image: postgres:16
container_name: keycloakpostgres
hostname: keycloakpostgres
volumes:
- /docker/compose/keycloakpostgres/postgres_data:/var/lib/postgresql/data
- /docker/compose/keycloakpostgres/backup:/backup
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
networks:
- keycloak-network
networks:
keycloak-network:
external: true

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it keycloakpostgres bash

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1,2 @@
docker compose up -d

View file

@ -0,0 +1,6 @@
POSTGRES_DB=keycloak
POSTGRES_USER=keycloakuser
POSTGRES_PASSWORD=REDACTED
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=REDACTED
KEYCLOAK_HOSTNAME=YOUR_HOSTNAME

View file

@ -0,0 +1,36 @@
services:
keycloak:
image: quay.io/keycloak/keycloak:26.0
container_name: keycloakserver
hostname: keycloakserver
command: start
environment:
KC_PROXY_ADDRESS_FORWARDING: true
KC_HOSTNAME_STRICT: false
KC_HOSTNAME: ${KEYCLOAK_HOSTNAME}
KC_PROXY: edge
KC_HTTP_ENABLED: true
KC_HEALTH_ENABLED: true
KC_HTTP_RELATIVE_PATH: /sso
KC_PROXY_HEADERS: xforwarded
PROXY_ADDRESS_FORWARDING: true
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloakpostgres/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- 8080:8080
restart: always
networks:
- keycloak-network
networks:
keycloak-network:
external: true

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it keycloakserver bash

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1,2 @@
docker compose up -d

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,26 @@
services:
overleafnginx:
image: nginx:stable-alpine
container_name: nginx
hostname: nginx
restart: always
volumes:
- "/docker/compose/nginx/key.pem:/certs/nginx_key.pem:ro"
- "/docker/compose/nginx/ca.pem:/certs/nginx_certificate.pem:ro"
- "/docker/compose/nginx/nginx.conf:/etc/nginx/nginx.conf:ro"
ports:
- "0.0.0.0:443:443"
- "0.0.0.0:80:80"
environment:
NGINX_WORKER_PROCESSES: "4"
NGINX_WORKER_CONNECTIONS: "768"
networks:
- overleaf-network
- keycloak-network
networks:
overleaf-network:
external: true
keycloak-network:
external: true

View file

@ -0,0 +1,4 @@
docker compose down
docker compose up -d
docker compose logs -f

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1,32 @@
events {}
http {
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /certs/nginx_certificate.pem;
ssl_certificate_key /certs/nginx_key.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
server_tokens off;
client_max_body_size 50M;
location /sso {
proxy_pass http://keycloakserver:8080/sso;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View file

@ -0,0 +1,41 @@
events {}
http {
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /certs/nginx_certificate.pem;
ssl_certificate_key /certs/nginx_key.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
server_tokens off;
client_max_body_size 50M;
location /sso {
proxy_pass http://keycloakserver:8080/sso;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /nodedev {
proxy_pass http://nodedev:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View file

@ -0,0 +1,97 @@
events {}
http {
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /certs/nginx_certificate.pem;
ssl_certificate_key /certs/nginx_key.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
server_tokens off;
client_max_body_size 50M;
location / {
proxy_pass http://overleafserver:80;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /articles {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /templates {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /latex/templates {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /learn {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /sso {
proxy_pass http://keycloakserver:8080/sso;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View file

@ -0,0 +1,108 @@
events {}
http {
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /certs/nginx_certificate.pem;
ssl_certificate_key /certs/nginx_key.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
server_tokens off;
client_max_body_size 50M;
location / {
proxy_pass http://overleafserver:80;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /register {
proxy_pass http://overleafregister:80;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /articles {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /templates {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /latex/templates {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /learn {
proxy_pass https://www.overleaf.com;
proxy_set_header Host www.overleaf.com;
proxy_ssl_verify off;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
location /sso {
proxy_pass http://keycloakserver:8080/sso;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View file

@ -0,0 +1,2 @@
docker compose up -d

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@
docker exec overleafmongo bash -c "mongodump --out /backup/"

View file

@ -0,0 +1,33 @@
services:
overleafmongo:
image: "mongo:6.0"
container_name: overleafmongo
hostname: overleafmongo
restart: always
healthcheck:
test: "mongosh --quiet --eval 'rs.hello().setName ? rs.hello().setName : rs.initiate({_id: \"overleaf\",members:[{_id: 0, host:\"overleafmongo:27017\"}]})'"
interval: 10s
timeout: 10s
retries: 5
command: "--replSet overleaf"
expose:
- 27017
volumes:
- /docker/compose/overleafmongo/data_db:/data/db
- /docker/compose/overleafmongo/data_configdb:/data/configdb
- /docker/compose/overleafmongo/backup:/backup
- /var/run/docker.sock:/var/run/docker.sock
networks:
- overleaf-network
extra_hosts:
- "mongo:127.0.0.1"
- "overleafmongo:127.0.0.1"
volumes:
overleaf_mongo:
overleaf_mongo_cdb:
networks:
overleaf-network:
external: true

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it overleafmongo bash

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1,2 @@
docker compose up -d

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,6 @@
vm.overcommit_memory = 1
/etc/sysctl.conf
sysctl vm.overcommit_memory=1

View file

@ -0,0 +1 @@
docker exec overleafredis sh -c "cp -f dump.rdb /backup/"

View file

@ -0,0 +1,31 @@
# docker network create overleaf-network
services:
overleafredis:
image: "redis:6.2-alpine"
container_name: overleafredis
hostname: overleafredis
restart: always
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
command: --save 60 1 --loglevel warning
volumes:
- /docker/compose/overleafredis/data:/data
- /docker/compose/overleafredis/backup:/backup
- /var/run/docker.sock:/var/run/docker.sock
expose:
- 6379
networks:
- overleaf-network
environment:
REDIS_AOF_PERSISTENCE: "true"
volumes:
overleaf_redis:
networks:
overleaf-network:
external: true

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it overleafredis sh

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1,2 @@
docker compose up -d

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,24 @@
FROM python:3.12.5
RUN apt-get update
RUN apt -y install mc
RUN apt -y install docker.io
RUN pip install pymongo
RUN pip install email_validator
RUN pip install flask
RUN pip install gunicorn
RUN pip install requests
RUN pip install BeautifulSoup4
RUN apt -y install bash
RUN pip install --upgrade pip
RUN pip install flask_wtf
RUN pip install wtforms
RUN pip install flask_recaptcha
RUN pip install Markup
RUN pip install captcha Pillow
EXPOSE 80
ENTRYPOINT ["/bin/bash", "-c", "cd data && gunicorn wsgi:app --bind 0.0.0.0:80"]

View file

@ -0,0 +1,57 @@
First of all, build the docker image:
```
>> sh make_image.sh
```
* Build the docker image with make_image.sh
* In data you need to edit the following files:
* allowed_domains.json: This allows to configure domains endings (like uni-bremen.de which allows davrot@uni-bremen.de and davrot@neuro.uni-bremen.de) that are automatically allowed even if the person is not invited.
* blocked_users.json: Here you can name email addresses you want to block.
* config.json: Here you need to adapt the FQDNs and the two passwords:
```
{
"keycloak_url": "https://psintern.neuro.uni-bremen.de/sso",
"keycloak_login": "https://psintern.neuro.uni-bremen.de/login/oidc",
"admin_username": "automation@non.no",
"admin_password": "REDACTED",
"client_id": "admin-cli",
"client_secret": "REDACTED"
}
```
* Set a secret key in data/secret_key.json.
* In data/main.py: (pip install captcha flask email_validator pymongo)
* I simplied the captcha to 6x A . If you want something else change line 34.
* I am not using the generated captcha_image in the webpage. I thing modern machine learning makes no difference if it is a image or a text. I am using the captcha just to block simple bots. They cannot create an account anyhow. But if you want to, you can add it back to in templates/post.html in by using the "image"
```
<img src="data:image/png;base64,{{ captcha_image }}" alt="CAPTCHA">
```
* Change keycloak_url to your installation
* And finally change the logos in data/static and the text in templates/post.html
Start the container with:
```
>> sh up.sh
```
Or for development activate the
```
entrypoint: ["sh", "-c", "sleep infinity"]
```
in the compose.yaml. Then enter the container with
```
>> sh exec.sh
```
Inside the container you can use
```
>> cd data
>> sh run.sh
```
to start the server. Every change in the code, html file or logos requires run.sh to be stopped and started again!

View file

@ -0,0 +1,24 @@
services:
overleafregister:
image: "overleafregister_image"
container_name: overleafregister
hostname: overleafregister
restart: always
networks:
- overleaf-network
- keycloak-network
volumes:
- /docker/compose/overleafregister/data:/data
- /var/run/docker.sock:/var/run/docker.sock
# entrypoint: ["sh", "-c", "sleep infinity"]
networks:
keycloak-network:
external: true
overleaf-network:
external: true

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,82 @@
import requests # type: ignore
import json
from requests.auth import HTTPBasicAuth # type: ignore
def add_keycloak_user(username):
print(f"Start to create user {username} via OIDC")
with open("config.json", "r") as file:
config = json.load(file)
token_url = f"{config['keycloak_url']}/realms/master/protocol/openid-connect/token"
token_data = {
"grant_type": "password",
"username": config["admin_username"],
"password": config["admin_password"],
}
users_url = f"{config['keycloak_url']}/admin/realms/master/users"
print("-- Get token")
# Get token
try:
response = requests.post(
token_url,
data=token_data,
auth=HTTPBasicAuth(config["client_id"], config["client_secret"]),
)
response.raise_for_status()
except requests.exceptions.HTTPError:
return False
access_token = response.json()["access_token"]
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
# Check if user exists
params = {"username": username, "exact": "true"}
print("-- Check if user exists")
try:
response = requests.get(users_url, headers=headers, params=params)
response.raise_for_status()
# Response is a list of users matching the criteria
users = response.json()
# If we found any users with exact username match, the user exists
if len(users) > 0:
return False
except requests.exceptions.HTTPError:
return False
print("-- Make new user")
# Make new user
new_user = {
"username": username,
"enabled": True,
"emailVerified": False,
"firstName": " ",
"lastName": " ",
"email": username,
"requiredActions": ["UPDATE_PASSWORD"],
}
try:
# Create the user
response = requests.post(users_url, headers=headers, data=json.dumps(new_user))
response.raise_for_status()
except requests.exceptions.HTTPError:
return False
print("-- DONE")
return True

View file

@ -0,0 +1,5 @@
{
"allowed_domains": [
"uni-bremen.de"
]
}

View file

@ -0,0 +1,6 @@
{
"blocked_users": [
""
]
}

View file

@ -0,0 +1,15 @@
import pymongo
def check_invites(
email_to_find: str, container_name: str = "overleafmongo", port: int = 27017
) -> bool:
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
project_invites = db.projectInvites
search_result = project_invites.find_one({"email": email_to_find})
if search_result is None:
return False
else:
return True

View file

@ -0,0 +1,15 @@
import pymongo
def check_user(
email_to_find: str, container_name: str = "overleafmongo", port: int = 27017
) -> bool:
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
users = db.users
search_result = users.find_one({"email": email_to_find})
if search_result is None:
return False
else:
return True

View file

@ -0,0 +1,8 @@
{
"keycloak_url": "https://overleaf.fb1.uni-bremen.de/sso",
"keycloak_login": "https://overleaf.pip.uni-bremen.de/login/oidc",
"admin_username": "automation@non.no",
"admin_password": "REDACTED",
"client_id": "admin-cli",
"client_secret": "REDACTED"
}

View file

@ -0,0 +1,70 @@
import json
from flask import (
Flask,
render_template,
request,
Response,
send_from_directory,
session,
redirect,
)
from io import BytesIO
from captcha.image import ImageCaptcha
import random
import base64
from process_emails import process_emails
with open("config.json", "r") as file:
config: dict = json.load(file)
container_name_mongo: str = "overleafmongo"
port_mongo: int = 27017
container_name_overleaf: str = "overleafserver"
keycloak_url: str = config["keycloak_login"]
app = Flask(__name__)
with open("secret_key.json", "r") as file:
secret_key: dict = json.load(file)
assert secret_key is not None
assert secret_key["secret_key"] is not None
app.config["SECRET_KEY"] = secret_key["secret_key"]
def generate_captcha():
image = ImageCaptcha(width=280, height=90)
# I simplied the Captcha
captcha_text = "".join(random.choices("A", k=6))
data = image.generate(captcha_text)
return captcha_text, data
@app.route("/register", methods=["GET", "POST"])
def index() -> Response:
if request.method == "GET":
captcha_text, captcha_image = generate_captcha()
session["captcha"] = captcha_text
captcha_base64 = base64.b64encode(captcha_image.getvalue()).decode("utf-8")
return render_template("post.html", captcha_image=captcha_base64)
elif request.method == "POST":
email = request.form.get("email")
user_captcha = request.form.get("captcha")
if user_captcha and user_captcha.upper() == session.get("captcha"):
if process_emails(
mail_address=email,
container_name_mongo=container_name_mongo,
port_mongo=port_mongo,
):
return redirect(keycloak_url)
else:
return f"We couldn't register your email {email}."
else:
return "There was a problem with solving the captcha. Try again. Sorry!"
@app.route("/register/static/<path:path>", methods=["GET"])
def serve_static_files(path) -> Response:
return send_from_directory("static", path)

View file

@ -0,0 +1,63 @@
from email_validator import validate_email # type: ignore
import email_validator
import json
from check_invites import check_invites
from check_user import check_user
from add_user import add_keycloak_user
def process_emails(
mail_address: str,
config_file: str = "allowed_domains.json",
blocked_user_file: str = "blocked_users.json",
container_name_mongo: str = "overleafmongo",
port_mongo: int = 27017,
) -> bool:
with open(config_file, "r") as file:
allowed_domains: dict = json.load(file)
with open(blocked_user_file, "r") as file:
blocked_users: dict = json.load(file)
if (mail_address == "") or (mail_address is None):
return False
try:
emailinfo = validate_email(mail_address, check_deliverability=False)
mail_address = emailinfo.normalized
except email_validator.exceptions_types.EmailSyntaxError:
return False
except email_validator.exceptions_types.EmailNotValidError:
return False
for blocked_user in blocked_users["blocked_users"]:
if mail_address == blocked_user:
return False
print(f"{mail_address} -- is not blocked")
is_email_allowed: bool = False
if check_invites(
email_to_find=mail_address, container_name=container_name_mongo, port=port_mongo
):
is_email_allowed = True
print(f"{mail_address} -- eMail is invited")
if check_user(
email_to_find=mail_address, container_name=container_name_mongo, port=port_mongo
):
is_email_allowed = True
print(f"{mail_address} -- eMail is already registered")
if is_email_allowed is False:
domain_found: bool = False
for domain in allowed_domains["allowed_domains"]:
if mail_address.endswith(domain):
domain_found = True
print(f"{mail_address} -- domain was found")
if domain_found is False:
return False
return add_keycloak_user(mail_address)

View file

@ -0,0 +1 @@
gunicorn wsgi:app --bind 0.0.0.0:80

View file

@ -0,0 +1,4 @@
{
"secret_key": "REDACTED"
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register your HajTex account</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
input[type="email"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
textarea {
height: 150px;
width: 100%;
resize: vertical;
}
.status-marker {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: 10px;
}
.submit-btn {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
margin-top: 15px;
}
.submit-btn:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<header>
<img src="/register/static/hajtex.svg" alt="Logo Hajtex" style="max-width: 200px;">
</header>
<h1>Register your HajTex account</h1>
<h2>Who can register?</h2>
1. You don't need to register here if your OIDC account is ready for you. Got here to login <a href="/login">login</a>. <p>
2. If someone has <font color="red"><b>invited</b></font> you <font color="red"><b>to a project</b></font>.<p>
In the case of 2. use this form. Afterwards set your password via the "Forgot Password?" option during the login process.
<form method="POST" id="demo-form">
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<p>
<div class="form-group">
<label for="captcha">CAPTCHA:</label>
<input type="text" id="captcha" name="captcha" maxlength="6" size="6" required>
</div>
Please enter the following six letters: <font color="green"><b>AAAAAA</b></font>
<p>
<button type="submit" class="submit-btn">Register</button>
</form>
</body>
</html>

View file

@ -0,0 +1,4 @@
from main import app
if __name__ == "__main__":
app.run(debug=True)

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it overleafregister bash

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1 @@
docker build --network host -t overleafregister_image .

View file

@ -0,0 +1,2 @@
docker compose up -d

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,77 @@
FQDN="psintern.neuro.uni-bremen.de"
# KeyCloak
OIDC_CLIENT_ID=overleaf
OIDC_CLIENT_SECRET=REDACTED
OIDC_ENABLE=true
OIDC_NAME_SHORT="OIDC"
OIDC_NAME_LONG="OIDC"
# Email
OVERLEAF_EMAIL_PASSWORD=REDACTED
OVERLEAF_EMAIL_FROM_ADDRESS=overleaf@uni-bremen.de
OVERLEAF_EMAIL_SMTP_HOST=smtp.uni-bremen.de
OVERLEAF_EMAIL_SMTP_PORT=465
OVERLEAF_EMAIL_SMTP_SECURE=true
OVERLEAF_EMAIL_SMTP_USER=overleaf
# Other
OVERLEAF_APP_NAME="University of Bremen -- HajTex"
OVERLEAF_NAV_TITLE="Uni Bremen HajTex"
OVERLEAF_CUSTOM_EMAIL_FOOTER="University of Bremen -- HajTex"
# ##################################################
OVERLEAF_SITE_URL=https://${FQDN}
URL=https://${FQDN}/sso/realms/master/.well-known/openid-configuration
OIDC_CALLBACK_URL=https://${FQDN}/login/oidc/callback
echo ${URL}
wget -O openid-configuration ${URL}
echo OIDC_ISSUER
OIDC_ISSUER=$(cat openid-configuration | sed s/","/"\n"/g | grep \"issuer\" | sed s/'\":'/'\n'/g | grep https | sed s/'\"'/''/g)
echo $OIDC_ISSUER
echo OIDC_AUTHORIZATION_URL
OIDC_AUTHORIZATION_URL=$(cat openid-configuration | sed s/","/"\n"/g | grep ^\"authorization_endpoint\" | sed s/'\":'/'\n'/g | grep https | sed s/'\"'/''/g | head -1)
echo $OIDC_AUTHORIZATION_URL
echo OIDC_TOKEN_URL
OIDC_TOKEN_URL=$(cat openid-configuration | sed s/","/"\n"/g | grep ^\"token_endpoint\" | sed s/'\":'/'\n'/g | grep https | sed s/'\"'/''/g | head -1)
echo $OIDC_TOKEN_URL
echo OIDC_USERINFO_URL
OIDC_USERINFO_URL=$(cat openid-configuration | sed s/","/"\n"/g | grep ^\"userinfo_endpoint\" | sed s/'\":'/'\n'/g | grep https | sed s/'\"'/''/g | head -1)
echo $OIDC_USERINFO_URL
echo "# Keycloak OpenID Connect Configuration" > .env
echo "OIDC_ENABLE=${OIDC_ENABLE}" >> .env
echo "OIDC_NAME_SHORT=${OIDC_NAME_SHORT}" >> .env
echo "OIDC_NAME_LONG=${OIDC_NAME_LONG}" >> .env
echo "OIDC_ISSUER=${OIDC_ISSUER}" >> .env
echo "OIDC_AUTHORIZATION_URL=${OIDC_AUTHORIZATION_URL}" >> .env
echo "OIDC_TOKEN_URL=${OIDC_TOKEN_URL}" >> .env
echo "OIDC_USERINFO_URL=${OIDC_USERINFO_URL}" >> .env
echo "OIDC_CLIENT_ID=${OIDC_CLIENT_ID}" >> .env
echo "OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}" >> .env
echo "OIDC_CALLBACK_URL=${OIDC_CALLBACK_URL}" >> .env
rm openid-configuration
echo "" >> .env
echo "# eMail Account Configuration" >> .env
echo "OVERLEAF_EMAIL_PASSWORD=${OVERLEAF_EMAIL_PASSWORD}" >> .env
echo "OVERLEAF_EMAIL_FROM_ADDRESS=${OVERLEAF_EMAIL_FROM_ADDRESS}" >> .env
echo "OVERLEAF_EMAIL_SMTP_HOST=${OVERLEAF_EMAIL_SMTP_HOST}" >> .env
echo "OVERLEAF_EMAIL_SMTP_PORT=${OVERLEAF_EMAIL_SMTP_PORT}" >> .env
echo "OVERLEAF_EMAIL_SMTP_SECURE=${OVERLEAF_EMAIL_SMTP_SECURE}" >> .env
echo "OVERLEAF_EMAIL_SMTP_USER=${OVERLEAF_EMAIL_SMTP_USER}" >> .env
echo "" >> .env
echo "# Other Overleaf Configurations Configuration" >> .env
echo "OVERLEAF_SITE_URL=${OVERLEAF_SITE_URL}" >> .env
echo "OVERLEAF_ADMIN_EMAIL=${OVERLEAF_EMAIL_FROM_ADDRESS}" >> .env
echo "OVERLEAF_APP_NAME=\"${OVERLEAF_APP_NAME}\"" >> .env
echo "OVERLEAF_NAV_TITLE=\"${OVERLEAF_NAV_TITLE}\"" >> .env
echo "OVERLEAF_CUSTOM_EMAIL_FOOTER=\"${OVERLEAF_CUSTOM_EMAIL_FOOTER}\"" >> .env

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,2 @@
docker compose down

View file

@ -0,0 +1 @@
docker exec -it overleafserver bash

View file

@ -0,0 +1,2 @@
docker compose logs -f

View file

@ -0,0 +1,7 @@
docker compose down
cd /docker/features
sh _tools/configure_features.sh
sh _tools/generate_prep.sh
cd /docker/compose/overleafserver
docker compose up -d

View file

@ -0,0 +1 @@
docker pull texlive/texlive:latest-full

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -0,0 +1,41 @@
FROM ubuntu:24.04
# Ensure non-interactive installation
ENV DEBIAN_FRONTEND=noninteractive
# Update and install packages
RUN apt update
RUN apt -y install mc
RUN apt -y install bash
RUN apt -y install openssh-server
RUN apt -y install python3-pip
RUN apt -y install python3-argh
RUN apt -y install git-all
RUN apt -y install golang-go
RUN apt -y install curl
RUN apt -y install openssl
RUN apt -y install libpam-script
RUN apt -y install sudo
RUN apt -y install rush
RUN apt -y install inetutils-syslogd
RUN apt -y install python3-docker
RUN mkdir -p /compile && cd /compile && git clone https://github.com/kha7iq/kc-ssh-pam.git && cd /compile/kc-ssh-pam && go build && mkdir -p /etc/kc-ssh-pam && cp /compile/kc-ssh-pam/kc-ssh-pam /etc/kc-ssh-pam
RUN cp -a /etc /etc_original
RUN rm -f /etc_original/hostname
RUN rm -f /etc_original/hosts
RUN rm -f /etc_original/resolv.conf
# Copy initialization script
COPY files/init.sh /init.sh
RUN chmod +x /init.sh
# Expose SSH port
EXPOSE 22
ENTRYPOINT ["/init.sh"]

View file

@ -0,0 +1,156 @@
If the user logs in via git (in the moment on port 993, please don't forget to allow port 993 via ufw allow 993), the projects for that user are automatically updated.
Every 5 minutes, cron checks the userdata base of overleaf and new user from the database are created.
## Get the ssh keys for a user
```
git clone ssh://[USERNAME]@[FQDN]:[PORT]/sshkey.git
```
e.g.
```
git clone ssh://davrot@uni-bremen.de@psintern.neuro.uni-bremen.de:993/sshkey.git
```
## Get the project list for a user
```
git clone ssh://[USERNAME]@[FQDN]:[PORT]/projects.git
```
e.g.
```
git clone ssh://davrot@uni-bremen.de@psintern.neuro.uni-bremen.de:993/projects.git
```
## Get a project
```
git clone ssh://[USERNAME]@[FQDN]:[PORT]/[PROJECT_ID].git
```
e.g.
```
git clone ssh://davrot@uni-bremen.de@psintern.neuro.uni-bremen.de:993/6759fdf66ca7b8bc5b81b184.git
```
On the one side this backup container communicates with the user via git and with the overleaf server via docker socket.
Don't forget the crontab entry for host:
```
# m h dom mon dow command
*/5 * * * * sh /docker/compose/hajtex_sshd/exec_update_userlist.sh
```
Otherwise, login will fail without the user directories. You can also run it manually:
```
sh /docker/compose/hajtex_sshd/exec_update_userlist.sh
```
# Port 993
If you don't like port 993 you can change the compose.yaml
```
ports:
- 993:22
```
accordingly. But don't forget you firewall:
```
ufw allow 993:22
```
# ssh / scp / git-shell authentification against KeyCloak
## Create the client in keycloak:
```
urn:ietf:wg:oauth:2.0:oob
```
![A](01.png)
---
![B](02.png)
---
![C](03.png)
---
## Update files/config.toml
Change clientsecret and the endpoint.
```
realm = "master"
endpoint = "https://psintern.neuro.uni-bremen.de/sso/"
clientid = "linux-ssh"
clientsecret = "REDACTED"
clientscope = "openid"
```
## Create image:
```
>> make_image.sh
```
## Change the name of the HajTex server container:
Default is "/overleafserver"
If your installation is different then change in the files download_files.py, auth_against_docker.py and update_userlist.py modifiy the line accordingly:
```
container_name: str = "/overleafserver",
```
# Files
* Dockerfile
Dockerfile for creating the container image
* compose.yaml
Compose file to start the container
* crontab_host.txt
This needs to be placed into the crontab of the host
* down.sh
For stoping the container
* exec.sh
For entering the container for an interactive session
* init.sh
Init script that is ran during starting the container. The make_image.sh places it into the container.
* logs.sh
Shows the logs of the running container
* make_image.sh
Needs to be run for generating the container image
* exec_update_userlist.sh
Is run by the cron to update the user basis in the container based on the overleaf user database

View file

@ -0,0 +1,44 @@
services:
hajtexsshd:
image: hajtex_sshd_image
container_name: hajtexsshd
hostname: hajtexsshd
restart: always
volumes:
- /docker/compose/scp_git_bridge/downloads:/downloads
- /docker/compose/scp_git_bridge/etc:/etc
- /docker/compose/scp_git_bridge/log:/var/log
- /docker/compose/scp_git_bridge/files/auth_against_docker.py:/auth_against_docker.py:ro
- /docker/compose/scp_git_bridge/files/build_jail.sh:/build_jail.sh:ro
- /docker/compose/scp_git_bridge/files/download_files.py:/download_files.py:ro
- /docker/compose/scp_git_bridge/files/get_projects.py:/get_projects.py:ro
- /docker/compose/scp_git_bridge/files/pam_sshd:/etc/pam.d/sshd:ro
- /docker/compose/scp_git_bridge/files/process_user_auth.sh:/process_user_auth.sh:ro
- /docker/compose/scp_git_bridge/files/sshd_config:/etc/ssh/sshd_config:ro
- /docker/compose/scp_git_bridge/files/update_user_jail.sh:/update_user_jail.sh:ro
- /docker/compose/scp_git_bridge/files/update_userlist.py:/update_userlist.py:ro
- /docker/compose/scp_git_bridge/files/rush.rc:/etc/rush.rc:ro
- /docker/compose/scp_git_bridge/files/pre-rush.sh:/pre-rush.sh:ro
- /docker/compose/scp_git_bridge/files/update_project_list.py:/update_project_list.py:ro
- /docker/compose/scp_git_bridge/files/config.toml:/etc/kc-ssh-pam/config.toml:ro
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 993:22
environment:
PUID: 1000
PGID: 1000
TZ: Etc/UTC
networks:
- overleaf-network
- keycloak-network
networks:
overleaf-network:
external: true
keycloak-network:
external: true

View file

@ -0,0 +1,2 @@
# m h dom mon dow command
*/5 * * * * sh /docker/compose/scp_git_bridge/exec_update_userlist.sh

View file

@ -0,0 +1,22 @@
These tools need to be placed inside the overleaf docker container. We do this automatically when this container goes up via the install.sh.
* auth_check_user.js
Is used for checking a user and the password against the overleaf user database
* download_zip.js
Downloads the data for a project id into a zip file.
* export_project_list_of_user.js
Given a userid the get a list of the project ids of the user
* get_user_list.js
We get the list of the list of all users in the overleaf user database
* id_user.js
We get the userid for a username (==email)

View file

@ -0,0 +1,66 @@
const { waitForDb } = require('/overleaf/services/web/app/src/infrastructure/mongodb')
const { User } = require('/overleaf/services/web/app/src/models/User')
const bcrypt = require('/overleaf/services/web/node_modules/bcrypt')
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: node auth_check_user.js <username> <password>');
process.exit(1);
}
const username = args[0];
const password = args[1];
const query = {"email": username };
try {
await waitForDb()
} catch (err) {
console.error('Cannot connect to mongodb')
process.exit(1); // fail
}
const user = await User.findOne(query).exec();
if (!user || !user.hashedPassword) {
process.exit(1); // fail
}
let rounds = 0
try {
rounds = bcrypt.getRounds(user.hashedPassword)
} catch (err) {
let prefix, suffix, length
if (typeof user.hashedPassword === 'string') {
length = user.hashedPassword.length
if (user.hashedPassword.length > 50) {
// A full bcrypt hash is 60 characters long.
prefix = user.hashedPassword.slice(0, '$2a$12$x'.length)
suffix = user.hashedPassword.slice(-4)
} else if (user.hashedPassword.length > 20) {
prefix = user.hashedPassword.slice(0, 4)
suffix = user.hashedPassword.slice(-4)
} else {
prefix = user.hashedPassword.slice(0, 4)
}
}
process.exit(1); // fail
}
const match = await bcrypt.compare(password, user.hashedPassword)
if (match) {
process.exit(0);
}
else {
process.exit(1); // fail
}
}
main().catch(err => {
console.error("An unexpected error occurred:", err); // Catch any unexpected errors
process.exit(1); // fail
});

Some files were not shown because too many files have changed in this diff Show more