Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
502c035
fix missing legal tag issue
JohnnyReichmanPariveda Mar 8, 2022
892e28e
add update_token to simple client
JohnnyReichmanPariveda Mar 11, 2022
30832d4
add update_token with aws client
JohnnyReichmanPariveda Mar 11, 2022
6102810
add service principal refresh token, and some small coding style fixes
JohnnyReichmanPariveda Mar 11, 2022
89604ca
check token expiry on every service call. Attempt update if expired
JohnnyReichmanPariveda Mar 14, 2022
b4a8c02
add refresh token info in readme
JohnnyReichmanPariveda Mar 14, 2022
1644325
comment formatting
JohnnyReichmanPariveda Mar 14, 2022
536df48
readme formatting
JohnnyReichmanPariveda Mar 14, 2022
df7ce27
move update_token functions from AuthenticationService to respective …
JohnnyReichmanPariveda Mar 16, 2022
6b8de66
code cleanup--remove unused property getter/setter and update comments
JohnnyReichmanPariveda Mar 16, 2022
58cf62d
responding to coding-style PR comments
JohnnyReichmanPariveda Mar 16, 2022
a6206e9
add raise_for_status() check in SP client
JohnnyReichmanPariveda Mar 16, 2022
c1ea1ac
add log entry to service_principal_util
JohnnyReichmanPariveda Mar 16, 2022
42bb111
raise exception for if aws client fails update due to no OSDU_PASSWORD
JohnnyReichmanPariveda Mar 16, 2022
01d2cb4
remove aws client password issue
JohnnyReichmanPariveda Mar 17, 2022
9330816
add legal service requests and tests
JohnnyReichmanPariveda Mar 28, 2022
bf68415
move shared strings, variables to class constructor
JohnnyReichmanPariveda Mar 29, 2022
b0d0ca6
update README--add legal service
JohnnyReichmanPariveda Mar 29, 2022
48a313f
update comments
JohnnyReichmanPariveda Mar 29, 2022
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ For OSDU on AWS, this client is useful in the case where you may want to perform
- add_group_member
- delete_group_member
- create_group
- [legal](osdu/services/legal.py)
- get_legaltag
- create_legaltag
- delete_legaltag
- get_legaltags
- update_legaltag
- batch_retrive_legaltags
- validate_legaltags
- get_legaltag_properties

## Installation

