Skip to content

Commit 0656409

Browse files
committed
Added support for token-based authentication; added get_subscribers function to MultiTenantCumulocityApp.
1 parent 615bf5d commit 0656409

File tree

5 files changed

+95
-16
lines changed

5 files changed

+95
-16
lines changed

c8y_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
from c8y_api._base_api import CumulocityRestApi
88
from c8y_api._main_api import CumulocityApi
99
from c8y_api._registry_api import CumulocityDeviceRegistry
10+
from c8y_api._auth import HTTPBasicAuth, HTTPBearerAuth

c8y_api/_auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self, token: str):
2222

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

2627

2728
class AuthUtil:

c8y_api/app/__init__.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
from cachetools import TTLCache
1212
from requests.auth import HTTPBasicAuth, AuthBase
1313

14-
from c8y_api._auth import AuthUtil
14+
from c8y_api._auth import AuthUtil, HTTPBearerAuth
1515
from c8y_api._main_api import CumulocityApi
1616
from c8y_api._util import c8y_keys
1717

1818

1919
class _CumulocityAppBase(object):
20-
"""Internal class, base for both Per Tenant and Multi Tenant specifc
20+
"""Internal class, base for both Per Tenant and Multi Tenant specific
2121
implementation."""
2222

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

@@ -131,10 +131,16 @@ def __init__(self, application_key: str = None, cache_size: int = 100, cache_ttl
131131
"""
132132
baseurl = self._get_env('C8Y_BASEURL')
133133
tenant_id = self._get_env('C8Y_TENANT')
134-
username = self._get_env('C8Y_USER')
135-
password = self._get_env('C8Y_PASSWORD')
134+
# authentication is either token or username/password
135+
try:
136+
token = self._get_env('C8Y_TOKEN')
137+
auth = HTTPBearerAuth(token)
138+
except ValueError:
139+
username = self._get_env('C8Y_USER')
140+
password = self._get_env('C8Y_PASSWORD')
141+
auth = HTTPBasicAuth(f'{tenant_id}/{username}', password)
136142
super().__init__(log=self._log, cache_size=cache_size, cache_ttl=cache_ttl,
137-
base_url=baseurl, tenant_id=tenant_id, auth=HTTPBasicAuth(f'{tenant_id}/{username}', password),
143+
base_url=baseurl, tenant_id=tenant_id, auth=auth,
138144
application_key=application_key)
139145

140146
def _build_user_instance(self, auth) -> CumulocityApi:
@@ -149,7 +155,7 @@ class MultiTenantCumulocityApp(_CumulocityAppBase):
149155
150156
The MultiTenantCumulocityApp class is intended to be used as base within
151157
a multi-tenant micro service hosted on Cumulocity. It evaluates the
152-
environment to teh resolve the bootstrap authentication information
158+
environment to the resolve the bootstrap authentication information
153159
automatically.
154160
155161
Note: This class is intended to be used in Cumulocity micro services
@@ -178,25 +184,42 @@ def _get_tenant_auth(self, tenant_id: str) -> AuthBase:
178184
try:
179185
return self._subscribed_auths[tenant_id]
180186
except KeyError:
181-
self._subscribed_auths = self._read_subscriptions(self.bootstrap_instance)
187+
self._subscribed_auths = self._read_subscription_auths(self.bootstrap_instance)
182188
return self._subscribed_auths[tenant_id]
183189

