Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3156655
Added in initial version of gc email regex
May 20, 2020
d3f4664
added pool_pre_ping to db create engine
May 20, 2020
521ac0e
added default argument for exp time, and started work on email upon a…
May 20, 2020
9cd323f
Implemented first iteration of verification emails
May 20, 2020
3f183dc
Added email_validated field to Users table
May 20, 2020
f317198
Create email verification mutation
May 20, 2020
328d289
Fix email sending email exception raising
May 20, 2020
60dcd75
add error check to see if user does exist
May 20, 2020
30ef1fa
work on send_verification_email.py and its tests
May 21, 2020
f44776e
Fixed checks in send_verification_email.py
May 21, 2020
6968398
Moved sandbox creation to when user verifies their account
May 21, 2020
bb7f97f
Cleaned up imports
May 21, 2020
b657998
Created new hybrid method in Users.py that verifies the user's account
May 21, 2020
a4b0c40
Updated all tests with new verify account which in turn creates the u…
May 21, 2020
3cd7664
increase wait time
May 21, 2020
c2c904a
changed up user create, to create the user then attempt to send verif…
May 21, 2020
d37508c
Add tests for successful, and temporary fail
May 21, 2020
59eac87
Corrected verify account function to actually set field to true, and …
May 21, 2020
bca7e3c
Implemented new mutation for sending verification email, if failed in…
May 21, 2020
1e98919
Tests for new mutation
May 21, 2020
60c2275
Black ran on files
May 21, 2020
260be42
Added api/api.current.graphql to gitignore, and updated description f…
May 21, 2020
cdd647b
Updated schema.faker.graphql, and added new email_verified field to u…
May 21, 2020
e62e077
remove pre_pool_ping, and wait for new branch
May 22, 2020
18e6744
Moved NotificationsAPI client to be a function argument
May 22, 2020
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
platform/**/*.json
frontend/public
api/schema.json
api/api.current.graphql

**/node_modules

Expand Down
5 changes: 3 additions & 2 deletions api/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
DB_PORT = os.getenv("DB_PORT")
DB_NAME = os.getenv("DB_NAME")


# Add this in later branch -> pool_pre_ping=True,
Comment thread
tparrott-cse marked this conversation as resolved.
Outdated
engine = create_engine(
f"postgresql+psycopg2://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
f"postgresql+psycopg2://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}",
)

db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine)
)
Expand Down
2 changes: 1 addition & 1 deletion api/functions/auth_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def check_user_claims(user_id):
"permission": select["permission"],
}
user_roles.append(temp_dict)
return user_roles
return user_roles


def require_token(method):
Expand Down
59 changes: 59 additions & 0 deletions api/functions/verification_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import time

from flask import request
from graphql import GraphQLError
from notifications_python_client.notifications import NotificationsAPIClient
from requests import HTTPError

from json_web_token import tokenize
from models import Users


def send_verification_email(user: Users, client: NotificationsAPIClient):
"""
This function allows a user object to be passed in during account creation
and send an email to be used for verifying accounts
:param user: A instance of /models/User.py
:param client: An instance of NotificationsAPIClient
:return: None
"""
# Create Notify Client
notify_client = client

# Check to see if users preferred lang is English or French
if user.preferred_lang == "french":
email_template_id = "f2c9b64a-c754-4ffd-93e9-33fdb0b5ae0b"
else:
email_template_id = "6e3368a7-0d75-47b1-b4b2-878234e554c9"

# URL Generation
token = tokenize(user_id=user.user_name, exp_period=24)
url = str(request.url_root) + "validate/" + str(token)

# Send Email
try:
response = notify_client.send_email_notification(
email_address=user.user_name,
template_id=email_template_id,
personalisation={"user": "", "verify_email_url": url},
)

except HTTPError:
raise GraphQLError(
"Error, when sending verification email, error: {}".format(HTTPError)
)

# Sleep to wait and see if email was successful
time.sleep(1.5)
email_status = notify_client.get_notification_by_id(response.get("id")).get(
"status"
)

