Skip to content

Commit 58abe74

Browse files
PLAT-10460: Implemented OBO-enabled endpoints (#123)
* Implemented OBO-enabled endpoints * Renamed exception classes with Error suffix as advised by PEP-8
1 parent a1fb522 commit 58abe74

24 files changed

+559
-141
lines changed

docs/authentication.md

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Authentication
2+
The Symphony BDK authentication API allows developers to authenticate their bots and apps using RSA authentication mode.
3+
Please mind we only support RSA authentication.
4+
5+
The following sections will explain you:
6+
- how to authenticate your bot service account
7+
- how to authenticate your app to use OBO (On Behalf Of) authentication
8+
9+
## Bot authentication
10+
In this section we will see how to authenticate a bot service account using RSA authentication.
11+
12+
> Read more about RSA authentication [here](https://developers.symphony.com/symphony-developer/docs/rsa-bot-authentication-workflow)
13+
14+
Required `config.yaml` setup:
15+
```yaml
16+
host: acme.symphony.com
17+
bot:
18+
username: bot-username
19+
privateKey:
20+
path: /path/to/rsa/private-key.pem
21+
```
22+
23+
### Bot authentication deep-dive
24+
The code snippet below explains how to manually retrieve your bot authentication session. However, note that by default
25+
those operations are done behind the scene through the `SymphonyBdk` entry point.
26+
27+
```python
28+
import logging
29+
30+
from symphony.bdk.core.config.loader import BdkConfigLoader
31+
from symphony.bdk.core.symphony_bdk import SymphonyBdk
32+
33+
async def run():
34+
config = BdkConfigLoader.load_from_symphony_dir("config.yaml")
35+
async with SymphonyBdk(config) as bdk:
36+
auth_session = bdk.bot_session()
37+
logging.info(await auth_session.key_manager_token)
38+
logging.info(await auth_session.session_token)
39+
```
40+
41+
### Multiple bot instances
42+
By design, the `SymphonyBdk` object contains a single bot session. However, you might want to create an application that
43+
has to handle multiple bot sessions, potentially using different authentication modes. This is possible by creating
44+
multiple instances of `SymphonyBdk` using different configurations:
45+
```python
46+
from symphony.bdk.core.config.loader import BdkConfigLoader
47+
from symphony.bdk.core.symphony_bdk import SymphonyBdk
48+
49+
async def run():
50+
config_a = BdkConfigLoader.load_from_symphony_dir("config_a.yaml")
51+
config_b = BdkConfigLoader.load_from_symphony_dir("config_b.yaml")
52+
53+
async with SymphonyBdk(config_a) as bdk_a, SymphonyBdk(config_b) as bdk_b:
54+
# use your two service accounts
55+
```
56+
57+
## App authentication
58+
Application authentication is completely optional but remains required if you want to use OBO.
59+
60+
Required `config.yaml` setup:
61+
```yaml
62+
host: acme.symphony.com
63+
app:
64+
appId: app-id
65+
privateKey:
66+
path: /path/to/rsa/private-key.pem
67+
```
68+
69+
### OBO (On Behalf Of) authentication
70+
> Read more about OBO authentication [here](https://developers.symphony.com/symphony-developer/docs/obo-overview)
71+
72+
The following example shows how to retrieve OBO sessions using `username` (type `str`) or `user_id` (type `int`)
73+
and to call services which have OBO endpoints (users, streams, connections and messages so far):
74+
75+
```python
76+
from symphony.bdk.core.config.loader import BdkConfigLoader
77+
from symphony.bdk.core.symphony_bdk import SymphonyBdk
78+
79+
80+
async def run():
81+
config = BdkConfigLoader.load_from_symphony_dir("config.yaml")
82+
async with SymphonyBdk(config) as bdk:
83+
obo_auth_session = bdk.obo(username="username")
84+
async with bdk.obo_services(obo_auth_session) as obo_services:
85+
obo_services.messages().send_message("stream_id", "<messageML>Hello on behalf of user!</messageML>")
86+
```
87+
88+
### BDK running without Bot username (service account) configured
89+
90+
When the bot `username` (service account) is not configured in the Bdk configuration, the bot project will be still
91+
runnable but only in the OBO mode if the app authentication is well-configured.
92+
93+
The `config.yaml` requires at least the application configuration:
94+
95+
```yaml
96+
host: acme.symphony.com
97+
app:
98+
appId: app-id
99+
privateKey:
100+
path: /path/to/private-key.pem
101+
```
102+
103+
If users still try to access to Bdk services directly from `SymphonyBdk` facade object, a `BotNotConfiguredError`
104+
will be thrown.
105+
106+
The example in [part above](#obo-on-behalf-of-authentication) shows how a bot project works without bot `username`
107+
configured.

symphony/bdk/core/auth/auth_session.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from symphony.bdk.core.auth.exception import AuthInitializationException
1+
from symphony.bdk.core.auth.exception import AuthInitializationError
22

33

44
class AuthSession:
@@ -73,10 +73,10 @@ def __init__(self, authenticator, user_id: int = None, username: str = None):
7373
"""
7474
super().__init__(authenticator)
7575
if user_id is not None and username is not None:
76-
raise AuthInitializationException('Username and user id for OBO authentication should not be defined at '
76+
raise AuthInitializationError('Username and user id for OBO authentication should not be defined at '
7777
'a same time.')
7878
if user_id is None and username is None:
79-
raise AuthInitializationException('At least username or user id should be defined for '
79+
raise AuthInitializationError('At least username or user id should be defined for '
8080
'OBO authentication.')
8181
self.user_id = user_id
8282
self.username = username
@@ -97,4 +97,4 @@ async def session_token(self):
9797

9898
@property
9999
async def key_manager_token(self):
100-
return None
100+
return ""

symphony/bdk/core/auth/authenticator_factory.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from symphony.bdk.core.auth.bot_authenticator import BotAuthenticator, BotAuthenticatorRsa
2-
from symphony.bdk.core.auth.exception import AuthInitializationException
2+
from symphony.bdk.core.auth.exception import AuthInitializationError
33
from symphony.bdk.core.auth.obo_authenticator import OboAuthenticator, OboAuthenticatorRsa
44
from symphony.bdk.core.client.api_client_factory import ApiClientFactory
55
from symphony.bdk.core.config.model.bdk_config import BdkConfig
@@ -28,13 +28,13 @@ def get_bot_authenticator(self) -> BotAuthenticator:
2828
2929
:return: a new BotAuthenticator instance.
3030
"""
31-
if self._config.bot.is_rsa_authentication_configured() and self._config.bot.is_rsa_configuration_valid():
31+
if self._config.bot.is_rsa_configuration_valid():
3232
return BotAuthenticatorRsa(
3333
bot_config=self._config.bot,
3434
login_api_client=self._api_client_factory.get_login_client(),
3535
relay_api_client=self._api_client_factory.get_relay_client()
3636
)
37-
raise AuthInitializationException("RSA authentication should be configured. Only one field among private key "
37+
raise AuthInitializationError("RSA authentication should be configured. Only one field among private key "
3838
"path or content should be configured for bot authentication.")
3939

4040
def get_obo_authenticator(self) -> OboAuthenticator:
@@ -47,5 +47,5 @@ def get_obo_authenticator(self) -> OboAuthenticator:
4747
app_config=self._config.app,
4848
login_api_client=self._api_client_factory.get_login_client()
4949
)
50-
raise AuthInitializationException("Only one of private key path or content should be configured for "
50+
raise AuthInitializationError("Only one of private key path or content should be configured for "
5151
"extension app authentication.")

symphony/bdk/core/auth/bot_authenticator.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABC, abstractmethod
22

33
from symphony.bdk.core.auth.auth_session import AuthSession
4-
from symphony.bdk.core.auth.exception import AuthUnauthorizedException
4+
from symphony.bdk.core.auth.exception import AuthUnauthorizedError
55
from symphony.bdk.core.auth.jwt_helper import create_signed_jwt
66
from symphony.bdk.core.config.model.bdk_bot_config import BdkBotConfig
77
from symphony.bdk.gen.api_client import ApiClient
@@ -28,15 +28,20 @@ async def authenticate_bot(self) -> AuthSession:
2828
:return: the authentication session.
2929
:rtype: AuthSession
3030
"""
31-
pass
3231

3332
@abstractmethod
3433
async def retrieve_session_token(self):
35-
pass
34+
"""Authenticates and retrieves a new session token.
35+
36+
:return: the retrieved session token.
37+
"""
3638

3739
@abstractmethod
3840
async def retrieve_key_manager_token(self):
39-
pass
41+
"""Authenticated and retrieved a new key manager session.
42+
43+
:return: the retrieved key manager session.
44+
"""
4045

4146

4247
class BotAuthenticatorRsa(BotAuthenticator):
@@ -65,7 +70,7 @@ async def _authenticate_and_get_token(self, api_client: ApiClient) -> str:
6570
token = await AuthenticationApi(api_client).pubkey_authenticate_post(req)
6671
return token.token
6772
except ApiException as e:
68-
raise AuthUnauthorizedException(unauthorized_message, e)
73+
raise AuthUnauthorizedError(unauthorized_message, e)
6974

7075
async def retrieve_session_token(self) -> str:
7176
"""Make the api call to the pod to get the pod's session token.

symphony/bdk/core/auth/exception.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
"""
33

44

5-
class AuthInitializationException(Exception):
5+
class AuthInitializationError(Exception):
66
"""Thrown when unable to read/parse a RSA Private Key or a certificate.
77
"""
88

99
def __init__(self, message: str):
1010
self.message = message
1111

1212

13-
class AuthUnauthorizedException(Exception):
13+
class AuthUnauthorizedError(Exception):
1414
"""When thrown, it means that authentication cannot be performed for several reasons
1515
"""
1616

symphony/bdk/core/auth/obo_authenticator.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABC, abstractmethod
22

33
from symphony.bdk.core.auth.auth_session import OboAuthSession
4-
from symphony.bdk.core.auth.exception import AuthUnauthorizedException
4+
from symphony.bdk.core.auth.exception import AuthUnauthorizedError
55
from symphony.bdk.core.auth.jwt_helper import create_signed_jwt
66
from symphony.bdk.core.config.model.bdk_app_config import BdkAppConfig
77
from symphony.bdk.gen.api_client import ApiClient
@@ -44,7 +44,6 @@ async def retrieve_obo_session_token_by_user_id(
4444
:return: The obo session token.
4545
4646
"""
47-
pass
4847

4948
@abstractmethod
5049
async def retrieve_obo_session_token_by_username(
@@ -59,7 +58,6 @@ async def retrieve_obo_session_token_by_username(
5958
:return: The obo session token.
6059
6160
"""
62-
pass
6361

6462
def authenticate_by_username(self, username: str):
6563
"""Authenticate On-Behalf-Of user by username.
@@ -97,7 +95,7 @@ async def _authenticate_and_retrieve_app_session_token(self):
9795
token = await self._authentication_api.pubkey_app_authenticate_post(req)
9896
return token.token
9997
except ApiException:
100-
raise AuthUnauthorizedException(self.unauthorized_message)
98+
raise AuthUnauthorizedError(self.unauthorized_message)
10199

102100
async def _authenticate_and_retrieve_obo_session_token(
103101
self,
@@ -118,7 +116,7 @@ async def _authenticate_and_retrieve_obo_session_token(
118116
token = await self._authentication_api.pubkey_app_username_username_authenticate_post(**params)
119117
return token.token
120118
except ApiException:
121-
raise AuthUnauthorizedException(self.unauthorized_message)
119+
raise AuthUnauthorizedError(self.unauthorized_message)
122120

123121
async def retrieve_obo_session_token_by_user_id(self, user_id: int):
124122
"""

symphony/bdk/core/config/exception.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"""
33

44

5-
class BdkConfigException(Exception):
5+
class BdkConfigError(Exception):
66
"""Exception class raised when configuration is invalid.
77
"""
88

99

10-
class BotNotConfiguredException(Exception):
10+
class BotNotConfiguredError(Exception):
1111
"""Thrown when the bot configuration is not specified."""
1212

1313
def __init__(self, message="Bot (service account) credentials have not been configured."):

symphony/bdk/core/config/loader.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import yaml
55

6-
from symphony.bdk.core.config.exception import BdkConfigException
6+
from symphony.bdk.core.config.exception import BdkConfigError
77
from symphony.bdk.core.config.model.bdk_config import BdkConfig
88

99

@@ -26,7 +26,7 @@ def load_from_file(cls, config_path: str) -> BdkConfig:
2626
if config_path.exists():
2727
config_content = config_path.read_text()
2828
return cls.load_from_content(config_content)
29-
raise BdkConfigException(f"Config file has not been found at: {config_path.absolute()}")
29+
raise BdkConfigError(f"Config file has not been found at: {config_path.absolute()}")
3030

3131
@classmethod
3232
def load_from_content(cls, content: str) -> BdkConfig:
@@ -77,4 +77,4 @@ def parse(cls, config_content: str):
7777
except yaml.YAMLError:
7878
pass
7979

80-
raise BdkConfigException("Config file is neither in JSON nor in YAML format.")
80+
raise BdkConfigError("Config file is neither in JSON nor in YAML format.")

symphony/bdk/core/service/connection/connection_service.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
from symphony.bdk.gen.pod_model.user_connection_request import UserConnectionRequest
66

77

8-
class ConnectionService:
9-
"""Service class for managing the connections between users
8+
class OboConnectionService:
9+
"""Class exposing OBO-enabled endpoints for connection management.
1010
11-
This service is used for retrieving the connection status if the calling user with a specified user or with many
11+
This service is used for retrieving the connection status between the OBO user and a specified user or several
1212
other internal or external users in the pod, and perform some actions related to the connection status like:
1313
1414
* Send a connection request to an user
15-
* Accept a connection request from an user
16-
* Reject a connection request from an user
17-
* Remove a connection with an user
15+
* Accept a connection request from a user
16+
* Reject a connection request from a user
17+
* Remove a connection with a user
1818
"""
1919

2020
def __init__(self, connection_api: ConnectionApi, auth_session: AuthSession):
@@ -131,3 +131,16 @@ async def remove_connection(self, user_id: int) -> None:
131131
'session_token': await self._auth_session.session_token
132132
}
133133
await self._connection_api.v1_connection_user_uid_remove_post(**params)
134+
135+
136+
class ConnectionService(OboConnectionService):
137+
"""Service class for managing the connections between users
138+
139+
This service is used for retrieving the connection status between the calling user and a specified user or several
140+
other internal or external users in the pod, and perform some actions related to the connection status like:
141+
142+
* Send a connection request to an user
143+
* Accept a connection request from a user
144+
* Reject a connection request from a user
145+
* Remove a connection with a user
146+
"""

symphony/bdk/core/service/datafeed/on_disk_datafeed_id_repository.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
logger = logging.getLogger(__name__)
88

9+
910
class DatafeedIdRepository(ABC):
1011
"""A repository interface for storing a datafeed id.
1112
By using the DatafeedLoopV1, the created datafeed id and agent base url has to be persisted on the BDK side.

0 commit comments

Comments
 (0)