Skip to content

Commit 48eae8a

Browse files
committed
SBOM upload client_secret failure
Problem: SBOM upload results in a 401 code and the error handler fails to deal wth this correctly. Solution: Modified the error handling to properly handle when response body has non-JSON format. Added extra functional tests for appidp and sboms endpoints. Signed-off-by: Paul Hewlett <phewlett76@gmail.com>
1 parent 8675b44 commit 48eae8a

File tree

7 files changed

+257
-21
lines changed

7 files changed

+257
-21
lines changed

archivist/errors.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
"""
66

77
import json
8+
import logging
89
from typing import Optional
910

1011
from .constants import HEADERS_RETRY_AFTER
1112
from .headers import _headers_get
1213

14+
LOGGER = logging.getLogger(__name__)
15+
1316

1417
class ArchivistError(Exception):
1518
"""Base exception for archivist package"""
@@ -90,21 +93,31 @@ class Archivist5xxError(ArchivistError):
9093
def __identity(response):
9194
identity = "unknown"
9295
if response.request:
96+
LOGGER.debug("Request %s", response.request)
9397
req = response.request
9498
body = getattr(req, "body", None)
9599
if body:
96100
# when uploading a file the body attribute is a
97101
# MultiPartEncoder
98102
try:
99103
body = json.loads(body)
100-
except TypeError:
104+
except (TypeError, json.decoder.JSONDecodeError):
101105
pass
102106
else:
103107
identity = body.get("identity", "unknown")
104108

105109
return identity
106110

107111

112+
def __description(response):
113+
status_code = response.status_code
114+
if status_code == 404:
115+
return f"{__identity(response)} not found ({status_code})"
116+
117+
text = response.text or ""
118+
return f"{text} ({status_code})"
119+
120+
108121
def _parse_response(response):
109122
"""Parse REST response
110123
@@ -120,35 +133,33 @@ def _parse_response(response):
120133
"""
121134

122135
status_code = response.status_code
136+
LOGGER.debug("Status %s", status_code)
123137
if status_code < 400:
124138
return None
125139

126-
text = response.text or ""
140+
desc = __description(response)
127141

128142
if status_code == 429:
129143
return ArchivistTooManyRequestsError(
130144
_headers_get(response.headers, HEADERS_RETRY_AFTER),
131-
f"{text} ({status_code})",
145+
desc,
132146
)
133147

134148
if 400 <= status_code < 500:
135149
err, arg = {
136-
400: (ArchivistBadRequestError, f"{text} ({status_code})"),
137-
401: (ArchivistUnauthenticatedError, f"{text} ({status_code})"),
138-
402: (ArchivistPaymentRequiredError, f"{text} ({status_code})"),
139-
403: (ArchivistForbiddenError, f"{text} ({status_code})"),
140-
404: (
141-
ArchivistNotFoundError,
142-
f"{__identity(response)} not found ({status_code})",
143-
),
144-
}.get(status_code, (Archivist4xxError, f"{text} ({status_code})"))
150+
400: (ArchivistBadRequestError, desc),
151+
401: (ArchivistUnauthenticatedError, desc),
152+
402: (ArchivistPaymentRequiredError, desc),
153+
403: (ArchivistForbiddenError, desc),
154+
404: (ArchivistNotFoundError, desc),
155+
}.get(status_code, (Archivist4xxError, desc))
145156
return err(arg)
146157

147158
if 500 <= status_code < 600:
148159
err, arg = {
149-
501: (ArchivistNotImplementedError, f"{text} ({status_code})"),
150-
503: (ArchivistUnavailableError, f"{text} ({status_code})"),
151-
}.get(status_code, (Archivist5xxError, f"{text} ({status_code})"))
160+
501: (ArchivistNotImplementedError, desc),
161+
503: (ArchivistUnavailableError, desc),
162+
}.get(status_code, (Archivist5xxError, desc))
152163
return err(arg)
153164

154-
return ArchivistError(f"{text} ({status_code})")
165+
return ArchivistError(desc)

archivist/sboms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def upload(
9292
9393
"""
9494

