Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix import calendar; add cookie-based auth #233

Merged
merged 50 commits into from
May 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
01768db
Switched to EmailMessage format, added time to confirmations, updated…
KaitlynKeil Apr 30, 2018
1a7e8d5
Merge branch 'dev' into origin/seamless/new-emails
KaitlynKeil Apr 30, 2018
6aaed59
Flake8 the top-level (celery) *.py files
osteele May 3, 2018
f99047b
Test update to python 3.6.5 and force env in travis
jaredbriskman May 3, 2018
ae4cc1e
Trying out more cutting edge travis
jaredbriskman May 3, 2018
7eab537
Don't need to install 3.6.5 with pyenv
jaredbriskman May 3, 2018
25a454d
Merge pull request #209 from olinlibrary/jwb/python-upgrade
osteele May 3, 2018
d63d19f
Added emailing back a link to edit event
KaitlynKeil May 3, 2018
4f2de51
Merge branch 'dev' into origin/seamless/new-emails
KaitlynKeil May 3, 2018
7560950
Merge pull request #205 from olinlibrary/osteele/flake8-celery
osteele May 3, 2018
e5e45b6
Merge branch 'dev' into origin/seamless/new-emails
KaitlynKeil May 3, 2018
92bf778
Fixed flake8 errors
KaitlynKeil May 3, 2018
d7cb83e
Removed unnecessary mock objects
KaitlynKeil May 3, 2018
8d2416f
Scaffolds use of a cookie for auth
jaredbriskman May 3, 2018
9a13163
Removed extra steps to formating email
KaitlynKeil May 3, 2018
acdea31
Run flake8 against tests
osteele May 4, 2018
b7f354e
Add more event tests
osteele May 4, 2018
6541525
Fixes comment typo
jaredbriskman May 4, 2018
272af43
Adds working cookie auth
jaredbriskman May 4, 2018
41c97e7
Upgrade to flask 1.0, breaks tests
jaredbriskman May 4, 2018
c91f2e1
Revert "Upgrade to flask 1.0, breaks tests"
jaredbriskman May 4, 2018
7d8f547
Adresses test failures
jaredbriskman May 4, 2018
4eefc3e
Adds tests for auth cookie
jaredbriskman May 4, 2018
30f7453
merge test auth flake8 fixes
jaredbriskman May 4, 2018
b41f87e
Adds more test cases
jaredbriskman May 4, 2018
e7dd469
Update SHARED_SECRET description
jaredbriskman May 4, 2018
c78d26c
Factors out auth check
jaredbriskman May 4, 2018
158217d
Merge pull request #220 from olinlibrary/osteele/flake8-tests
osteele May 4, 2018
4b35300
Wrapps cookie tests in subtests
jaredbriskman May 4, 2018
eb42fe2
Merge branch 'dev' into jwb/auth-secret
jaredbriskman May 4, 2018
3bc56f9
Github merge editor doesn't lint for PEP8
jaredbriskman May 4, 2018
937445b
Minor improvements to test_events.py
osteele May 4, 2018
2adaea2
Add test cases for /labels
osteele May 4, 2018
ba99c7c
Merge branch 'dev' into origin/seamless/new-emails
KaitlynKeil May 5, 2018
4817476
Fixing flake8
KaitlynKeil May 5, 2018
0a26c49
adding indents
KaitlynKeil May 5, 2018
21d6071
Actually managed correct indenting
KaitlynKeil May 5, 2018
5eebfda
Remove unused import
May 5, 2018
2f5355f
Merge pull request #219 from olinlibrary/origin/seamless/new-emails
osteele May 5, 2018
d765131
Merge pull request #223 from olinlibrary/osteele/test-events
osteele May 5, 2018
4d3a248
Merge pull request #222 from olinlibrary/jwb/auth-secret
osteele May 5, 2018
8ed00fb
Fixes auth tests
jaredbriskman May 5, 2018
8432b1f
clean up tests
jaredbriskman May 5, 2018
fbc7724
A little more tidying
jaredbriskman May 5, 2018
12c4956
Merge pull request #226 from olinlibrary/jwb/fixtests
osteele May 5, 2018
73a21fa
Fixed and added tests for failure to import URL
HALtheWise May 7, 2018
0523cad
Fixed error with URL imports and added tests
HALtheWise May 7, 2018
702a7d8
Reenabled new test.
HALtheWise May 7, 2018
e4dd6de
fix a typo
osteele May 7, 2018
26af86a
Merge pull request #232 from olinlibrary/import-fix
osteele May 7, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
HOST="127.0.0.1"
PORT="3000"
HSTS_ENABLE=""
SHARED_SECRET=""
17 changes: 12 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
language: python

