Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' into signup_text
Browse files Browse the repository at this point in the history
  • Loading branch information
temparus authored Dec 17, 2023
2 parents d74b97e + ece574e commit 7d52386
Show file tree
Hide file tree
Showing 14 changed files with 304 additions and 59 deletions.
17 changes: 16 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
interval: "monthly"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
Expand All @@ -13,3 +17,14 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
eve:
patterns:
- "eve"
- "flask"
- "pymongo"
test:
patterns:
- "pytest*"
- "tox"
- "flake8"
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Now it's time to configure AMIV API. Create a file `config.py`


```python
import logging

# Root password, *definitely* change this!
ROOT_PASSWORD = 'root'

Expand All @@ -96,6 +98,8 @@ MONGO_DBNAME = 'amivapi'
MONGO_USERNAME = 'amivapi'
MONGO_PASSWORD = 'amivapi'

LOG_LEVEL = logging.INFO

# Sentry error logging
# SENTRY_DSN = "https://<key>@sentry.io/<project>"
# SENTRY_ENVIRONMENT = 'production'
Expand Down
34 changes: 17 additions & 17 deletions amivapi/auth/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""Sessions endpoint."""

import datetime
import re

from amivapi import ldap
from amivapi.auth import AmivTokenAuth
Expand All @@ -17,13 +18,7 @@
from flask import abort
from flask import current_app as app
from ldap3.core.exceptions import LDAPException


# Change when we drop python3.5 support
try:
from secrets import token_urlsafe
except ImportError:
from amivapi.utils import token_urlsafe
from secrets import token_urlsafe


class SessionAuth(AmivTokenAuth):
Expand Down Expand Up @@ -173,28 +168,33 @@ def process_login(items):
items (list): List of items as passed by EVE to post hooks.
"""
for item in items:
username = item['username']
username = ldap_username = item['username']
password = item['password']

# If the username matches an ethz email address, we just take the part
# before the @ as the username for the authentication against LDAP.
if re.match(r"^[^@]+@([^@]+[.]{1}){0,1}ethz.ch$", username):
ldap_username = username.split('@', 2)[0]

# LDAP
if (app.config.get('ldap_connector') and
ldap.authenticate_user(username, password)):
ldap.authenticate_user(ldap_username, password)):
# Success, sync user and get token
try:
user = ldap.sync_one(username)
user = ldap.sync_one(ldap_username)
app.logger.info(
"User '%s' was authenticated with LDAP" % username)
"User '%s' was authenticated with LDAP" % ldap_username)
except LDAPException:
# Sync failed! Try to find user in db.
user = _find_user(username)
user = _find_user(ldap_username)
if user:
app.logger.error(
f"User '{username}' authenticated with LDAP and found "
"in db, but LDAP sync failed.")
f"User '{ldap_username}' authenticated with LDAP and "
"found in db, but LDAP sync failed.")
else:
status = (f"Login failed: user '{username}' authenticated "
"with LDAP but not found in db, and LDAP sync "
"failed.")
status = (f"Login failed: user '{ldap_username}' "
"authenticated with LDAP but not found in db, "
"and LDAP sync failed.")
app.logger.error(status)
abort(401, description=debug_error_message(status))

Expand Down
2 changes: 2 additions & 0 deletions amivapi/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from os import getcwd, getenv
from os.path import abspath
import logging

from eve import Eve
from flask import Config
Expand Down Expand Up @@ -88,6 +89,7 @@ def create_app(config_file=None, **kwargs):
app = Eve("amivapi", # Flask needs this name to find the static folder
settings=config,
validator=ValidatorAMIV)
app.logger.setLevel(app.config.get('LOG_LEVEL') or logging.INFO)
app.logger.info(config_status)

# Set up error logging with sentry
Expand Down
4 changes: 4 additions & 0 deletions amivapi/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
)
from amivapi.events.queue import (
add_accepted_before_insert,
notify_users_after_update,
update_waiting_list_after_delete,
update_waiting_list_after_insert,
)
Expand Down Expand Up @@ -72,4 +73,7 @@ def init_app(app):
app.on_deleted_item_eventsignups += notify_signup_deleted
app.on_deleted_item_eventsignups += update_waiting_list_after_delete

# Notify users on manual updates
app.on_updated_eventsignups += notify_users_after_update

app.register_blueprint(email_blueprint)
32 changes: 30 additions & 2 deletions amivapi/events/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
Needed when users are notified about their event signups.
"""
from datetime import datetime, timezone
from flask import current_app, url_for
from itsdangerous import URLSafeSerializer

from amivapi.events.utils import get_token_secret
from amivapi.utils import mail_from_template
from amivapi.utils import mail_from_template, get_calendar_invite


def find_reply_to_email(event):
Expand Down Expand Up @@ -49,13 +50,39 @@ def notify_signup_accepted(event, signup, waiting_list=False):

deletion_link = url_for('emails.on_delete_signup', token=token,
_external=True)
event_id = event[id_field]
title_en = event['title_en']
title_de = event['title_de']
description_en = event['description_en']
description_de = event['description_de']
location = (event['location'] or '')
time_start = event['time_start']
time_end = event['time_end']
time_now = datetime.now(timezone.utc)

signup_additional_info_en = event['signup_additional_info_en']
signup_additional_info_de = event['signup_additional_info_de']

reply_to_email = find_reply_to_email(event)

# Time is a required property for ics calendar events
if time_start:
calendar_invite = (
get_calendar_invite('events_accept_calendar_invite', dict(
title=(title_en or title_de),
event_id=event_id,
time_start=time_start,
time_end=time_end,
time_now=time_now,
description=(description_en or description_de or ''),
location=location,
signup_additional_info=(signup_additional_info_en or
signup_additional_info_de or
''),
)))
else:
calendar_invite = None

if waiting_list:
mail_from_template(
to=[email],
Expand Down Expand Up @@ -83,7 +110,8 @@ def notify_signup_accepted(event, signup, waiting_list=False):
signup_additional_info_de=(signup_additional_info_de or
signup_additional_info_en),
deadline=event['time_deregister_end']),
reply_to=reply_to_email)
reply_to=reply_to_email,
calendar_invite=calendar_invite)


def notify_signup_deleted(signup):
Expand Down
12 changes: 12 additions & 0 deletions amivapi/events/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,15 @@ def update_waiting_list_after_delete(signup):
return

update_waiting_list(signup['event'])


def notify_users_after_update(signup_updates, original_signup):
"""Hook to notify users after a signup is updated."""
if signup_updates.get('accepted') and not original_signup.get('accepted'):
# User was on the waitinglist and got accepted: Notify him
lookup = {current_app.config['ID_FIELD']: original_signup.get('event')}
event = current_app.data.find_one('events', None, **lookup)
if event is not None:
new_signup = original_signup.copy()
new_signup.update(signup_updates)
notify_signup_accepted(event, new_signup, False)
10 changes: 7 additions & 3 deletions amivapi/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,13 @@ def _process_data(data):
to the correct fields for the user resource.
"""
res = {'nethz': data.get('cn', [None])[0],
'legi': data.get('swissEduPersonMatriculationNumber'),
'firstname': data.get('givenName', [None])[0],
'lastname': data.get('sn', [None])[0]}
if ('swissEduPersonMatriculationNumber' in data and
isinstance(data['swissEduPersonMatriculationNumber'], str)):
# add legi only if the LDAP value is a string as it might also be an
# empty array.
res['legi'] = data['swissEduPersonMatriculationNumber']
if res['nethz'] is not None:
# email can be removed when Eve switches to Cerberus 1.x, then
# We could do this as a default value in the user model
Expand All @@ -165,7 +169,7 @@ def _process_data(data):
res['gender'] = \
u"male" if int(data['swissEduPersonGender']) == 1 else u"female"

# See file docstring for explanation of `deparmentNumber` field
# See file docstring for explanation of `departmentNumber` field
# In some rare cases, the departmentNumber field is either empty
# or missing -> normalize to empty string
department_info = next(iter(
Expand Down Expand Up @@ -210,7 +214,7 @@ def _create_or_update_user(ldap_data):
with admin_permissions():
if db_data:
# Membership will not be downgraded and email not be overwritten
# Newletter settings will also not be adjusted
# Newsletter settings will also not be adjusted
ldap_data.pop('email', None)
if db_data.get('membership') != u"none":
ldap_data.pop('membership', None)
Expand Down
19 changes: 19 additions & 0 deletions amivapi/templates/events_accept_calendar_invite.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:https://www.amiv.ethz.ch/
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
METHOD:PUBLISH
BEGIN:VEVENT
UID:{{ event_id }}@amiv.ethz.ch
LOCATION:{{ location }}
SUMMARY:{{ title }}
{% if signup_additional_info is not none and signup_additional_info|length -%}
DESCRIPTION:{{ signup_additional_info }}\n\n{{ description }}
{% else -%}
DESCRIPTION:{{ description }}
{% endif -%}
DTSTART:{{ time_start.strftime('%Y%m%dT%H%M%SZ') }}
DTEND:{{ time_end.strftime('%Y%m%dT%H%M%SZ') }}
DTSTAMP:{{ time_now.strftime('%Y%m%dT%H%M%SZ') }}
END:VEVENT
END:VCALENDAR
4 changes: 2 additions & 2 deletions amivapi/tests/auth/test_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_relogin(self):
user = self.new_object('users')
token = self.get_user_token(user['_id'])

self.api.set_cookie('localhost', 'token', token)
self.api.set_cookie('token', token, domain='localhost')
login_page = self.api.get(
'/oauth?'
'response_type=token'
Expand Down Expand Up @@ -245,7 +245,7 @@ def test_personal_greeting(self):
user = self.new_object('users', firstname='Pablito')
token = self.get_user_token(user['_id'])

self.api.set_cookie('localhost', 'token', token)
self.api.set_cookie('token', token, domain='localhost')
login_page = self.api.get(
'/oauth?'
'response_type=token'
Expand Down
Loading

0 comments on commit 7d52386

Please sign in to comment.