From d93ea216c51d6aab4d9304f675514ae7ae528bae Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 2 Feb 2017 09:35:27 -0800 Subject: [PATCH 01/20] Change characterization of a missing template file as a warning instead of an error. Added function documentation. --- app/Email/services.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/Email/services.py b/app/Email/services.py index 2fab71c..0f13575 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -59,15 +59,27 @@ def _read_templates(template, server, locale, params): def _read_template(template, server, locale, part, params, none_if_missing=False): + """ + Render a mustache template. + + :param template: Base template name + :param server: Host server + :param locale: Language designator + :param part: Generic message type descriptor + :param params: Text string to replace tags embedded within the template + :param none_if_missing: Return 'none' if the requested template is not found + + :return: Upon success, return a Renderer object. Return none or a general + 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) - #print(rendered) return rendered else: - logging.error('Looking for template file: %s, but the file is missing', template_file) + logging.warn('Looking for template file: %s, but the file is missing', template_file) if none_if_missing: return None else: From 947d00bb673e396d43e9811e107521a778e4fa96 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Mon, 6 Feb 2017 17:24:33 -0800 Subject: [PATCH 02/20] Add artifacts from package operation to the .gitignore file. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c7771ae..47e079a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.sqlite *.log venv/ +build.sh +CloudSession-Pkg.tar.gz ################# ## NetBeans From c733908e918102c9404f9d5c4c5d34ca8c000cc0 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Mon, 27 Feb 2017 10:53:54 -0800 Subject: [PATCH 03/20] Add example configuration file to project --- cloudsession.properties.example | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 cloudsession.properties.example diff --git a/cloudsession.properties.example b/cloudsession.properties.example new file mode 100644 index 0000000..f2898f3 --- /dev/null +++ b/cloudsession.properties.example @@ -0,0 +1,38 @@ +# +# Sample Cloud Session configuration file +# + +# Database connection +database.url = jdbc:mysql://database.example.com:3306/cloudsession +database.username = database_user +database.password = database_user_password + +# SMTP server configuration +mail.host = smtp.example.com +mail.from = no_reply@example.com +mail.authenticated = true +mail.user = authenticated_username +mail.password = authenticated_user_password +mail.tls = true + +# The email notification system relies on a number of templates +# to produce the email messages sent to users. +email.template.path = /usr/share/tomcat7/templates + +# Rate limiting +# Starting number of compiles +bucket.compile.size = 100 + +# Number of tokens added per interval +bucket.compile.input = 50 + +# Token add interval (ms) +bucket.compile.freq = 500000 + +# Enable detailed statistics on applicaiton operation +metrics.console.enable = false + +# Bucket types are objects that can be applied to the rate-limiting service built +# into the server. Bucket types are arbitrary units. There can be more than one +# bucket type defined. +bucket.types = compile From 61df32f9bc345a1a9ee3a58bf5d54ee58cf3e0af Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 26 Apr 2017 19:09:27 -0700 Subject: [PATCH 04/20] Refactor minor details --- app/User/controllers.py | 17 ++++++++++++++--- app/__init__.py | 30 ++++++++++++++++-------------- requirements.txt | 1 + 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/User/controllers.py b/app/User/controllers.py index c4c8dbc..92e2c78 100644 --- a/app/User/controllers.py +++ b/app/User/controllers.py @@ -12,6 +12,7 @@ from app.User import services as user_service from models import User +# Define the endpoint prefix for user services user_app = Blueprint('user', __name__, url_prefix='/user') api = Api(user_app) @@ -72,7 +73,7 @@ def get(self, id_user): # Parse numbers try: id_user = int(id_user) - except: + except ValueError: return Failures.not_a_number('idUser', id_user) # Validate user exists, is validated and is not blocked @@ -133,6 +134,7 @@ class DoInfoChange(Resource): def post(self, id_user): screen_name = request.form.get('screenname') + # Validate required fields validation = Validation() validation.add_required_field('id-user', id_user) @@ -143,7 +145,7 @@ def post(self, id_user): # Parse numbers try: id_user = int(id_user) - except: + except ValueError: return Failures.not_a_number('idUser', id_user) # Validate user exists, is validated and is not blocked @@ -174,6 +176,7 @@ class DoLocaleChange(Resource): def post(self, id_user): locale = request.form.get('locale') + # Validate required fields validation = Validation() validation.add_required_field('id-user', id_user) @@ -184,7 +187,7 @@ def post(self, id_user): # Parse numbers try: id_user = int(id_user) - except: + except ValueError: return Failures.not_a_number('idUser', id_user) # Validate user exists, is validated and is not blocked @@ -206,9 +209,17 @@ def post(self, id_user): }} +# Supported endpoints +# Register a new user account api.add_resource(Register, '/register') + +# Retrieve details about an existing user account api.add_resource(GetUserById, '/id/') api.add_resource(GetUserByEmail, '/email/') api.add_resource(GetUserByScreenname, '/screenname/') + +# Update a user screen name api.add_resource(DoInfoChange, '/info/') + +# Update the local defined in the user account api.add_resource(DoLocaleChange, '/locale/') diff --git a/app/__init__.py b/app/__init__.py index 0ca2971..e90bda0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,17 +1,19 @@ -# Import flask and template operators +""" +Cloud Session server application initialization + +""" # Import properties files utils import logging from ConfigParser import ConfigParser - -import os from FakeSecHead import FakeSecHead from os.path import expanduser, isfile # Import Flask -from flask import Flask, render_template +# from flask import Flask, render_template +from flask import Flask -# Import SQLAlchemy +# Import SQLAlchemy database mapper from flask.ext.sqlalchemy import SQLAlchemy # Import Mail @@ -20,6 +22,13 @@ # Define the WSGI application object from raven.contrib.flask import Sentry +from app.AuthToken.controllers import auth_token_app +from app.Authenticate.controllers import authenticate_app +from app.User.controllers import user_app +from app.LocalUser.controllers import local_user_app +from app.RateLimiting.controllers import rate_limiting_app +from app.OAuth.controllers import oauth_app + app = Flask(__name__) # Load basic configurations @@ -86,11 +95,10 @@ logging.info("No Sentry configuration") -app.config['SQLALCHEMY_DATABASE_URI'] = app.config['CLOUD_SESSION_PROPERTIES']['database.url'] - # Define the database object which is imported # by modules and controllers logging.info("Initializing database connection") +app.config['SQLALCHEMY_DATABASE_URI'] = app.config['CLOUD_SESSION_PROPERTIES']['database.url'] db = SQLAlchemy(app) app.config['MAIL_SERVER'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.host'] @@ -101,6 +109,7 @@ app.config['MAIL_PORT'] = 25 else: app.config['MAIL_PORT'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.port'] + app.config['MAIL_USE_TLS'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.tls'] app.config['MAIL_USE_SSL'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.ssl'] app.config['MAIL_DEBUG'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.debug'] @@ -108,18 +117,11 @@ app.config['MAIL_PASSWORD'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.password'] app.config['DEFAULT_MAIL_SENDER'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.from'] -#app.debug = True logging.info("Initializing mail") mail = Mail(app) # -------------------------------------------- Services -------------------------------------------------------- logging.info("Initializing services") -from app.AuthToken.controllers import auth_token_app -from app.Authenticate.controllers import authenticate_app -from app.User.controllers import user_app -from app.LocalUser.controllers import local_user_app -from app.RateLimiting.controllers import rate_limiting_app -from app.OAuth.controllers import oauth_app app.register_blueprint(auth_token_app) app.register_blueprint(authenticate_app) diff --git a/requirements.txt b/requirements.txt index 9fd50b6..680ec6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ SQLAlchemy==1.0.12 validate-email==1.3 Werkzeug==0.11.5 wheel==0.24.0 +validate_email==1.3 From bdf5f0fcd1d133c2db56b76935274c9313927b2a Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 27 Apr 2017 14:00:18 -0700 Subject: [PATCH 05/20] Add code for birthdate and parent email --- .gitignore | 2 ++ app/AuthToken/controllers.py | 3 +-- app/AuthToken/models.py | 1 - app/User/controllers.py | 43 +++++++++++++++++++++++++++++++----- app/User/models.py | 5 +++++ app/User/services.py | 15 +++++++++++-- app/__init__.py | 10 ++++++--- 7 files changed, 65 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 47e079a..13d8b65 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ nbactions.xml ################# .idea +/deploy-test.sh +deploy-test.sh diff --git a/app/AuthToken/controllers.py b/app/AuthToken/controllers.py index 5f0664a..0fb64fa 100644 --- a/app/AuthToken/controllers.py +++ b/app/AuthToken/controllers.py @@ -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 diff --git a/app/AuthToken/models.py b/app/AuthToken/models.py index dd6e1d6..bea10aa 100644 --- a/app/AuthToken/models.py +++ b/app/AuthToken/models.py @@ -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')) diff --git a/app/User/controllers.py b/app/User/controllers.py index 92e2c78..4d24cdf 100644 --- a/app/User/controllers.py +++ b/app/User/controllers.py @@ -28,6 +28,11 @@ def post(self): locale = request.form.get('locale') screen_name = request.form.get('screenname') + # COPPA support + birth_month = request.form.get('bdmonth') + birth_year = request.form.get('bdyear') + parent_email = request.form.get('parent-email') + # Validate required fields validation = Validation() validation.add_required_field('server', server) @@ -36,6 +41,16 @@ def post(self): validation.add_required_field('password-confirm', password_confirm) validation.add_required_field('locale', locale) validation.add_required_field('screenname', screen_name) + + # COPPA support + validation.add_required_field('bdmonth', birth_month) + validation.add_required_field('bdyear', birth_year) + if parent_email: + validation.check_email('parent-email', parent_email) + if not validation.is_valid(): + return validation.get_validation_response() + + # Verify user email address validation.check_email('email', email) if not validation.is_valid(): return validation.get_validation_response() @@ -56,7 +71,8 @@ def post(self): if not user_service.check_password_complexity(password): return Failures.password_complexity() - id_user = user_service.create_local_user(server, email, password, locale, screen_name) + id_user = user_service.create_local_user( + server, email, password, locale, screen_name, birth_month, birth_year, parent_email) user_service.send_email_confirm(id_user, server) db.session.commit() @@ -88,7 +104,10 @@ def get(self, id_user): '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 }} @@ -107,7 +126,10 @@ def get(self, email): '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 }} @@ -126,7 +148,10 @@ def get(self, screen_name): '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 }} @@ -168,7 +193,10 @@ def post(self, id_user): '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 }} @@ -205,7 +233,10 @@ def post(self, id_user): '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 }} diff --git a/app/User/models.py b/app/User/models.py index 08310b0..8fcfb31 100644 --- a/app/User/models.py +++ b/app/User/models.py @@ -12,6 +12,11 @@ class User(db.Model): confirmed = db.Column(db.Boolean) screen_name = db.Column(db.String(250)) + # COPPA support + birth_month = db.Column(db.INTEGER, nullable=False) + birth_year = db.Column(db.INTEGER, nullable=False) + parent_email = db.Column(db.String(250)) + def __init__(self): self.blocked = False self.confirmed = False diff --git a/app/User/services.py b/app/User/services.py index 1a71da8..2bc9458 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -39,7 +39,7 @@ def check_password_complexity(password): return 8 <= len(password) < 200 -def create_local_user(server, email, password, locale, screen_name): +def create_local_user(server, email, password, locale, screen_name, birth_month, birth_year, parent_email): salt, password_hash = get_password_hash(password) # Save user @@ -51,6 +51,11 @@ def create_local_user(server, email, password, locale, screen_name): user.password = password_hash user.salt = salt + #COPPA support + user.birth_month = birth_month + user.birth_year = birth_year + user.parent_email = parent_email + db.session.add(user) db.session.flush() db.session.refresh(user) @@ -58,7 +63,7 @@ def create_local_user(server, email, password, locale, screen_name): return user.id -def create_oauth_user(server, email, source, locale, screen_name): +def create_oauth_user(server, email, source, locale, screen_name, birth_month, birth_year, parent_email): # Save user user = User() user.email = email @@ -68,6 +73,12 @@ def create_oauth_user(server, email, source, locale, screen_name): user.confirmed = True user.blocked = False + # COPPA support + user.birth_month = birth_month + user.birth_year = birth_year + user.parent_email = parent_email + + # Add the user record db.session.add(user) db.session.flush() db.session.refresh(user) diff --git a/app/__init__.py b/app/__init__.py index e90bda0..74fe84a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -67,8 +67,11 @@ 'bucket.email-confirm.freq': '1800000' } +logging.basicConfig(level=logging.DEBUG) + configfile = expanduser("~/cloudsession.properties") -print('Looking for config file: %s' % configfile) +logging.info('Looking for config file: %s', configfile) + if isfile(configfile): configs = ConfigParser(defaults) configs.readfp(FakeSecHead(open(configfile))) @@ -82,8 +85,6 @@ # -------------------------------------- Module initialization ------------------------------------------------- -logging.basicConfig(level=logging.DEBUG) - if app.config['CLOUD_SESSION_PROPERTIES']['sentry-dsn'] is not None: logging.info("Initializing Sentry") sentry = Sentry(app, @@ -101,6 +102,7 @@ app.config['SQLALCHEMY_DATABASE_URI'] = app.config['CLOUD_SESSION_PROPERTIES']['database.url'] db = SQLAlchemy(app) +logging.info("Configuring SMTP properties") app.config['MAIL_SERVER'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.host'] if app.config['CLOUD_SESSION_PROPERTIES']['mail.port'] is None: if app.config['CLOUD_SESSION_PROPERTIES']['mail.tls']: @@ -110,6 +112,8 @@ else: app.config['MAIL_PORT'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.port'] +logging.info("SMTP port: %s", app.config['MAIL_PORT'] ) + app.config['MAIL_USE_TLS'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.tls'] app.config['MAIL_USE_SSL'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.ssl'] app.config['MAIL_DEBUG'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.debug'] From d1223e8753536476584c8b93815301cc060f5e13 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 27 Apr 2017 14:49:48 -0700 Subject: [PATCH 06/20] Add application version number --- app/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 74fe84a..201c103 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -22,14 +22,16 @@ # Define the WSGI application object from raven.contrib.flask import Sentry -from app.AuthToken.controllers import auth_token_app from app.Authenticate.controllers import authenticate_app +from app.AuthToken.controllers import auth_token_app from app.User.controllers import user_app from app.LocalUser.controllers import local_user_app from app.RateLimiting.controllers import rate_limiting_app from app.OAuth.controllers import oauth_app app = Flask(__name__) +version = "1.0.1" +db = None # Load basic configurations app.config.from_object('config') @@ -79,7 +81,9 @@ app_configs = {} for (key, value) in configs.items('section'): app_configs[key] = value + app.config['CLOUD_SESSION_PROPERTIES'] = app_configs + else: app.config['CLOUD_SESSION_PROPERTIES'] = defaults @@ -95,14 +99,14 @@ else: logging.info("No Sentry configuration") - # Define the database object which is imported # by modules and controllers -logging.info("Initializing database connection") +# logging.info("Initializing database connection") app.config['SQLALCHEMY_DATABASE_URI'] = app.config['CLOUD_SESSION_PROPERTIES']['database.url'] db = SQLAlchemy(app) -logging.info("Configuring SMTP properties") + +# logging.info("Configuring SMTP properties") app.config['MAIL_SERVER'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.host'] if app.config['CLOUD_SESSION_PROPERTIES']['mail.port'] is None: if app.config['CLOUD_SESSION_PROPERTIES']['mail.tls']: From 45b4b4de653cfbd0b44a640597db20a99f599610 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 28 Apr 2017 14:08:40 -0700 Subject: [PATCH 07/20] Add more COPPA fields --- app/Email/services.py | 10 ++++++++-- app/User/controllers.py | 23 +++++++++++++++++------ app/User/models.py | 1 + app/User/services.py | 33 +++++++++++++++++++++++++++++++-- app/__init__.py | 25 ++++++++++++++++--------- 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/app/Email/services.py b/app/Email/services.py index 0f13575..f61163f 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -9,7 +9,7 @@ def send_email_template_for_user(id_user, template, server, **kwargs): - from app.User.services import get_user + from app.User.services import get_user, is_coppa_covered logging.info("Sending email to user: %s (%s)", id_user, template) @@ -23,7 +23,13 @@ def send_email_template_for_user(id_user, template, server, **kwargs): params['screenname'] = user.screen_name - send_email_template_to_address(user.email, template, server, user.locale, params) + # Send email to parent if user is under 13 years old + if is_coppa_covered(user.birth_month, user.birth_year): + user_email = user.parent_email + else: + user_email = user.email + + send_email_template_to_address(user_email, template, server, user.locale, params) def send_email_template_to_address(recipient, template, server, locale, params=None, **kwargs): diff --git a/app/User/controllers.py b/app/User/controllers.py index 4d24cdf..11289f3 100644 --- a/app/User/controllers.py +++ b/app/User/controllers.py @@ -32,6 +32,7 @@ def post(self): birth_month = request.form.get('bdmonth') birth_year = request.form.get('bdyear') parent_email = request.form.get('parent-email') + parent_email_source = request.form.get('parent-email-source') # Validate required fields validation = Validation() @@ -71,10 +72,15 @@ def post(self): if not user_service.check_password_complexity(password): return Failures.password_complexity() + # Write user details to the database id_user = user_service.create_local_user( - server, email, password, locale, screen_name, birth_month, birth_year, parent_email) + server, email, password, locale, screen_name, + birth_month, birth_year, parent_email, parent_email_source) + + # Send a confirmation request email to user or parent user_service.send_email_confirm(id_user, server) + # Commit the database record db.session.commit() logging.info('User-controller: register success: %s', id_user) @@ -107,7 +113,8 @@ def get(self, id_user): 'authentication-source': user.auth_source, 'bdmonth': user.birth_month, 'bdyear': user.birth_year, - 'parent-email': user.parent_email + 'parent-email': user.parent_email, + 'parent-email-source': user.parent_email_source }} @@ -129,7 +136,8 @@ def get(self, email): 'authentication-source': user.auth_source, 'bdmonth': user.birth_month, 'bdyear': user.birth_year, - 'parent-email': user.parent_email + 'parent-email': user.parent_email, + 'parent-email-source': user.parent_email_source }} @@ -151,7 +159,8 @@ def get(self, screen_name): 'authentication-source': user.auth_source, 'bdmonth': user.birth_month, 'bdyear': user.birth_year, - 'parent-email': user.parent_email + 'parent-email': user.parent_email, + 'parent-email-source': user.parent_email_source }} @@ -196,7 +205,8 @@ def post(self, id_user): 'authentication-source': user.auth_source, 'bdmonth': user.birth_month, 'bdyear': user.birth_year, - 'parent-email': user.parent_email + 'parent-email': user.parent_email, + 'parent-email-source': user.parent_email_source }} @@ -236,7 +246,8 @@ def post(self, id_user): 'authentication-source': user.auth_source, 'bdmonth': user.birth_month, 'bdyear': user.birth_year, - 'parent-email': user.parent_email + 'parent-email': user.parent_email, + 'parent-email-source': user.parent_email_source }} diff --git a/app/User/models.py b/app/User/models.py index 8fcfb31..71ee9b2 100644 --- a/app/User/models.py +++ b/app/User/models.py @@ -16,6 +16,7 @@ class User(db.Model): birth_month = db.Column(db.INTEGER, nullable=False) birth_year = db.Column(db.INTEGER, nullable=False) parent_email = db.Column(db.String(250)) + parent_email_source = db.Column(db.INTEGER) def __init__(self): self.blocked = False diff --git a/app/User/services.py b/app/User/services.py index 2bc9458..dd24f39 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -39,7 +39,10 @@ def check_password_complexity(password): return 8 <= len(password) < 200 -def create_local_user(server, email, password, locale, screen_name, birth_month, birth_year, parent_email): +def create_local_user( + server, email, password, locale, screen_name, + birth_month, birth_year, parent_email, parent_email_source): + salt, password_hash = get_password_hash(password) # Save user @@ -55,6 +58,7 @@ def create_local_user(server, email, password, locale, screen_name, birth_month, user.birth_month = birth_month user.birth_year = birth_year user.parent_email = parent_email + user.parent_email_source = parent_email_source db.session.add(user) db.session.flush() @@ -63,7 +67,10 @@ def create_local_user(server, email, password, locale, screen_name, birth_month, return user.id -def create_oauth_user(server, email, source, locale, screen_name, birth_month, birth_year, parent_email): +def create_oauth_user( + server, email, source, locale, screen_name, + birth_month, birth_year, parent_email, parent_email_source): + # Save user user = User() user.email = email @@ -77,6 +84,7 @@ def create_oauth_user(server, email, source, locale, screen_name, birth_month, b user.birth_month = birth_month user.birth_year = birth_year user.parent_email = parent_email + user.parent_email_source = parent_email_source # Add the user record db.session.add(user) @@ -148,3 +156,24 @@ def send_password_reset(id_user, server): email_services.send_email_template_for_user(id_user, 'reset', server, token=token) return True, 0, 'Success' + +# Return true if the date is less than 13 years +def is_coppa_covered(month,year): + # This is the number of months a typical thirteen years old has been on planet Earth. + cap = 156 + + # This is the actual number of months a typical user has been on the same planet. + user_age = (year * 12) + month + + # Current year and month + current_month = datetime.now().month + current_year = datetime.now().year + + # This represents the number of months since the inception of AD + # Unless you want to count that first year as part of BC. + current_cap = (current_year * 12) + current_month + + if current_cap - user_age > cap: + return False + else + return True diff --git a/app/__init__.py b/app/__init__.py index 201c103..8ab180d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -22,13 +22,6 @@ # Define the WSGI application object from raven.contrib.flask import Sentry -from app.Authenticate.controllers import authenticate_app -from app.AuthToken.controllers import auth_token_app -from app.User.controllers import user_app -from app.LocalUser.controllers import local_user_app -from app.RateLimiting.controllers import rate_limiting_app -from app.OAuth.controllers import oauth_app - app = Flask(__name__) version = "1.0.1" db = None @@ -116,8 +109,6 @@ else: app.config['MAIL_PORT'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.port'] -logging.info("SMTP port: %s", app.config['MAIL_PORT'] ) - app.config['MAIL_USE_TLS'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.tls'] app.config['MAIL_USE_SSL'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.ssl'] app.config['MAIL_DEBUG'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.debug'] @@ -125,12 +116,28 @@ app.config['MAIL_PASSWORD'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.password'] app.config['DEFAULT_MAIL_SENDER'] = app.config['CLOUD_SESSION_PROPERTIES']['mail.from'] + logging.info("Initializing mail") +logging.info("SMTP port: %s", app.config['MAIL_PORT']) +logging.info("TLS: %s",app.config['MAIL_USE_TLS']) +logging.info("SSL: %s",app.config['MAIL_USE_SSL']) +logging.info("Sender: %s",app.config['DEFAULT_MAIL_SENDER']) + mail = Mail(app) # -------------------------------------------- Services -------------------------------------------------------- logging.info("Initializing services") +# All of these imports need the database +if db is not None: + from app.Authenticate.controllers import authenticate_app + from app.AuthToken.controllers import auth_token_app + from app.User.controllers import user_app + from app.LocalUser.controllers import local_user_app + from app.RateLimiting.controllers import rate_limiting_app + from app.OAuth.controllers import oauth_app + + app.register_blueprint(auth_token_app) app.register_blueprint(authenticate_app) app.register_blueprint(user_app) From 04f03fd6d83a160c6fe43e99490010c6ca0f94e9 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 28 Apr 2017 16:06:46 -0700 Subject: [PATCH 08/20] Correct error in date selection --- app/User/services.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/User/services.py b/app/User/services.py index dd24f39..a63de3e 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -166,8 +166,8 @@ def is_coppa_covered(month,year): user_age = (year * 12) + month # Current year and month - current_month = datetime.now().month - current_year = datetime.now().year + current_month = datetime.date.today().month + current_year = datetime.date.today().year # This represents the number of months since the inception of AD # Unless you want to count that first year as part of BC. @@ -175,5 +175,5 @@ def is_coppa_covered(month,year): if current_cap - user_age > cap: return False - else + else: return True From 4a11ee560a5e6b8ca7052c4a44899e51ba865e17 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 3 May 2017 09:30:56 -0700 Subject: [PATCH 09/20] Add trap for missing SMTP server --- .gitignore | 4 ++-- app/LocalUser/controllers.py | 17 ++++++++++++----- app/User/controllers.py | 2 +- app/User/services.py | 5 ++++- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 13d8b65..1b5d2b6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,5 @@ nbactions.xml ################# .idea -/deploy-test.sh -deploy-test.sh +build +deploy-test diff --git a/app/LocalUser/controllers.py b/app/LocalUser/controllers.py index ba0a45e..8d51dec 100644 --- a/app/LocalUser/controllers.py +++ b/app/LocalUser/controllers.py @@ -95,11 +95,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): diff --git a/app/User/controllers.py b/app/User/controllers.py index 11289f3..f754cf2 100644 --- a/app/User/controllers.py +++ b/app/User/controllers.py @@ -10,7 +10,7 @@ from Validation import Validation from app.User import services as user_service -from models import User +# from models import User # Define the endpoint prefix for user services user_app = Blueprint('user', __name__, url_prefix='/user') diff --git a/app/User/services.py b/app/User/services.py index a63de3e..78ba6ef 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -122,7 +122,10 @@ def send_email_confirm(id_user, server): confirm_token.validity = datetime.datetime.now() + datetime.timedelta(hours=token_validity_time) db.session.add(confirm_token) - email_services.send_email_template_for_user(id_user, 'confirm', server, token=token) + try: + email_services.send_email_template_for_user(id_user, 'confirm', server, token=token) + except Exception as ex: + return False, 99, 'Unable to contact SMTP server' return True, 0, 'Success' From 8b6a76a1b01a131fc81061278f415bcd0b87e572 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 5 May 2017 15:35:09 -0700 Subject: [PATCH 10/20] Add missing mustache templates to project. Update missing SMTP server error handler. --- app/User/controllers.py | 22 ++++++++++++------- app/User/services.py | 6 +++-- .../en/confirm/blocklyprop/header.mustache | 1 + .../en/confirm/blocklyprop/plain.mustache | 8 +++++++ .../en/reset/blocklyprop/header.mustache | 1 + templates/en/reset/blocklyprop/plain.mustache | 11 ++++++++++ .../en_US/confirm/blocklyprop/header.mustache | 1 + .../en_US/confirm/blocklyprop/plain.mustache | 8 +++++++ .../en_US/reset/blocklyprop/header.mustache | 1 + .../en_US/reset/blocklyprop/plain.mustache | 11 ++++++++++ 10 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 templates/en/confirm/blocklyprop/header.mustache create mode 100644 templates/en/confirm/blocklyprop/plain.mustache create mode 100644 templates/en/reset/blocklyprop/header.mustache create mode 100644 templates/en/reset/blocklyprop/plain.mustache create mode 100644 templates/en_US/confirm/blocklyprop/header.mustache create mode 100644 templates/en_US/confirm/blocklyprop/plain.mustache create mode 100644 templates/en_US/reset/blocklyprop/header.mustache create mode 100644 templates/en_US/reset/blocklyprop/plain.mustache diff --git a/app/User/controllers.py b/app/User/controllers.py index f754cf2..769baf9 100644 --- a/app/User/controllers.py +++ b/app/User/controllers.py @@ -17,6 +17,7 @@ api = Api(user_app) +# Register a new user class Register(Resource): def post(self): @@ -78,15 +79,17 @@ def post(self): birth_month, birth_year, parent_email, parent_email_source) # Send a confirmation request email to user or parent - user_service.send_email_confirm(id_user, server) + (result, errno, mesg) = user_service.send_email_confirm(id_user, server) + if result: + # Commit the database record + db.session.commit() + logging.info('User-controller: register success: %s', id_user) - # Commit the database record - db.session.commit() - - logging.info('User-controller: register success: %s', id_user) - - # Create user - return {'success': True, 'user': id_user} + # Create user + return {'success': True, 'user': id_user} + else: + logging.error("Unable to register user. Error %s: %s", errno, mesg) + return {'success': False, 'user': 0} class GetUserById(Resource): @@ -252,6 +255,9 @@ def post(self, id_user): # Supported endpoints +# Note: The url_prefix is '/user'. All user endpoints are in the form +# of host:port/user/_service_ +# # Register a new user account api.add_resource(Register, '/register') diff --git a/app/User/services.py b/app/User/services.py index 78ba6ef..155c080 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -1,6 +1,6 @@ import hashlib import uuid - +import logging import datetime from app import db, app @@ -124,7 +124,9 @@ def send_email_confirm(id_user, server): try: email_services.send_email_template_for_user(id_user, 'confirm', server, token=token) - except Exception as ex: + except Exception as ex: + print("Exception {0}", ex.args) + logging.error("Unable to send email. Message is: %s", ex.message) return False, 99, 'Unable to contact SMTP server' return True, 0, 'Success' diff --git a/templates/en/confirm/blocklyprop/header.mustache b/templates/en/confirm/blocklyprop/header.mustache new file mode 100644 index 0000000..8d1f20a --- /dev/null +++ b/templates/en/confirm/blocklyprop/header.mustache @@ -0,0 +1 @@ +Please confirm your email address for BlocklyProp diff --git a/templates/en/confirm/blocklyprop/plain.mustache b/templates/en/confirm/blocklyprop/plain.mustache new file mode 100644 index 0000000..ff39fcb --- /dev/null +++ b/templates/en/confirm/blocklyprop/plain.mustache @@ -0,0 +1,8 @@ +Dear {{screenname}}, + +Please go to http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. + +If the url does not work, please go to http://localhost:8080/blockly/confirm and enter your email address and the token: {{token}} + + +The Parallax team diff --git a/templates/en/reset/blocklyprop/header.mustache b/templates/en/reset/blocklyprop/header.mustache new file mode 100644 index 0000000..630929b --- /dev/null +++ b/templates/en/reset/blocklyprop/header.mustache @@ -0,0 +1 @@ +Reset your BlocklyProp password diff --git a/templates/en/reset/blocklyprop/plain.mustache b/templates/en/reset/blocklyprop/plain.mustache new file mode 100644 index 0000000..9a5e5f5 --- /dev/null +++ b/templates/en/reset/blocklyprop/plain.mustache @@ -0,0 +1,11 @@ +Dear {{screenname}}, + +A request was made to reset your BlocklyProp account password. + +If this request was made by you, please go to http://localost:8080/blockly/reset?locale={{locale}}&email={{email}}&token={{token}} to reset your password now. + +If the url does not work, please go to http://localhost:8080/blockly/reset and use the token: {{token}} + +If you did not make this request, you may safely ignore this message. + +The Parallax team diff --git a/templates/en_US/confirm/blocklyprop/header.mustache b/templates/en_US/confirm/blocklyprop/header.mustache new file mode 100644 index 0000000..8d1f20a --- /dev/null +++ b/templates/en_US/confirm/blocklyprop/header.mustache @@ -0,0 +1 @@ +Please confirm your email address for BlocklyProp diff --git a/templates/en_US/confirm/blocklyprop/plain.mustache b/templates/en_US/confirm/blocklyprop/plain.mustache new file mode 100644 index 0000000..ff39fcb --- /dev/null +++ b/templates/en_US/confirm/blocklyprop/plain.mustache @@ -0,0 +1,8 @@ +Dear {{screenname}}, + +Please go to http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. + +If the url does not work, please go to http://localhost:8080/blockly/confirm and enter your email address and the token: {{token}} + + +The Parallax team diff --git a/templates/en_US/reset/blocklyprop/header.mustache b/templates/en_US/reset/blocklyprop/header.mustache new file mode 100644 index 0000000..630929b --- /dev/null +++ b/templates/en_US/reset/blocklyprop/header.mustache @@ -0,0 +1 @@ +Reset your BlocklyProp password diff --git a/templates/en_US/reset/blocklyprop/plain.mustache b/templates/en_US/reset/blocklyprop/plain.mustache new file mode 100644 index 0000000..59f709e --- /dev/null +++ b/templates/en_US/reset/blocklyprop/plain.mustache @@ -0,0 +1,11 @@ +Dear {{screenname}}, + +A request was made to reset your BlocklyProp account password. + +If this request was made by you, please go to http://localhost:8080/blockly/reset?locale={{locale}}&email={{email}}&token={{token}} to reset your password now. + +If the url does not work, please go to http://localhost:8080/blockly/reset and use the token: {{token}} + +If you did not make this request, you may safely ignore this message. + +The Parallax team From 53557d497c33f05e18bcf0e33cd5bad63325b2b5 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Mon, 8 May 2017 16:02:31 -0700 Subject: [PATCH 11/20] Correct missing COPPA data block --- app/Authenticate/controllers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Authenticate/controllers.py b/app/Authenticate/controllers.py index 836517d..2ad1e6c 100644 --- a/app/Authenticate/controllers.py +++ b/app/Authenticate/controllers.py @@ -66,7 +66,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') From 6621b410b0ccb05b5334cb326cc9fa915d3cb10c Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 11 May 2017 11:20:27 -0700 Subject: [PATCH 12/20] Add SponsorType enum to make code more readable --- app/User/services.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/User/services.py b/app/User/services.py index 155c080..a5bb9d9 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -11,6 +11,14 @@ from models import User, ConfirmToken, ResetToken +# Implementing an enum-like structure for the user sponsor email type +class SponsorType: + INDIVIDUAL = 0 + PARENT = 1 + GUARDIAN = 2 + TEACHER = 3 + + def get_password_hash(password): salt = str(uuid.uuid1()) password_hash = hashlib.sha256("%s:%s" % (password, salt)).hexdigest() From db7b859fdacbc9758d825d2a0d80ad0c8f8c59ae Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 11 May 2017 11:21:32 -0700 Subject: [PATCH 13/20] Add code to send registration confirmation email to the correct email address and with the correct email template --- app/Email/services.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/Email/services.py b/app/Email/services.py index f61163f..8e498c2 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -1,8 +1,7 @@ -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.services import SponsorType import pystache import logging @@ -24,8 +23,18 @@ def send_email_template_for_user(id_user, template, server, **kwargs): params['screenname'] = user.screen_name # Send email to parent if user is under 13 years old - if is_coppa_covered(user.birth_month, user.birth_year): - user_email = user.parent_email + if template == 'confirm': + if is_coppa_covered(user.birth_month, user.birth_year): + user_email = user.parent_email + + if user.parent_email_source == SponsorType.TEACHER: + # Teacher handles the account confirmation + send_email_template_to_address(user_email, 'confim_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]", id_user, user.parent_email_source) else: user_email = user.email From 39224fcf4ab5d0fba806852b4acc8872b00813f7 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 11 May 2017 14:40:50 -0700 Subject: [PATCH 14/20] Add templates for teachers and parent emails. Moved SponsorType to email processing source file. --- .gitignore | 1 + app/Email/services.py | 16 ++++++++++++--- app/LocalUser/controllers.py | 4 +++- app/User/services.py | 9 +-------- .../blocklyprop/header.mustache | 1 + .../confim-teacher/blocklyprop/plain.mustache | 20 +++++++++++++++++++ .../en/confirm/blocklyprop/header.mustache | 2 +- .../en/confirm/blocklyprop/plain.mustache | 1 + 8 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 templates/en/confim-teacher/blocklyprop/header.mustache create mode 100644 templates/en/confim-teacher/blocklyprop/plain.mustache diff --git a/.gitignore b/.gitignore index 1b5d2b6..2459a51 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ nbactions.xml .idea build deploy-test +/CloudSession-Templates.tar.gz diff --git a/app/Email/services.py b/app/Email/services.py index 8e498c2..6eaeb70 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -1,11 +1,20 @@ from app import mail, app from os.path import expanduser, isfile from flask.ext.mail import Message -from app.User.services import SponsorType import pystache import logging +""" +""" + + +class SponsorType: + INDIVIDUAL=0 + PARENT=1 + GUARDIAN=2 + TEACHER=3 + def send_email_template_for_user(id_user, template, server, **kwargs): from app.User.services import get_user, is_coppa_covered @@ -26,13 +35,14 @@ def send_email_template_for_user(id_user, template, server, **kwargs): if template == 'confirm': if is_coppa_covered(user.birth_month, user.birth_year): 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, 'confim_teacher', server, user.locale, params) + send_email_template_to_address(user_email, 'confim-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) + send_email_template_to_address(user_email, 'confirm-parent', server, user.locale, params) else: logging.info("COPPA account %s has invalid sponsor type [%s]", id_user, user.parent_email_source) else: diff --git a/app/LocalUser/controllers.py b/app/LocalUser/controllers.py index 8d51dec..6af2289 100644 --- a/app/LocalUser/controllers.py +++ b/app/LocalUser/controllers.py @@ -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) diff --git a/app/User/services.py b/app/User/services.py index a5bb9d9..6045407 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -11,14 +11,6 @@ from models import User, ConfirmToken, ResetToken -# Implementing an enum-like structure for the user sponsor email type -class SponsorType: - INDIVIDUAL = 0 - PARENT = 1 - GUARDIAN = 2 - TEACHER = 3 - - def get_password_hash(password): salt = str(uuid.uuid1()) password_hash = hashlib.sha256("%s:%s" % (password, salt)).hexdigest() @@ -131,6 +123,7 @@ def send_email_confirm(id_user, server): db.session.add(confirm_token) try: + # Send an email to the user or user's responsible party to confirm the account request email_services.send_email_template_for_user(id_user, 'confirm', server, token=token) except Exception as ex: print("Exception {0}", ex.args) diff --git a/templates/en/confim-teacher/blocklyprop/header.mustache b/templates/en/confim-teacher/blocklyprop/header.mustache new file mode 100644 index 0000000..19c1a1c --- /dev/null +++ b/templates/en/confim-teacher/blocklyprop/header.mustache @@ -0,0 +1 @@ +{{! This is the email Subject line }}Please confirm your student's email address for BlocklyProp diff --git a/templates/en/confim-teacher/blocklyprop/plain.mustache b/templates/en/confim-teacher/blocklyprop/plain.mustache new file mode 100644 index 0000000..02ba7dc --- /dev/null +++ b/templates/en/confim-teacher/blocklyprop/plain.mustache @@ -0,0 +1,20 @@ +{{! This is the text body of the email}} +Hello, + +A student has created an account on the Parallax BlocklyProp web site under the +screen name {{screenname}}. When the account was created, your email address was +registered as the classroom instructor. If this is incorrect, please accept our +apologies. There is nothing more you need to do. The request will automatically expire. + +If this is your student, please confirm the registration by copying the link below into +your browser or by navigating to the second link and confirming the student's email address. + +Copy and paste into your browser +http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. + +If the above link is unable to complete your registration, please go to +http://localhost:8080/blockly/confirm and enter your email address and the token: {{token}} + +Regards, + +The Parallax team diff --git a/templates/en/confirm/blocklyprop/header.mustache b/templates/en/confirm/blocklyprop/header.mustache index 8d1f20a..ed52201 100644 --- a/templates/en/confirm/blocklyprop/header.mustache +++ b/templates/en/confirm/blocklyprop/header.mustache @@ -1 +1 @@ -Please confirm your email address for BlocklyProp +{{! This is the email Subject line }}Please confirm your email address for BlocklyProp diff --git a/templates/en/confirm/blocklyprop/plain.mustache b/templates/en/confirm/blocklyprop/plain.mustache index ff39fcb..023480c 100644 --- a/templates/en/confirm/blocklyprop/plain.mustache +++ b/templates/en/confirm/blocklyprop/plain.mustache @@ -1,3 +1,4 @@ +{{! This is the text body of the email}} Dear {{screenname}}, Please go to http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. From 3bfd977ae9ba14974fb98a370a3cfbc502e75c83 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 12 May 2017 09:41:00 -0700 Subject: [PATCH 15/20] Create parent templates. Rename teacher template path --- .../blocklyprop/header.mustache | 1 + .../confirm-parent/blocklyprop/plain.mustache | 35 +++++++++++++++++++ .../blocklyprop/header.mustache | 0 .../blocklyprop/plain.mustache | 4 ++- 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 templates/en/confirm-parent/blocklyprop/header.mustache create mode 100644 templates/en/confirm-parent/blocklyprop/plain.mustache rename templates/en/{confim-teacher => confirm-teacher}/blocklyprop/header.mustache (100%) rename templates/en/{confim-teacher => confirm-teacher}/blocklyprop/plain.mustache (90%) diff --git a/templates/en/confirm-parent/blocklyprop/header.mustache b/templates/en/confirm-parent/blocklyprop/header.mustache new file mode 100644 index 0000000..6a65a8e --- /dev/null +++ b/templates/en/confirm-parent/blocklyprop/header.mustache @@ -0,0 +1 @@ +{{! This is the email Subject line }}New user registration confirmation request \ No newline at end of file diff --git a/templates/en/confirm-parent/blocklyprop/plain.mustache b/templates/en/confirm-parent/blocklyprop/plain.mustache new file mode 100644 index 0000000..e02f5fa --- /dev/null +++ b/templates/en/confirm-parent/blocklyprop/plain.mustache @@ -0,0 +1,35 @@ +{{! + This is the text body of the email new account notification to + a parent or guardian +}} +Hello, + +Someone, perhaps your child, has requested a new account on the Parallax +BlocklyProp web site (http://blockly.parallax.com) under the screen name +{{screenname}}. When the account was created, your email address was +received as the parent or guardian of the registrant. If this is incorrect, +please accept our apologies. There is nothing more you need to do. The +request will automatically expire. + +If this is your child, please confirm the registration by copying the link below into +your browser or by navigating to the second link and confirming the student's email address. + +Confirm account registration: +Copy and paste into your browser +http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. + +If the above link is unable to complete your registration, please go to +http://localhost:8080/blockly/confirm and enter your email address and the token: {{token}} + + +You may also elect to cancel the registration process immediately. To do +so, please copy and paste the link below. This will remove the pending registration +request and the account that was created during the registration process. + +[NEED A CANCEL LINK HERE] + + + +Regards, + +The Parallax team diff --git a/templates/en/confim-teacher/blocklyprop/header.mustache b/templates/en/confirm-teacher/blocklyprop/header.mustache similarity index 100% rename from templates/en/confim-teacher/blocklyprop/header.mustache rename to templates/en/confirm-teacher/blocklyprop/header.mustache diff --git a/templates/en/confim-teacher/blocklyprop/plain.mustache b/templates/en/confirm-teacher/blocklyprop/plain.mustache similarity index 90% rename from templates/en/confim-teacher/blocklyprop/plain.mustache rename to templates/en/confirm-teacher/blocklyprop/plain.mustache index 02ba7dc..d690344 100644 --- a/templates/en/confim-teacher/blocklyprop/plain.mustache +++ b/templates/en/confirm-teacher/blocklyprop/plain.mustache @@ -1,4 +1,6 @@ -{{! This is the text body of the email}} +{{! + This is the text body of the new account confirmation email directed to instructors +}} Hello, A student has created an account on the Parallax BlocklyProp web site under the From 9627ff969882384100880652caf086bd48cad282 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 12 May 2017 09:42:45 -0700 Subject: [PATCH 16/20] Update code to handle COPPA compliant email templates --- app/Email/services.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/app/Email/services.py b/app/Email/services.py index 6eaeb70..34fe8a9 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -29,24 +29,27 @@ def send_email_template_for_user(id_user, template, server, **kwargs): 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['sponsoremail'] = user.parent_email + + user_email = user.email # Send email to parent if user is under 13 years old - if template == 'confirm': - if is_coppa_covered(user.birth_month, user.birth_year): - 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, 'confim-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]", id_user, user.parent_email_source) - else: - user_email = user.email + if template == 'confirm' and is_coppa_covered(user.birth_month, user.birth_year): + 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]", id_user, user.parent_email_source) send_email_template_to_address(user_email, template, server, user.locale, params) From 5eb6e213b712a6cb9f38972d52988d769f680ab8 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 12 May 2017 09:43:19 -0700 Subject: [PATCH 17/20] Add logging to help track code paths. --- app/LocalUser/controllers.py | 2 +- app/User/services.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/LocalUser/controllers.py b/app/LocalUser/controllers.py index 6af2289..e3ec51d 100644 --- a/app/LocalUser/controllers.py +++ b/app/LocalUser/controllers.py @@ -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 diff --git a/app/User/services.py b/app/User/services.py index 6045407..c388f64 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -95,16 +95,23 @@ def create_oauth_user( def send_email_confirm(id_user, server): + logging.info("Preparing new account confirmation email for user %s", id_user) + user = get_user(id_user) + if user is None: + logging.debug("Unknown user id: %s", id_user) return False, 1, 'User id not known' if user.confirmed: + logging.debug("User account %s has already been verified", id_user) return False, 2, 'Account already verified' if user.blocked: + logging.debug("User account %s has been blocked", id_user) return False, 3, 'Account Blocked' # check rate limiting if not rate_limiting_services.consume_tokens(id_user, 'email-confirm', 1): + logging.debug("Too many attempts to confirm account for user %s", id_user) return False, 10, 'Rate limiter exceeded' # Delete token if any exists From afd17dd4576e157e3126f02de0c582fb09917312 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 12 May 2017 11:49:03 -0700 Subject: [PATCH 18/20] Complete draft email to teacher to register a student. --- app/Email/services.py | 19 ++++++++++++++++--- app/User/services.py | 5 +++-- .../confirm-parent/blocklyprop/plain.mustache | 2 +- .../blocklyprop/plain.mustache | 4 ++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/Email/services.py b/app/Email/services.py index 34fe8a9..57357de 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -19,12 +19,14 @@ class SponsorType: def send_email_template_for_user(id_user, template, server, **kwargs): from app.User.services import get_user, is_coppa_covered - logging.info("Sending email to user: %s (%s)", id_user, template) + logging.info("Sending email to user: %s using template (%s)", id_user, template) params = {} for key, value in kwargs.items(): + logging.debug("Logging parameter %s = %s", key, value) params[key] = value + # Get a copy of the user record user = get_user(id_user) if user is None: return False @@ -33,12 +35,14 @@ def send_email_template_for_user(id_user, template, server, **kwargs): # available to the email templates. params['screenname'] = user.screen_name params['email'] = user.email + params['registrant-email'] = user.email params['sponsoremail'] = user.parent_email user_email = user.email # Send email to parent if user is under 13 years old if template == 'confirm' and 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) @@ -51,23 +55,32 @@ def send_email_template_for_user(id_user, template, server, **kwargs): else: logging.info("COPPA account %s has invalid sponsor type [%s]", id_user, user.parent_email_source) - send_email_template_to_address(user_email, template, 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 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) + 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(), diff --git a/app/User/services.py b/app/User/services.py index c388f64..d4c063f 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -130,11 +130,12 @@ def send_email_confirm(id_user, server): db.session.add(confirm_token) try: + logging.info("Sending account confirmation email to user: %s ", id_user) # Send an email to the user or user's responsible party to confirm the account request email_services.send_email_template_for_user(id_user, 'confirm', server, token=token) + logging.info("Completed email send process.") except Exception as ex: - print("Exception {0}", ex.args) - logging.error("Unable to send email. Message is: %s", ex.message) + logging.error("Error while sending email: %s", ex.message) return False, 99, 'Unable to contact SMTP server' return True, 0, 'Success' diff --git a/templates/en/confirm-parent/blocklyprop/plain.mustache b/templates/en/confirm-parent/blocklyprop/plain.mustache index e02f5fa..ea06899 100644 --- a/templates/en/confirm-parent/blocklyprop/plain.mustache +++ b/templates/en/confirm-parent/blocklyprop/plain.mustache @@ -16,7 +16,7 @@ your browser or by navigating to the second link and confirming the student's em Confirm account registration: Copy and paste into your browser -http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. +http://localhost:8080/blockly/confirm?locale={{locale}}&email={{registrant-email}}&token={{token}} to confirm your email address. If the above link is unable to complete your registration, please go to http://localhost:8080/blockly/confirm and enter your email address and the token: {{token}} diff --git a/templates/en/confirm-teacher/blocklyprop/plain.mustache b/templates/en/confirm-teacher/blocklyprop/plain.mustache index d690344..1e76d32 100644 --- a/templates/en/confirm-teacher/blocklyprop/plain.mustache +++ b/templates/en/confirm-teacher/blocklyprop/plain.mustache @@ -12,10 +12,10 @@ If this is your student, please confirm the registration by copying the link bel your browser or by navigating to the second link and confirming the student's email address. Copy and paste into your browser -http://localhost:8080/blockly/confirm?locale={{locale}}&email={{email}}&token={{token}} to confirm your email address. +http://localhost:8080/blockly/confirm?locale={{locale}}&email={{registrant-email}}&token={{token}} to confirm your email address. If the above link is unable to complete your registration, please go to -http://localhost:8080/blockly/confirm and enter your email address and the token: {{token}} +http://localhost:8080/blockly/confirm and enter your student's email address ({{registrant-email}}) and the token: {{token}} Regards, From 6fe37c1ef91b3d69f6e844dead51af9b17a7afe1 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Tue, 16 May 2017 09:25:19 -0700 Subject: [PATCH 19/20] Updates to make the COPPA parent email work --- app/Authenticate/controllers.py | 4 +- app/Email/services.py | 36 ++++++++++-------- app/User/coppa.py | 38 +++++++++++++++++++ app/User/services.py | 29 +++----------- app/__init__.py | 2 + .../blocklyprop/header.mustache | 3 +- .../confirm-parent/blocklyprop/plain.mustache | 14 +++++++ .../blocklyprop/header.mustache | 3 +- 8 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 app/User/coppa.py diff --git a/app/Authenticate/controllers.py b/app/Authenticate/controllers.py index 2ad1e6c..efc02a6 100644 --- a/app/Authenticate/controllers.py +++ b/app/Authenticate/controllers.py @@ -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 diff --git a/app/Email/services.py b/app/Email/services.py index 57357de..be15412 100644 --- a/app/Email/services.py +++ b/app/Email/services.py @@ -1,35 +1,37 @@ 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 """ -class SponsorType: - INDIVIDUAL=0 - PARENT=1 - GUARDIAN=2 - TEACHER=3 +def send_email_template_for_user(id_user, template, server, **kwargs): + from app.User.services import get_user + # 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) -def send_email_template_for_user(id_user, template, server, **kwargs): - from app.User.services import get_user, is_coppa_covered + if user is None: + logging.error("Cannot send email: Invalid user record") + return False + else: + logging.info("Email template received user: %s", user.id) - logging.info("Sending email to user: %s using template (%s)", id_user, template) + 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 - # Get a copy of the user record - 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. @@ -38,10 +40,12 @@ def send_email_template_for_user(id_user, template, server, **kwargs): params['registrant-email'] = user.email params['sponsoremail'] = user.parent_email + #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 is_coppa_covered(user.birth_month, user.birth_year): + 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) @@ -49,11 +53,12 @@ def send_email_template_for_user(id_user, template, server, **kwargs): 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: + 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]", id_user, user.parent_email_source) + logging.info("COPPA account %s has invalid sponsor type [%s]", user.id, user.parent_email_source) return else: @@ -62,6 +67,7 @@ def send_email_template_for_user(id_user, template, server, **kwargs): return + def send_email_template_to_address(recipient, template, server, locale, params=None, **kwargs): params = params or {} diff --git a/app/User/coppa.py b/app/User/coppa.py new file mode 100644 index 0000000..88fa09a --- /dev/null +++ b/app/User/coppa.py @@ -0,0 +1,38 @@ + +import datetime + +# Enumerate the sponsor types for COPPA eligible user accounts +class SponsorType: + INDIVIDUAL = 0 + PARENT = 1 + GUARDIAN = 2 + TEACHER = 3 + + def __init__(self): + pass + + +class Coppa: + def __init__(self): + pass + + # Return true if the date is less than 13 years + def is_coppa_covered(self, month, year): + # This is the number of months a typical thirteen years old has been on planet Earth. + cap = 156 + + # This is the actual number of months a typical user has been on the same planet. + user_age = (year * 12) + month + + # Current year and month + current_month = datetime.date.today().month + current_year = datetime.date.today().year + + # This represents the number of months since the inception of AD + # Unless you want to count that first year as part of BC. + current_cap = (current_year * 12) + current_month + + if current_cap - user_age > cap: + return False + else: + return True diff --git a/app/User/services.py b/app/User/services.py index d4c063f..d72d0be 100644 --- a/app/User/services.py +++ b/app/User/services.py @@ -11,6 +11,10 @@ from models import User, ConfirmToken, ResetToken +def get_user(id_user): + return User.query.get(id_user) + + def get_password_hash(password): salt = str(uuid.uuid1()) password_hash = hashlib.sha256("%s:%s" % (password, salt)).hexdigest() @@ -23,10 +27,6 @@ def check_password(id_user, password): return user.password == password_hash -def get_user(id_user): - return User.query.get(id_user) - - def get_user_by_email(email): return User.query.filter_by(email=email).first() @@ -96,6 +96,7 @@ def create_oauth_user( def send_email_confirm(id_user, server): logging.info("Preparing new account confirmation email for user %s", id_user) + logging.info("Account request received from server: %s", server) user = get_user(id_user) @@ -171,23 +172,3 @@ def send_password_reset(id_user, server): return True, 0, 'Success' -# Return true if the date is less than 13 years -def is_coppa_covered(month,year): - # This is the number of months a typical thirteen years old has been on planet Earth. - cap = 156 - - # This is the actual number of months a typical user has been on the same planet. - user_age = (year * 12) + month - - # Current year and month - current_month = datetime.date.today().month - current_year = datetime.date.today().year - - # This represents the number of months since the inception of AD - # Unless you want to count that first year as part of BC. - current_cap = (current_year * 12) + current_month - - if current_cap - user_age > cap: - return False - else: - return True diff --git a/app/__init__.py b/app/__init__.py index 8ab180d..3b71c6d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -33,6 +33,8 @@ defaults = { 'database.url': 'mysql+mysqldb://cloudsession:cloudsession@localhost:3306/cloudsession', + 'request.host': 'http://localhost:8080/blockly', + 'sentry-dsn': None, 'mail.host': 'localhost', diff --git a/templates/en/confirm-parent/blocklyprop/header.mustache b/templates/en/confirm-parent/blocklyprop/header.mustache index 6a65a8e..3425542 100644 --- a/templates/en/confirm-parent/blocklyprop/header.mustache +++ b/templates/en/confirm-parent/blocklyprop/header.mustache @@ -1 +1,2 @@ -{{! This is the email Subject line }}New user registration confirmation request \ No newline at end of file +{{! This is the email Subject line }} +New user registration confirmation request \ No newline at end of file diff --git a/templates/en/confirm-parent/blocklyprop/plain.mustache b/templates/en/confirm-parent/blocklyprop/plain.mustache index ea06899..3840702 100644 --- a/templates/en/confirm-parent/blocklyprop/plain.mustache +++ b/templates/en/confirm-parent/blocklyprop/plain.mustache @@ -11,6 +11,20 @@ received as the parent or guardian of the registrant. If this is incorrect, please accept our apologies. There is nothing more you need to do. The request will automatically expire. +Why are we sending this? In the US, the federal Children's Online Privacy +Protection Act, known as COPPA, requires that we communicate with a parent +or guardian if the person registering an BlocklyProp account is under the +age of 13. The Act allows you to decline the registration request. If you +choose this option, we will immediately remove the registration information +and the associated account. If you choose to confirm the the request and +activate the account, you may close the account at any time by clicking on +the link provided below. This action will close the account and remove any +projects that are associated with the account. + +A full copy of our Child Privacy Policy is available online at: +http://localhost:8080/blockly/child-privacy-policy + + If this is your child, please confirm the registration by copying the link below into your browser or by navigating to the second link and confirming the student's email address. diff --git a/templates/en/confirm-teacher/blocklyprop/header.mustache b/templates/en/confirm-teacher/blocklyprop/header.mustache index 19c1a1c..7447513 100644 --- a/templates/en/confirm-teacher/blocklyprop/header.mustache +++ b/templates/en/confirm-teacher/blocklyprop/header.mustache @@ -1 +1,2 @@ -{{! This is the email Subject line }}Please confirm your student's email address for BlocklyProp +{{! This is the email Subject line }} +Please confirm your student's email address for BlocklyProp From 49aec572bccdb84fe6483f2311be21b71450d579 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Tue, 16 May 2017 15:12:06 -0700 Subject: [PATCH 20/20] Bumping up the version number. 1.1.0 include support for US COPPA regulations --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 3b71c6d..362949a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -23,7 +23,7 @@ from raven.contrib.flask import Sentry app = Flask(__name__) -version = "1.0.1" +version = "1.1.0" db = None # Load basic configurations