python:
- '3.6'
group: travis_latest

cache: pip
services: mongodb
python: '3.6.5'

cache:
- pip

services:
- mongodb

env:
- PIPENV_IGNORE_VIRTUALENVS=1

install:
- cp .env.template .env
Expand All @@ -13,7 +20,7 @@ install:

script:
- pipenv run coverage run --source abe -m unittest
- pipenv run flake8 abe
- pipenv run flake8 abe tests *.py

after_success:
- codecov
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ url = "https://pypi.python.org/simple"
verify_ssl = true

[requires]
python_full_version = "3.6.4" #https://devcenter.heroku.com/articles/python-runtimes
python_full_version = "3.6.5" #https://devcenter.heroku.com/articles/python-runtimes

[packages]
amqp = ">=1.4.9"
Expand Down
320 changes: 142 additions & 178 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ $ open htmlcov/index.html
Lint all the things:

```shell
$ flake8 abe *.py
$ flake8 abe tests *.py
```

## API Documentation
Expand Down
9 changes: 8 additions & 1 deletion abe/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Main flask app"""
from flask import Flask, render_template, jsonify
from flask import Flask, render_template, jsonify, g
from flask_restplus import Api
from flask_cors import CORS
from flask_sslify import SSLify # redirect to https
Expand Down Expand Up @@ -72,6 +72,13 @@ def output_json(data, code, headers=None):
return resp


@app.after_request
def call_after_request_callbacks(response): # For deferred callbacks
for callback in getattr(g, 'after_request_callbacks', ()):
callback(response)
return response


# Route resources
api.add_namespace(event_api)
api.add_namespace(label_api)
Expand Down
34 changes: 29 additions & 5 deletions abe/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
from functools import wraps

from flask import request, abort
from flask import request, abort, g
from netaddr import IPNetwork, IPSet

# A set of IP addresses with edit permission.
Expand All @@ -17,14 +17,38 @@
INTRANET_IPS = (IPSet([IPNetwork(s) for s in os.environ.get('INTRANET_IPS', '').split(',')])
if 'INTRANET_IPS' in os.environ else IPSet(['0.0.0.0/0', '0000:000::/0']))

shared_secret = os.environ.get("SHARED_SECRET", '')


def after_this_request(f): # For setting cookie
if not hasattr(g, 'after_request_callbacks'):
g.after_request_callbacks = []
g.after_request_callbacks.append(f)
return f


def check_auth(req):
"""
Checks if a request is from an IP whitelist, or if it has a secret cookie.
If the request is in the IP whitelist, sets the secret cookie.
Returns a Bool of passing.
"""
client_ip = req.headers.get(
'X-Forwarded-For', req.remote_addr).split(',')[-1]
if client_ip in INTRANET_IPS:
@after_this_request
def remember_computer(response):
response.set_cookie('app_secret', shared_secret)
return True
else:
return bool(shared_secret) and (req.cookies.get('app_secret') == shared_secret)


def edit_auth_required(f):
"Decorates f to raise an HTTP UNAUTHORIZED exception if the client IP is not in the list of authorized IPs."
"Decorates f to raise an HTTP UNAUTHORIZED exception if the auth check fails."
@wraps(f)
def wrapped(*args, **kwargs):
client_ip = request.headers.get(
'X-Forwarded-For', request.remote_addr).split(',')[-1]
if client_ip not in INTRANET_IPS:
if not check_auth(request):
abort(401)
return f(*args, **kwargs)
return wrapped
52 changes: 24 additions & 28 deletions abe/helper_functions/email_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import icalendar as ic
from mongoengine import ValidationError
from datetime import datetime as dt
from email.message import EmailMessage

from abe import database as db
from abe.helper_functions.converting_helpers import mongo_to_dict
Expand All @@ -17,6 +19,7 @@
ABE_EMAIL_PASSWORD = os.environ.get('ABE_EMAIL_PASSWORD', None)
ABE_EMAIL_HOST = os.environ.get('ABE_EMAIL_HOST', 'mail.privateemail.com')
ABE_EMAIL_PORT = int(os.environ.get('ABE_EMAIL_PORT', 465))
APP_URL = os.environ.get('APP_URL', 'events.olin.build')


def get_msg_list(pop_items, pop_conn):
Expand Down Expand Up @@ -159,45 +162,38 @@ def error_reply(to, error):
""" Given the error, sends an email with the errors that
occured to the original sender. """
server, sent_from = smtp_connect()
subject = 'Event Failed to Add'
msg = EmailMessage()
body = "ABE didn't manage to add the event, sorry. Here's what went wrong: \n"
for err in error.errors:
body = body + str(err) + '\n'
body = body + "Final error message: " + error.message

email_text = """
From: {}
To: {}
Subject: {}

