Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
61df32f
Refactor minor details
pjewald Apr 27, 2017
bdf5f0f
Add code for birthdate and parent email
pjewald Apr 27, 2017
d1223e8
Add application version number
pjewald Apr 27, 2017
45b4b4d
Add more COPPA fields
pjewald Apr 28, 2017
04f03fd
Correct error in date selection
pjewald Apr 28, 2017
4a11ee5
Add trap for missing SMTP server
pjewald May 3, 2017
631edee
Exclude local shell scripts
pjewald May 5, 2017
8b6a76a
Add missing mustache templates to project. Update missing SMTP server…
pjewald May 5, 2017
53557d4
Correct missing COPPA data block
pjewald May 8, 2017
6621b41
Add SponsorType enum to make code more readable
pjewald May 11, 2017
db7b859
Add code to send registration confirmation email to the correct email…
pjewald May 11, 2017
39224fc
Add templates for teachers and parent emails. Moved SponsorType to em…
pjewald May 11, 2017
3bfd977
Create parent templates. Rename teacher template path
pjewald May 12, 2017
9627ff9
Update code to handle COPPA compliant email templates
pjewald May 12, 2017
5eb6e21
Add logging to help track code paths.
pjewald May 12, 2017
afd17dd
Complete draft email to teacher to register a student.
pjewald May 12, 2017
6fe37c1
Updates to make the COPPA parent email work
pjewald May 16, 2017
49aec57
Bumping up the version number. 1.1.0 include support for US COPPA reg…
pjewald May 16, 2017
282c643
Add parent notification for US-EN language. Add paragraph to link to …
pjewald May 19, 2017
ef71d2a
Add documentation and model version number
pjewald May 26, 2017
8143144
Hide build files
pjewald May 26, 2017
9210266
Merge branch 'demo' of github.com:zfi/Cloud-Session into demo
pjewald May 26, 2017
642eebc
Add template macro for blocklyprop host. Updated templates to use the…
pjewald Jun 12, 2017
cabbadc
Add to configuration file documentation
pjewald Jun 20, 2017
4c2d481
Add deployment scripts to ignore
pjewald Jun 20, 2017
4ff17d7
Update templates to use new email aliases for studdent and teacher. A…
pjewald Jun 20, 2017
d957386
Update email template to note seven day registration expiration.
pjewald Jun 20, 2017
8a53392
Add database migration scripts. 0001 and 0002 were moved from the Blo…
pjewald Jun 20, 2017
27e8c52
Add base database schema to project
pjewald Jun 20, 2017
5562ce6
Update email message sent to parent of registering child.
pjewald Jun 21, 2017
6c60d2d
Update message text. Add logging to capture a failed template parse.
pjewald Jun 21, 2017
372e40d
Bumping version to 1.1.1
pjewald Jun 21, 2017
59394bc
Add code to return a timestamp to indicate whent he next token will b…
pjewald Jun 27, 2017
e59f91d
Merge updates to demo branch
pjewald Jun 27, 2017
53361bf
Correct timestamp issue in error 470 handler
pjewald Jun 28, 2017
f6e5d55
Correct datetime string formatting error. Bump patch level
pjewald Jun 28, 2017
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ nbactions.xml
#################

.idea
build
copy-test
deploy-test
deploy2coppa
/CloudSession-Templates.tar.gz
8 changes: 7 additions & 1 deletion Failures.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,17 @@ def screen_name_already_in_use(screen_name):
}, 500


def rate_exceeded():
def rate_exceeded(time):
"""
Service requested to frequently.

time - string representing the date and time the service will be available again
"""
logging.debug('Failures: Rate exceeded')
return {
'success': False,
'message': 'Insufficient bucket tokens',
'data': time,
'code': 470
}, 500

Expand Down
3 changes: 1 addition & 2 deletions app/AuthToken/controllers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# Import the database object from the main app module
import json
import logging
import uuid
import datetime

import Failures

from app import db
from app.User import services as user_service

Expand Down
1 change: 0 additions & 1 deletion app/AuthToken/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# We will define this inside /app/__init__.py in the next sections.
from app import db


