Skip to content

Commit

Permalink
Added support for token-based authentication; added get_subscribers f…
Browse files Browse the repository at this point in the history
…unction to MultiTenantCumulocityApp.
  • Loading branch information
chsou committed Dec 7, 2023
1 parent 615bf5d commit 0656409
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 16 deletions.
1 change: 1 addition & 0 deletions c8y_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from c8y_api._base_api import CumulocityRestApi
from c8y_api._main_api import CumulocityApi
from c8y_api._registry_api import CumulocityDeviceRegistry
from c8y_api._auth import HTTPBasicAuth, HTTPBearerAuth
1 change: 1 addition & 0 deletions c8y_api/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self, token: str):

def __call__(self, r):
r.headers['Authorization'] = 'Bearer ' + self.token
return r


class AuthUtil:
Expand Down
49 changes: 36 additions & 13 deletions c8y_api/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
from cachetools import TTLCache
from requests.auth import HTTPBasicAuth, AuthBase

from c8y_api._auth import AuthUtil
from c8y_api._auth import AuthUtil, HTTPBearerAuth
from c8y_api._main_api import CumulocityApi
from c8y_api._util import c8y_keys


class _CumulocityAppBase(object):
"""Internal class, base for both Per Tenant and Multi Tenant specifc
"""Internal class, base for both Per Tenant and Multi Tenant specific
implementation."""

def __init__(self, log: logging.Logger, cache_size: int = 100, cache_ttl: int = 3600, **kwargs):
Expand All @@ -37,7 +37,7 @@ def get_user_instance(self, headers: dict = None) -> CumulocityApi:
previously created instances are cached.
Args:
headers (dict): A dictionarity of HTTP header entries. The user
headers (dict): A dictionary of HTTP header entries. The user
access is based on the Authorization header within.
Returns:
Expand Down Expand Up @@ -101,15 +101,15 @@ class SimpleCumulocityApp(_CumulocityAppBase, CumulocityApi):
The SimpleCumulocityApp class is intended to be used as base within
a single-tenant micro service hosted on Cumulocity. It evaluates the
environment to teh resolve the authentication information automatically.
environment to the resolve the authentication information automatically.
Note: This class should be used in Cumulocity micro services using the
PER_TENANT authentication mode only. It will not function in environments
using the MULTITENANT mode.
The SimpleCumulocityApp class is an enhanced version of the standard
CumulocityApi class. All Cumulocity functions can be used directly.
Additionally it can be used to provide CumulocityApi instances for
Additionally, it can be used to provide CumulocityApi instances for
specific named users via the `get_user_instance` function.
"""