95-
LOGGER.debug("Upload SBOM")
95+
LOGGER.debug("Upload SBOM %s", params)
9696

9797
sbom = SBOM(
9898
**self._archivist.post_file(

functests/execapplications.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from uuid import uuid4
1010

1111
from archivist.archivist import Archivist
12+
from archivist.errors import ArchivistUnauthenticatedError
1213
from archivist.logger import set_logger
1314
from archivist.proof_mechanism import ProofMechanism
1415

@@ -171,6 +172,22 @@ def test_appidp_token(self):
171172
)
172173
print("appidp", json_dumps(appidp, indent=4))
173174

175+
def test_appidp_token_404(self):
176+
"""
177+
Test appidp token
178+
"""
179+
application = self.arch.applications.create(
180+
self.display_name,
181+
CUSTOM_CLAIMS,
182+
)
183+
client_id = application["client_id"]
184+
client_secret = "X" + application["credentials"][0]["secret"][1:]
185+
with self.assertRaises(ArchivistUnauthenticatedError):
186+
appidp = self.arch.appidp.token(
187+
client_id,
188+
client_secret,
189+
)
190+
174191
def test_archivist_token(self):
175192
"""
176193
Test archivist with client id/secret

functests/execsboms.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from unittest import TestCase
1010

1111
from archivist.archivist import Archivist
12+
from archivist.errors import ArchivistBadRequestError
1213
from archivist.logger import set_logger
1314
from archivist.timestamp import now_timestamp
1415

@@ -25,6 +26,7 @@
2526
}
2627

2728
TEST_SBOM_PATH = "functests/test_resources/bom.xml"
29+
TEST_SBOM_SPDX_PATH = "functests/test_resources/bom.spdx"
2830
TEST_SBOM_DOWNLOAD_PATH = "functests/test_resources/downloaded_bom.xml"
2931

3032
if "TEST_DEBUG" in environ and environ["TEST_DEBUG"]:
@@ -57,15 +59,15 @@ def tearDown(cls) -> None:
5759
with suppress(FileNotFoundError):
5860
remove(TEST_SBOM_DOWNLOAD_PATH)
5961

60-
def test_sbom_upload_with_public_privacy(self):
62+
def test_sbom_upload_with_private_privacy(self):
6163
"""
6264
Test sbom upload with privacy
6365
"""
6466
now = now_timestamp()
65-
print("Title:", self.title, now)
67+
print("Public Upload Title:", self.title, now)
6668
with open(TEST_SBOM_PATH, "rb") as fd:
6769
metadata = self.arch.sboms.upload(
68-
fd, confirm=True, params={"privacy": "PUBLIC"}
70+
fd, confirm=True, params={"privacy": "PRIVATE"}
6971
)
7072
print("first upload", json_dumps(metadata.dict(), indent=4))
7173
identity = metadata.identity
@@ -78,12 +80,57 @@ def test_sbom_upload_with_public_privacy(self):
7880
msg="Metadata not correct",
7981
)
8082

83+
def test_sbom_upload_with_illegal_privacy(self):
84+
"""
85+
Test sbom upload with privacy
86+
"""
87+
now = now_timestamp()
88+
print("Illegal Upload Title:", self.title, now)
89+
with open(TEST_SBOM_PATH, "rb") as fd:
90+
with self.assertRaises(ArchivistBadRequestError):
91+
metadata = self.arch.sboms.upload(
92+
fd, confirm=True, params={"privacy": "XXXXXX"}
93+
)
94+
95+
def test_sbom_upload_with_spdx(self):
96+
"""
97+
Test sbom upload with spdx
98+
"""
99+
now = now_timestamp()
100+
print("SPDX Upload Title:", self.title, now)
101+
with open(TEST_SBOM_SPDX_PATH, "rb") as fd:
102+
metadata = self.arch.sboms.upload(
103+
fd, confirm=True, params={"sbomType": "spdx-tag"}
104+
)
105+
print("first upload", json_dumps(metadata.dict(), indent=4))
106+
identity = metadata.identity
107+
108+
metadata1 = self.arch.sboms.read(identity)
109+
print("read", json_dumps(metadata1.dict(), indent=4))
110+
self.assertEqual(
111+
metadata,
112+
metadata1,
113+
msg="Metadata not correct",
114+
)
115+
116+
def test_sbom_upload_with_illegal_format(self):
117+
"""
118+
Test sbom upload with illegal format
119+
"""
120+
now = now_timestamp()
121+
print("SPDX Upload Title:", self.title, now)
122+
with open(TEST_SBOM_SPDX_PATH, "rb") as fd:
123+
with self.assertRaises(ArchivistBadRequestError):
124+
metadata = self.arch.sboms.upload(
125+
fd, confirm=True, params={"sbomType": "xxxxxxxx"}
126+
)
127+
81128
def test_sbom_upload_with_confirmation(self):
82129
"""
83130
Test sbom upload with confirmation
84131
"""
85132
now = now_timestamp()
86-
print("Title:", self.title, now)
133+
print("Confirmed Upload Title:", self.title, now)
87134
with open(TEST_SBOM_PATH, "rb") as fd:
88135
metadata = self.arch.sboms.upload(fd, confirm=True)
89136
print("first upload", json_dumps(metadata.dict(), indent=4))
@@ -105,6 +152,35 @@ def test_sbom_upload_with_confirmation(self):
105152
msg="No. of SBOMS should be 1",
106153
)
107154

155+
def test_sbom_upload_with_cyclonedx_xml(self):
156+
"""
157+
Test sbom upload with cyclonedx-xml
158+
"""
159+
now = now_timestamp()
160+
print("CycloneDX-XML Upload Title:", self.title, now)
161+
with open(TEST_SBOM_PATH, "rb") as fd:
162+
metadata = self.arch.sboms.upload(
163+
fd, params={"sbomType": "cyclonedx-xml"}, confirm=True
164+
)
165+
print("first upload", json_dumps(metadata.dict(), indent=4))
166+
identity = metadata.identity
167+
168+
metadata1 = self.arch.sboms.read(identity)
169+
print("read", json_dumps(metadata1.dict(), indent=4))
170+
self.assertEqual(
171+
metadata,
172+
metadata1,
173+
msg="Metadata not correct",
174+
)
175+
176+
sleep(1) # the data may have not reached cogsearch
177+
metadatas = list(self.arch.sboms.list(metadata={"uploaded_since": now}))
178+
self.assertEqual(
179+
len(metadatas),
180+
1,
181+
msg="No. of SBOMS should be 1",
182+
)
183+
108184
def test_sbom_upload_and_download(self):
109185
"""
110186
Test sbom upload and download through the SDK

functests/test_resources/bom.spdx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
SPDXVersion: SPDX-2.2
2+
DataLicense: CC0-1.0
3+
SPDXID: SPDXRef-DOCUMENT
4+
DocumentName: hello
5+
DocumentNamespace: https://swinslow.net/spdx-examples/example1/hello-v3
6+
Creator: Person: Steve Winslow (steve@swinslow.net)
7+
Creator: Tool: github.com/spdx/tools-golang/builder
8+
Creator: Tool: github.com/spdx/tools-golang/idsearcher
9+
Created: 2021-08-26T01:46:00Z
10+
11+
##### Package: hello
12+
13+
PackageName: hello
14+
SPDXID: SPDXRef-Package-hello
15+
PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example1/content
16+
FilesAnalyzed: true
17+
PackageVerificationCode: 9d20237bb72087e87069f96afb41c6ca2fa2a342
18+
PackageVersion: v0.1.0
19+
PackageLicenseConcluded: GPL-3.0-or-later
20+
PackageLicenseInfoFromFiles: GPL-3.0-or-later
21+
PackageLicenseDeclared: GPL-3.0-or-later
22+
PackageCopyrightText: NOASSERTION
23+
24+
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello
25+
26+
FileName: /build/hello
27+
SPDXID: SPDXRef-hello-binary
28+
FileType: BINARY
29+
FileChecksum: SHA1: 20291a81ef065ff891b537b64d4fdccaf6f5ac02
30+
FileChecksum: SHA256: 83a33ff09648bb5fc5272baca88cf2b59fd81ac4cc6817b86998136af368708e
31+
FileChecksum: MD5: 08a12c966d776864cc1eb41fd03c3c3d
32+
LicenseConcluded: GPL-3.0-or-later
33+
LicenseInfoInFile: NOASSERTION
34+
FileCopyrightText: NOASSERTION
35+
36+
FileName: /src/Makefile
37+
SPDXID: SPDXRef-Makefile
38+
FileType: SOURCE
39+
FileChecksum: SHA1: 69a2e85696fff1865c3f0686d6c3824b59915c80
40+
FileChecksum: SHA256: 5da19033ba058e322e21c90e6d6d859c90b1b544e7840859c12cae5da005e79c
41+
FileChecksum: MD5: 559424589a4f3f75fd542810473d8bc1
42+
LicenseConcluded: GPL-3.0-or-later
43+
LicenseInfoInFile: GPL-3.0-or-later
44+
FileCopyrightText: NOASSERTION
45+
46+
FileName: /src/hello.c
47+
SPDXID: SPDXRef-hello-src
48+
FileType: SOURCE
49+
FileChecksum: SHA1: 20862a6d08391d07d09344029533ec644fac6b21
50+
FileChecksum: SHA256: b4e5ca56d1f9110ca94ed0bf4e6d9ac11c2186eb7cd95159c6fdb50e8db5a823
51+
FileChecksum: MD5: 935054fe899ca782e11003bbae5e166c
52+
LicenseConcluded: GPL-3.0-or-later
53+
LicenseInfoInFile: GPL-3.0-or-later
54+
FileCopyrightText: Copyright Contributors to the spdx-examples project.
55+
56+
Relationship: SPDXRef-hello-binary GENERATED_FROM SPDXRef-hello-src
57+
Relationship: SPDXRef-hello-binary GENERATED_FROM SPDXRef-Makefile
58+
Relationship: SPDXRef-Makefile BUILD_TOOL_OF SPDXRef-Package-hello

unittests/testerrors.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,60 @@ class Object:
202202
msg="incorrect error",
203203
)
204204