class AuthenticationToken(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
id_user = db.Column(db.BigInteger, db.ForeignKey('user.id'))
Expand Down
10 changes: 6 additions & 4 deletions app/Authenticate/controllers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Import the database object from the main app module
import logging
import uuid
import datetime

import Failures

from app import db
from app.User import services as user_services
from app.RateLimiting import services as rate_limiting_services
Expand Down Expand Up @@ -66,7 +64,11 @@ def post(self):
'email': user.email,
'locale': user.locale,
'screenname': user.screen_name,
'authentication-source': user.auth_source
'authentication-source': user.auth_source,
'bdmonth': user.birth_month,
'bdyear': user.birth_year,
'parent-email': user.parent_email,
'parent-email-source': user.parent_email_source
}}

api.add_resource(AuthenticateLocalUser, '/local')
95 changes: 84 additions & 11 deletions app/Email/services.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,102 @@
from app import mail, app, db

from app import mail, app
from os.path import expanduser, isfile

from flask.ext.mail import Message
from app.User.coppa import Coppa, SponsorType

import pystache
import logging


"""
TODO: System documentation goes here
"""


def send_email_template_for_user(id_user, template, server, **kwargs):
from app.User.services import get_user

logging.info("Sending email to user: %s (%s)", id_user, template)
# Get a copy of the user record
logging.info("Checking for a valid user record for user ID: %s", id_user)
user = get_user(id_user)

if user is None:
logging.error("Cannot send email: Invalid user record")
return False
else:
logging.info("Valid record found for user: %s", user.id)

logging.info("Sending email to user: %s using template: '%s'.", user.id, template)

params = {}
for key, value in kwargs.items():
logging.debug("Logging parameter %s = %s", key, value)
params[key] = value

user = get_user(id_user)
if user is None:
return False

# The elements in the params array represent the data elements that are
# available to the email templates.
params['screenname'] = user.screen_name
params['email'] = user.email
params['registrant-email'] = user.email
params['sponsoremail'] = user.parent_email
params['blocklyprop-host'] = app.config['CLOUD_SESSION_PROPERTIES']['response.host']

#Default the recipient email address
user_email = user.email
coppa = Coppa()

# Send email to parent if user is under 13 years old
if template == 'confirm' and coppa.is_coppa_covered(user.birth_month, user.birth_year):
# Send email only to the sponsor address
user_email = user.parent_email
logging.info("COPPA account has a sponsor type of %s", user.parent_email_source)

if user.parent_email_source == SponsorType.TEACHER:
# Teacher handles the account confirmation
send_email_template_to_address(user_email, 'confirm-teacher', server, user.locale, params)
elif user.parent_email_source == SponsorType.PARENT or\
user.parent_email_source == SponsorType.GUARDIAN:
# Parent handles the account confirmation
send_email_template_to_address(user_email, 'confirm-parent', server, user.locale, params)
else:
logging.info("COPPA account %s has invalid sponsor type [%s]", user.id, user.parent_email_source)

send_email_template_to_address(user.email, template, server, user.locale, params)
return
elif template == 'reset' and coppa.is_coppa_covered(user.birth_month, user.birth_year):
# Send email only to the sponsor address
logging.info("COPPA account has a sponsor type of %s", user.parent_email_source)

# Send password reset to student and parent
send_email_template_to_address(user.email, 'reset-coppa', server, user.locale, params)
send_email_template_to_address(user.parent_email, 'reset-coppa', server, user.locale, params)
return
else:
# Registration not subject to COPPA regulations
send_email_template_to_address(user_email, template, server, user.locale, params)

return


def send_email_template_to_address(recipient, template, server, locale, params=None, **kwargs):
# Read templates
logging.info("Preparing email template: %s", template)
params = params or {}

# Add any supplied arguments to the parameter dictionary
for key, value in kwargs.items():
params[key] = value

params['email'] = recipient
params['locale'] = locale

# Read templates
(subject, plain, rich) = _read_templates(template, server, locale, params)
# Add error checking here to detect any issues with parsing the template.

logging.info("Sending email to %s", recipient)
send_email(recipient, subject, plain, rich)


def send_email(recipient, subject, email_text, rich_email_text=None):