if (
email_status == "permanent-failure"
or email_status == "temporary-failure"
or email_status == "technical-failure"
):
return "Email Send Error: {}".format(email_status)
else:
Comment thread
sleepycat marked this conversation as resolved.
Outdated
return email_status
3 changes: 2 additions & 1 deletion api/json_web_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ def tokenize(
exp=None,
# TODO: SUPER_SECRET_SALT isn't actually a salt! Give this a better name.
secret=environ.get("SUPER_SECRET_SALT", ""),
exp_period=1,
):
if not iat:
iat = dt.timestamp(dt.utcnow())
if not exp:
exp = dt.timestamp(dt.utcnow() + timedelta(hours=1))
exp = dt.timestamp(dt.utcnow() + timedelta(hours=exp_period))
return jwt.encode(
{"exp": exp, "iat": iat, "user_id": user_id}, secret, algorithm="HS256",
).decode("utf-8")
Expand Down
28 changes: 28 additions & 0 deletions api/migrations/versions/2a25ac8483bc_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""empty message

Revision ID: 2a25ac8483bc
Revises: c264f0905a36
Create Date: 2020-05-20 13:38:34.078690

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "2a25ac8483bc"
down_revision = "c264f0905a36"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("users", sa.Column("email_validated", sa.Boolean(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "email_validated")
# ### end Alembic commands ###
35 changes: 18 additions & 17 deletions api/models/Users.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import bcrypt

from sqlalchemy.types import Integer, Boolean, Float
from sqlalchemy.orm import relationship, validates
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy import event
from sqlalchemy import Column, String

from db import Base
from functions.orm_to_dict import orm_to_dict
from functions.slugify import slugify_value
from models.Organizations import Organizations
from models.User_affiliations import User_affiliations
from db import Base


class Users(Base):
Expand All @@ -28,18 +26,7 @@ class Users(Base):
user_affiliation = relationship(
"User_affiliations", back_populates="user", passive_deletes=True,
)

def __init__(self, **kwargs):
super(Users, self).__init__(**kwargs)
# XXX: This is gross but matches the expections of the
# Acronym scalar type.
acronym = slugify_value(self.user_name).upper()[:50]
self.user_affiliation.append(
User_affiliations(
permission="admin",
user_organization=Organizations(name=self.user_name, acronym=acronym,),
)
)
email_validated = Column(Boolean, default=False)

@hybrid_method
def find_by_user_name(self, user_name):
Expand Down Expand Up @@ -73,3 +60,17 @@ def password(self, password):
self.user_password = bcrypt.hashpw(
password.encode("utf8"), bcrypt.gensalt()
).decode("utf8")

@hybrid_method
def verify_account(self):
# Set user email_validated field to true0
self.email_validated = True

# Create users sandbox org
acronym = slugify_value(self.user_name).upper()[:50]
self.user_affiliation.append(
User_affiliations(
permission="admin",
user_organization=Organizations(name=self.user_name, acronym=acronym,),
)
)
Comment thread
sleepycat marked this conversation as resolved.
Outdated
14 changes: 14 additions & 0 deletions api/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
# Request Scan Mutation
from schemas.scans_mutation import RequestScan

# Verify Account Through Email
from schemas.email_verify_account.email_verify_account import EmailVerifyAccount

# Re-send verification email
from schemas.send_email_verification.send_email_verification import (
SendEmailVerification,
)

# Update User Role Mutation
from schemas.user_affiliations import UpdateUserRole

Expand Down Expand Up @@ -225,6 +233,12 @@ class Mutation(graphene.ObjectType):
description="Allows users to give their credentials and be " "authenticated"
)
sign_up = SignUp.Field(description="Allows users to sign up to our service")
email_verify_account = EmailVerifyAccount.Field(
description="Allows users to use token sent through email to verify their account."
)
send_email_verification = SendEmailVerification.Field(
description="Allows users to resend verification if failed during sign-up"
)


schema = graphene.Schema(query=Query, mutation=Mutation)
4 changes: 4 additions & 0 deletions api/scalars/email_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[
\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"""

# GC_EMAIL_ADDRESS_REGEX = r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[
# \x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[
# a-z0-9])?\.)+(gc|canada)\.(ca))"""

EMAIL_ADDRESS_REGEX = compile(EMAIL_ADDRESS_REGEX)


Expand Down
7 changes: 7 additions & 0 deletions api/schemas/User/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Meta:
"user_password",
"password",
"roles",
"email_validated"
)

user_name = EmailAddress(description="Email that the user signed up with")
Expand All @@ -46,6 +47,9 @@ class Meta:
tfa = graphene.Boolean(
description="Has the user completed two factor authentication"
)
email_validated = graphene.Boolean(
description="Has the user verified their account"
)
affiliations = graphene.ConnectionField(
UserAffClass._meta.connection, description="Users access to organizations"
)
Expand All @@ -64,6 +68,9 @@ def resolve_lang(self: UserModel, info):
def resolve_tfa(self: UserModel, info):
return self.tfa_validated

def resolve_email_validated(self: UserModel, info):
return self.email_validated

@require_token
def resolve_affiliations(self: UserModel, info, **kwargs):
user_roles = kwargs.get("user_roles")
Expand Down
Empty file.
57 changes: 57 additions & 0 deletions api/schemas/email_verify_account/email_verify_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import graphene
import jwt
import os

from graphql import GraphQLError

from app import app
from db import db_session
from functions.input_validators import cleanse_input
from models.Users import Users


class EmailVerifyAccount(graphene.Mutation):
"""
Mutation that allows the user to verify their account through a token
sent in an email
"""
class Arguments:
token_string = graphene.String(
description="Token in sent via email, and located in url", required=True
)

status = graphene.Boolean(
description="Informs user if account was successfully verified."
)

@staticmethod
def mutate(self, info, **kwargs):
token_string = cleanse_input(kwargs.get("token_string"))

try:
payload = jwt.decode(
token_string, os.getenv("SUPER_SECRET_SALT"), algorithms=["HS256"]
)
except jwt.ExpiredSignatureError:
raise GraphQLError("Signature expired, please login again")
except jwt.InvalidTokenError:
raise GraphQLError("Invalid token, please login again")
user_name = payload.get("user_id")

with app.app_context():
# Check to see if user exists
user = db_session.query(Users).filter(Users.user_name == user_name).first()

if not user:
raise GraphQLError("Error, User does not exist")

user.verify_account()

try:
db_session.commit()
except Exception as e:
db_session.rollback()
db_session.flush()
raise GraphQLError("Error, unable to verify account.")

return EmailVerifyAccount(status=True)
Empty file.
57 changes: 57 additions & 0 deletions api/schemas/send_email_verification/send_email_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import graphene
import os

from graphql import GraphQLError
from notifications_python_client.notifications import NotificationsAPIClient

from db import db_session
from functions.input_validators import cleanse_input
from functions.verification_email import send_verification_email
from models.Users import Users
from scalars.email_address import EmailAddress


class SendEmailVerification(graphene.Mutation):
"""
This mutation is used for re-sending a verification email if it failed
during user creation
"""

class Arguments:
user_name = EmailAddress(
description="The users email address used for sending email", required=True,
)

status = graphene.Boolean(
description="If email is successfully sent status will be true"
)

@staticmethod
def mutate(self, info, **kwargs):
# Get information from mutation arguments
user_name = cleanse_input(kwargs.get("user_name"))

# Find user
user = db_session.query(Users).filter(Users.user_name == user_name).first()

# Check to see if user is found, or if they are already validated
if user is None:
raise GraphQLError("Error, cannot find user.")
elif user.email_validated:
raise GraphQLError("Error, user is already validated.")

# Send validation email
email_status = send_verification_email(
user=user,
client=NotificationsAPIClient(
api_key=os.getenv("NOTIFICATION_API_KEY"),
base_url=os.getenv("NOTIFICATION_API_URL"),
)
)

if email_status.__contains__("Email Send Error"):
raise GraphQLError(
"Error, when sending verification email, please try again."
)

return SendEmailVerification(status=True)
Loading