Skip to content

Commit

Permalink
Release 2.1.0 (#41)
Browse files Browse the repository at this point in the history
* Finally deprecated legacy auth. method

Authentication by client_id and password was finally removed from the listener.

* Added config file parameter to Listener constructor

* Updated semver and release notes

* Removed unused code and restored get_uri_context

* Revamped README and CHANGELOG
  • Loading branch information
AlanKev117 authored May 15, 2024
1 parent ff485d4 commit ff2dc9d
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 181 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,9 @@ the subscription *and* consuming it. Instead they will just consume an already e

2.0.5 / 2022-11-02
==================
- [changed] - Removed threaded article quota limit check.
- [changed] - Removed threaded article quota limit check.

2.1.0 / 2023-03-15
==================
- [removed] - Fully removed legacy authentication method by password
- [added] - Explicit argument to provide the path to the customer_config.json file to the Listener constructor (view README for more details)
37 changes: 24 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ There is currently one way to authenticate, which is by using **your user key**.
Configuring
___________

To run this code, you need to provide credentials from one of the authentication methods and your subscriptions. There are 3 ways to do this. You can either set environment variables or you can use a configuration file.
To run this code, you need to provide credentials from one of the authentication methods and your subscriptions. There are 3 ways to do this: you can set environment variables, use a configuration file or pass an argument to a constructor.

1. Set environment variables.
###################################################################

To set your service account credentials, set:
To set your service account credentials, set the `USER_KEY` environment variable:

.. code-block::
export USER_KEY="1234567890-098765432"
- An environment variable named 'USER_KEY'.
To set your subscription ID, simply set an environment variable named 'SUBSCRIPTION_ID' like so

Expand All @@ -43,18 +46,33 @@ The code above is the command line expression for setting this environment varia
2. Using the configuration file.
###################################################################

In this codebase you will find a file named 'customer_config.json'. You are not required to use this file. If you prefer to use this configuration file, follow these directions: Open this file and add your service account credentials. Then add your subscription IDs. Remember that this is a JSON file so follow basic JSON formatting and syntax conventions.
In this codebase you will find a file named 'customer_config.json'. You are not required to use this file. If you prefer to use this option, fill the JSON object within by adding your user key and your subscription ID. Remember that this is a JSON file so follow basic JSON formatting and syntax conventions.

> The listener will search for the `customer_config.json` file inside your `$HOME` directory by default.

If you prefer using an explicit path to your configuration file, pass the absolute path to the Listener constructor like so:

.. code-block:: python
from dnaStreaming.listener import Listener
# Config. file authentication
listener = Listener(config_file=<ABSOLUTE PATH TO YOUR CONFIG. FILE>)
3. Pass in variables as function arguments.
###################################################################

You may pass your service account credentials to the Listener constructor like so:
You may pass your user key to the Listener constructor and your subscription ID to the listen method like so:

.. code-block:: python
from dnaStreaming.listener import Listener
# User key authentication
# Use the user_key argument to provide your credentials
listener = Listener(user_key=<YOUR USER KEY>)
# Use the subscription_id argument to provide your subscription id to the listener
listener.listen(callback, subscription_id=<YOUR SUBSCRIPTION ID>)
# same parameter for the async variation
listener.listen_async(callback, subscription_id=<YOUR SUBSCRIPTION ID>)
Or you may use the environment variables.
Expand Down Expand Up @@ -200,13 +218,6 @@ Or
python ./dnaStreaming/demo/show_stream_async.py -s
If you are having `ImportError: No module named ...` run this in your terminal before running the demo:

.. code-block::
export PYTHONPATH='.'
Running Docker Demo

Execute the following at the project root:
Expand Down
119 changes: 13 additions & 106 deletions dnaStreaming/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,23 @@
import errno
import json
import os
import requests
from pathlib import Path

class Config(object):
OAUTH_URL = 'https://accounts.dowjones.com/oauth2/v1/token'

DEFAULT_HOST = 'https://api.dowjones.com'

DEFAULT_CUST_CONFIG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), './customer_config.json'))
DEFAULT_CUST_CONFIG_PATH = str(Path.home()) + '/customer_config.json'
ENV_VAR_SUBSCRIPTION_ID = 'SUBSCRIPTION_ID'
ENV_VAR_USER_KEY = 'USER_KEY'
ENV_VAR_SERVICE_ACCOUNT_ID = 'SERVICE_ACCOUNT_ID'
ENV_VAR_USER_ID = 'USER_ID'
ENV_VAR_CLIENT_ID = 'CLIENT_ID'
ENV_VAR_PASSWORD = 'PASSWORD'
ENV_VAR_API_HOST = 'API_HOST'

