Skip to content

Commit 258e227

Browse files
committed
2 parents 3fad1a6 + d47f83a commit 258e227

13 files changed

+316
-42
lines changed

docs/configuration.md

+71-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,75 @@ config_3 = BdkConfigLoader.load_from_symphony_dir("config.yaml") # 3
3838
```
3939
1. Load configuration from a file
4040
2. Load configuration from a string object
41-
3. Load configuration from the Symphony directory. The Symphony directory is located under `.symphony` folder in your home directory
42-
. It can be useful when you don't want to share your own Symphony credentials within your project codebase
41+
3. Load configuration from the Symphony directory. The Symphony directory is located under `.symphony` folder in your
42+
home directory. It can be useful when you don't want to share your own Symphony credentials within your project
43+
codebase
4344

45+
## Full configuration example
46+
```yaml
47+
scheme: https
48+
host: localhost.symphony.com
49+
port: 8443
50+
51+
proxy:
52+
host: proxy.symphony.com
53+
port: 1234
54+
username: proxyuser
55+
password: proxypassword
56+
57+
pod:
58+
host: dev.symphony.com
59+
port: 443
60+
61+
agent:
62+
host: dev-agent.symphony.com
63+
port: 5678
64+
proxy:
65+
host: agent-proxy
66+
port: 3396
67+
68+
keyManager:
69+
host: dev-key.symphony.com
70+
port: 8444
71+
72+
sessionAuth:
73+
host: dev-session.symphony.com
74+
port: 8444
75+
76+
ssl:
77+
trustStore:
78+
path: /path/to/truststore.pem
79+
80+
bot:
81+
username: bot-name
82+
privateKey:
83+
path: /path/to/bot/rsa-private-key.pem
84+
85+
app:
86+
appId: app-id
87+
privateKey:
88+
path: path/to/private-key.pem
89+
```
90+
91+
### Configuration structure
92+
93+
The BDK configuration now includes the following properties:
94+
- The BDK configuration can contain the global properties for `host`, `port`, `context` and `scheme`.
95+
These global properties can be used by the client configuration by default or can be overridden if
96+
user specify the dedicated `host`, `port`, `context`, `scheme` inside the client configuration.
97+
- `proxy` contains proxy related information. This field is optional.
98+
If set, it will use the provided `host` (mandatory), `port` (mandatory), `username` and `password`.
99+
It can be overridden in each of the `pod`, `agent`, `keyManager` and `sessionAuth` fields.
100+
- `pod` contains information like host, port, scheme, context, proxy... of the pod on which
101+
the service account using by the bot is created.
102+
- `agent` contains information like host, port, scheme, context, proxy... of the agent which
103+
the bot connects to.
104+
- `keyManager` contains information like host, port, scheme, context, proxy... of the key
105+
manager which manages the key token of the bot.
106+
- `ssl` contains the path to a file of concatenated CA certificates in PEM format. As we are using python SSL library
107+
under the hood, you can check
108+
[ssl lib documentation on certificates](https://docs.python.org/3/library/ssl.html#certificates) for more information.
109+
- `bot` contains information about the bot like the username, the private key for authenticating the service account
110+
on pod.
111+
- `app` contains information about the extension app that the bot will use like
112+
the appId, the private key for authenticating the extension app.

symphony/bdk/core/client/api_client_factory.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Module containing the ApiClientFactory class.
22
"""
3+
import urllib3
4+
35
from symphony.bdk.gen.configuration import Configuration
46
from symphony.bdk.gen.api_client import ApiClient
57

@@ -61,8 +63,19 @@ async def close_clients(self):
6163
await self._agent_client.close()
6264
await self._session_auth_client.close()
6365

64-
@staticmethod
65-
def _get_api_client(server_config, context='') -> ApiClient:
66-
path = server_config.get_base_path() + context
67-
configuration = Configuration(host=path)
66+
def _get_api_client(self, server_config, context) -> ApiClient:
67+
configuration = Configuration(host=(server_config.get_base_path() + context))
68+
configuration.verify_ssl = True
69+
configuration.ssl_ca_cert = self._config.ssl.trust_store_path
70+
71+
if server_config.proxy is not None:
72+
self._configure_proxy(server_config.proxy, configuration)
73+
6874
return ApiClient(configuration=configuration)
75+
76+
@staticmethod
77+
def _configure_proxy(proxy_config, configuration):
78+
configuration.proxy = proxy_config.get_url()
79+
80+
if proxy_config.are_credentials_defined():
81+
configuration.proxy_headers = urllib3.util.make_headers(proxy_basic_auth=proxy_config.get_credentials())

symphony/bdk/core/config/model/bdk_client_config.py

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from symphony.bdk.core.config.model.bdk_server_config import BdkServerConfig
1+
from symphony.bdk.core.config.model.bdk_server_config import BdkServerConfig, BdkProxyConfig
22

33

44
class BdkClientConfig(BdkServerConfig):
@@ -17,17 +17,15 @@ def __init__(self, parent_config, config):
1717
:param parent_config: the parent configuration of type BdkConfig
1818
:param config: client configuration parameters of type dict
1919
"""
20+
if config is None:
21+
config = {}
22+
23+
self._scheme = config.get("scheme")
24+
self._port = config.get("port")
25+
self._host = config.get("host")
26+
self._context = config.get("context")
27+
self._proxy = BdkProxyConfig(**config.get("proxy")) if "proxy" in config else None
2028
self.parent_config = parent_config
21-
if config is not None:
22-
self._scheme = config.get("scheme")
23-
self._port = config.get("port")
24-
self._host = config.get("host")
25-
self._context = config.get("context")
26-
else:
27-
self._scheme = None
28-
self._port = None
29-
self._host = None
30-
self._context = None
3129

3230
@property
3331
def scheme(self):
@@ -61,6 +59,15 @@ def context(self):
6159
"""
6260
return self._self_or_parent(self._context, self.parent_config.context)
6361

62+
@property
63+
def proxy(self):
64+
"""Return the applicable proxy information: either the one configured at child level (e.g. 'pod')
65+
or at global level.
66+
67+
:return: the applicable proxy information
68+
"""
69+
return self._self_or_parent(self._proxy, self.parent_config.proxy)
70+
6471
@staticmethod
6572
def _self_or_parent(instance_value, parent_value):
6673
"""Get the parent configuration field if the current client's field is not defined

symphony/bdk/core/config/model/bdk_config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __init__(self, **config):
1616
:param config: the dict containing the server configuration parameters.
1717
"""
1818
super().__init__(scheme=config.get("scheme"), host=config.get("host"), port=config.get("port"),
19-
context=config.get("context"))
19+
context=config.get("context"), proxy=config.get("proxy"))
2020
self.agent = BdkClientConfig(self, config.get("agent"))
2121
self.pod = BdkClientConfig(self, config.get("pod"))
2222
self.key_manager = BdkClientConfig(self, config.get("keyManager"))

symphony/bdk/core/config/model/bdk_server_config.py

+41-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
class BdkServerConfig:
2-
"""Base class for server and client configurations
3-
"""
2+
"""Base class for server and client configurations"""
43
DEFAULT_SCHEME: str = "https"
54
DEFAULT_HTTPS_PORT: int = 443
65

7-
def __init__(self, scheme=None, port=None, context="", host=None):
6+
def __init__(self, scheme=None, port=None, context="", host=None, proxy=None):
87
self.scheme = scheme if scheme is not None else self.DEFAULT_SCHEME
98
self.port = self.port = port if port is not None else self.DEFAULT_HTTPS_PORT
109
self.context = context
1110
self.host = host
11+
self.proxy = BdkProxyConfig(**proxy) if proxy is not None else None
1212

1313
def get_base_path(self) -> str:
1414
"""Constructs the base path of the current config
@@ -39,3 +39,41 @@ def get_port_as_string(self) -> str:
3939
:return: the port information to be appended to the built URL
4040
"""
4141
return ":" + str(self.port) if self.port else ""
42+
43+
44+
class BdkProxyConfig:
45+
"""Class to configure a proxy with a host, port and optional proxy credentials"""
46+
47+
def __init__(self, host, port, username=None, password=None):
48+
"""
49+
50+
:param host: host of the proxy (mandatory)
51+
:param port: port of the proxy (mandatory)
52+
:param username: username for proxy basic authentication (optional)
53+
:param password: password for proxy basic authentication (optional, must be not None if username specified)
54+
"""
55+
self.host = host
56+
self.port = port
57+
self.username = username
58+
self.password = password
59+
60+
def get_url(self):
61+
"""Builds the proxy URL.
62+
63+
:return: the URL of the http proxy to target
64+
"""
65+
return f"http://{self.host}:{self.port}"
66+
67+
def are_credentials_defined(self):
68+
"""Check if proxy credentials were set
69+
70+
:return: True if username and password set
71+
"""
72+
return self.username and self.password is not None
73+
74+
def get_credentials(self):
75+
"""Builds the credentials information to pass to the proxy-authorization header before base64 encoding.
76+
77+
:return: username + ":" + password
78+
"""
79+
return f"{self.username}:{self.password}"
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
from symphony.bdk.core.config.model.bdk_certificate_config import BdkCertificateConfig
2-
31
TRUST_STORE = "trustStore"
42

53

64
class BdkSslConfig:
75
"""Class containing the SSL configuration.
8-
"""
6+
self.trust_store_path should be the path to a file of concatenated CA certificates in PEM format."""
97

108
def __init__(self, config):
119
"""
1210
1311
:param config: the dict containing the SSL specific configuration.
1412
"""
13+
self.trust_store_path = None
1514
if config is not None and TRUST_STORE in config:
16-
truststore_config = config.get(TRUST_STORE)
17-
self.trust_store = BdkCertificateConfig(path=truststore_config.get("path"),
18-
password=truststore_config.get("password"))
19-
else:
20-
self.trust_store = BdkCertificateConfig()
15+
self.trust_store_path = config[TRUST_STORE].get("path")

symphony/bdk/gen/rest.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ def __init__(self, configuration, pools_size=4, maxsize=None):
4949
maxsize = configuration.connection_pool_maxsize
5050

5151
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile=configuration.ssl_ca_cert)
52+
ssl_context.verify_mode = ssl.CERT_REQUIRED
53+
5254
if configuration.cert_file:
53-
ssl_context.load_verify_locations(configuration.cert_file)
55+
ssl_context.load_cert_chain(
56+
configuration.cert_file, keyfile=configuration.key_file
57+
)
5458

5559
if not configuration.verify_ssl:
5660
ssl_context.check_hostname = False

tests/core/client/api_client_factory_test.py

+78-11
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,87 @@
33
import pytest
44

55
from symphony.bdk.core.client.api_client_factory import ApiClientFactory
6-
from symphony.bdk.core.config.loader import BdkConfigLoader
7-
from tests.utils.resource_utils import get_config_resource_filepath
6+
from symphony.bdk.core.config.model.bdk_config import BdkConfig
7+
from symphony.bdk.core.config.model.bdk_server_config import BdkProxyConfig
8+
from symphony.bdk.core.config.model.bdk_ssl_config import BdkSslConfig
9+
10+
HOST = "acme.symphony.com"
811

912

1013
@pytest.fixture()
1114
def config():
12-
return BdkConfigLoader.load_from_file(get_config_resource_filepath("config.yaml"))
15+
return BdkConfig(host=HOST)
16+
17+
18+
@pytest.mark.asyncio
19+
async def test_host_configured(config):
20+
client_factory = ApiClientFactory(config)
21+
22+
assert_host_configured_only(client_factory.get_pod_client().configuration, "/pod")
23+
assert_host_configured_only(client_factory.get_login_client().configuration, "/login")
24+
assert_host_configured_only(client_factory.get_agent_client().configuration, "/agent")
25+
assert_host_configured_only(client_factory.get_session_auth_client().configuration, "/sessionauth")
26+
assert_host_configured_only(client_factory.get_relay_client().configuration, "/relay")
27+
28+
29+
@pytest.mark.asyncio
30+
async def test_proxy_configured(config):
31+
proxy_host = "proxy.com"
32+
proxy_port = 1234
33+
config.proxy = BdkProxyConfig(proxy_host, proxy_port)
34+
client_factory = ApiClientFactory(config)
35+
36+
assert_host_and_proxy_configured(client_factory.get_pod_client().configuration, "/pod", proxy_host, proxy_port)
37+
assert_host_and_proxy_configured(client_factory.get_login_client().configuration, "/login", proxy_host, proxy_port)
38+
assert_host_and_proxy_configured(client_factory.get_agent_client().configuration, "/agent", proxy_host, proxy_port)
39+
assert_host_and_proxy_configured(client_factory.get_session_auth_client().configuration, "/sessionauth", proxy_host,
40+
proxy_port)
41+
assert_host_and_proxy_configured(client_factory.get_relay_client().configuration, "/relay", proxy_host, proxy_port)
42+
43+
44+
@pytest.mark.asyncio
45+
async def test_proxy_credentials_configured(config):
46+
proxy_host = "proxy.com"
47+
proxy_port = 1234
48+
config.proxy = BdkProxyConfig(proxy_host, proxy_port, "user", "pass")
49+
client_factory = ApiClientFactory(config)
50+
51+
assert_host_and_proxy_credentials_configured(client_factory.get_pod_client().configuration, "/pod", proxy_host,
52+
proxy_port)
53+
assert_host_and_proxy_credentials_configured(client_factory.get_login_client().configuration, "/login", proxy_host,
54+
proxy_port)
55+
assert_host_and_proxy_credentials_configured(client_factory.get_agent_client().configuration, "/agent", proxy_host,
56+
proxy_port)
57+
assert_host_and_proxy_credentials_configured(client_factory.get_session_auth_client().configuration, "/sessionauth",
58+
proxy_host,
59+
proxy_port)
60+
assert_host_and_proxy_credentials_configured(client_factory.get_relay_client().configuration, "/relay", proxy_host,
61+
proxy_port)
62+
63+
64+
def test_trust_store_configured(config):
65+
with patch("symphony.bdk.gen.rest.RESTClientObject"):
66+
truststore_path = "/path/to/truststore.pem"
67+
config.ssl = BdkSslConfig({"trustStore": {"path": truststore_path}})
68+
69+
client_factory = ApiClientFactory(config)
70+
71+
assert client_factory.get_pod_client().configuration.ssl_ca_cert == truststore_path
72+
73+
74+
def assert_host_configured_only(configuration, url_suffix):
75+
assert configuration.host == f"https://{HOST}:443{url_suffix}"
76+
assert configuration.proxy is None
77+
assert configuration.proxy_headers is None
78+
79+
80+
def assert_host_and_proxy_configured(configuration, url_suffix, proxy_host, proxy_port):
81+
assert configuration.host == f"https://{HOST}:443{url_suffix}"
82+
assert configuration.proxy == f"http://{proxy_host}:{proxy_port}"
83+
assert configuration.proxy_headers is None
1384

1485

15-
def test_get_api_client(config):
16-
with patch('symphony.bdk.core.client.api_client_factory.ApiClient', autospec=True):
17-
api_client_factory = ApiClientFactory(config)
18-
assert api_client_factory.get_pod_client() is not None
19-
assert api_client_factory.get_login_client() is not None
20-
assert api_client_factory.get_agent_client() is not None
21-
assert api_client_factory.get_session_auth_client() is not None
22-
assert api_client_factory.get_relay_client() is not None
86+
def assert_host_and_proxy_credentials_configured(configuration, url_suffix, proxy_host, proxy_port):
87+
assert configuration.host == f"https://{HOST}:443{url_suffix}"
88+
assert configuration.proxy == f"http://{proxy_host}:{proxy_port}"
89+
assert "proxy-authorization" in configuration.proxy_headers

tests/core/config/models/test_bdk_server_config.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from symphony.bdk.core.config.model.bdk_server_config import BdkServerConfig
1+
from symphony.bdk.core.config.model.bdk_server_config import BdkServerConfig, BdkProxyConfig
22

33

44
def test_get_base_path():
@@ -24,3 +24,18 @@ def test_get_port_as_string():
2424

2525
config.port = None
2626
assert config.get_port_as_string() == ""
27+
28+
29+
def test_proxy_config_no_credentials():
30+
proxy = BdkProxyConfig("proxy.symphony.com", 1234)
31+
32+
assert proxy.get_url() == "http://proxy.symphony.com:1234"
33+
assert not proxy.are_credentials_defined()
34+
35+
36+
def test_proxy_config_with_credentials():
37+
proxy = BdkProxyConfig("proxy.symphony.com", 1234, "user", "password")
38+
39+
assert proxy.get_url() == "http://proxy.symphony.com:1234"
40+
assert proxy.are_credentials_defined()
41+
assert proxy.get_credentials() == "user:password"

0 commit comments

Comments
 (0)