Skip to content
This repository has been archived by the owner on Aug 5, 2024. It is now read-only.

Feature RT-66: SDS Client #22

Merged
merged 6 commits into from
Jul 3, 2019
Merged

Feature RT-66: SDS Client #22

merged 6 commits into from
Jul 3, 2019

Conversation

Philip-Woods
Copy link
Contributor

@Philip-Woods Philip-Woods commented Jun 28, 2019

This PR contains SDS Lookup implementation for RT-66. Primarily includes the following:

  • SDS Client: Handles LDAP lookup for accredited supplier details and MHS details of a supplier.
  • MHSAttributeLookup: Acts mainly as a placeholder wrapper for the SDS Client and what will be the caching of the SDS responses
  • Testing for the above implementations, also added an async test wrapper to the common folder allowing us to execute asynchronous tests easily

@Philip-Woods Philip-Woods changed the title Feature/rt66 sds client Feature RT-66: SDS Client Jun 28, 2019
@Philip-Woods Philip-Woods force-pushed the feature/RT66-SDS-Client branch 2 times, most recently from 2aa98c6 to 8bcb5e4 Compare July 1, 2019 09:41
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate there are quite a lot of comments here, but I've been fairly detailed with it being the first review, so we can make sure we're all on the same page in terms of approach. Generally, this looks good though!

One point I wanted to raise was regarding logging. I appreciate we've still got an outstanding task to define standards around levels, etc. and the required output format, but we should be trying to ensure that we include appropriate logging as we go, rather than having to include it afterwards. If you use the standard logging module with sensible log levels, it should mean it's relatively easy to convert to whatever format we settle on, and we won't have to actually add the log messages later. (CC @ismail-s) One good example here would be that I think we should log at warning level if we find more than one result for an SDS query - the spec says we shouldn't, and we can safely continue using the first result, but we should flag the fact that it's happened.

.gitignore Show resolved Hide resolved
common/utilities/test_utilities.py Show resolved Hide resolved
result = await client._mhs_details_lookup(PARTY_KEY, SERVICE_ID)

self.assertIsNotNone(result)
self.assertEqual(result[0]['attributes']['nhsMHSEndPoint'], ['https://vpn-client-1411.opentest.hscic.gov.uk/'])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably test that the full list of attributes is returned, since it's one of the acceptance criteria on the story. Also, we need to verify that attributes not on the list are filtered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the test to check for all attributes, however because of the mhs attribute call returning 2 sets of attributes if I check the values for an exact match it will fail half the time (due to responses including a unique id). I've included all the attributes bar the two unique ones and still checked that they are present, just not their values. Does this seem reasonable?

Also added length checking to the attribute list to ensure it contains exactly the number we need

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this does the job, but isn't the MHS attribute call returning 2 sets of attributes because that's what we have in the dummy LDAP server data? Couldn't we edit it so that it doesn't return two sets for this query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point, I've removed the duplicate data and added all attribute checks in so the test should be complete now.

@Philip-Woods Philip-Woods force-pushed the feature/RT66-SDS-Client branch from 8bcb5e4 to 0817497 Compare July 1, 2019 16:21
This commit includes the code for the SDS lookup client. Including the
following:
- SDSClient: This handles LDAP calls to the directory
- SDSHandler which handles the interaction with the SDSClient, in future
this will consult a cache for SDS data
- Test code which emulates the opentest directory to provide responses
for unit tests
- .gitignore for credentials
@Philip-Woods Philip-Woods force-pushed the feature/RT66-SDS-Client branch from 8070641 to 564ff81 Compare July 1, 2019 16:28
Copy link
Contributor

@ismail-s ismail-s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bear in mind that I didn't look at Gareth's comments before doing my review.

async def test_tls(self):
pass
# client = sds.SDSClient(DEV_ADDRESS)
# result = await client.get_mhs_details(ODS_CODE, SERVICE_ID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in a previous commit

@@ -9,6 +9,7 @@ verify_ssl = true
integration-adaptors-common = {editable = true,path = "./../common"}
requests = "*"
tornado = "*"
ldap3 = "*"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question not specific to this PR, but should we be specifying versions (ie the major version)?

# These need replacing as the appropriate TLS certs are received for PTL
PRIVATE_KEY = pathlib.Path(ROOT_DIR) / 'data' / 'certs' / 'client.key'
LOCAL_CERT_FILE = pathlib.Path(ROOT_DIR) / 'data' / 'certs' / 'client.cert'
CA_CERTS_FILE = pathlib.Path(ROOT_DIR) / 'data' / 'certs' / 'client.cert'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the same as the previous constant and may change, then simpler to do:

Suggested change
CA_CERTS_FILE = pathlib.Path(ROOT_DIR) / 'data' / 'certs' / 'client.cert'
CA_CERTS_FILE = LOCAL_CERT_FILE

for the timebeing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately these will be different files, I'll update the paths to demonstrate this and I've added a constant holding the certs dir path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed these completely actually

coro = asyncio.coroutine(f)
future = coro(*args, **kwargs)
loop = asyncio.get_event_loop()
loop.run_until_complete(future)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like asyncio.new_event_loop() would be better for test isolation.

