Skip to content

Commit f7274f8

Browse files
authored
vendors: token helper for SAP BTP, ABAP env service (#159)
1 parent 2be3fad commit f7274f8

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
66

77
## [Unreleased]
88

9+
### Fixed
10+
- Helper for obtaining a token for services coming from SAP BTP, ABAP environment - Stoyko Stoev
11+
912
## [1.7.0]
1013

1114
### Added

docs/usage/vendors.rst

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Vendor-specific helpers
2+
==============
3+
4+
This document presents the helpers found in ``pyodata.vendor``
5+
6+
Get the service proxy client for an OData service published on SAP BTP, ABAP environment
7+
--------------------------------------------------------------------------
8+
9+
Let's assume you have an ABAP environment service instance running on SAP Business Technology
10+
Platform. You have used this instance to provide an OData service by using, for example, the
11+
ABAP RESTful Application Programming Model. To connect to it, you need to provide several attributes
12+
found in the JSON service key of the instance, as well as your username and password for SAP BTP.
13+
14+
``pyodata.vendor.SAP`` offers a helper that takes the arguments described above, as well as an
15+
existing ``requests.Session`` object (or another one conforming to the same API), and adds the
16+
required token to the session object's authorization header.
17+
18+
The following code demonstrates using the helper.
19+
20+
.. code-block:: python
21+
22+
import pyodata
23+
from pyodata.vendor import SAP
24+
import requests
25+
import json
26+
27+
with open('key.txt', 'r') as f:
28+
KEY = json.loads(f.read())
29+
30+
USER = "MyBtpUser"
31+
PASSWORD = "MyBtpPassword"
32+
SERVICE_URL = KEY["url"] + "/sap/opu/odata/sap/" + "ZMyBtpService"
33+
34+
session = SAP.add_btp_token_to_session(requests.Session(), KEY, USER, PASSWORD)
35+
# do something more with session object if necessary (e.g. adding sap-client parameter, or CSRF token)
36+
client = pyodata.Client(SERVICE_URL, session)

pyodata/vendor/SAP.py

+23
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ def json_get(obj, member, typ, default=None):
2424
return value
2525

2626

27+
def add_btp_token_to_session(session, key, user, password):
28+
"""Using the provided credentials, the function tries to add the
29+
necessary token for establishing a connection to an OData service
30+
coming from SAP BTP, ABAP environment.
31+
32+
If any of the provided credentials are invalid, the server will
33+
respond with 401, and the function will raise HttpError.
34+
"""
35+
token_url = key['uaa']['url'] + f'/oauth/token?grant_type=password&username={user}&password={password}'
36+
token_response = session.post(token_url, auth=(key['uaa']['clientid'], key['uaa']['clientsecret']))
37+
38+
if token_response.status_code != 200:
39+
raise HttpError(
40+
f'Token request failed, status code: {token_response.status_code}, body:\n{token_response.content}',
41+
token_response)
42+
43+
token_response = json.loads(token_response.text)
44+
token = token_response['id_token']
45+
46+
session.headers.update({'Authorization': f'Bearer {token}'})
47+
return session
48+
49+
2750
class BusinessGatewayError(HttpError):
2851
"""To display the right error message"""
2952

tests/test_vendor_sap.py

+88
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import pytest
66
from pyodata.exceptions import PyODataException, HttpError
77
from pyodata.vendor import SAP
8+
import responses
9+
import requests
10+
import json
811

912

1013
class MockResponse(NamedTuple):
@@ -183,3 +186,88 @@ def test_vendor_http_error(response_with_error):
183186
sap_error = HttpError('Another foo bar', response_with_error)
184187
assert isinstance(sap_error, SAP.BusinessGatewayError)
185188
assert str(sap_error) == 'Gateway Error'
189+
190+
191+
MOCK_AUTH_URL = "https://example.authentication.hana.ondemand.com"
192+
MOCK_BTP_USER = "[email protected]"
193+
MOCK_BTP_PASSWORD = "example_password"
194+
MOCK_KEY = {
195+
"uaa": {
196+
"url": MOCK_AUTH_URL,
197+
"clientid": "example-client-id",
198+
"clientsecret": "example-client-secret"
199+
}
200+
}
201+
202+
203+
@responses.activate
204+
def test_add_btp_token_to_session_valid():
205+
"""Valid username, password and key return a session with set token"""
206+
207+
responses.add(
208+
responses.POST,
209+
MOCK_AUTH_URL + f'/oauth/token?grant_type=password&username={MOCK_BTP_USER}&password={MOCK_BTP_PASSWORD}',
210+
headers={'Content-type': 'application/json'},
211+
json={
212+
'access_token': 'valid_access_token',
213+
'token_type': 'bearer',
214+
'id_token': 'valid_id_token',
215+
'refresh_token': 'valid_refresh_token',
216+
'expires_in': 43199,
217+
'scope': 'openid uaa.user',
218+
'jti': 'valid_jti'
219+
},
220+
status=200)
221+
222+
result = SAP.add_btp_token_to_session(requests.Session(), MOCK_KEY, MOCK_BTP_USER, MOCK_BTP_PASSWORD)
223+
assert result.headers['Authorization'] == 'Bearer valid_id_token'
224+
225+
226+
@responses.activate
227+
def test_add_btp_token_to_session_invalid_user():
228+
"""Invalid username returns an HttpError"""
229+
230+
invalid_user = "[email protected]"
231+
232+
responses.add(
233+
responses.POST,
234+
MOCK_AUTH_URL + f'/oauth/token?grant_type=password&username={invalid_user}&password={MOCK_BTP_PASSWORD}',
235+
headers={'Content-type': 'application/json'},
236+
json={
237+
'error': 'unauthorized',
238+
'error_description': {
239+
'error': 'invalid_grant',
240+
'error_description': 'User authentication failed.'
241+
}
242+
},
243+
status=401)
244+
245+
with pytest.raises(HttpError) as caught:
246+
SAP.add_btp_token_to_session(requests.Session(), MOCK_KEY, invalid_user, MOCK_BTP_PASSWORD)
247+
248+
assert caught.value.response.status_code == 401
249+
assert json.loads(caught.value.response.text)['error_description']['error'] == 'invalid_grant'
250+
251+
252+
@responses.activate
253+
def test_add_btp_token_to_session_invalid_clientid():
254+
"""Invalid clientid in key returns an HttpError"""
255+
256+
invalid_key = MOCK_KEY.copy()
257+
invalid_key['uaa']['clientid'] = 'invalid-client-id'
258+
259+
responses.add(
260+
responses.POST,
261+
MOCK_AUTH_URL + f'/oauth/token?grant_type=password&username={MOCK_BTP_USER}&password={MOCK_BTP_PASSWORD}',
262+
headers={'Content-type': 'application/json'},
263+
json={
264+
'error': 'unauthorized',
265+
'error_description': 'Bad credentials'
266+
},
267+
status=401)
268+
269+
with pytest.raises(HttpError) as caught:
270+
SAP.add_btp_token_to_session(requests.Session(), invalid_key, MOCK_BTP_USER, MOCK_BTP_PASSWORD)
271+
272+
assert caught.value.response.status_code == 401
273+
assert json.loads(caught.value.response.text)['error_description'] == 'Bad credentials'

0 commit comments

Comments
 (0)