diff --git a/.env.sample b/.env.sample index f9f3e95..1dcf8b6 100644 --- a/.env.sample +++ b/.env.sample @@ -9,3 +9,5 @@ DB_PASSWD=postgres DB_HOST=db DB_PORT=5432 EMAIL_API_KEY= +GROUP_KEY= +GOOGLE_CREDS_JSON='{}' diff --git a/.gitignore b/.gitignore index 88217a7..ab0875c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ node_modules/ .idea/ *.code-workspace venv/ -.venv/ +.venv/ \ No newline at end of file diff --git a/members/views/applications.py b/members/views/applications.py index a7a0220..b78fa3e 100644 --- a/members/views/applications.py +++ b/members/views/applications.py @@ -12,6 +12,7 @@ import logging import html from webapp.utils import send_email +from webapp.utils import add_to_mailinglist from members.views.utils import * from members.tables import RequestTable @@ -88,6 +89,9 @@ def application_accept(request, *args, **kwargs): ).format(application.email), ) + if application.jas: + add_to_mailinglist(application.email) + member = application.to_member() member.save() application.delete() diff --git a/poetry.lock b/poetry.lock index c22a0e2..867bda3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -45,6 +45,14 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "cachetools" +version = "5.2.0" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = "~=3.7" + [[package]] name = "certifi" version = "2022.6.15" @@ -328,6 +336,85 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "google-api-core" +version = "2.8.2" +description = "Google API client core library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +google-auth = ">=1.25.0,<3.0dev" +googleapis-common-protos = ">=1.56.2,<2.0dev" +protobuf = ">=3.15.0,<5.0.0dev" +requests = ">=2.18.0,<3.0.0dev" + +[package.extras] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] + +[[package]] +name = "google-api-python-client" +version = "2.54.0" +description = "Google API Client Library for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.19.0,<3.0.0dev" +google-auth-httplib2 = ">=0.1.0" +httplib2 = ">=0.15.0,<1dev" +uritemplate = ">=3.0.1,<5" + +[[package]] +name = "google-auth" +version = "2.9.1" +description = "Google Authentication Library" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} +six = ">=1.9.0" + +[package.extras] +aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"] +enterprise_cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +pyopenssl = ["pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] + +[[package]] +name = "google-auth-httplib2" +version = "0.1.0" +description = "Google Authentication Library: httplib2 transport" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.15.0" +six = "*" + +[[package]] +name = "googleapis-common-protos" +version = "1.56.4" +description = "Common protobufs used in Google APIs" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +protobuf = ">=3.15.0,<5.0.0dev" + +[package.extras] +grpc = ["grpcio (>=1.0.0,<2.0.0dev)"] + [[package]] name = "gunicorn" version = "20.1.0" @@ -342,6 +429,17 @@ gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "httplib2" +version = "0.20.4" +description = "A comprehensive HTTP client library." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + [[package]] name = "idna" version = "3.3" @@ -502,6 +600,14 @@ python-versions = ">=3.7" docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +[[package]] +name = "protobuf" +version = "4.21.3" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "psycopg2-binary" version = "2.9.3" @@ -510,6 +616,25 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyasn1-modules" +version = "0.2.8" +description = "A collection of ASN.1-based protocols modules." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.5.0" + [[package]] name = "pyexcel" version = "0.5.15" @@ -574,7 +699,7 @@ tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" +category = "main" optional = false python-versions = ">=3.6.8" @@ -653,6 +778,17 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "ruamel.yaml" version = "0.17.21" @@ -817,6 +953,14 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "urllib3" version = "1.26.11" @@ -890,7 +1034,9 @@ content-hash = "f51b15fd5c0f2623c253aba571dbfc587eb8323676bd7a59ee06a97d1c3f07a8 [metadata.files] asgiref = [] attrs = [] +babel = [] black = [] +cachetools = [] certifi = [] charset-normalizer = [] click = [ @@ -935,7 +1081,10 @@ djangorestframework = [ {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, ] -djangorestframework-simplejwt = [] +djangorestframework-simplejwt = [ + {file = "djangorestframework_simplejwt-5.2.0-py3-none-any.whl", hash = "sha256:bcc4cb74dcb637ca1e17eed35276bd618ab19491f8c53e65dee6271177c355e8"}, + {file = "djangorestframework_simplejwt-5.2.0.tar.gz", hash = "sha256:a60b09afb27d91ad1d7ac904cc632bd52cecead8f389f0fa1532ceb0fb757a74"}, +] dparse = [ {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, @@ -944,14 +1093,24 @@ et-xmlfile = [ {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, ] +google-api-core = [] +google-api-python-client = [] +google-auth = [] +google-auth-httplib2 = [] +googleapis-common-protos = [] gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] +httplib2 = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -importlib-metadata = [] +importlib-metadata = [ + {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, + {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, +] jdcal = [ {file = "jdcal-1.4.1-py2.py3-none-any.whl", hash = "sha256:1abf1305fce18b4e8aa248cf8fe0c56ce2032392bc64bbd61b5dff2a19ec8bba"}, {file = "jdcal-1.4.1.tar.gz", hash = "sha256:472872e096eb8df219c23f2689fc336668bdb43d194094b5cc1707e1640acfc8"}, @@ -990,7 +1149,10 @@ platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] +protobuf = [] psycopg2-binary = [] +pyasn1 = [] +pyasn1-modules = [] pyexcel = [ {file = "pyexcel-0.5.15-py2.py3-none-any.whl", hash = "sha256:7fac067e65567c380933b4d382587a5ce6581d0ad85992f6f0bc7c3f16012184"}, {file = "pyexcel-0.5.15.tar.gz", hash = "sha256:f0a7797f3a0de9e6f81151c9581fa90c4e1afce207dc47d2f0ba728dd2e24467"}, @@ -1003,7 +1165,10 @@ pyexcel-xlsx = [ {file = "pyexcel-xlsx-0.5.8.tar.gz", hash = "sha256:ab3913b465d0d645a51e3c896dc006738a398d36ceaad2dad133056132facb92"}, {file = "pyexcel_xlsx-0.5.8-py2.py3-none-any.whl", hash = "sha256:9bae2820c5767440d8a387695e0f8e8f73c97bcde0a5680c200ae82a2f6d5cc6"}, ] -pyjwt = [] +pyjwt = [ + {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, + {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, +] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, @@ -1080,6 +1245,7 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] requests = [] +rsa = [] "ruamel.yaml" = [] "ruamel.yaml.clib" = [] safety = [] @@ -1116,6 +1282,7 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [] +uritemplate = [] urllib3 = [] uwsgi = [ {file = "uwsgi-2.0.20.tar.gz", hash = "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9"}, diff --git a/production_entrypoint.sh b/production_entrypoint.sh index c58bfac..c39f2f1 100755 --- a/production_entrypoint.sh +++ b/production_entrypoint.sh @@ -10,6 +10,9 @@ fi if test -f "$DB_PASSWD_FILE"; then export DB_PASSWD=$(cat $DB_PASSWD_FILE) fi +if test -f "$GOOGLE_CREDS_JSON"; then + export GOOGLE_CREDS_JSON=$(cat $GOOGLE_CRED_JSON_FILE) +fi # Collect static files echo "Collect static files" diff --git a/pyproject.toml b/pyproject.toml index 261f713..6d3f63c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,8 @@ sentry-sdk = "^1.4.3" django-polymorphic = "^3.1.0" python-dotenv = "^0.20.0" djangorestframework-simplejwt = "^5.2.0" +google-auth = "^2.9.1" +google-api-python-client = "^2.54.0" [tool.poetry.dev-dependencies] coverage = "^6.4.2" diff --git a/sikweb/settings.py b/sikweb/settings.py index 97d2293..b979c15 100644 --- a/sikweb/settings.py +++ b/sikweb/settings.py @@ -15,6 +15,7 @@ import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sikweb.base import * from datetime import timedelta +import json load_dotenv() # loads the configs from .env @@ -79,6 +80,9 @@ DATABASES = { } } +# Google api settings +GROUP_KEY = os.getenv("GROUP_KEY", "") +GOOGLE_SERVICE_ACCOUNT = json.loads(os.getenv("GOOGLE_CREDS_JSON", "{}")) # JWT authentication SIMPLE_JWT = { diff --git a/stack-compose.yml b/stack-compose.yml index d90f298..15e6bdc 100644 --- a/stack-compose.yml +++ b/stack-compose.yml @@ -34,11 +34,13 @@ services: - SECRET_KEY_FILE=/run/secrets/BACKEND_SECRET_KEY - DB_PASSWD_FILE=/run/secrets/BACKEND_DB_PASSWD - EMAIL_API_KEY_FILE=/run/secrets/BACKEND_EMAIL_API_KEY + - GOOGLE_CREDS_JSON=/run/secrets/GOOGLE_CREDS_JSON secrets: - BACKEND_SECRET_KEY - BACKEND_DB_PASSWD - BACKEND_EMAIL_API_KEY + - GOOGLE_CREDS_JSON secrets: BACKEND_SECRET_KEY: external: true @@ -46,3 +48,5 @@ secrets: external: true BACKEND_EMAIL_API_KEY: external: true + GOOGLE_CREDS_JSON: + EXTERNAL: true diff --git a/webapp/utils.py b/webapp/utils.py index af24679..6f696c7 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -18,14 +18,21 @@ from sendgrid.helpers.mail import ( from django.template.loader import render_to_string from django.core.files.base import ContentFile from sikweb.settings import ( + DEPLOY_ENV, FRONTEND_URL, EMAIL_API_KEY, DEFAULT_EMAIL_FROM, DEFAULT_EMAIL_FROM_ADDR, ENABLE_AUTOMATIC_EMAILS, + GROUP_KEY, + GOOGLE_SERVICE_ACCOUNT, ) from datetime import timedelta +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + def get_file_extension(file_name, decoded_file): extension = imghdr.what(file_name, decoded_file) @@ -119,3 +126,34 @@ def send_signup_email(to, subject, id, uuid, content): def admin_send_email_signupees(list, subject, content): for to in list: send_email(to.email, subject, markdown.markdown(content), True) + + +def add_to_mailinglist(email: str): + try: + # get data + SCOPES = ["https://www.googleapis.com/auth/admin.directory.group"] + + # create credentials, with subject is used to impersonate admin account + # jas_manager has groups editor rights in google admin + credentials = service_account.Credentials.from_service_account_info( + info=GOOGLE_SERVICE_ACCOUNT, scopes=SCOPES + ).with_subject("jas_manager@sahkoinsinoorikilta.fi") + + service = build("admin", "directory_v1", credentials=credentials) + service.members().insert(groupKey=GROUP_KEY, body={"email": email}).execute() + except HttpError as err: + # Already in list, do nothing + if err.status_code == 409: + pass + else: + logging.exception("Failed adding user to list") + + # Send email notificcation to maintainer, only in prod + if DEPLOY_ENV == "production": + to = "ilari.ojakorpi@sahkoinsinoorikilta.fi" + subject = "Web error: Failed adding to google groups" + body = "Error code: {}\nError details: {}\nEmail that was not added: {}\n\nAdd user manually to jäsenet groups.".format( + err.status_code, err.error_details, email + ) + + send_email(to, subject, body)