{}
""".format(sent_from, to, subject, body)

send_email(server, email_text, sent_from, to)
msg['Subject'] = 'Event Failed to Add'
msg['From'] = sent_from
msg['To'] = [to]
msg.set_content(body)
server.send_message(msg)
server.close()


def reply_email(to, event_dict):
""" Responds after a successful posting with
the tags under which the event was saved. """
server, sent_from = smtp_connect()
subject = '{} added to ABE!'.format(event_dict['title'])
tags = ', '.join(event_dict['labels']).strip()
body = "Your event was added to ABE! Here's the details: "

email_text = """
From: {}
To: {}
Subject: {}

{}
Description: {}
Tags: {}
""".format(sent_from, to, subject, body, event_dict['description'], tags)
send_email(server, email_text, sent_from, to)


def send_email(server, email_text, sent_from, sent_to):
server.sendmail(sent_from, sent_to, email_text.encode('utf-8'))
start = dt.strptime(event_dict['start'][:16], '%Y-%m-%d %H:%M').strftime('%I:%M %m/%d')
end = dt.strptime(event_dict['end'][:16], '%Y-%m-%d %H:%M').strftime('%I:%M %m/%d')
body = f"""Your event was added to ABE! Here's the details:
Time: {start} to {end}
Description: {event_dict['description']}
Tags: {tags}
Something wrong? Edit this event at {APP_URL}/edit/{event_dict['id']}
"""
msg = EmailMessage()
msg['Subject'] = f"{event_dict['title']} added to ABE!"
msg['From'] = sent_from
msg['To'] = [to]
msg.set_content(body)
server.send_message(msg)
server.close()


Expand Down
10 changes: 5 additions & 5 deletions abe/helper_functions/ics_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ def convert_timezone(a):
event_def['end'].replace(hour=23, minute=59, second=59)

elif isinstance(event_def['end'], datetime.date):
event_def['end'] = event_def['end'] - timedelta(day=1)
event_def['end'] = event_def['end'] - timedelta(days=1)
midnight_time = time(23, 59, 59)
event_def['end'] = datetime.combine(event_def['end'], midnight_time)
event_def['end'] = datetime.datetime.combine(event_def['end'], midnight_time)
event_def['allDay'] = True

event_def['labels'] = labels
Expand Down Expand Up @@ -253,7 +253,7 @@ def extract_ics(cal, ics_url, labels=None):
for component in cal.walk():
if component.name == "VEVENT":
last_modified = component.get('LAST-MODIFIED').dt
now = datetime.now(timezone.utc)
now = datetime.datetime.now(timezone.utc)
difference = now - last_modified
# if an event has been modified in the last two hours
if difference.total_seconds() < 7200:
Expand All @@ -278,11 +278,11 @@ def extract_ics(cal, ics_url, labels=None):
temporary_dict.append(com_dict)
logging.debug("temporarily saved recurring event as dict")
else: # if this is a regular event
com_dict['self'] = open('README.md')
try:
new_event = db.Event(**com_dict).save()
except: # FIXME: bare except # noqa: E722
logging.exception("com_dict: {}", com_dict)
logging.exception("com_dict: {}".format(com_dict))
continue
if not new_event.labels: # if the event has no labels
new_event.labels = ['unlabeled']
if 'recurrence' in new_event: # if the event has no recurrence_end
Expand Down
12 changes: 6 additions & 6 deletions abe/resource_models/label_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class LabelApi(Resource):
def get(self, label_name=None):
"""Retrieve labels"""
if label_name: # use label name/object id if present
logging.debug('Label requested: ' + label_name)
logging.debug('Label requested: %s', label_name)
search_fields = ['name', 'id']
result = multi_search(db.Label, label_name, search_fields)
if not result:
Expand All @@ -52,12 +52,12 @@ def get(self, label_name=None):
def post(self):
"""Create new label with parameters passed in through args or form"""
received_data = request_to_dict(request)
logging.debug("Received POST data: {}".format(received_data))
logging.debug("Received POST data: %s", received_data)
try:
new_label = db.Label(**received_data)
new_label.save()
except ValidationError as error:
logging.warning("POST request rejected: {}".format(str(error)))
logging.debug("POST request rejected: %s", error)
return {'error_type': 'validation',
'validation_errors': [str(err) for err in error.errors],
'error_message': error.message}, 400
Expand All @@ -69,7 +69,7 @@ def post(self):
def put(self, label_name):
"""Modify individual label"""
received_data = request_to_dict(request)
logging.debug("Received PUT data: {}".format(received_data))
logging.debug("Received PUT data: %s", received_data)
search_fields = ['name', 'id']
result = multi_search(db.Label, label_name, search_fields)
if not result:
Expand All @@ -88,14 +88,14 @@ def put(self, label_name):
@edit_auth_required
def delete(self, label_name):
"""Delete individual label"""
logging.debug('Label requested: ' + label_name)
logging.debug('Label requested: %s', label_name)
search_fields = ['name', 'id']
result = multi_search(db.Label, label_name, search_fields)
if not result:
return "Label not found with identifier '{}'".format(label_name), 404

received_data = request_to_dict(request)
logging.debug("Received DELETE data: {}".format(received_data))
logging.debug("Received DELETE data: %s", received_data)
result.delete()
return mongo_to_dict(result)

Expand Down
14 changes: 9 additions & 5 deletions abe/sample_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
sample_data_dir = Path(__file__).parent.parent / 'tests/data'
sample_events_file = sample_data_dir / 'sample-events.json'
sample_labels_file = sample_data_dir / 'sample-labels.json'
sample_ics_file = sample_data_dir / 'sample-ics-import.ics'

kelly_colors = [
'#F2F3F4', '#222222', '#F3C300', '#875692', '#F38400', '#A1CAF1', '#BE0032',
Expand All @@ -39,7 +40,8 @@

sample_ics = [
{
"url": "webcal://http://www.olin.edu/calendar-node-field-cal-event-date/ical/calendar.ics"
"url": "http://www.olin.edu/calendar-node-field-cal-event-date/ical/2018-05/calendar.ics",
"labels": ['Featured'],
}
]

Expand Down Expand Up @@ -112,7 +114,7 @@ def insert_data(db, event_data=None, label_data=None, ics_data=None):
db.ICS(**ics).save()


SampleData = namedtuple('SampleData', ['events', 'labels', 'icss'])
SampleData = namedtuple('SampleData', ['events', 'labels', 'icss', 'ics_data'])


def load_sample_data():
Expand All @@ -124,13 +126,15 @@ def load_sample_data():
event_data = load_event_data(fp)
with open(sample_labels_file) as fp:
label_data = json.load(fp)
return SampleData(event_data, label_data, sample_ics)
with open(sample_ics_file) as fp:
ics_data = fp.read()
return SampleData(event_data, label_data, sample_ics, ics_data)


def load_data(db):
"""Load the database with sample data. The Heroku postdeploy
script calls this."""
event_data, label_data, sample_ics = load_sample_data()
event_data, label_data, sample_ics, _ = load_sample_data()
insert_data(db, event_data, label_data, sample_ics)


Expand All @@ -147,7 +151,7 @@ def main(events=None, labels=None):
label_data = json.load(labels) if labels else None
ics_data = None
if all(data is None for data in (event_data, label_data, ics_data)):
event_data, label_data, ics_data = load_sample_data()
event_data, label_data, ics_data, _ = load_sample_data()
insert_data(db, event_data, label_data, ics_data)


Expand Down
4 changes: 4 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"description": "Enforce SSL via HSTS",
"value": "True",
"required": true
},
"SHARED_SECRET": {
"description": "A shared secret for an authorized user cookie",
"generator": "secret"
}
},
"formation": {
Expand Down
15 changes: 8 additions & 7 deletions celeryconfig.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
from celery.schedules import crontab
from datetime import timedelta

'''
CELERY_RESULT_BACKEND = "mongodb"
CELERY_MONGODB_BACKEND_SETTINGS = {
"host": "127.0.0.1",
"port": 27017,
"database": "jobs",
"database": "jobs",
"taskmeta_collection": "stock_taskmeta_collection",
}
'''
#used to schedule tasks periodically and passing optional arguments
#Can be very useful. Celery does not seem to support scheduled task but only periodic

# used to schedule tasks periodically and passing optional arguments
# Can be very useful. Celery does not seem to support scheduled task but only periodic
CELERYBEAT_SCHEDULE = {
'refresh-every-2-hours': {
'task': 'tasks.refresh_calendar',
'schedule': timedelta(seconds=7200),
},
'refresh-every-minute': {
'task':'tasks.parse_email_icals',
'schedule': timedelta(seconds=60),
'task': 'tasks.parse_email_icals',
'schedule': timedelta(seconds=60),
}
}
}
Loading