Skip to content

Commit dd8a74f

Browse files
committed
Add support for mTLS
1 parent 82e11e5 commit dd8a74f

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def __init__(
6363
agent_identifying_attributes: Mapping[str, AnyValue],
6464
agent_non_identifying_attributes: Mapping[str, AnyValue] | None = None,
6565
transport: HttpTransport | None = None,
66+
# this matches requests but can be mapped to other http libraries APIs
67+
tls_certificate: str | bool = True,
68+
tls_client_certificate: str | None = None,
69+
tls_client_key: str | None = None,
6670
):
6771
self._timeout_millis = timeout_millis
6872
self._transport = (
@@ -72,6 +76,9 @@ def __init__(
7276
self._endpoint = endpoint
7377
headers = headers or {}
7478
self._headers = {**_OPAMP_HTTP_HEADERS, **headers}
79+
self._tls_certificate = tls_certificate
80+
self._tls_client_certificate = tls_client_certificate
81+
self._tls_client_key = tls_client_key
7582

7683
self._agent_description = messages.build_agent_description(
7784
identifying_attributes=agent_identifying_attributes,
@@ -159,6 +166,9 @@ def send(self, data: bytes):
159166
headers=self._headers,
160167
data=data,
161168
timeout_millis=self._timeout_millis,
169+
tls_certificate=self._tls_certificate,
170+
tls_client_certificate=self._tls_client_certificate,
171+
tls_client_key=self._tls_client_key,
162172
)
163173
return response
164174
finally:

opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import abc
1618
from typing import Mapping
1719

@@ -26,9 +28,13 @@ class HttpTransport(abc.ABC):
2628
@abc.abstractmethod
2729
def send(
2830
self,
31+
*,
2932
url: str,
3033
headers: Mapping[str, str],
3134
data: bytes,
3235
timeout_millis: int,
36+
tls_certificate: str | bool,
37+
tls_client_certificate: str | None = None,
38+
tls_client_key: str | None = None,
3339
) -> opamp_pb2.ServerToAgent:
3440
pass

opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,30 @@ def __init__(self, session: requests.Session | None = None):
3232

3333
def send(
3434
self,
35+
*,
3536
url: str,
3637
headers: Mapping[str, str],
3738
data: bytes,
3839
timeout_millis: int,
40+
tls_certificate: str | bool,
41+
tls_client_certificate: str | None = None,
42+
tls_client_key: str | None = None,
3943
):
4044
headers = {**base_headers, **headers}
4145
timeout: float = timeout_millis / 1e3
46+
client_cert = (
47+
(tls_client_certificate, tls_client_key)
48+
if tls_client_certificate and tls_client_key
49+
else tls_client_certificate
50+
)
4251
try:
4352
response = self.session.post(
44-
url, headers=headers, data=data, timeout=timeout
53+
url,
54+
headers=headers,
55+
data=data,
56+
timeout=timeout,
57+
verify=tls_certificate,
58+
cert=client_cert,
4559
)
4660
response.raise_for_status()
4761
except Exception as exc:

opamp/opentelemetry-opamp-client/tests/opamp/test_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def test_can_instantiate_opamp_client_with_defaults():
5353
"Content-Type": "application/x-protobuf",
5454
"User-Agent": "OTel-OpAMP-Python/" + __version__,
5555
}
56+
assert client._tls_certificate is True
57+
assert client._tls_client_certificate is None
58+
assert client._tls_client_key is None
5659
assert client._timeout_millis == 1_000
5760
assert client._sequence_num == 0
5861
assert isinstance(client._instance_uid, bytes)
@@ -72,6 +75,9 @@ def test_can_instantiate_opamp_client_all_params():
7275
agent_identifying_attributes={"foo": "bar"},
7376
agent_non_identifying_attributes={"bar": "baz"},
7477
transport=transport,
78+
tls_certificate="ca.pem",
79+
tls_client_certificate="client.pem",
80+
tls_client_key="client-key.pem",
7581
)
7682

7783
assert client
@@ -80,6 +86,9 @@ def test_can_instantiate_opamp_client_all_params():
8086
"User-Agent": "OTel-OpAMP-Python/" + __version__,
8187
"an": "header",
8288
}
89+
assert client._tls_certificate == "ca.pem"
90+
assert client._tls_client_certificate == "client.pem"
91+
assert client._tls_client_key == "client-key.pem"
8392
assert client._timeout_millis == 2_000
8493
assert client._sequence_num == 0
8594
assert isinstance(client._instance_uid, bytes)
@@ -111,6 +120,9 @@ def test_client_headers_override_defaults():
111120
},
112121
data=b"",
113122
timeout_millis=1000,
123+
tls_certificate=True,
124+
tls_client_certificate=None,
125+
tls_client_key=None,
114126
)
115127