And then it looks like asyncio.run(future) would be better than that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced the loop = ... and loop.run_until_complete(future) with just asyncio.run(future). Is this the correct interpretation that we don't need asyncio.new_event_loop()?

f"ods: {ods_code} - interaction: {interaction_id}")
return details[0]['attributes']

async def _accredited_system_lookup(self, ods_code: str, interaction_id: str):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general comment (not sure that we have/want a rule around whether we want type hints everywhere or not), if you want to type hint the return type, then you can do async def blah() -> someType:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

'nhsMhsFQDN': 'vpn-client-1411.opentest.hscic.gov.uk',
'nhsMhsSvcIA': 'urn:nhs:names:services:psis:MCCI_IN010000UK13',
'nhsProductKey': '7374',
# 'uniqueIdentifier': ['S918999410559']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

'nhsMHSSyncReplyMode': 'MSHSignalsOnly',
'nhsMHsIN': 'MCCI_IN010000UK13',
'nhsMHsSN': 'urn:nhs:names:services:psis',
# 'nhsMhsCPAId': 'S918999410559', # Exclude this value since there seems to be two responses causing errors on this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sds call should return exactly 1 value for this lookup, however my opentest instance seems to return 2, this had been copied to the test data and caused an issue due to this value being an unique identifier. I've deleted the second instance in the test data so I've readded these attributes

'nhsMhsFQDN': 'vpn-client-1411.opentest.hscic.gov.uk',
'nhsMhsSvcIA': 'urn:nhs:names:services:psis:MCCI_IN010000UK13',
'nhsProductKey': '7374',
# 'uniqueIdentifier': ['S918999410559']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

import mhs.routing.sds as sds
import mhs.routing.tests.ldap_mocks as mocks
from utilities.test_utilities import async_test
import mhs.routing.routing_exception as re
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: re is the name of the regex stdlib module, so it's a little confusing to use that name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to just use the module name



class TestMHSAttributeLookupHandler(TestCase):
opentest_expected_reliability = {'nhsMHSAckRequested': 'always',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this variable being used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, it's no longer being used since we're using the full attribute set defined out of the class

mhs-reference-implementation/mhs/routing/sds.py Outdated Show resolved Hide resolved
mhs-reference-implementation/mhs/routing/sds.py Outdated Show resolved Hide resolved
raise re.RoutingException('No response from accredited system lookup')

if len(accredited_system_lookup) > 1:
logging.warning(f"More than one accredited system details returned on inputs: "
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues with the log statements added, but we also need to think about how we would trace successful flows through the code, etc. Given log aggregation tools are pretty standard nowadays, our logging approach should be "more is better". As long as we make use of the correct log levels to distinguish between what we're logging, an admin using Splunk, etc. should be able to filter our log messages easily enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the module reference Ismail suggested and added a couple extra logs around message IDs. Noted that we should try to consist logs throughout

mhs-reference-implementation/mhs/routing/tests/test_sds.py Outdated Show resolved Hide resolved
raise re.RoutingException('No response from accredited system lookup')

if len(accredited_system_lookup) > 1:
logging.warning(f"More than one accredited system details returned on inputs: "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should start including logger = logging.getLogger(__name__) in files that we log in, so as to include the location of the file the log message came from. This is a standard line from the Python website.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this

@briandiggle
Copy link
Contributor

I'm noticing in the code that there is a mixture of terminology used for the interation ID. It is called "Service ID" in places, and "interaction ID" in others. Can we standardise on "interaction ID" as this is commonly accepted term.

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good, just a couple of comments on logging that I think we should address before this goes in.

mhs-reference-implementation/mhs/routing/tests/test_sds.py Outdated Show resolved Hide resolved
mhs-reference-implementation/mhs/routing/sds.py Outdated Show resolved Hide resolved
ghost
ghost previously approved these changes Jul 3, 2019
ismail-s
ismail-s previously approved these changes Jul 3, 2019
self.connection = sds_connection
self.timeout = timeout

async def get_mhs_details(self, ods_code: str, interaction_id: str) -> Dict:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another point: Dict can be made more specific like eg Dict[keyType, valueType]. As can other container types eg List[str]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'd noticed this when looking at type hints, I refrained from using it here since the value type can be more than 1 type but I think in the future we should probably include the key/value types

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, you could use Any, or Union[someType, someOtherType], though that could get long-winded.

return response

async def _mhs_details_lookup(self, party_key: str, interaction_id: str):
async def _mhs_details_lookup(self, party_key: str, interaction_id: str) -> List:
"""
Given a party key and a service, this will return an object containing the attributes of that party key,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming interaction id and service mean the same thing (not familiar with all this terminology yet).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, I will change this as Brian has specified previously that we should try to stick to interaction id, must have missed this one!


def build_sds_connection(ldap_address: str) -> ldap3.Connection:
"""
Given an ip address this will return a ldap3 connection object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantic/minor: do you have to pass an ip address or any address?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the ldap lib it seems like any valid ldap address with be accepted, I'll update to reflect this

@Philip-Woods Philip-Woods dismissed stale reviews from ismail-s and ghost via 66bd93b July 3, 2019 13:34
@Philip-Woods Philip-Woods merged commit 7876d4b into develop Jul 3, 2019
@Philip-Woods Philip-Woods deleted the feature/RT66-SDS-Client branch July 16, 2019 15:23
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants