-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'remotes/origin/master' into st_dh_email…
…_routes # Conflicts: # microsetta_private_api/admin/admin_impl.py
- Loading branch information
Showing
22 changed files
with
1,428 additions
and
1,073 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from ._account import ( | ||
find_accounts_for_login, register_account, claim_legacy_acct, | ||
read_account, update_account, check_email_match, verify_authrocket, | ||
) | ||
from ._consent import ( | ||
render_consent_doc, | ||
) | ||
from ._source import ( | ||
create_source, read_source, update_source, delete_source, | ||
read_sources, create_human_source_from_consent | ||
) | ||
from ._survey import ( | ||
read_survey_template, read_survey_templates, read_answered_survey, | ||
read_answered_surveys, submit_answered_survey, | ||
read_answered_survey_associations, | ||
) | ||
from ._sample import ( | ||
read_sample_association, associate_sample, read_sample_associations, | ||
update_sample_association, dissociate_answered_survey, | ||
dissociate_sample, read_kit, associate_answered_survey | ||
) | ||
|
||
__all__ = [ | ||
'find_accounts_for_login', | ||
'register_account', | ||
'claim_legacy_acct', | ||
'read_account', | ||
'update_account', | ||
'check_email_match', | ||
'render_consent_doc', | ||
'create_source', | ||
'read_source', | ||
'update_source', | ||
'delete_source', | ||
'read_sources', | ||
'create_human_source_from_consent', | ||
'read_survey_template', | ||
'read_survey_templates', | ||
'read_answered_survey', | ||
'read_answered_surveys', | ||
'read_answered_survey_associations', | ||
'read_sample_association', | ||
'associate_sample', | ||
'read_sample_associations', | ||
'update_sample_association', | ||
'dissociate_answered_survey', | ||
'dissociate_sample', | ||
'read_kit', | ||
'associate_answered_survey', | ||
'submit_answered_survey', | ||
'verify_authrocket', | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import uuid | ||
|
||
import jwt | ||
from flask import jsonify | ||
from jwt import InvalidTokenError | ||
|
||
from werkzeug.exceptions import Unauthorized, Forbidden, NotFound | ||
|
||
from microsetta_private_api.api.literals import AUTHROCKET_PUB_KEY, \ | ||
INVALID_TOKEN_MSG, JWT_ISS_CLAIM_KEY, JWT_SUB_CLAIM_KEY, \ | ||
JWT_EMAIL_CLAIM_KEY, ACCT_NOT_FOUND_MSG | ||
from microsetta_private_api.model.account import Account, AuthorizationMatch | ||
from microsetta_private_api.model.address import Address | ||
from microsetta_private_api.repo.account_repo import AccountRepo | ||
from microsetta_private_api.repo.kit_repo import KitRepo | ||
from microsetta_private_api.repo.transaction import Transaction | ||
|
||
|
||
def find_accounts_for_login(token_info): | ||
# Note: Returns an array of accounts accessible by token_info because | ||
# we'll use that functionality when we add in administrator accounts. | ||
with Transaction() as t: | ||
acct_repo = AccountRepo(t) | ||
acct = acct_repo.find_linked_account( | ||
token_info[JWT_ISS_CLAIM_KEY], | ||
token_info[JWT_SUB_CLAIM_KEY]) | ||
|
||
if acct is None: | ||
return jsonify([]), 200 | ||
|
||
return jsonify([acct.to_api()]), 200 | ||
|
||
|
||
def claim_legacy_acct(token_info): | ||
# If there exists a legacy account for the email in the token, which the | ||
# user represented by the token does not already own but can claim, this | ||
# claims the legacy account for the user and returns a 200 code with json | ||
# list containing the object for the claimed account. Otherwise, this | ||
# returns an empty json list. This function can also trigger a 422 from the | ||
# repo layer in the case of inconsistent account data. | ||
|
||
email = token_info[JWT_EMAIL_CLAIM_KEY] | ||
auth_iss = token_info[JWT_ISS_CLAIM_KEY] | ||
auth_sub = token_info[JWT_SUB_CLAIM_KEY] | ||
|
||
with Transaction() as t: | ||
acct_repo = AccountRepo(t) | ||
acct = acct_repo.claim_legacy_account(email, auth_iss, auth_sub) | ||
t.commit() | ||
|
||
if acct is None: | ||
return jsonify([]), 200 | ||
|
||
return jsonify([acct.to_api()]), 200 | ||
|
||
|
||
def register_account(body, token_info): | ||
# First register with AuthRocket, then come here to make the account | ||
new_acct_id = str(uuid.uuid4()) | ||
body["id"] = new_acct_id | ||
account_obj = Account.from_dict(body, token_info[JWT_ISS_CLAIM_KEY], | ||
token_info[JWT_SUB_CLAIM_KEY]) | ||
|
||
with Transaction() as t: | ||
kit_repo = KitRepo(t) | ||
kit = kit_repo.get_kit_all_samples(body['kit_name']) | ||
if kit is None: | ||
return jsonify(code=404, message="Kit name not found"), 404 | ||
|
||
acct_repo = AccountRepo(t) | ||
acct_repo.create_account(account_obj) | ||
new_acct = acct_repo.get_account(new_acct_id) | ||
t.commit() | ||
|
||
response = jsonify(new_acct.to_api()) | ||
response.status_code = 201 | ||
response.headers['Location'] = '/api/accounts/%s' % new_acct_id | ||
return response | ||
|
||
|
||
def read_account(account_id, token_info): | ||
acc = _validate_account_access(token_info, account_id) | ||
return jsonify(acc.to_api()), 200 | ||
|
||
|
||
def check_email_match(account_id, token_info): | ||
acc = _validate_account_access(token_info, account_id) | ||
|
||
match_status = acc.account_matches_auth( | ||
token_info[JWT_EMAIL_CLAIM_KEY], token_info[JWT_ISS_CLAIM_KEY], | ||
token_info[JWT_SUB_CLAIM_KEY]) | ||
|
||
if match_status == AuthorizationMatch.AUTH_ONLY_MATCH: | ||
result = {'email_match': False} | ||
elif match_status == AuthorizationMatch.FULL_MATCH: | ||
result = {'email_match': True} | ||
else: | ||
raise ValueError("Unexpected authorization match value") | ||
|
||
return jsonify(result), 200 | ||
|
||
|
||
def update_account(account_id, body, token_info): | ||
acc = _validate_account_access(token_info, account_id) | ||
|
||
with Transaction() as t: | ||
acct_repo = AccountRepo(t) | ||
acc.first_name = body['first_name'] | ||
acc.last_name = body['last_name'] | ||
acc.email = body['email'] | ||
acc.address = Address( | ||
body['address']['street'], | ||
body['address']['city'], | ||
body['address']['state'], | ||
body['address']['post_code'], | ||
body['address']['country_code'] | ||
) | ||
|
||
# 422 handling is done inside acct_repo | ||
acct_repo.update_account(acc) | ||
t.commit() | ||
|
||
return jsonify(acc.to_api()), 200 | ||
|
||
|
||
def verify_authrocket(token): | ||
email_verification_key = 'email_verified' | ||
|
||
try: | ||
token_info = jwt.decode(token, | ||
AUTHROCKET_PUB_KEY, | ||
algorithms=["RS256"], | ||
verify=True, | ||
issuer="https://authrocket.com") | ||
except InvalidTokenError as e: | ||
raise(Unauthorized(INVALID_TOKEN_MSG, e)) | ||
|
||
if JWT_ISS_CLAIM_KEY not in token_info or \ | ||
JWT_SUB_CLAIM_KEY not in token_info or \ | ||
JWT_EMAIL_CLAIM_KEY not in token_info: | ||
# token is malformed--no soup for you | ||
raise Unauthorized(INVALID_TOKEN_MSG) | ||
|
||
# if the user's email is not yet verified, they are forbidden to | ||
# access their account even regardless of whether they have | ||
# authenticated with authrocket | ||
if email_verification_key not in token_info or \ | ||
token_info[email_verification_key] is not True: | ||
raise Forbidden("Email is not verified") | ||
|
||
return token_info | ||
|
||
|
||
def _validate_account_access(token_info, account_id): | ||
with Transaction() as t: | ||
account_repo = AccountRepo(t) | ||
token_associated_account = account_repo.find_linked_account( | ||
token_info['iss'], | ||
token_info['sub']) | ||
account = account_repo.get_account(account_id) | ||
if account is None: | ||
raise NotFound(ACCT_NOT_FOUND_MSG) | ||
else: | ||
# Whether or not the token_info is associated with an admin acct | ||
token_authenticates_admin = \ | ||
token_associated_account is not None and \ | ||
token_associated_account.account_type == 'admin' | ||
|
||
# Enum of how closely token info matches requested account_id | ||
auth_match = account.account_matches_auth( | ||
token_info[JWT_EMAIL_CLAIM_KEY], | ||
token_info[JWT_ISS_CLAIM_KEY], | ||
token_info[JWT_SUB_CLAIM_KEY]) | ||
|
||
# If token doesn't match requested account id, and doesn't grant | ||
# admin access to the system, deny. | ||
if auth_match == AuthorizationMatch.NO_MATCH and \ | ||
not token_authenticates_admin: | ||
raise Unauthorized() | ||
|
||
return account |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from flask import render_template, jsonify | ||
|
||
from microsetta_private_api import localization | ||
from microsetta_private_api.api._account import \ | ||
_validate_account_access | ||
|
||
|
||
def render_consent_doc(account_id, language_tag, consent_post_url, token_info): | ||
_validate_account_access(token_info, account_id) | ||
|
||
# NB: Do NOT need to explicitly pass account_id into template for | ||
# integration into form submission URL because form submit URL builds on | ||
# the base of the URL that called it (which includes account_id) | ||
|
||
localization_info = localization.LANG_SUPPORT[language_tag] | ||
consent_html = render_template( | ||
"new_participant.jinja2", | ||
tl=localization_info[localization.NEW_PARTICIPANT_KEY], | ||
lang_tag=language_tag, | ||
post_url=consent_post_url | ||
) | ||
return jsonify({"consent_html": consent_html}), 200 |
Oops, something went wrong.