Skip to content

Commit

Permalink
Entitlements (V2) + support for Cognito app client with secret (#30)
Browse files Browse the repository at this point in the history
* Add Entitlements service (V2)
* Include optional secret hash for AWS Cognito authorization with client secret
* Update README instructions and include return value examples
  • Loading branch information
eternelpanic authored Oct 5, 2021
1 parent c40f045 commit be211b1
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 11 deletions.
110 changes: 107 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ A simple python client for the [OSDU](https://community.opengroup.org/osdu) data
- [Search with paging](#search-with-paging)
- [Get a record](#get-a-record)
- [Upsert records](#upsert-records)
- [List groupmembership for the current user](#list-groups)
- [List membership of a particular group](#list-membership)
- [Add a user to a particular group](#add-group)
- [Release Notes](release-notes.md)

## Clients
Expand Down Expand Up @@ -55,6 +58,12 @@ with the boto3 library directly through the Cognito service. You have to supply
- delete_record
- [delivery](osdu/delivery.py)
- get_signed_urls
- [entitlement](osdu/entitlement.py)
- get_groups
- get_group_members
- add_group_member
- delete_group_member
- create_group

## Installation

Expand Down Expand Up @@ -107,11 +116,12 @@ Environment variables:
1. `OSDU_USER`
1. `OSDU_PASSWORD`
1. `AWS_PROFILE`
1. `AWS_SECRETHASH`

```python
from osdu.client.aws import AwsOsduClient

data_partition = 'opendes'
data_partition = 'osdu'

osdu = AwsOsduClient(data_partition)
```
Expand All @@ -126,20 +136,28 @@ api_url = 'https://your.api.url.com' # Must be base URL only
client_id = 'YOURCLIENTID'
user = '[email protected]'
password = getpass()
data_partition = 'yourpartition'
data_partition = 'osdu'
profile = 'osdu-dev'

message = user + client_id
dig = hmac.new(client_secret.encode('UTF-8'), msg=message.encode('UTF-8'),
digestmod=hashlib.sha256).digest()
secretHash = base64.b64encode(dig).decode()



osdu = AwsOsduClient(data_partition,
api_url=api_url,
client_id=client_id,
user=user,
password=password,
secret_hash=secretHash,
profile=profile)
```

### Using the client

Below are just a few usage examples. See [integration tests](https://github.com/pariveda/osdupy/blob/master/tests/tests_integration.py) for more copmrehensive usage examples.
Below are just a few usage examples. See [integration tests](https://github.com/pariveda/osdupy/blob/master/tests/tests_integration.py) for more comprehensive usage examples.

#### Search for records by query

Expand Down Expand Up @@ -187,3 +205,89 @@ with open(new_or_updated_record, 'r') as _file:
result = osdu.storage.store_records([record])

```

#### List groupmembership for the current user

```python
result = osduClient.entitlements.get_groups()
# {
# "desId": "[email protected]",
# "groups": [
# {
# "description": "Datalake Plugin-Manager users",
# "email": "[email protected]",
# "name": "service.plugin.user"
# },
# {
# "description": "Datalake csv-parser admins",
# "email": "[email protected]",
# "name": "service.csv-parser.admin"
# },
# #...
# {
# "description": "The viewer of the datalake csv-parser service",
# "email": "[email protected]",
# "name": "service.csv-parser.viewer"
# }
# ],
# "memberEmail": "[email protected]"
# }
```

### List membership of a particular group

```python
result = osduClient.entitlements.get_group_members('[email protected]')
#{
# "members": [
# {
# "email": "[email protected]",
# "role": "OWNER"
# },
# {
# "email": "[email protected]",
# "role": "OWNER"
# },
# {
# "email": "[email protected]",
# "role": "OWNER"
# }
# ]
#}
```

### Add a user to a particular group
Add a user ([email protected]) to groups to give entitlement to search for and retrieve data.

```python
query = {
"email": "[email protected]",
#OWNER or MEMBER
"role": "MEMBER",
}
result = osduClient.entitlements.add_group_member('[email protected]',query)
query = {
"email": "[email protected]",
#OWNER or MEMBER
"role": "OWNER",
}
result = osduClient.entitlements.add_group_member('[email protected]',query)
```

### Delete user from a particular group
Remove a user ([email protected]) from a group.

```python
query = {
"email": "[email protected]",
#OWNER or MEMBER
"role": "MEMBER",
}
result = osduClient.entitlements.delete_group_member('[email protected]',query)
query = {
"email": "[email protected]",
#OWNER or MEMBER
"role": "OWNER",
}
result = osduClient.entitlements.delete_group_member('[email protected]',query)
```
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.0
0.3.0
22 changes: 16 additions & 6 deletions osdu/client/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ def profile(self):
def profile(self, val):
self._profile = val

def __init__(self, data_partition_id, api_url: str = None, client_id: str = None, user: str = None, password: str = None, profile: str = None) -> None:
def __init__(self, data_partition_id, api_url:str=None, client_id:str=None, secret_hash:str=None,user:str=None, password:str=None, profile:str=None) -> None:
"""Authenticate and instantiate a new AWS OSDU client. Uses Cognito directly to obtain an access token.
:param data_partition_id: [Required] OSDU data partition ID, e.g. 'opendes'
:param api_url: Must be only the base URL, e.g. 'https://myapi.myregion.mydomain.com'
If not provided as arg, client will attempt to load value from
environment variable: OSDU_API_URL.
:param client_id: OSDU client ID. Must be a Cognito App Client with no client secret.
:param secret_hash: Amazon Cognito Secret hash. This is described here: 'https://aws.amazon.com/premiumsupport/knowledge-center/cognito-unable-to-verify-secret-hash/'
:param user: OSDU username. If not provided as arg, client will attempt to load value from
environment variable: OSDU_USER.
:param password: OSDU password. If not provided as arg, client will attempt to load value from
Expand All @@ -41,25 +42,34 @@ def __init__(self, data_partition_id, api_url: str = None, client_id: str = None
self._client_id = client_id or os.environ.get('OSDU_CLIENT_ID')
self._user = user or os.environ.get('OSDU_USER')
self._profile = profile or os.environ.get('AWS_PROFILE')
self._secret_hash = secret_hash or os.environ.get('AWS_SECRETHASH')
if password:
self.get_tokens(password)
password = None # Don't leave password lying around.
self.get_tokens(password, secret_hash)
password = None # Don't leave password lying around.
else:
self.get_tokens(os.environ.get('OSDU_PASSWORD'))
self.get_tokens(os.environ.get('OSDU_PASSWORD'), secret_hash)

def get_tokens(self, password) -> None:
def get_tokens(self, password, secret_hash) -> None:
if self._profile:
session = boto3.Session(profile_name=self._profile)
print('Created boto3 session with profile: ', self._profile)
cognito = session.client('cognito-idp')
else:
cognito = boto3.client('cognito-idp')

auth_params = {
'USERNAME': self._user,
'PASSWORD': password
}
if secret_hash:
auth_params['SECRET_HASH'] = secret_hash

response = cognito.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
ClientId=self._client_id,
AuthParameters={'USERNAME': self._user, 'PASSWORD': password}
AuthParameters=auth_params
)


self._access_token = response['AuthenticationResult']['AccessToken']
self._refresh_token = response['AuthenticationResult']['RefreshToken']
7 changes: 6 additions & 1 deletion osdu/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..services.search import SearchService
from ..services.storage import StorageService
from ..services.dataset import DatasetService
from ..services.entitlements import EntitlementsService


class BaseOsduClient:
Expand All @@ -27,6 +28,10 @@ def search(self):
def storage(self):
return self._storage

@property
def entitlements(self):
return self._entitlements

@property
def delivery(self):
return self._delivery
Expand Down Expand Up @@ -57,9 +62,9 @@ def __init__(self, data_partition_id, api_url: str = None):
self._storage = StorageService(self)
self._delivery = DeliveryService(self)
self._dataset = DatasetService(self)
self._entitlements = EntitlementsService(self)
# TODO: Implement these services.
# self.__legal = LegaService(self)
# self.__entitlements = EntitlementsService(self)

# Abstract Method
def get_tokens(self, password):
Expand Down
93 changes: 93 additions & 0 deletions osdu/services/entitlements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
""" Provides a simple Python interface to the OSDU Entitlements API.
"""
import json
import requests
from .base import BaseService


class EntitlementsService(BaseService):

def __init__(self, client):
super().__init__(client, 'entitlements', service_version=2)


def get_groups(self) -> dict:
"""Retrieves all the groups for the user or service extracted from the OSDU Entitlements Service.
:returns: dict containing results
- desId: str: User Id.
- groups: list: of records resutling from search query
- memberEmail: str: User email address
"""

url = f'{self._service_url}/groups'
query = {}
response = requests.get(url=url, headers=self._headers(), json=query)
response.raise_for_status()
return response.json()

def get_group_members(self, groupEmail:str=None) -> dict:
"""Returns the members of an OSDU Group.
:param groupEmail: String representing the email adress of the group to be listed.
:returns: dict members containing list of group members:
- email: str: Email Address of user
- roles: str: OWNER or MEMBER
"""

url = f'{self._service_url}/groups/' + groupEmail + '/members'
query = ''
response = requests.get(url=url, headers=self._headers(), json=query)
response.raise_for_status()
return response.json()

def add_group_member(self, groupEmail:str, query: dict) -> dict:
"""Adds a member to an OSDU Group.
:param query: dict representing the JSON-style query to be sent to the entitlements API. Must adhere to
the syntax suported by OSDU. For more details, see:
https://community.opengroup.org/osdu/documentation/-/blob/master/platform/tutorials/core-services/EntitlementsService.md
:returns: dict query that was input
"""

url = f'{self._service_url}/groups/' + groupEmail + '/members'
response = requests.post(url=url, headers=self._headers(), json=query)
response.raise_for_status()
return response.json()

def delete_group_member(self, groupEmail:str, query: dict) -> dict:
"""Deletes a member from an OSDU Group.
:param query: dict representing the JSON-style query to be sent to the entitlements API. Must adhere to
the syntax suported by OSDU. For more details, see:
https://community.opengroup.org/osdu/documentation/-/blob/master/platform/tutorials/core-services/EntitlementsService.md
:returns: dict query that was input
"""

url = f'{self._service_url}/groups/' + groupEmail + '/members'
response = requests.delete(url=url, headers=self._headers(), json=query)
response.raise_for_status()
return response.json()


def create_group(self, groupEmail:str, query: dict) -> dict:
"""Create an OSDU Group
:param query: dict representing the JSON-style query to be sent to the entitlements API. Must adhere to
the syntax suported by OSDU. For more details, see:
https://community.opengroup.org/osdu/documentation/-/blob/master/platform/tutorials/core-services/EntitlementsService.md
:returns: dict containing members: aggregations, results, totalCount
- aggregations: dict: returned only if 'aggregateBy' specified in query
- results: list: of records resutling from search query
- totalCount: int: the total number of results despite any 'limit' specified in the
query or the 1,000 record limit of the API
"""

url = f'{self._service_url}/groups/' + groupEmail + '/members'
response = requests.delete(url=url, headers=self._headers(), json=query)
response.raise_for_status()
return response.json()
12 changes: 12 additions & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Release Notes

## `0.3.0`

**Release Date**: 2021-10-01

Changes by Chris Parsons from Petrosys to include Entitlements service.

## `0.2.1`

**Release Date**: 2021-10-01

Changes by Chris Parsons from Petrosys to include secret hash for more security around Cognito Authorization.

## `0.1.0`

**Release Date**: 2020.10.13
Expand Down
13 changes: 13 additions & 0 deletions tests/unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from osdu.client.aws import AwsOsduClient
from osdu.client.simple import SimpleOsduClient

import hmac
import hashlib
import base64


class TestAwsOsduClient(TestCase):

Expand All @@ -11,10 +15,19 @@ def test_initialize_aws_client_with_args(self, mock_session):
partition = 'opendes'
api_url = 'https://your.api.url.com'
client_id = 'YOURCLIENTID'
client_secret = 'YOURCLIENTSECRET'
user = '[email protected]'
password = 'p@ssw0rd'
profile = 'osdu-dev'



message = user + client_id
dig = hmac.new(client_secret.encode('UTF-8'), msg=message.encode('UTF-8'),digestmod=hashlib.sha256).digest()
secretHash = base64.b64encode(dig).decode()



client = AwsOsduClient(partition,
api_url=api_url,
client_id=client_id,
Expand Down

0 comments on commit be211b1

Please sign in to comment.