184190
@classmethod
185-
def _read_subscriptions(cls, bootstrap_instance: CumulocityApi):
191+
def _read_subscriptions(cls, bootstrap_instance: CumulocityApi) -> list[dict]:
192+
"""Read subscribed tenants details.
193+
194+
Returns:
195+
A list of tenant details dicts.
196+
"""
197+
subscriptions = bootstrap_instance.get('/application/currentApplication/subscriptions')
198+
return subscriptions['users']
199+
200+
@classmethod
201+
def _read_subscription_auths(cls, bootstrap_instance: CumulocityApi):
186202
"""Read subscribed tenant's auth information.
187203
188204
Returns:
189205
A dict of tenant auth information by ID
190206
"""
191-
subscriptions = bootstrap_instance.get('/application/currentApplication/subscriptions')
192207
cache = {}
193-
for subscription in subscriptions['users']:
208+
for subscription in cls._read_subscriptions(bootstrap_instance):
194209
tenant = subscription['tenant']
195210
username = subscription['name']
196211
password = subscription['password']
197212
cache[tenant] = HTTPBasicAuth(f'{tenant}/{username}', password)
198213
return cache
199214

215+
def get_subscribers(self) -> list[str]:
216+
"""Query the subscribed tenants.
217+
218+
Returns:
219+
A list of tenant ID.
220+
"""
221+
return [x['tenant'] for x in self._read_subscriptions(self.bootstrap_instance)]
222+
200223
@classmethod
201224
def _create_bootstrap_instance(cls) -> CumulocityApi:
202225
"""Build the bootstrap instance from the environment."""

integration_tests/test_apps.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) 2020 Software AG,
2+
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3+
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4+
# Use, reproduction, transfer, publication or disclosure is prohibited except
5+
# as specifically provided for in your License Agreement with Software AG.
6+
7+
import pytest
8+
import requests
9+
10+
from c8y_api import CumulocityRestApi, HTTPBearerAuth
11+
from c8y_api.app import SimpleCumulocityApp
12+
from c8y_api.model import ManagedObject
13+
14+
15+
@pytest.fixture(name='token_app')
16+
def fix_token_app(test_environment):
17+
"""Provide a token-based REST API instance."""
18+
# First, create an instance for basic auth
19+
c8y = SimpleCumulocityApp()
20+
# Submit auth request
21+
form_data = {
22+
'grant_type': 'PASSWORD',
23+
'username': c8y.auth.username,
24+
'password': c8y.auth.password
25+
}
26+
r = requests.post(url=c8y.base_url + '/tenant/oauth', data=form_data)
27+
# Obtain token from response
28+
assert r.status_code == 200
29+
cookie = r.headers['Set-Cookie']
30+
# split by ; to separate parts, then map a=b items to dictionary
31+
cookie_parts = {x[0]:x[1] for x in [c.split('=') for c in cookie.split(';')] if len(x) == 2}
32+
auth_token = cookie_parts['authorization']
33+
assert auth_token
34+
# build token-based app
35+
return CumulocityRestApi(
36+
base_url=c8y.base_url,
37+
tenant_id=c8y.tenant_id,
38+
auth=HTTPBearerAuth(auth_token)
39+
)
40+
41+
42+
def test_token_based_app_headers(token_app):
43+
"""Verify that a token-based app only features a 'Bearer' auth header."""
44+
response = token_app.session.get("https://httpbin.org/headers")
45+
auth_header = response.json()['headers']['Authorization']
46+
assert auth_header.startswith('Bearer')
47+
48+
49+
def test_token_based_app(token_app):
50+
"""Verify that a token-based app can be used for all kind of requests."""
51+
mo = ManagedObject(token_app, name='test-object', type='test-object-type').create()
52+
mo['new_Fragment'] = {}
53+
mo.update()
54+
mo.delete()

tests/test_app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ def test_multi_tenant__caching_instances():
127127

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

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

196196
# we just need any CumulocityApi to do this call
197197
c8y = CumulocityApi(base_url=base_url, tenant_id=tenant_id, username=user, password=password)
198-
subscriptions = MultiTenantCumulocityApp._read_subscriptions(c8y)
198+
subscriptions = MultiTenantCumulocityApp._read_subscription_auths(c8y)
199199
# -> subscriptions were parsed correctly
200200
assert 't12345' in subscriptions
201201
assert 't54321' in subscriptions

0 commit comments

Comments
 (0)