116128

@@ -331,6 +343,9 @@ def test_send(client):
331343
},
332344
data=b"foo",
333345
timeout_millis=1000,
346+
tls_certificate=True,
347+
tls_client_certificate=None,
348+
tls_client_key=None,
334349
)
335350

336351

opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,104 @@ def test_can_send():
4747
with mock.patch.object(transport, "session") as session_mock:
4848
session_mock.post.return_value = response_mock
4949
response = transport.send(
50-
"http://127.0.0.1/v1/opamp",
50+
url="http://127.0.0.1/v1/opamp",
5151
headers=headers,
5252
data=data,
5353
timeout_millis=1_000,
54+
tls_certificate=True,
5455
)
5556

5657
session_mock.post.assert_called_once_with(
5758
"http://127.0.0.1/v1/opamp",
5859
headers=expected_headers,
5960
data=data,
6061
timeout=1,
62+
verify=True,
63+
cert=None,
64+
)
65+
66+
assert isinstance(response, opamp_pb2.ServerToAgent)
67+
68+
69+
def test_send_tls_certificate_mapped_to_verify():
70+
transport = RequestsTransport()
71+
serialized_message = opamp_pb2.ServerToAgent().SerializeToString()
72+
response_mock = mock.Mock(content=serialized_message)
73+
data = b""
74+
with mock.patch.object(transport, "session") as session_mock:
75+
session_mock.post.return_value = response_mock
76+
response = transport.send(
77+
url="https://127.0.0.1/v1/opamp",
78+
headers={},
79+
data=data,
80+
timeout_millis=1_000,
81+
tls_certificate=False,
82+
)
83+
84+
session_mock.post.assert_called_once_with(
85+
"https://127.0.0.1/v1/opamp",
86+
headers=base_headers,
87+
data=data,
88+
timeout=1,
89+
verify=False,
90+
cert=None,
91+
)
92+
93+
assert isinstance(response, opamp_pb2.ServerToAgent)
94+
95+
96+
def test_send_mtls():
97+
transport = RequestsTransport()
98+
serialized_message = opamp_pb2.ServerToAgent().SerializeToString()
99+
response_mock = mock.Mock(content=serialized_message)
100+
data = b""
101+
with mock.patch.object(transport, "session") as session_mock:
102+
session_mock.post.return_value = response_mock
103+
response = transport.send(
104+
url="https://127.0.0.1/v1/opamp",
105+
headers={},
106+
data=data,
107+
timeout_millis=1_000,
108+
tls_certificate="server.pem",
109+
tls_client_certificate="client.pem",
110+
tls_client_key="client.key",
111+
)
112+
113+
session_mock.post.assert_called_once_with(
114+
"https://127.0.0.1/v1/opamp",
115+
headers=base_headers,
116+
data=data,
117+
timeout=1,
118+
verify="server.pem",
119+
cert=("client.pem", "client.key"),
120+
)
121+
122+
assert isinstance(response, opamp_pb2.ServerToAgent)
123+
124+
125+
def test_send_mtls_no_client_key():
126+
transport = RequestsTransport()
127+
serialized_message = opamp_pb2.ServerToAgent().SerializeToString()
128+
response_mock = mock.Mock(content=serialized_message)
129+
data = b""
130+
with mock.patch.object(transport, "session") as session_mock:
131+
session_mock.post.return_value = response_mock
132+
response = transport.send(
133+
url="https://127.0.0.1/v1/opamp",
134+
headers={},
135+
data=data,
136+
timeout_millis=1_000,
137+
tls_certificate="server.pem",
138+
tls_client_certificate="client.pem",
139+
)
140+
141+
session_mock.post.assert_called_once_with(
142+
"https://127.0.0.1/v1/opamp",
143+
headers=base_headers,
144+
data=data,
145+
timeout=1,
146+
verify="server.pem",
147+
cert="client.pem",
61148
)
62149

63150
assert isinstance(response, opamp_pb2.ServerToAgent)
@@ -74,15 +161,18 @@ def test_send_exceptions_raises_opamp_exception():
74161
response_mock.raise_for_status.side_effect = Exception
75162
with pytest.raises(OpAMPException):
76163
transport.send(
77-
"http://127.0.0.1/v1/opamp",
164+
url="http://127.0.0.1/v1/opamp",
78165
headers=headers,
79166
data=data,
80167
timeout_millis=1_000,
168+
tls_certificate=True,
81169
)
82170

83171
session_mock.post.assert_called_once_with(
84172
"http://127.0.0.1/v1/opamp",
85173
headers=expected_headers,
86174
data=data,
87175
timeout=1,
176+
verify=True,
177+
cert=None,
88178
)

0 commit comments

Comments
 (0)