Skip to content

Commit 08fe405

Browse files
authored
Merge pull request #297 from shokakucarrier/main
Radas sign request implementation
2 parents 1921e41 + c7ebf31 commit 08fe405

File tree

2 files changed

+203
-4
lines changed

2 files changed

+203
-4
lines changed

charon/pkgs/radas_signature_handler.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
import os
2020
import asyncio
2121
import sys
22+
import uuid
2223
from typing import List, Any, Tuple, Callable, Dict, Optional
2324
from charon.config import get_config, RadasConfig
2425
from charon.pkgs.oras_client import OrasClient
25-
from proton import Event
26+
from proton import SSLDomain, Message, Event
2627
from proton.handlers import MessagingHandler
28+
from proton.reactor import Container
2729

2830
logger = logging.getLogger(__name__)
2931

@@ -48,6 +50,7 @@ def __init__(self, sign_result_loc: str, request_id: str) -> None:
4850
super().__init__()
4951
self.sign_result_loc = sign_result_loc
5052
self.request_id = request_id
53+
self.conn = None
5154
self.sign_result_status: Optional[str] = None
5255
self.sign_result_errors: List[str] = []
5356

@@ -63,8 +66,23 @@ def on_start(self, event: Event) -> None:
6366
# explicit check to pass the type checker
6467
if rconf is None:
6568
sys.exit(1)
66-
conn = event.container.connect(rconf.umb_target())
67-
event.container.create_receiver(conn, rconf.result_queue())
69+
70+
ssl_domain = SSLDomain(SSLDomain.MODE_CLIENT)
71+
ssl_domain.set_credentials(
72+
rconf.client_ca(),
73+
rconf.client_key(),
74+
rconf.client_key_password()
75+
)
76+
ssl_domain.set_trusted_ca_db(rconf.root_ca())
77+
ssl_domain.set_peer_authentication(SSLDomain.VERIFY_PEER)
78+
79+
self.conn = event.container.connect(
80+
url=rconf.umb_target(),
81+
ssl_domain=ssl_domain
82+
)
83+
event.container.create_receiver(
84+
self.conn, rconf.result_queue(), dynamic=True
85+
)
6886
logger.info("Listening on %s, queue: %s", rconf.umb_target(), rconf.result_queue())
6987

7088
def on_message(self, event: Event) -> None:
@@ -122,6 +140,60 @@ def _process_message(self, msg: Any) -> None:
122140
logger.info("Number of files pulled: %d, path: %s", len(files), files[0])
123141

124142