msg = Message(
recipients=[recipient],
subject=subject.rstrip(),
Expand All @@ -51,8 +108,13 @@ def send_email(recipient, subject, email_text, rich_email_text=None):


def _read_templates(template, server, locale, params):
logging.info("Loading header text for template: %s", template)
header = _read_template(template, server, locale, 'header', params)

logging.info("Loading plain message text for template: %s", template)
plain = _read_template(template, server, locale, 'plain', params)

logging.info("Loading rich message text for template: %s", template)
rich = _read_template(template, server, locale, 'rich', params, True)

return header, plain, rich
Expand All @@ -73,10 +135,21 @@ def _read_template(template, server, locale, part, params, none_if_missing=False
error message if the none_is_missing flag is false
"""
template_file = expanduser("~/templates/%s/%s/%s/%s.mustache" % (locale, template, server, part))

if isfile(template_file):
logging.debug('Looking for template file: %s', template_file)

renderer = pystache.Renderer()
rendered = renderer.render_path(template_file, params)

logging.debug('Rendering the template file')
try:
rendered = renderer.render_path(template_file, params)
except Exception as ex:
logging.error('Unable to render template file %s', template_file)
logging.error('Error message: %s', ex.message)
return 'Template format error.'

logging.debug('Returning rendered template file.')
return rendered
else:
logging.warn('Looking for template file: %s, but the file is missing', template_file)
Expand Down
23 changes: 16 additions & 7 deletions app/LocalUser/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def post(self):

confirm_token = ConfirmToken.query.filter_by(token=token).first()
if confirm_token is None:
# Unkown token
# Unknown token
return {'success': False, 'code': 510}
if confirm_token.id_user != user.id:
# Token is not for this user
Expand All @@ -65,9 +65,11 @@ def post(self):
class RequestConfirm(Resource):

def get(self, email):
# Get values
# Get server URL
server = request.headers.get('server')

logging.info("Requesting email confirmation for %s from server %s", email, server)

# Validate required fields
validation = Validation()
validation.add_required_field('email', email)
Expand Down Expand Up @@ -95,11 +97,18 @@ def get(self, email):
else:
if code == 10:
return Failures.rate_exceeded()
return {
'success': False,
'message': message,
'code': 520
}
elif code == 99:
return {
'success': False,
'message': message,
'code': 540
}
else:
return {
'success': False,
'message': message,
'code': 520
}


class PasswordReset(Resource):
Expand Down
16 changes: 12 additions & 4 deletions app/RateLimiting/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@ def get(self, bucket_type, id_user):
return Failures.email_not_confirmed()

bucket_types = app.config['CLOUD_SESSION_PROPERTIES']['bucket.types'].split(',')

if bucket_type not in bucket_types:
return Failures.unknown_bucket_type(bucket_type)

if not rate_limiting_services.consume_tokens(user.id, bucket_type, 1):
# Decrement a token count
result, next_time = rate_limiting_services.consume_tokens(user.id, bucket_type, 1)

if not result:
db.session.commit()
return Failures.rate_exceeded()
return Failures.rate_exceeded(next_time.strftime("%Y-%m-%d %H:%M:%S"))

db.session.commit()

Expand Down Expand Up @@ -82,6 +86,7 @@ def get(self, bucket_type, id_user, count):

# Validate user exists, is validated and is not blocked
user = user_services.get_user(id_user)

if user is None:
return Failures.unknown_user_id(id_user)
if user.blocked:
Expand All @@ -90,12 +95,15 @@ def get(self, bucket_type, id_user, count):
return Failures.email_not_confirmed()

bucket_types = app.config['CLOUD_SESSION_PROPERTIES']['bucket.types'].split(',')

if bucket_type not in bucket_types:
return Failures.unknown_bucket_type(bucket_type)

if not rate_limiting_services.consume_tokens(user.id, bucket_type, 1):
result, next_time = rate_limiting_services.consume_tokens(user.id, bucket_type, 1)

if not result:
db.session.commit()
return Failures.rate_exceeded()
return Failures.rate_exceeded(next_time.strftime("%Y-%m-%d %H:%M:%S"))

db.session.commit()

Expand Down
6 changes: 3 additions & 3 deletions app/RateLimiting/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ def consume_tokens(id_user, bucket_type, token_count):
milliseconds_till_enough = (token_count - old_bucket_content) * bucket_stream_frequency
date_when_enough = bucket.timestamp + datetime.timedelta(milliseconds=milliseconds_till_enough)
# Log and return or throw error
return False
return False, date_when_enough

bucket.content = bucket.content - token_count
bucket.timestamp = datetime.datetime.now()
return True
return True, bucket.timestamp


def has_sufficient_tokens(id_user, bucket_type, token_count):
Expand Down Expand Up @@ -70,6 +70,6 @@ def has_sufficient_tokens(id_user, bucket_type, token_count):
milliseconds_till_enough = (token_count - old_bucket_content) * bucket_stream_frequency
date_when_enough = bucket.timestamp + datetime.timedelta(milliseconds=milliseconds_till_enough)
# Log and return or throw error
return False
return False, date_when_enough

return True
Loading