def __init__(self, service_account_id=None, user_key=None, user_id=None, client_id=None, password=None):
self.customer_config_path = self.DEFAULT_CUST_CONFIG_PATH
def __init__(self, service_account_id=None, user_key=None, config_file=None):
self.customer_config_path = self.DEFAULT_CUST_CONFIG_PATH if config_file is None else config_file
self.initialized = False
self.service_account_id = service_account_id
self.user_key = user_key
self.user_id = user_id
self.client_id = client_id
self.password = password

self.headers = None

Expand Down Expand Up @@ -53,110 +47,26 @@ def get_headers(self):
return self.headers

def get_authentication_headers(self):
if self.oauth2_credentials():
return {
'Authorization': self._fetch_jwt()
}

# missing oauth creds, authenticate the old way via user key
user_key = self.get_user_key()
if user_key:
return {
'user-key': user_key
}

else:
msg = '''Could not find determine credentials:
Must specify account credentials as user_id, client_id, and password, either through env vars, customer_config.json, or as args to Listener constructor
(see README.rst)'''
raise Exception(msg)

def _fetch_jwt(self):
oauth2_credentials = self.oauth2_credentials()
user_id = oauth2_credentials.get('user_id')
client_id = oauth2_credentials.get('client_id')
password = oauth2_credentials.get('password')

# two requests need to be made, to the same URL, with slightly different params, to finally obtain a JWT
# the second request contains params returned in the response of the first request
# I know this makes no sense but it is what it is
body = {
'username': user_id,
'client_id': client_id,
'password': password,
'connection': 'service-account',
'grant_type': 'password',
'scope': 'openid service_account_id'
}

try:

response = _get_requests().post(self.OAUTH_URL, data=body).json()
body['scope'] = 'openid pib'
body['grant_type'] = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
body['access_token'] = response.get('access_token')
body['assertion'] = response.get('id_token')

response = _get_requests().post(self.OAUTH_URL, data=body).json()

return '{0} {1}'.format(response['token_type'], response['access_token'])
except (KeyError, ValueError):
msg = '''Unable to retrieve JWT with the given credentials:
User ID: {0}
Client ID: {1}
Password: {2}
'''.format(user_id, client_id, password)
msg = """
Unable to find credentials. Specify your account credentials by choosing one of the following ways:
- set your user key to an env var under the name 'USER_KEY'
- place your customer_config.json file in your home directory (e.g. ~/, $HOME/)
- pass the absolute path of your customer_config.json file to the constructor of the Listener class
- pass the user key as a parameter to the constructor of the Listener class
"""
raise Exception(msg)

def get_uri_context(self):
headers = self.get_headers()
host = os.getenv(self.ENV_VAR_API_HOST, self.DEFAULT_HOST)
if "Authorization" in headers:
return host + '/dna'
elif 'user-key' in headers:
return host
else:
msg = '''Could not determine user credentials:
Must specify account credentials as user_id, client_id, and password, either through env vars, customer_config.json, or as args to Listener constructor
(see README.rst)'''
raise Exception(msg)

# return credentials (user_id, client_id, and password) for obtaining a JWT via OAuth2 if all these fields are defined in the constructor, env vars or config file
# otherwise return None (the client will have to authenticate API request with an user key, i.e. the original standard)
def oauth2_credentials(self):
creds = self._build_oauth2_credentials(
self.user_id,
self.client_id,
self.password
)
if not creds:
creds = self._build_oauth2_credentials(
os.getenv(self.ENV_VAR_USER_ID),
os.getenv(self.ENV_VAR_CLIENT_ID),
os.getenv(self.ENV_VAR_PASSWORD)
)
if not creds:
creds = self._oauth2_credentials_from_file()
return creds

def _oauth2_credentials_from_file(self):
if not self.initialized:
self._initialize()

return self._build_oauth2_credentials(
self.customer_config.get('user_id'),
self.customer_config.get('client_id'),
self.customer_config.get('password')
)

def _build_oauth2_credentials(self, user_id, client_id, password):
if user_id and client_id and password:
return {
'user_id': user_id,
'client_id': client_id,
'password': password
}
return None
return host

# in the following two methods, note that we use "SERVICE_ACCOUNT_ID" as a legacy,
# alternate name for the "USER_KEY" parameter, from the customer's perspective
Expand Down Expand Up @@ -197,6 +107,3 @@ def _subscription_id_from_file(self):

return self.customer_config['subscription_id']


