Skip to content

Commit 757f031

Browse files
authored
feat(CVEs): Adds the API endpoint to check CVEs availability for given snap (#5100)
* Add API endpoint to check snap CVEs data availability * Add tests for the helper function and the API endpoint * Fix catching 404 error from GH API * Consolidate error messages. * Improve error handling and tests based on review comments
1 parent f4b0782 commit 757f031

5 files changed

Lines changed: 184 additions & 29 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
4+
from webapp.publisher.cve.cve_helper import CveHelper
5+
6+
7+
class HasCvesTest(unittest.TestCase):
8+
9+
def setUp(self):
10+
self.file_metadata = {"download_url": "https://example.com/file.json"}
11+
12+
@patch("requests.get")
13+
def test_has_cve_data(self, mock_get):
14+
mock_get.side_effect = [
15+
MagicMock(status_code=200, json=lambda: self.file_metadata),
16+
]
17+
18+
result = CveHelper.has_cve_data("my-snap")
19+
self.assertTrue(result)
20+
21+
@patch("requests.get")
22+
def test_has_cve_data_not_found(self, mock_get):
23+
mock_get.side_effect = [
24+
MagicMock(status_code=404, json=lambda: {}),
25+
]
26+
27+
result = CveHelper.has_cve_data("my-snap")
28+
self.assertFalse(result)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from unittest import TestCase
2+
from webapp.app import create_app
3+
from unittest.mock import patch
4+
5+
6+
class TestEndpoints(TestCase):
7+
def setUp(self):
8+
self.app = create_app(testing=True)
9+
self.client = self.app.test_client()
10+
self._log_in(is_canonical=False)
11+
12+
def _log_in(self, is_canonical=False):
13+
test_macaroon = "test_macaroon"
14+
with self.client.session_transaction() as s:
15+
s["publisher"] = {
16+
"account_id": "test_account_id",
17+
"image": None,
18+
"nickname": "XYZ",
19+
"fullname": "ABC XYZ",
20+
"email": "testing@testing.com",
21+
"stores": [],
22+
"is_canonical": is_canonical,
23+
}
24+
s["macaroons"] = test_macaroon
25+
s["developer_token"] = test_macaroon
26+
s["exchanged_developer_token"] = True
27+
28+
def _set_user_is_canonical(self, value):
29+
self._log_in(is_canonical=value)
30+
31+
32+
class TestModelServiceEndpoints(TestEndpoints):
33+
@patch(
34+
"webapp.publisher.cve.cve_helper.CveHelper.has_cve_data",
35+
return_value=True,
36+
)
37+
@patch(
38+
"canonicalwebteam.store_api.dashboard.Dashboard.get_snap_info",
39+
return_value={"snap_id": "id"},
40+
)
41+
def test_has_cves_for_canonical_user(self, mock_get_snap_info, mock_get):
42+
self._set_user_is_canonical(True)
43+
44+
response = self.client.get("api/snaps/cve/test")
45+
data = response.json
46+
47+
self.assertEqual(response.status_code, 200)
48+
self.assertEqual(data["success"], True)
49+
50+
@patch(
51+
"webapp.publisher.cve.cve_helper.CveHelper.has_cve_data",
52+
return_value=False,
53+
)
54+
@patch(
55+
"canonicalwebteam.store_api.dashboard.Dashboard.get_snap_info",
56+
return_value={"snap_id": "id"},
57+
)
58+
def test_has_cves_no_data(self, mock_get_snap_info, mock_get):
59+
self._set_user_is_canonical(True)
60+
61+
response = self.client.get("api/snaps/cve/test")
62+
data = response.json
63+
64+
self.assertEqual(response.status_code, 404)
65+
self.assertEqual(data["success"], False)
66+
67+
def test_has_cves_for_non_canonical_user(self):
68+
response = self.client.get("api/snaps/cve/test")
69+
data = response.json
70+
71+
self.assertEqual(response.status_code, 403)
72+
self.assertEqual(data["success"], False)

webapp/publisher/cve/cve_helper.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ def _fetch_file_content(snap_name, revision, file_metadata):
109109
else:
110110
raise NotFound
111111

112+
@staticmethod
113+
def has_cve_data(snap_name):
114+
try:
115+
CveHelper._get_cve_file_metadata(
116+
"snap-cves/{}.json".format(snap_name)
117+
)
118+
return True
119+
except NotFound:
120+
return False
121+
112122
@staticmethod
113123
def get_cve_with_revision(snap_name, revision):
114124
file_metadata = CveHelper._get_cve_file_metadata(

webapp/publisher/cve/cve_views.py

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,83 @@
11
import flask
22
from canonicalwebteam.store_api.dashboard import Dashboard
3-
3+
from canonicalwebteam.exceptions import StoreApiResourceNotFound, StoreApiError
44
from webapp.helpers import api_publisher_session
55
from webapp.decorators import login_required
66
from webapp.publisher.cve.cve_helper import CveHelper
77

88
dashboard = Dashboard(api_publisher_session)
99

1010

11+
def can_user_access_cve_data(snap_name):
12+
"""
13+
Check if the user has access to CVE data for the given snap.
14+
15+
:return: A tuple containing:
16+
has_access (bool): True if the user has access, False otherwise.
17+
error_message (str): Error message if access is denied.
18+
status_code (int): HTTP status code for the response.
19+
"""
20+
is_user_canonical = flask.session["publisher"].get("is_canonical", False)
21+
22+
# TODO: in future with brand store support we will need more specific
23+
# checks, such as those implemented in CveHelper.can_user_access_cve_data
24+
# For now, we only check if user is Canonical member and has
25+
# publisher access to the snap.
26+
if not is_user_canonical:
27+
return (False, "User is not allowed to see snap's CVE data.", 403)
28+
29+
try:
30+
snap_details = dashboard.get_snap_info(flask.session, snap_name)
31+
except StoreApiResourceNotFound:
32+
return (False, f"CVEs data for '{snap_name}' snap not found.", 404)
33+
except StoreApiError:
34+
return (False, f"Error fetching '{snap_name}' snap details.", 500)
35+
36+
if not snap_details:
37+
return (False, f"CVEs data for '{snap_name}' snap not found.", 404)
38+
39+
return (True, None, 200)
40+
41+
42+
@login_required
43+
def has_cves(snap_name):
44+
45+
# Check if the user has access to CVE data for the given snap
46+
has_access, error_message, status_code = can_user_access_cve_data(
47+
snap_name
48+
)
49+
if not has_access:
50+
return (
51+
flask.jsonify({"success": False, "error": error_message}),
52+
status_code,
53+
)
54+
55+
snap_has_cves = CveHelper.has_cve_data(snap_name)
56+
if snap_has_cves:
57+
return flask.jsonify({"success": True})
58+
else:
59+
return (
60+
flask.jsonify(
61+
{
62+
"success": False,
63+
"error": f"CVEs data for '{snap_name}' snap not found.",
64+
}
65+
),
66+
404,
67+
)
68+
69+
1170
@login_required
1271
def get_cves(snap_name, revision):
72+
# Check if the user has access to CVE data for the given snap
73+
has_access, error_message, status_code = can_user_access_cve_data(
74+
snap_name
75+
)
76+
if not has_access:
77+
return (
78+
flask.jsonify({"success": False, "error": error_message}),
79+
status_code,
80+
)
1381

1482
# Filtering params
1583
usn_ids = flask.request.args.getlist("usn_id")
@@ -59,34 +127,6 @@ def get_cves(snap_name, revision):
59127
# Pagination params
60128
page = flask.request.args.get("page", default=1, type=int)
61129
page_size = flask.request.args.get("page_size", default=10, type=int)
62-
is_user_canonical = flask.session["publisher"].get("is_canonical", False)
63-
64-
# TODO: in future with brand store support we will need more specific
65-
# checks, such as those implemented in CveHelper.can_user_access_cve_data
66-
# For now, we only check if user is Canonical member and has
67-
# publisher access to the snap.
68-
if not is_user_canonical:
69-
return (
70-
flask.jsonify(
71-
{
72-
"success": False,
73-
"error": "User is not allowed to see snap's CVE data.",
74-
}
75-
),
76-
403,
77-
)
78-
79-
snap_details = dashboard.get_snap_info(flask.session, snap_name)
80-
if not snap_details:
81-
return (
82-
flask.jsonify(
83-
{
84-
"success": False,
85-
"error": f"Snap '{snap_name}' not found.",
86-
}
87-
),
88-
404,
89-
)
90130

91131
cves = CveHelper.get_cve_with_revision(snap_name, revision)
92132
cves = CveHelper.filter_cve_data(

webapp/publisher/snaps/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@
285285
view_func=cve_views.get_cves,
286286
)
287287

288+
publisher_snaps.add_url_rule(
289+
"/api/snaps/cve/<snap_name>",
290+
view_func=cve_views.has_cves,
291+
)
292+
288293

289294
@publisher_snaps.route("/account/snaps")
290295
@login_required

0 commit comments

Comments
 (0)