Skip to content

Commit

Permalink
Merge pull request #34 from brighthive/feat/HIVE-1144/add-secure-user…
Browse files Browse the repository at this point in the history
…-detail-endpoint

Feat/hive 1144/add secure user detail endpoint
  • Loading branch information
gregmundy authored Jan 12, 2021
2 parents 23d783b + 46109f3 commit b5aeb60
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 336 deletions.
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ flask-migrate = "*"
brighthive-authlib = "*"
requests = "*"
gevent = "*"
mci-database = {editable = true,ref = "master",git = "https://github.com/brighthive/mci-database.git"}
watchtower = "*"
boto3 = "*"
mci-database = {editable = true, git = "https://github.com/brighthive/mci-database.git", ref = "master"}

[dev-packages]
expects = "*"
Expand Down
624 changes: 317 additions & 307 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
version: '3'
services:
mci:
image: brighthive/master-client-index:1.0.5
# image: brighthive/master-client-index:1.0.5
image: brighthive/master-client-index:1.0.6
ports:
- "8001:8000"
environment:
Expand Down
2 changes: 1 addition & 1 deletion mci/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from mci.api.v1_0_0.helper_handler import HelperHandler as V1_0_0_HelperHandler
from mci.api.healthcheck import HealthCheckResource
from mci.api.errors import IndividualDoesNotExist
from mci.api.user import UserResource, UserDetailResource, UserRemovePIIResource
from mci.api.user import UserResource, UserDetailResource, UserRemovePIIResource, SecureUserDetailResource
from mci.api.helpers import SourceResource, GenderResource, AddressResource,\
DispositionResource, EthnicityRaceResource, EmploymentStatusResource,\
EducationLevelResource
17 changes: 17 additions & 0 deletions mci/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ def delete(self):
pass


class SecureUserDetailResource(UserResource):
""" A specific user. """

@token_required(Config.get_oauth2_provider(), scopes=['mci.secure-user-detail:get'])
def get(self, mci_id: str):
return self.get_request_handler(request.headers).create_secure_user_blob(mci_id)

def post(self):
pass

def put(self):
pass

def delete(self):
pass


class UserRemovePIIResource(UserResource):
""" A resource for removing the PII of an individual """

Expand Down
46 changes: 45 additions & 1 deletion mci/api/v1_0_0/user_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,41 @@ def create_user_blob(self, mci_id: str):
}
return user, 200

def create_secure_user_blob(self, mci_id: str):
"""
Creates an object with data of an existing user.
Called when GETing the `user` endpoint with an MCI ID.
Args:
mci_id (str): The MCI ID to query for.
Return:
dict, int: An object representing the specified user and the associated error code.
"""
user_obj = self._get_user(mci_id)

user = {
'mci_id': user_obj.mci_id,
'vendor_id': '' if user_obj.vendor_id is None else user_obj.vendor_id,
'registration_date': '' if user_obj.registration_date is None else datetime.strftime(user_obj.registration_date, '%Y-%m-%d'),
'vendor_creation_date': '' if user_obj.vendor_creation_date is None else datetime.strftime(user_obj.vendor_creation_date, '%Y-%m-%d'),
'ssn': '' if user_obj.ssn is None else user_obj.ssn,
'first_name': '' if user_obj.first_name is None else user_obj.first_name,
'suffix': '' if user_obj.suffix is None else user_obj.suffix,
'last_name': '' if user_obj.last_name is None else user_obj.last_name,
'middle_name': '' if user_obj.middle_name is None else user_obj.middle_name,
'mailing_address': self._get_mailing_address(user_obj),
'date_of_birth': '' if user_obj.date_of_birth is None else str(user_obj.date_of_birth),
'email_address': '' if user_obj.email_address is None else user_obj.email_address,
'telephone': '' if user_obj.telephone is None else user_obj.telephone,
'gender': self._find_gender_type(user_obj),
'ethnicity_race': self._find_user_ethnicity(user_obj),
'education_level': '',
'employment_status': self._find_employment_status_type(user_obj),
'source': self._find_source_type(user_obj)
}
return user, 200

def create_new_user(self, user_object):
"""
Creates a new user.
Expand Down Expand Up @@ -298,6 +333,7 @@ def _get_mailing_address(self, user: Individual):
'city': '',
'state': '',
'postal_code': '',
'county': '',
'country': ''
}

Expand All @@ -309,6 +345,7 @@ def _get_mailing_address(self, user: Individual):
mailing_address['city'] = '' if address.city is None else address.city
mailing_address['state'] = '' if address.state is None else address.state
mailing_address['postal_code'] = '' if address.postal_code is None else address.postal_code
mailing_address['county'] = '' if address.county is None else address.county
mailing_address['country'] = '' if address.country is None else address.country

return mailing_address
Expand Down Expand Up @@ -349,15 +386,22 @@ def _find_address_id(self, address):
except Exception:
_postal_code = None

try:
_county = address.get('county')
except Exception:
_county = None

try:
_country = address.get('country', '').upper()
except Exception:
_country = None

new_address = Address(_address, _city, _state, _postal_code, _country)
new_address = Address(_address, _city, _state,
_postal_code, _county, _country)

address = Address.query.filter_by(address=new_address.address, city=new_address.city,
state=new_address.state, postal_code=new_address.postal_code,
county=new_address.county,
country=new_address.country).first()
if address is not None:
result['id'] = address.id
Expand Down
4 changes: 3 additions & 1 deletion mci/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
EducationLevelResource, EmploymentStatusResource,
EthnicityRaceResource, GenderResource,
HealthCheckResource, SourceResource, UserDetailResource,
UserResource, UserRemovePIIResource)
UserResource, SecureUserDetailResource, UserRemovePIIResource)
from mci.api.errors import IndividualDoesNotExist
from mci.config import ConfigurationFactory
from mci_database.db import db
Expand Down Expand Up @@ -111,6 +111,8 @@ def create_app():
api.add_resource(UserResource, '/users', endpoint='users_ep')
api.add_resource(UserDetailResource, '/users/<mci_id>',
endpoint='user_detail_ep')
api.add_resource(SecureUserDetailResource,
'/user-details/<mci_id>', endpoint='secure_user_detail_ep')
api.add_resource(UserRemovePIIResource, '/users/remove-pii',
endpoint='user_remove_pii_ep')
# helper endpoints
Expand Down
8 changes: 3 additions & 5 deletions tests/test_helper_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@


class TestMCIAPI(object):
def test_health_check_endpoint(self, mocker, database, test_client):
mocker.patch(
'brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
headers = {'Authorization': 'Bearer 1qaz2wsx3edc'}
response = test_client.get('/health', headers=headers)
def test_health_check_endpoint(self, test_client):
headers = {'Content-Type': 'application/json'}
response = test_client.get('/', headers=headers)
expect(response.status_code).to(be(200))
expect(response.json).to(
have_keys('api_name', 'current_time', 'current_api_version', 'api_status'))
77 changes: 59 additions & 18 deletions tests/test_user_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .utils import post_new_individual


class TestMCIAPI(object):
@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_users_endpoint_empty(self, mocker, database, test_client):
Expand All @@ -21,7 +22,7 @@ def test_users_endpoint_empty(self, mocker, database, test_client):

assert response.status_code == 200
assert response.json['users'] == []

@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_users_endpoint_populated(self, mocker, database, individual_data, test_client, json_headers):
'''
Expand All @@ -45,31 +46,35 @@ def test_get_user_invalid(self, mocker, database, test_client):
assert response.status_code == 410
assert response.json['message']
assert response.json['message'] == 'An individual with that ID does not exist in the MCI.'

@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_get_user_valid(self, mocker, database, individual_data, test_client, json_headers):
'''
Tests that GETing a valid user returns the JSON and 200 status code.
'''
new_individual = post_new_individual(individual_data, test_client, json_headers)
new_individual = post_new_individual(
individual_data, test_client, json_headers)

response = test_client.get('/users/{}'.format(new_individual['mci_id']))
response = test_client.get(
'/users/{}'.format(new_individual['mci_id']))

assert response.status_code == 200
assert response.json

@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_post_users_existing(self, mocker, database, individual_data, test_client, json_headers):
'''
Tests that POSTing an existing user returns a 200 with correct user information.
'''
new_individual = post_new_individual(individual_data, test_client, json_headers)
'''
new_individual = post_new_individual(
individual_data, test_client, json_headers)

with requests_mock.Mocker() as m:
m.post("http://mcimatchingservice_mci_1:8000/compute-match",
json={"mci_id": new_individual['mci_id'], "score": 10.0}, status_code=201)
json={"mci_id": new_individual['mci_id'], "score": 10.0}, status_code=201)

response = test_client.post('/users', data=json.dumps(individual_data), headers=json_headers)
response = test_client.post(
'/users', data=json.dumps(individual_data), headers=json_headers)

assert response.status_code == 200
assert response.json['first_name'] == individual_data['first_name']
Expand All @@ -81,21 +86,23 @@ def test_post_users_existing(self, mocker, database, individual_data, test_clien
def test_post_users_bad_json(self, mocker, test_client, json_headers):
with requests_mock.Mocker() as m:
m.post("http://mcimatchingservice_mci_1:8000/compute-match",
json={"mci_id": "", "score": ""}, status_code=201)
json={"mci_id": "", "score": ""}, status_code=201)

bad_json = {
'first_name': "Single",
'middle_name': "Quote",
'last_name': "Mistake",
}

response = test_client.post('/users', data=bad_json, headers=json_headers)
response = test_client.post(
'/users', data=bad_json, headers=json_headers)
assert response.status_code == 400
assert response.json['error'] == 'Malformed or empty JSON object found in request body.'

@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_post_users_matching_down(self, mocker, individual_data, test_client, json_headers):
response = test_client.post('/users', data=json.dumps(individual_data), headers=json_headers)
response = test_client.post(
'/users', data=json.dumps(individual_data), headers=json_headers)

assert response.status_code == 400
assert response.json['error'] == 'The matching service did not return a response.'
Expand All @@ -104,15 +111,17 @@ def test_post_users_matching_down(self, mocker, individual_data, test_client, js
def test_remove_pii_invalid_id(self, mocker, database, test_client, json_headers):
response = test_client.post(
'/users/remove-pii', data=json.dumps({"mci_id": "123fakeid"}), headers=json_headers)

assert response.status_code == 410
assert response.json['message'] == 'An individual with that ID does not exist in the MCI.'

@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_remove_pii_valid_id(self, mocker, app_context, database, individual_data, test_client, json_headers):
new_individual = post_new_individual(individual_data, test_client, json_headers)

assert database.session.query(Individual).filter_by(first_name=individual_data['first_name']).first()
new_individual = post_new_individual(
individual_data, test_client, json_headers)

assert database.session.query(Individual).filter_by(
first_name=individual_data['first_name']).first()

response = test_client.post(
'/users/remove-pii',
Expand All @@ -121,11 +130,43 @@ def test_remove_pii_valid_id(self, mocker, app_context, database, individual_dat

assert response.status_code == 201

updated_individual = database.session.query(Individual).filter_by(mci_id=new_individual['mci_id']).first()
updated_individual = database.session.query(
Individual).filter_by(mci_id=new_individual['mci_id']).first()
assert updated_individual.first_name == None
assert updated_individual.last_name == None
assert updated_individual.middle_name == None
assert updated_individual.date_of_birth == None
assert updated_individual.email_address == None
assert updated_individual.telephone == None
assert updated_individual.ssn == None

@mock.patch('brighthive_authlib.providers.AuthZeroProvider.validate_token', return_value=True)
def test_user_detail_endpoint(self, mocker, test_client, json_headers):
new_user = {
"vendor_id": "abc-123",
"ssn": "9999",
"first_name": "Jacob",
"last_name": "Test",
"middle_name": "James",
"suffix": "Jr.",
"mailing_address": {
"address": "2000 Somewhere Street",
"city": "Arlington",
"state": "VA",
"postal_code": "25531",
"county": "Prince George",
"country": "US"
},
"date_of_birth": "2020-11-09",
"email_address": "[email protected]",
"telephone": "string"
}

new_individual = post_new_individual(
new_user, test_client, json_headers)
mci_id = new_individual['mci_id']
response = test_client.get(f'/user-details/{mci_id}',
headers=json_headers)
assert response.status_code == 200
assert 'ssn' in response.json.keys()
assert 'county' in response.json['mailing_address'].keys()
1 change: 0 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ def post_new_individual(individual_data, test_client, headers):
json={"mci_id": "", "score": ""}, status_code=201)

response = test_client.post('/users', data=json.dumps(individual_data), headers=headers)

assert response.status_code == 201
assert response.json['first_name'] == individual_data['first_name']
assert response.json['last_name'] == individual_data['last_name']
Expand Down

0 comments on commit b5aeb60

Please sign in to comment.