def _get_requests():
return requests
3 changes: 0 additions & 3 deletions dnaStreaming/customer_config.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{

"user_key": "",
"user_id": "",
"client_id": "",
"password": "",
"subscription_id": ""
}
5 changes: 2 additions & 3 deletions dnaStreaming/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
class Listener(object):
DEFAULT_UNLIMITED_MESSAGES = None

def __init__(self, service_account_id=None, user_key=None, user_id=None, client_id=None, password=None):
config = Config(service_account_id, user_key,
user_id, client_id, password)
def __init__(self, service_account_id=None, user_key=None, config_file=None):
config = Config(service_account_id, user_key, config_file)
self._initialize(config)
self.current_subscription_index = 0

Expand Down
50 changes: 0 additions & 50 deletions dnaStreaming/test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ def tearDown(self):
self.ensure_remove_environment_variable(Config.ENV_VAR_USER_KEY)
self.ensure_remove_environment_variable(Config.ENV_VAR_SUBSCRIPTION_ID)
self.ensure_remove_environment_variable(Config.ENV_VAR_API_HOST)
self.ensure_remove_environment_variable(Config.ENV_VAR_USER_ID)
self.ensure_remove_environment_variable(Config.ENV_VAR_CLIENT_ID)
self.ensure_remove_environment_variable(Config.ENV_VAR_PASSWORD)

def ensure_remove_environment_variable(self, key):
if key in os.environ:
Expand Down Expand Up @@ -55,22 +52,15 @@ def test_get_vals_from_file_success(self):
# Act
user_key = config.get_user_key()
subscription = config.subscription()
oauth2_credentials = config.oauth2_credentials()

# Assert
assert user_key
assert subscription == 'bar'
assert oauth2_credentials.get('user_id')
assert oauth2_credentials.get('password')
assert oauth2_credentials.get('client_id')

def test_environment_variables_success(self):
# Arrange
os.environ[Config.ENV_VAR_USER_KEY] = '123'
os.environ[Config.ENV_VAR_SUBSCRIPTION_ID] = 'ABC'
os.environ[Config.ENV_VAR_USER_ID] = 'user'
os.environ[Config.ENV_VAR_CLIENT_ID] = 'client'
os.environ[Config.ENV_VAR_PASSWORD] = 'password'

# Act
config = Config()
Expand All @@ -82,9 +72,6 @@ def test_environment_variables_success(self):
assert os.environ[Config.ENV_VAR_USER_KEY] == config.get_user_key()
subscription_id = config.subscription()
assert subscription_id == 'ABC'
assert os.environ[Config.ENV_VAR_USER_ID] == config.oauth2_credentials().get('user_id')
assert os.environ[Config.ENV_VAR_CLIENT_ID] == config.oauth2_credentials().get('client_id')
assert os.environ[Config.ENV_VAR_PASSWORD] == config.oauth2_credentials().get('password')

def test_environment_variable_service_account_id_success(self):
# Arrange
Expand Down Expand Up @@ -128,42 +115,5 @@ def test_service_account_id_passed_success(self):
# Assert
assert config.get_user_key() == '123'

@patch.object(Config, '_fetch_jwt', return_value='test_jwt_value')
def test_get_headers_jwt(self, fetch_jwt_mock):
# Arrange
config = Config()

fileFolder = os.path.dirname(os.path.realpath(__file__))
config._set_customer_config_path(os.path.join(fileFolder, 'test_customer_config.json'))

headers_expected = {
'Authorization': 'test_jwt_value'
}

# Act
headers_actual = config.get_authentication_headers()

# Assert
assert headers_actual == headers_expected
fetch_jwt_mock.assert_called_once()

@patch.object(Config, '_fetch_jwt', return_value='test_jwt_value')
def test_get_headers_user_key(self, fetch_jwt_mock):
# Arrange
user_key = "just some user key"
config = Config(user_key)

headers_expected = {
'user-key': user_key
}

# Act
headers_actual = config.get_authentication_headers()

# Assert
assert headers_actual == headers_expected
fetch_jwt_mock.assert_not_called()


if __name__ == '__main__' and __package__ is None:
unittest.main()
5 changes: 1 addition & 4 deletions dnaStreaming/test/test_customer_config.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"user_key": "foo",
"service_account_id": "fu",
"subscription_id": "bar",
"user_id": "bill",
"client_id": "nye",
"password": "the science guy"
"subscription_id": "bar"
}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()

VERSION = "2.0.5"
VERSION = "2.1.0"
RELEASE_TAG = f"release-{VERSION}"

setup(
Expand Down

0 comments on commit ff2dc9d

Please sign in to comment.