143+
class RadasSender(MessagingHandler):
144+
"""
145+
This simple sender will send given string massage to UMB message queue to request signing.
146+
Attributes:
147+
payload (str): payload json string for radas to read,
148+
this value construct from the cmd flag
149+
"""
150+
def __init__(self, payload: str):
151+
super().__init__()
152+
self.payload = payload
153+
self.container = None
154+
self.conn = None
155+
self.sender = None
156+
157+
def on_start(self, event):
158+
"""
159+
On start callback
160+
"""
161+
conf = get_config()
162+
if not (conf and conf.is_radas_enabled()):
163+
sys.exit(1)
164+
165+
rconf = conf.get_radas_config()
166+
if rconf is None:
167+
sys.exit(1)
168+
169+
ssl_domain = SSLDomain(SSLDomain.MODE_CLIENT)
170+
ssl_domain.set_credentials(
171+
rconf.client_ca(),
172+
rconf.client_key(),
173+
rconf.client_key_password()
174+
)
175+
ssl_domain.set_trusted_ca_db(rconf.root_ca())
176+
ssl_domain.set_peer_authentication(SSLDomain.VERIFY_PEER)
177+
178+
self.container = event.container
179+
self.conn = event.container.connect(
180+
url=rconf.umb_target(),
181+
ssl_domain=ssl_domain
182+
)
183+
self.sender = event.container.create_sender(self.conn, rconf.request_queue())
184+
185+
def on_sendable(self):
186+
"""
187+
On message able to send callback
188+
"""
189+
request = self.payload
190+
msg = Message(body=request)
191+
if self.sender:
192+
self.sender.send(msg)
193+
if self.container:
194+
self.container.stop()
195+
196+
125197
def generate_radas_sign(top_level: str, sign_result_loc: str) -> Tuple[List[str], List[str]]:
126198
"""
127199
Generate .asc files based on RADAS sign result json file
@@ -215,4 +287,33 @@ def sign_in_radas(repo_url: str,
215287
result_path: str,
216288
ignore_patterns: List[str],
217289
radas_config: RadasConfig):
218-
logger.info("Start signing for %s", repo_url)
290+
"""
291+
This function will be responsible to do the overall controlling of the whole process,
292+
like trigger the send and register the receiver, and control the wait and timeout there.
293+
"""
294+
logger.debug("params. repo_url: %s, requester: %s, sign_key: %s, result_path: %s,"
295+
"radas_config: %s", repo_url, requester, sign_key, result_path, radas_config)
296+
request_id = str(uuid.uuid4())
297+
exclude = ignore_patterns if ignore_patterns else []
298+
299+
payload = {
300+
"request_id": request_id,
301+
"requested_by": requester,
302+
"type": "mrrc",
303+
"file_reference": repo_url,
304+
"sig_keyname": sign_key,
305+
"exclude": exclude
306+
}
307+
308+
listener = RadasReceiver(result_path, request_id)
309+
sender = RadasSender(json.dumps(payload))
310+
311+
try:
312+
Container(sender).run()
313+
logger.info("Successfully sent signing request ID: %s", request_id)
314+
Container(listener).run()
315+
finally:
316+
if listener.conn is not None:
317+
listener.conn.close()
318+
if sender.conn is not None:
319+
sender.conn.close()

tests/test_radas_send_handler.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import tempfile
2+
import os
3+
from unittest import mock
4+
import unittest
5+
from charon.pkgs.radas_signature_handler import sign_in_radas
6+
7+
8+
class RadasSignHandlerTest(unittest.TestCase):
9+
def setUp(self) -> None:
10+
super().setUp()
11+
12+
def tearDown(self) -> None:
13+
super().tearDown()
14+
15+
def test_sign_in_radas_normal_flow(self):
16+
with tempfile.TemporaryDirectory() as tmpdir:
17+
# Mock configuration
18+
mock_config = mock.MagicMock()
19+
mock_config.is_radas_enabled.return_value = True
20+
mock_radas_config = mock.MagicMock()
21+
mock_config.get_radas_config.return_value = mock_radas_config
22+
23+
# Mock Container run to avoid real AMQP connection
24+
with mock.patch(
25+
"charon.pkgs.radas_signature_handler.Container") as mock_container, \
26+
mock.patch(
27+
"charon.pkgs.radas_signature_handler.get_config", return_value=mock_config), \
28+
mock.patch(
29+
"charon.pkgs.radas_signature_handler.uuid.uuid4", return_value="mocked-uuid"):
30+
31+
test_result_path = os.path.join(tmpdir, "results")
32+
os.makedirs(test_result_path)
33+
34+
sign_in_radas(
35+
repo_url="quay.io/test/repo",
36+
requester="test-user",
37+
sign_key="test-key",
38+
result_path=test_result_path,
39+
ignore_patterns=[],
40+
radas_config=mock_radas_config
41+
)
42+
43+
# Verify Container.run() was called twice (sender and receiver)
44+
self.assertEqual(mock_container.call_count, 2)
45+
46+
# Verify request ID propagation
47+
receiver_call = mock_container.call_args_list[1]
48+
self.assertEqual(receiver_call.args[0].request_id, "mocked-uuid")
49+
50+
def test_sign_in_radas_with_disabled_config(self):
51+
mock_config = mock.MagicMock()
52+
mock_config.is_radas_enabled.return_value = False
53+
54+
with mock.patch(
55+
"charon.pkgs.radas_signature_handler.get_config", return_value=mock_config), \
56+
self.assertRaises(SystemExit):
57+
58+
sign_in_radas(
59+
repo_url="quay.io/test/repo",
60+
requester="test-user",
61+
sign_key="test-key",
62+
result_path="/tmp/results",
63+
ignore_patterns=[],
64+
radas_config=mock.MagicMock()
65+
)
66+
67+
def test_sign_in_radas_connection_cleanup(self):
68+
mock_config = mock.MagicMock()
69+
mock_config.is_radas_enabled.return_value = True
70+
mock_radas_config = mock.MagicMock()
71+
72+
with mock.patch("charon.pkgs.radas_signature_handler.Container") as mock_container, \
73+
mock.patch("charon.pkgs.radas_signature_handler.get_config", return_value=mock_config):
74+
75+
mock_sender_conn = mock.MagicMock()
76+
mock_listener_conn = mock.MagicMock()
77+
78+
def container_side_effect(*args, **kwargs):
79+
if args[0].__class__.__name__ == "RadasReceiver":
80+
args[0].conn = mock_listener_conn
81+
elif args[0].__class__.__name__ == "RadasSender":
82+
args[0].conn = mock_sender_conn
83+
return mock.MagicMock()
84+
85+
mock_container.side_effect = container_side_effect
86+
87+
sign_in_radas(
88+
repo_url="quay.io/test/repo",
89+
requester="test-user",
90+
sign_key="test-key",
91+
result_path="/tmp/results",
92+
ignore_patterns=[],
93+
radas_config=mock_radas_config
94+
)
95+
96+
# Verify connections are closed
97+
mock_sender_conn.close.assert_called_once()
98+
mock_listener_conn.close.assert_called_once()

0 commit comments

Comments
 (0)