Expand All @@ -131,10 +131,16 @@ def __init__(self, application_key: str = None, cache_size: int = 100, cache_ttl
"""
baseurl = self._get_env('C8Y_BASEURL')
tenant_id = self._get_env('C8Y_TENANT')
username = self._get_env('C8Y_USER')
password = self._get_env('C8Y_PASSWORD')
# authentication is either token or username/password
try:
token = self._get_env('C8Y_TOKEN')
auth = HTTPBearerAuth(token)
except ValueError:
username = self._get_env('C8Y_USER')
password = self._get_env('C8Y_PASSWORD')
auth = HTTPBasicAuth(f'{tenant_id}/{username}', password)
super().__init__(log=self._log, cache_size=cache_size, cache_ttl=cache_ttl,
base_url=baseurl, tenant_id=tenant_id, auth=HTTPBasicAuth(f'{tenant_id}/{username}', password),
base_url=baseurl, tenant_id=tenant_id, auth=auth,
application_key=application_key)

def _build_user_instance(self, auth) -> CumulocityApi:
Expand All @@ -149,7 +155,7 @@ class MultiTenantCumulocityApp(_CumulocityAppBase):
The MultiTenantCumulocityApp class is intended to be used as base within
a multi-tenant micro service hosted on Cumulocity. It evaluates the
environment to teh resolve the bootstrap authentication information
environment to the resolve the bootstrap authentication information
automatically.
Note: This class is intended to be used in Cumulocity micro services
Expand Down Expand Up @@ -178,25 +184,42 @@ def _get_tenant_auth(self, tenant_id: str) -> AuthBase:
try:
return self._subscribed_auths[tenant_id]
except KeyError:
self._subscribed_auths = self._read_subscriptions(self.bootstrap_instance)
self._subscribed_auths = self._read_subscription_auths(self.bootstrap_instance)
return self._subscribed_auths[tenant_id]

@classmethod
def _read_subscriptions(cls, bootstrap_instance: CumulocityApi):
def _read_subscriptions(cls, bootstrap_instance: CumulocityApi) -> list[dict]:
"""Read subscribed tenants details.
Returns:
A list of tenant details dicts.
"""
subscriptions = bootstrap_instance.get('/application/currentApplication/subscriptions')
return subscriptions['users']

@classmethod
def _read_subscription_auths(cls, bootstrap_instance: CumulocityApi):
"""Read subscribed tenant's auth information.
Returns:
A dict of tenant auth information by ID
"""
subscriptions = bootstrap_instance.get('/application/currentApplication/subscriptions')
cache = {}
for subscription in subscriptions['users']:
for subscription in cls._read_subscriptions(bootstrap_instance):
tenant = subscription['tenant']
username = subscription['name']
password = subscription['password']
cache[tenant] = HTTPBasicAuth(f'{tenant}/{username}', password)
return cache

def get_subscribers(self) -> list[str]:
"""Query the subscribed tenants.
Returns:
A list of tenant ID.
"""
return [x['tenant'] for x in self._read_subscriptions(self.bootstrap_instance)]

@classmethod
def _create_bootstrap_instance(cls) -> CumulocityApi:
"""Build the bootstrap instance from the environment."""
Expand Down
54 changes: 54 additions & 0 deletions integration_tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) 2020 Software AG,
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
# and/or its subsidiaries and/or its affiliates and/or their licensors.
# Use, reproduction, transfer, publication or disclosure is prohibited except
# as specifically provided for in your License Agreement with Software AG.

import pytest
import requests

from c8y_api import CumulocityRestApi, HTTPBearerAuth
from c8y_api.app import SimpleCumulocityApp
from c8y_api.model import ManagedObject


@pytest.fixture(name='token_app')
def fix_token_app(test_environment):
"""Provide a token-based REST API instance."""
# First, create an instance for basic auth
c8y = SimpleCumulocityApp()
# Submit auth request
form_data = {
'grant_type': 'PASSWORD',
'username': c8y.auth.username,
'password': c8y.auth.password
}
r = requests.post(url=c8y.base_url + '/tenant/oauth', data=form_data)
# Obtain token from response
assert r.status_code == 200
cookie = r.headers['Set-Cookie']
# split by ; to separate parts, then map a=b items to dictionary
cookie_parts = {x[0]:x[1] for x in [c.split('=') for c in cookie.split(';')] if len(x) == 2}
auth_token = cookie_parts['authorization']
assert auth_token
# build token-based app
return CumulocityRestApi(
base_url=c8y.base_url,
tenant_id=c8y.tenant_id,
auth=HTTPBearerAuth(auth_token)
)


def test_token_based_app_headers(token_app):
"""Verify that a token-based app only features a 'Bearer' auth header."""
response = token_app.session.get("https://httpbin.org/headers")
auth_header = response.json()['headers']['Authorization']
assert auth_header.startswith('Bearer')


def test_token_based_app(token_app):
"""Verify that a token-based app can be used for all kind of requests."""
mo = ManagedObject(token_app, name='test-object', type='test-object-type').create()
mo['new_Fragment'] = {}
mo.update()
mo.delete()
6 changes: 3 additions & 3 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ def test_multi_tenant__caching_instances():

@mock.patch.dict(os.environ, env_multi_tenant, clear=True)
def test_multi_tenant__build_from_subscriptions():
"""Verify that a uncached instance is build using the subscriptions."""
"""Verify that an uncached instance is build using the subscriptions."""
# pylint: disable=protected-access

with patch.object(MultiTenantCumulocityApp, '_read_subscriptions') as read_subscriptions:
with patch.object(MultiTenantCumulocityApp, '_read_subscription_auths') as read_subscriptions:
# we mock _read_subscriptions so that we don't need an actual
# connection and it returns what we want
read_subscriptions.return_value = {'t12345': HTTPBasicAuth('username', 'password')}
Expand Down Expand Up @@ -195,7 +195,7 @@ def test_read_subscriptions():

# we just need any CumulocityApi to do this call
c8y = CumulocityApi(base_url=base_url, tenant_id=tenant_id, username=user, password=password)
subscriptions = MultiTenantCumulocityApp._read_subscriptions(c8y)
subscriptions = MultiTenantCumulocityApp._read_subscription_auths(c8y)
# -> subscriptions were parsed correctly
assert 't12345' in subscriptions
assert 't54321' in subscriptions
Expand Down

0 comments on commit 0656409

Please sign in to comment.