205+
def test_errors_404_response_body_is_string(self):
206+
"""
207+
Test errors
208+
"""
209+
210+
class Object:
211+
pass
212+
213+
request = Object()
214+
request.body = "xyz"
215+
response = MockResponse(
216+
404,
217+
request=request,
218+
)
219+
error = _parse_response(response)
220+
self.assertIsNotNone(
221+
error,
222+
msg="error should not be None",
223+
)
224+
with self.assertRaises(ArchivistNotFoundError) as ex:
225+
raise error
226+
self.assertEqual(
227+
str(ex.exception),
228+
"unknown not found (404)",
229+
msg="incorrect error",
230+
)
231+
232+
def test_errors_404_response_body_is_None(self):
233+
"""
234+
Test errors
235+
"""
236+
237+
class Object:
238+
pass
239+
240+
request = Object()
241+
request.body = None
242+
response = MockResponse(
243+
404,
244+
request=request,
245+
)
246+
error = _parse_response(response)
247+
self.assertIsNotNone(
248+
error,
249+
msg="error should not be None",
250+
)
251+
with self.assertRaises(ArchivistNotFoundError) as ex:
252+
raise error
253+
self.assertEqual(
254+
str(ex.exception),
255+
"unknown not found (404)",
256+
msg="incorrect error",
257+
)
258+
205259
def test_errors_429(self):
206260
"""
207261
Test errors

0 commit comments

Comments
 (0)