Expand Down Expand Up @@ -190,6 +199,21 @@ osdu_client = AwsOsduClient(data_partition,
profile=profile)
```

### Automatically re-authorizing the client
Each client will automatically attempt to re-authorize when its access token expires. In order for this re-authorization to succeed, you will need to supply the client with additional parameters (either through environment variables or in their consructor):

#### Simple Client:
1. OSDU_CLIENTWITHSECRET_ID
1. OSDU_CLIENTWITHSECRET_SECRET
1. REFRESH_TOKEN
1. REFRESH_URL

#### AWS Client:
1. OSDU_PASSWORD (in the environment variables, or somewhere else it can persist securely)

#### Service Principal:
N/A--this client can re-authorize with just the variables needed for it to instantiate

### 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 comprehensive usage examples.
Expand Down
37 changes: 34 additions & 3 deletions osdu/client/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
"""

import os

from time import time
from ..services.search import SearchService
from ..services.storage import StorageService
from ..services.dataset import DatasetService
from ..services.entitlements import EntitlementsService
from ..services.legal import LegalService


class BaseOsduClient:

@property
def access_token(self):
self._ensure_valid_token()
return self._access_token

@property
Expand All @@ -38,6 +40,10 @@ def delivery(self):
@property
def dataset(self):
return self._dataset

@property
def legal(self):
return self.__legal

@property
def data_partition_id(self):
Expand All @@ -64,6 +70,31 @@ def __init__(self, data_partition_id, api_url: str = None):
self._storage = StorageService(self)
self._dataset = DatasetService(self)
self._entitlements = EntitlementsService(self)
# TODO: Implement these services.
# self.__legal = LegaService(self)
self.__legal = LegalService(self)

def _need_update_token(self):
return hasattr(self, "_token_expiration") and self._token_expiration < time() or self._access_token is None

def _ensure_valid_token(self):
"""Determines if the current access token associated with the client has expired.
If the token is not expired, the current access_token will be returned, unchanged.
If the token has expired, this function will attempt to refresh it, update it on client, and return it.
For simple clients, refresh requires a OSDU_CLIENTWITHSECRET_ID, OSDU_CLIENTWITHSECRET_SECRET, REFRESH_TOKEN, and REFRESH_URL
For Service Principal clients, refresh requires a resource_prefix and AWS_PROFILE (same as initial auth)
For AWS clients, refresh requires OSDU_USER, OSDU_PASSWORD, AWS_PROFILE, and OSDU_CLIENT_ID

:param client: client in use

:returns: tuple containing 2 items: the new access token and it's expiration time
- access_token: used to access OSDU services
- expires_in: expiration time for the token
"""
if(self._need_update_token()):
token = self._update_token()
else:
token = self._access_token, self._token_expiration if hasattr(self, "_token_expiration") else None
return token

def _update_token(self):
pass #each client has their own update_token method

12 changes: 8 additions & 4 deletions osdu/client/_service_principal_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
# - Refactored _get_secret method to fix UnboundLocalError for local variable 'secret'.
# - Refactored _get_secret method to simplify try/except flow and to print secret_name on exception.
# - Updated formatting to be PEP8-compliant.
#
# 2022-03-16 johnny.reichman@parivedasolutions.com
# - Updated to return the token expiration in addition to the token
# - Added a more descriptive exception check after the POST request
import base64
from time import time
import boto3
import requests
import json
Expand Down Expand Up @@ -113,7 +116,8 @@ def get_service_principal_token(self, resource_prefix):

token_url = '{}?grant_type=client_credentials&client_id={}&scope={}'.format(
token_url, client_id, aws_oauth_custom_scope)

response = requests.post(url=token_url, headers=headers)

return json.loads(response.content.decode())['access_token']
response.raise_for_status()
response_json = json.loads(response.content.decode())
return response_json['access_token'], response_json['expires_in'] + time()
14 changes: 13 additions & 1 deletion osdu/client/aws.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from time import time
import boto3

from ._base import BaseOsduClient
Expand Down Expand Up @@ -71,6 +72,17 @@ def get_tokens(self, password, secret_hash) -> None:
AuthParameters=auth_params
)


self._token_expiration = response['AuthenticationResult']['ExpiresIn'] + time()
self._access_token = response['AuthenticationResult']['AccessToken']
self._refresh_token = response['AuthenticationResult']['RefreshToken']

# TODO: refresh can only be used if password is in environment variables. Is there another way to store the password securely?
def _update_token(self):
password = os.environ.get('OSDU_PASSWORD')
if(password):
self.get_tokens(password, self._secret_hash)
password = None
return self._access_token, self._token_expiration
else:
raise Exception('Expired or invalid access token. OSDU_PASSWORD env variable must be set for token to be auto refreshed.')

11 changes: 10 additions & 1 deletion osdu/client/aws_service_principal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@

class AwsServicePrincipalOsduClient(BaseOsduClient):

@property
def resource_prefix(self):
return self._resource_prefix

def __init__(self, data_partition_id: str, resource_prefix: str, profile: str = None, region: str = None):
self._sp_util = ServicePrincipalUtil(
resource_prefix, profile=profile, region=region)
self._resource_prefix = resource_prefix
self._access_token = self._get_tokens()
self._access_token,self._token_expiration = self._get_tokens()

super().__init__(data_partition_id, self._sp_util.api_url)

def _get_tokens(self):
return self._sp_util.get_service_principal_token(self._resource_prefix)

def _update_token(self):
self._access_token, self._token_expiration = self._sp_util.get_service_principal_token(self._resource_prefix)
return self._access_token, self._token_expiration
35 changes: 31 additions & 4 deletions osdu/client/simple.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os
import requests
from time import time
from ._base import BaseOsduClient


Expand All @@ -6,15 +9,39 @@ class SimpleOsduClient(BaseOsduClient):

This client assumes you are obtaining a token yourself (e.g. via your application's
login form or otheer mechanism. With this SimpleOsduClient, you simply provide that token.
With this simplicity, you are also then respnsible for reefreeshing the token as needed and
re-instantiating the client with the new token.
With this simplicity, you are also then respnsible for refreshing the token as needed either by manually
re-instantiating the client with the new token or by providing the authentication client id, secret, refresh token, and refresh url
and allowing the client to attempt the refresh automatically.
"""

def __init__(self, data_partition_id: str, access_token: str, api_url: str=None) -> None:
def __init__(self, data_partition_id: str, access_token: str=None, api_url: str=None, refresh_token: str=None, refresh_url: str=None) -> None:
"""
:param: access_token: The access token only (not including the 'Bearer ' prefix).
:param: api_url: must be only the base URL, e.g. https://myapi.myregion.mydomain.com
:param: refresh_token: The refresh token only (not including the 'Bearer ' prefix).
:param: refresh_url: The authentication Url, typically a Cognito URL ending in "/token".
"""
super().__init__(data_partition_id, api_url)

self._access_token = access_token
self._access_token = access_token
self._refresh_token = refresh_token or os.environ.get('OSDU_REFRESH_TOKEN')
self._refresh_url = refresh_url or os.environ.get('OSDU_REFRESH_URL')
self._client_id = os.environ.get('OSDU_CLIENTWITHSECRET_ID')
self._client_secret = os.environ.get('OSDU_CLIENTWITHSECRET_SECRET')

def _update_token(self) -> dict:
if not self._refresh_token or not self._refresh_url:
raise Exception('Expired or invalid access token. Both \'refresh_token\' and \'refresh_url\' must be set for token to be auto refreshed.')

data = {'grant_type': 'refresh_token',
'client_id': self._client_id,
'client_secret': self._client_secret,
'refresh_token': self._refresh_token,
'scope': 'openid email'}
headers = {}
headers["Content-Type"] = "application/x-www-form-urlencoded"
response = requests.post(url=self._refresh_url,headers=headers, data=data)
response.raise_for_status()
self._access_token = response.json()["access_token"]
self._token_expiration = response.json()["expires_in"] + time()
return self._access_token, self._token_expiration
3 changes: 2 additions & 1 deletion osdu/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ def __init__(self, client, service_name: str, service_version: int):
self._client = client
self._service_url = f'{self._client.api_url}/api/{service_name}/v{service_version}'


def _headers(self):
return {
"Content-Type": "application/json",
"data-partition-id": self._client._data_partition_id,
"Authorization": "Bearer " + self._client.access_token
}
}
98 changes: 98 additions & 0 deletions osdu/services/legal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
""" Provides a simple Python interface to the OSDU Legal API.
"""
from typing import List
import requests
from .base import BaseService


class LegalService(BaseService):

def __init__(self, client):
super().__init__(client, service_name='legal', service_version=1)

def get_legaltag(self, legaltag_name: str):
"""Returns information about the given legaltag.

param legaltag_name: the name of the legaltag of interest
"""
url = f'{self._service_url}/legaltags/{legaltag_name}'
response = self.__execute_request('get', url)

return response.json()

def create_legaltag(self, legaltag: dict):
"""Create a new legaltag.

param legaltag: a JSON representation of a legaltag
"""
url = f'{self._service_url}/legaltags'
response = self.__execute_request('post', url, json=legaltag)

return response.json()

def delete_legaltag(self, legaltag_name: str) -> bool:
"""Deletes the given legaltag. This operation cannot be reverted (except by re-creating the legaltag).

:param legaltag_name: the name of the legaltag to delete
:returns: True if legaltag deleted successfully. Otherwise False.
"""
url = f'{self._service_url}/legaltags/{legaltag_name}'
response = self.__execute_request('delete', url)

return response.status_code == 204

def get_legaltags(self, valid: bool = True):
"""Fetches all matching legaltags.

:param valid: Boolean to restrict results to only valid legaltags (true) or only invalid legal tags (false). Default is true
"""
url = f'{self._service_url}/legaltags/' + ('?valid=true' if valid else '?valid=false')
response = self.__execute_request('get', url)

return response.json()

def update_legaltag(self, legaltag: dict):
"""Updates a legaltag. Empty properties are ignored, not deleted.

:param legaltag: dictionary of properties to add/change to an existing legaltag
"""
url = f'{self._service_url}/legaltags'
response = self.__execute_request('put', url, json=legaltag)

return response.json()

def batch_retrive_legaltags(self, legaltag_names: List[str]):
"""Retrieves information about a list of legaltags

:param legaltag_names: List of legaltag names to fetch information about
"""
url = f'{self._service_url}/legaltags:batchRetrieve'
payload = {'names': legaltag_names}
response = self.__execute_request('post', url, json=payload)

return response.json()

def validate_legaltags(self, legaltag_names: List[str]):
"""Validates the given legaltags--returning a list of which legaltags are invalid.

:param legaltag_names: List of legaltag names to validate
"""
url = f'{self._service_url}/legaltags:validate'
payload = {'names': legaltag_names}
response = self.__execute_request('post', url, json=payload)

return response.json()

def get_legaltag_properties(self):
"""Fetch information about possible values for legaltag properties"""
url = f'{self._service_url}/legaltags:properties'
response = self.__execute_request('get', url)

return response.json()

def __execute_request(self, method: str, url: str, json=None):
headers = self._headers()
response = requests.request(method, url, headers=headers, json=json)
response.raise_for_status()

return response
Loading