Skip to content

Commit bc25471

Browse files
committed
Example code still used simple authtoken
Problem: Common usage is now to use client credentials from an application registration. Solution: Changed all example code to use client credentials. Allow auth to be None in Archivist creation Added functest that uses client credentials to publish SBOMS. Added confirmation of publish/withdraw of SBOMS. Signed-off-by: Paul Hewlett <phewlett76@gmail.com>
1 parent 0200fc9 commit bc25471

18 files changed

Lines changed: 527 additions & 92 deletions

archivist/archivist.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class Archivist: # pylint: disable=too-many-instance-attributes
113113
def __init__(
114114
self,
115115
url: str,
116-
auth: Union[str, MachineAuth],
116+
auth: Union[None, str, MachineAuth],
117117
*,
118118
fixtures: Optional[Dict] = None,
119119
verify: bool = True,
@@ -189,6 +189,7 @@ def auth(self) -> str:
189189
apptoken = self.appidp.token(self._client_id, self._client_secret) # type: ignore
190190
self._auth = apptoken["access_token"]
191191
self._expires_at = time() + apptoken["expires_in"] - 10 # fudge factor
192+
LOGGER.debug("Refresh token")
192193

193194
return self._auth # type: ignore
194195

@@ -218,7 +219,10 @@ def __add_headers(self, headers: Optional[Dict]) -> Dict:
218219
newheaders = self.headers
219220

220221
auth = self.auth # this may trigger a refetch so only do it once here
221-
newheaders["authorization"] = "Bearer " + auth.strip()
222+
# for appidp endpoint there may not be an authtoken
223+
if auth is not None:
224+
newheaders["authorization"] = "Bearer " + auth.strip()
225+
222226
return newheaders
223227

224228
@retry_429
@@ -336,7 +340,6 @@ def post(
336340
headers = self.__add_headers(headers)
337341
data = json.dumps(request) if request else None
338342

339-
LOGGER.debug("POST data %s", data)
340343
response = self._session.post(
341344
url,
342345
data=data,

archivist/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ class ArchivistUnconfirmedError(ArchivistError):
2323
"""asset or event failed to confirm after fixed timeout"""
2424

2525

26+
class ArchivistUnpublishedError(ArchivistError):
27+
"""Sbom failed to publish after fixed timeout"""
28+
29+
30+
class ArchivistUnwithdrawnError(ArchivistError):
31+
"""Sbom failed to be withdrawn after fixed timeout"""
32+
33+
2634
class ArchivistIllegalArgumentError(ArchivistError):
2735
"""Optional keyword arguments are inconsistent"""
2836

archivist/publisher.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""publisher interface
2+
3+
Wrap base methods with constants for assets (path, etc...
4+
"""
5+
6+
import logging
7+
8+
from typing import overload
9+
10+
import backoff
11+
12+
from .errors import ArchivistUnpublishedError
13+
14+
15+
# pylint:disable=unused-import # To prevent cyclical import errors forward referencing is used
16+
# pylint:disable=cyclic-import # but pylint doesn't understand this feature
17+
from . import sboms
18+
19+
MAX_TIME = 1200
20+
21+
LOGGER = logging.getLogger(__name__)
22+
23+
24+
def __lookup_max_time():
25+
return MAX_TIME
26+
27+
28+
# pylint: disable=consider-using-f-string
29+
def __backoff_handler(details):
30+
LOGGER.debug("MAX_TIME %s", MAX_TIME)
31+
LOGGER.debug(
32+
"Backing off {wait:0.1f} seconds afters {tries} tries "
33+
"calling function {target} with args {args} and kwargs "
34+
"{kwargs}".format(**details)
35+
)
36+
37+
38+
def __on_giveup_publication(details):
39+
identity = details["args"][1]
40+
elapsed = details["elapsed"]
41+
raise ArchivistUnpublishedError(
42+
f"publication for {identity} timed out after {elapsed} seconds"
43+
)
44+
45+
46+
# These overloads are used for type hinting, if self is sboms client then
47+
# an SBOM metadata will be returned.
48+
# Overloads are evaluated at startup but not at runtime, therefore
49+
# no test coverage be done directly.
50+
51+
52+
@overload
53+
def _wait_for_publication(
54+
self: "sboms._SbomsClient", identity: str
55+
) -> "sbommetadata.SBOM":
56+
... # pragma: no cover
57+
58+
59+
@backoff.on_predicate(
60+
backoff.expo,
61+
logger=LOGGER,
62+
max_time=__lookup_max_time,
63+
on_backoff=__backoff_handler,
64+
on_giveup=__on_giveup_publication,
65+
)
66+
def _wait_for_publication(self, identity):
67+
"""docstring"""
68+
entity = self.read(identity)
69+
70+
if entity.published_date:
71+
return entity
72+
73+
return None

archivist/sboms.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
SBOMS_WITHDRAW,
4444
SBOMS_PUBLISH,
4545
)
46+
from . import publisher, withdrawer
4647
from .dictmerge import _deepmerge
4748
from .sbommetadata import SBOM
4849

@@ -171,44 +172,86 @@ def list(
171172
)
172173
)
173174

174-
def publish(self, identity: str) -> SBOM:
175+
def publish(self, identity: str, confirm: bool = False) -> SBOM:
175176
"""Publish SBOMt
176177
177178
Makes an SBOM public.
178179
179180
Args:
180181
identity (str): identity of SBOM
182+
confirm (bool): if True wait for sbom to be published.
181183
182184
Returns:
183185
:class:`SBOM` instance
184186
185187
"""
186188
LOGGER.debug("Publish SBOM %s", identity)
187-
return SBOM(
189+
sbom = SBOM(
188190
**self._archivist.post(
189191
f"{SBOMS_SUBPATH}/{identity}",
190192
None,
191193
verb=SBOMS_PUBLISH,
192194
)
193195
)
196+
if not confirm:
197+
return sbom
198+
199+
return self.wait_for_publication(sbom.identity)
200+
201+
def wait_for_publication(self, identity: str) -> SBOM:
202+
"""Wait for sbom to be published.
203+
204+
Waits for sbom to be published.
205+
206+
Args:
207+
identity (str): identity of sbom
208+
209+
Returns:
210+
True if sbom is confirmed.
211+
212+
"""
213+
publisher.MAX_TIME = self._archivist.max_time
214+
# pylint: disable=protected-access
215+
return publisher._wait_for_publication(self, identity)
194216

195-
def withdraw(self, identity: str) -> SBOM:
217+
def withdraw(self, identity: str, confirm: bool = False) -> SBOM:
196218
"""Withdraw SBOM
197219
198220
Withdraws an SBOM.
199221
200222
Args:
201223
identity (str): identity of SBOM
224+
confirm (bool): if True wait for sbom to be withdrawn.
202225
203226
Returns:
204227
:class:`SBOM` instance
205228
206229
"""
207230
LOGGER.debug("Withdraw SBOM %s", identity)
208-
return SBOM(
231+
sbom = SBOM(
209232
**self._archivist.post(
210233
f"{SBOMS_SUBPATH}/{identity}",
211234
None,
212235
verb=SBOMS_WITHDRAW,
213236
)
214237
)
238+
if not confirm:
239+
return sbom
240+
241+
return self.wait_for_withdrawn(sbom.identity)
242+
243+
def wait_for_withdrawn(self, identity: str) -> SBOM:
244+
"""Wait for sbom to be withdrawn.
245+
246+
Waits for sbom to be withdrawn.
247+
248+
Args:
249+
identity (str): identity of sbom
250+
251+
Returns:
252+
True if sbom is confirmed.
253+
254+
"""
255+
withdrawer.MAX_TIME = self._archivist.max_time
256+
# pylint: disable=protected-access
257+
return withdrawer._wait_for_withdrawn(self, identity)

archivist/withdrawer.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""withdrawer interface
2+
3+
Wrap base methods with constants for assets (path, etc...
4+
"""
5+
6+
import logging
7+
8+
from typing import overload
9+
10+
import backoff
11+
12+
from .errors import ArchivistUnwithdrawnError
13+
14+
15+
# pylint:disable=unused-import # To prevent cyclical import errors forward referencing is used
16+
# pylint:disable=cyclic-import # but pylint doesn't understand this feature
17+
from . import sboms
18+
19+
MAX_TIME = 1200
20+
21+
LOGGER = logging.getLogger(__name__)
22+
23+
24+
def __lookup_max_time():
25+
return MAX_TIME
26+
27+
28+
# pylint: disable=consider-using-f-string
29+
def __backoff_handler(details):
30+
LOGGER.debug("MAX_TIME %s", MAX_TIME)
31+
LOGGER.debug(
32+
"Backing off {wait:0.1f} seconds afters {tries} tries "
33+
"calling function {target} with args {args} and kwargs "
34+
"{kwargs}".format(**details)
35+
)
36+
37+
38+
def __on_giveup_withdrawn(details):
39+
identity = details["args"][1]
40+
elapsed = details["elapsed"]
41+
raise ArchivistUnwithdrawnError(
42+
f"withdrawn for {identity} timed out after {elapsed} seconds"
43+
)
44+
45+
46+
# These overloads are used for type hinting, if self is sboms client then
47+
# an SBOM metadata will be returned.
48+
# Overloads are evaluated at startup but not at runtime, therefore
49+
# no test coverage be done directly.
50+
51+
52+
@overload
53+
def _wait_for_publication(
54+
self: "sboms._SbomsClient", identity: str
55+
) -> "sbommetadata.SBOM":
56+
... # pragma: no cover
57+
58+
59+
@backoff.on_predicate(
60+
backoff.expo,
61+
logger=LOGGER,
62+
max_time=__lookup_max_time,
63+
on_backoff=__backoff_handler,
64+
on_giveup=__on_giveup_withdrawn,
65+
)
66+
def _wait_for_withdrawn(self, identity):
67+
"""docstring"""
68+
entity = self.read(identity)
69+
70+
if entity.withdrawn_date:
71+
return entity
72+
73+
return None

examples/access_policies_filter.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
1-
"""Filter IAM access_policies of a archivist connection given url to Archivist and user Token.
1+
"""Filter IAM access_policies of a archivist connection given url to Archivist
2+
and user Token.
23
3-
Main function parses in a url to the Archivist and a token, which is a user authorization.
4-
The main function would initialize an archivist connection using the url and
5-
the token, called "arch", then call arch.access_policies.list() with suitable properties and
6-
attributes.
4+
Main function parses in a url to the Archivist and client credentials , which is
5+
a user authorization. The main function would initialize an archivist connection
6+
using the url and the credentials, called "arch", then call arch.access_policies.list()
7+
with suitable properties and attributes.
78
89
"""
10+
from os import getenv
911

1012
from archivist.archivist import Archivist
1113

1214

1315
def main():
1416
"""Main function of filtering access_policies."""
15-
with open(".auth_token", mode="r", encoding="utf-8") as tokenfile:
16-
authtoken = tokenfile.read().strip()
17+
18+
# client id and client secret is obtained from the appidp endpoint - see the
19+
# application registrations example code in examples/applications_registration.py
20+
#
21+
# client id is an environment variable. client_scret is stored in a file in a
22+
# directory that has 0700 permissions. The location of this file is set in
23+
# the client_secret_file environment variable.
24+
client_id = getenv("ARCHIVIST_CLIENT_ID")
25+
client_secret_file = getenv("ARCHIVIST_CLIENT_SECRET_FILE")
26+
with open(client_secret_file, mode="r", encoding="utf-8") as tokenfile:
27+
client_secret = tokenfile.read().strip()
1728

1829
# Initialize connection to Archivist
1930
arch = Archivist(
2031
"https://app.rkvst.io",
21-
authtoken,
32+
(client_id, client_secret),
2233
)
2334

2435
# count access_policies...

examples/access_policy_create.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""Create a IAM access_policy given url to Archivist and user Token.
22
33
Main function parses in
4-
a url to the Archivist and a token, which is a user authorization.
4+
a url to the Archivist and client credentials, which is a user authorization.
55
The main function would initialize an archivist connection using the url and
6-
the token, called "arch", then call arch.access_policys.create() and the access_policy
6+
the credentials, called "arch", then call arch.access_policies.create() and the access_policy
77
will be created.
88
"""
99

10+
from os import getenv
11+
1012
from archivist.archivist import Archivist
1113

1214

@@ -17,12 +19,20 @@ def main():
1719
create an example archivist connection and create an asset.
1820
1921
"""
20-
with open(".auth_token", mode="r", encoding="utf-8") as tokenfile:
21-
authtoken = tokenfile.read().strip()
22+
# client id and client secret is obtained from the appidp endpoint - see the
23+
# application registrations example code in examples/applications_registration.py
24+
#
25+
# client id is an environment variable. client_scret is stored in a file in a
26+
# directory that has 0700 permissions. The location of this file is set in
27+
# the client_secret_file environment variable.
28+
client_id = getenv("ARCHIVIST_CLIENT_ID")
29+
client_secret_file = getenv("ARCHIVIST_CLIENT_SECRET_FILE")
30+
with open(client_secret_file, mode="r", encoding="utf-8") as tokenfile:
31+
client_secret = tokenfile.read().strip()
2232

2333
arch = Archivist(
2434
"https://app.rkvst.io",
25-
authtoken,
35+
(client_id, client_secret),
2636
)
2737

2838
props = {

0 commit comments

Comments
 (0)