commit 70d158712a9d23639ba5a0d77506e7d7a7810460 Author: Marco Aceti Date: Mon Feb 21 18:32:14 2022 +0100 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bfd2757 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,213 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..f30ed84 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,35 @@ +name: Docker build and push + +on: + push: + branches: [ master ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Login to ghcr registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ secrets.GH_USERNAME }} + password: ${{ secrets.GH_ACCESS_TOKEN }} + + - name: Prepare version info + run: | + echo "LATEST_COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Build and push Docker images + uses: docker/build-push-action@v2.2.2 + with: + context: . + file: ./Dockerfile + tags: | + ghcr.io/studentiunimi/overleaf-registration:${{ env.LATEST_COMMIT_SHA }} + ghcr.io/studentiunimi/overleaf-registration:latest + push: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfd2757 --- /dev/null +++ b/.gitignore @@ -0,0 +1,213 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f70de41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:slim + +COPY requirements.txt . +RUN pip3 install -r requirements.txt + +COPY . . + +EXPOSE 8000 +ENTRYPOINT ["gunicorn", "wsgi:app", \ + "--env", "CAPTCHA_CLIENT_KEY=$CAPTCHA_CLIENT_KEY", \ + "--env", "CAPTCHA_SERVER_KEY=$CAPTCHA_SERVER_KEY", \ + "--env", "OF_INSTANCE=$OF_INSTANCE", \ + "--env", "OF_ADMIN_EMAIL=$OF_ADMIN_EMAIL", \ + "--env", "OF_ADMIN_PASSWORD=$OF_ADMIN_PASSWORD" \ +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..51ac0c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 StudentiUniMi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8d3d3b --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Overleaf registration worker + +Public user registration for a self-hosted +[Overleaf](https://github.com/overleaf/overleaf) instance. + +### Why? +We are planning to offer a public Overleaf instance to our users (students and teachers), +but the Community Edition does not support autonomous user registration: only the site +administrator can create users via the admin panel. + +This limitation is unacceptable for our use case, so we implemented it ourselves. + +### How? +A simple form (available on `/register` path) is offered to the user asking for its email; +the application then logs into the Overleaf instance with the administrator account +and sends a request to create a user. + +The user can now create an account by clicking the confirmation link on its mailbox. + +## Deployment +There is a Docker image available on +[ghcr.io/studentiunimi/overleaf-registration](https://ghcr.io/studentiunimi/overleaf-registration), +automatically built by GitHub Actions. +You can check the example `docker-compose.yml` file and tweak it with your configuration. + +### Environment variables +The Docker container needs all the following environment variables to function properly: + +| Environment variable | Description | +|----------------------|------------------------------------------------------| +| `CAPTCHA_SERVER_KEY` | reCAPTCHA v3 server key | +| `CAPTCHA_CLIENT_KEY` | reCAPTCHA v3 client key | +| `OL_INSTANCE` | Overleaf self-hosted instance (without trailing `/`) | +| `OL_ADMIN_EMAIL` | Overleaf administrator account email | +| `OL_ADMIN_PASSWORD` | Overleaf administrator account password | diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d02aec7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3' + +networks: + traefik_net: + external: true + +services: + overleaf-registration: + image: ghcr.io/studentiunimi/overleaf-registration + container_name: overleaf-registration + environment: + - CAPTCHA_SERVER_KEY= + - CAPTCHA_CLIENT_KEY= + - OL_INSTANCE= + - OL_ADMIN_EMAIL= + - OL_ADMIN_PASSWORD= + networks: + - traefik_net + deploy: + labels: + - "traefik.enable=true" + - "traefik.http.services.overleaf-registration.loadbalancer.server.port=8000" diff --git a/main.py b/main.py new file mode 100644 index 0000000..4b7d0dd --- /dev/null +++ b/main.py @@ -0,0 +1,42 @@ +import re +import os + +import requests +from flask import Flask, render_template, Response, request + +import overleaf + +app = Flask(__name__) + + +def _check_captcha(code: str) -> bool: + r = requests.post("https://www.google.com/recaptcha/api/siteverify", data={ + "secret": os.environ.get("CAPTCHA_SERVER_KEY"), + "response": code, + }) + return r.json()["success"] + + +def _check_email(email: str) -> bool: + return bool(re.fullmatch(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email)) + + +@app.route("/register", methods=["GET", "POST"]) +def index() -> Response: + if request.method == "GET": + return render_template("register.html", CAPTCHA_CLIENT_KEY=os.environ.get("CAPTCHA_CLIENT_KEY")) + + elif request.method == "POST": + captcha_code: str = request.form.get("g-recaptcha-response") + if not _check_captcha(captcha_code): + return render_template("error.html", error="Invalid CAPTCHA"), 403 + + email: str = request.form.get("email") + if not _check_email(email): + return render_template("error.html", error="Invalid email"), 400 + + ol = overleaf.Overleaf(os.environ.get("OL_INSTANCE")) + ol.login(os.environ.get("OL_ADMIN_EMAIL"), os.environ.get("OL_ADMIN_PASSWORD")) + ol.register_user(email) + ol.logout() + return render_template("done.html", submitted_email=email) diff --git a/overleaf.py b/overleaf.py new file mode 100644 index 0000000..33055a7 --- /dev/null +++ b/overleaf.py @@ -0,0 +1,45 @@ +import requests +from bs4 import BeautifulSoup + + +class Overleaf: + def __init__(self, instance_url: str): + self.url = instance_url + self.session = requests.session() + self._csrf = None + self._init_session() + + def _get(self, *args, **kwargs) -> requests.Response: + return self.session.get(self.url + args[0], *args[1:], **kwargs) + + def _post(self, url, data=None) -> requests.Response: + self._obtain_csrf() + url = self.url + url + if data is None: + data = {} + data["_csrf"] = self._csrf + return self.session.post(url, json=data) + + def _init_session(self) -> None: + self._get("/") + + def _obtain_csrf(self) -> str: + resp: requests.Response = self._get("/login") + soup = BeautifulSoup(resp.text, "html.parser") + self._csrf = soup.find("meta", {"name": "ol-csrfToken"})["content"] + + def login(self, email, password) -> None: + r = self._post("/login", data={ + "email": email, + "password": password, + }) + if r.status_code != 200: + raise ValueError("incorrect email or password") + + def logout(self) -> None: + self._post("/logout") + + def register_user(self, email) -> None: + self._post("/admin/register", data={ + "email": email, + }) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e4a286c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +gunicorn diff --git a/templates/done.html b/templates/done.html new file mode 100644 index 0000000..586ec95 --- /dev/null +++ b/templates/done.html @@ -0,0 +1,9 @@ + + + Overleaf registration: done + + +

Done!

+ Please check your email ({{ submitted_email }}) and click on the link to confirm your account. + + \ No newline at end of file diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..9f0d40b --- /dev/null +++ b/templates/error.html @@ -0,0 +1,9 @@ + + + Overleaf registration: done + + +

Error: {{ error }}

+ Please retry on the previous page. + + \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..d28507a --- /dev/null +++ b/templates/register.html @@ -0,0 +1,29 @@ + + + StudentiUniMi Overleaf registration + + +

StudentiUniMi Overleaf registration

+ +
+ + + +
+ We suggest you to use your @studenti.unimi.it or @unimi.it email. + +
+ +
+ + + + + \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..c867004 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,4 @@ +from main import app + +if __name__ == "__main__": + app.run(debug=False)