Skip to content

Commit dd81106

Browse files
committed
feat: add support for tokens and system keyring
1 parent 841f805 commit dd81106

File tree

5 files changed

+135
-7
lines changed

5 files changed

+135
-7
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
### Added
13+
14+
- Support authentication with API tokens
15+
- Added `Credentials` class for parsing credentials from environment similarly than in our Go based tooling.
16+
1217
## [2.7.0] - 2025-07-23
1318

1419
### Changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ python setup.py install
4444
More usage examples are available under [docs/]. If there's a specific thing you're interested in,
4545
but are not able to get working, please [contact UpCloud support](https://upcloud.com/contact/).
4646

47+
### Load credentials from environment
48+
49+
If credentials are not provided as parameters, the client will try to load them from environment variables or system keyring.
50+
51+
```python
52+
from upcloud_api import CloudManager
53+
54+
c = CloudManager()
55+
c.get_account()
56+
```
57+
4758
### Defining and creating servers
4859

4960
```python

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ install_requires =
2020
packages =
2121
upcloud_api
2222
upcloud_api.cloud_manager
23+
24+
[options.extras_require]
25+
keyring =
26+
keyring>=23.0

upcloud_api/cloud_manager/__init__.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from upcloud_api.cloud_manager.server_mixin import ServerManager
1111
from upcloud_api.cloud_manager.storage_mixin import StorageManager
1212
from upcloud_api.cloud_manager.tag_mixin import TagManager
13+
from upcloud_api.credentials import Credentials
1314

1415

1516
class CloudManager(
@@ -31,21 +32,19 @@ class CloudManager(
3132

3233
api: API
3334

34-
def __init__(self, username: str, password: str, timeout: int = 60) -> None:
35+
def __init__(
36+
self, username: str = None, password: str = None, timeout: int = 60, token: str = None
37+
) -> None:
3538
"""
3639
Initiates CloudManager that handles all HTTP connections with UpCloud's API.
3740
3841
Optionally determine a timeout for API connections (in seconds). A timeout with the value
3942
`None` means that there is no timeout.
4043
"""
41-
if not username or not password:
42-
raise Exception('Invalid credentials, please provide a username and password')
43-
44-
credentials = f'{username}:{password}'.encode()
45-
encoded_credentials = base64.b64encode(credentials).decode()
44+
credentials = Credentials.parse(username, password, token)
4645

4746
self.api = API(
48-
token=f'Basic {encoded_credentials}',
47+
token=credentials.authorization,
4948
timeout=timeout,
5049
)
5150

upcloud_api/credentials.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import base64
2+
import os
3+
4+
try:
5+
import keyring
6+
except ImportError:
7+
keyring = None
8+
9+
from upcloud_api.errors import UpCloudClientError
10+
11+
ENV_KEY_USERNAME = "UPCLOUD_USERNAME"
12+
ENV_KEY_PASSWORD = "UPCLOUD_PASSWORD" # noqa: S105
13+
ENV_KEY_TOKEN = "UPCLOUD_TOKEN" # noqa: S105
14+
15+
KEYRING_SERVICE_NAME = "UpCloud"
16+
KEYRING_TOKEN_USER = ""
17+
KEYRING_GO_PREFIX = "go-keyring-base64:"
18+
19+
20+
def _parse_keyring_value(value: str) -> str:
21+
if value.startswith(KEYRING_GO_PREFIX):
22+
value = value[len(KEYRING_GO_PREFIX) :]
23+
return base64.b64decode(value).decode()
24+
25+
return value
26+
27+
28+
def _read_keyring_value(username: str) -> str:
29+
if keyring is None:
30+
return None
31+
32+
value = keyring.get_password(KEYRING_SERVICE_NAME, username)
33+
try:
34+
return _parse_keyring_value(value) if value else None
35+
except Exception:
36+
raise UpCloudClientError(
37+
f"Failed to read keyring value for {username}. Ensure that the value saved to the system keyring is correct."
38+
) from None
39+
40+
41+
class Credentials:
42+
"""
43+
Class for handling UpCloud API credentials.
44+
"""
45+
46+
def __init__(self, username: str = None, password: str = None, token: str = None):
47+
"""
48+
Initializes the Credentials object with username, password and/or token. Use `parse` method to read credentials from environment variables or keyring.
49+
"""
50+
self._username = username
51+
self._password = password
52+
self._token = token
53+
54+
@property
55+
def authorization(self) -> str:
56+
"""
57+
Returns the authorization header value based on the provided credentials.
58+
"""
59+
if self._token:
60+
return f"Bearer {self._token}"
61+
62+
credentials = f"{self._username}:{self._password}".encode()
63+
encoded_credentials = base64.b64encode(credentials).decode()
64+
return f"Basic {encoded_credentials}"
65+
66+
@property
67+
def is_defined(self) -> bool:
68+
"""
69+
Checks if the credentials are defined.
70+
"""
71+
return bool(self._username and self._password or self._token)
72+
73+
def _read_from_env(self):
74+
if not self._username:
75+
self._username = os.getenv(ENV_KEY_USERNAME)
76+
if not self._password:
77+
self._password = os.getenv(ENV_KEY_PASSWORD)
78+
if not self._token:
79+
self._token = os.getenv(ENV_KEY_TOKEN)
80+
81+
def _read_from_keyring(self):
82+
if self._username and not self._password:
83+
self._password = _read_keyring_value(self._username)
84+
85+
if self.is_defined:
86+
return
87+
88+
self._token = _read_keyring_value(KEYRING_TOKEN_USER)
89+
90+
@classmethod
91+
def parse(cls, username: str = None, password: str = None, token: str = None):
92+
"""
93+
Parses credentials from the provided parameters, environment variables or the system keyring.
94+
"""
95+
credentials = cls(username, password, token)
96+
if credentials.is_defined:
97+
return credentials
98+
99+
credentials._read_from_env()
100+
if credentials.is_defined:
101+
return credentials
102+
103+
credentials._read_from_keyring()
104+
if credentials.is_defined:
105+
return credentials
106+
107+
raise UpCloudClientError(
108+
f"Credentials not found. These must be set in configuration, via environment variables or in the system keyring ({KEYRING_SERVICE_NAME})"
109+
)

0 commit comments

Comments
 (0)