Skip to content

Commit 5e283dc

Browse files
authored
Merge pull request #27 from roaldnefs/add-privkey-auth
2 parents 7771f01 + 3bccf8c commit 5e283dc

9 files changed

Lines changed: 473 additions & 57 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
```python
1313
>>> import transip
1414
# Initialize a TransIP API client
15-
>>> client = transip.TransIP(access_token="TOKEN")
15+
>>> client = transip.TransIP(
16+
... login="demouser",
17+
... private_key_file="/path/to/private.key"
18+
... )
1619
# Retrieve a list of VPSs
1720
>>> for vps in client.vpss.list():
1821
... print(vps)

docs/index.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ Release v\ |version|. (:ref:`Installation <install>`)
2929

3030
>>> import transip
3131
# Initialize a TransIP API client
32-
>>> client = transip.TransIP(access_token="TOKEN")
32+
>>> client = transip.TransIP(
33+
... login="demouser",
34+
... private_key_file="/path/to/private.key"
35+
... )
3336
# Retrieve a list of VPSs
3437
>>> for vps in client.vpss.list():
3538
... print(vps)

docs/user/quickstart.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ First, make sure that:
99

1010
* python-transip is :ref:`installed <install>`
1111

12+
Then you should be able to import the module::
13+
14+
>>> import transip
15+
1216
Below you'll find some simple example to get started.
1317

1418
Authentication
@@ -17,7 +21,20 @@ Authentication
1721
In order to make requests to the TransIP API we need to authenticate yourself
1822
using an access token. To get an access token, you should first login to the
1923
TransIP control panel. You can then generate a new token which will only be
20-
valid for limited time.
24+
valid for limited time or generate a private key to allow python-transip to
25+
request a access token on initialization.
26+
27+
Example of authentication using a private key::
28+
29+
>>> PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
30+
... ...
31+
... -----END RSA PRIVATE KEY-----"""
32+
>>> client = transip.TransIP(login="demouser", private_key=PRIVATE_KEY)
33+
>>> client = transip.TransIP(login="demouser", private_key_file='/path/to/private.key')
34+
35+
Example authentication using an access token::
36+
37+
>>> client = transip.TransIP(access_token="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImN3MiFSbDU2eDNoUnkjelM4YmdOIn0.eyJpc3MiOiJhcGkudHJhbnNpcC5ubCIsImF1ZCI6ImFwaS50cmFuc2lwLm5sIiwianRpIjoiY3cyIVJsNTZ4M2hSeSN6UzhiZ04iLCJpYXQiOjE1ODIyMDE1NTAsIm5iZiI6MTU4MjIwMTU1MCwiZXhwIjoyMTE4NzQ1NTUwLCJjaWQiOiI2MDQ0OSIsInJvIjpmYWxzZSwiZ2siOmZhbHNlLCJrdiI6dHJ1ZX0.fYBWV4O5WPXxGuWG-vcrFWqmRHBm9yp0PHiYh_oAWxWxCaZX2Rf6WJfc13AxEeZ67-lY0TA2kSaOCp0PggBb_MGj73t4cH8gdwDJzANVxkiPL1Saqiw2NgZ3IHASJnisUWNnZp8HnrhLLe5ficvb1D9WOUOItmFC2ZgfGObNhlL2y-AMNLT4X7oNgrNTGm-mespo0jD_qH9dK5_evSzS3K8o03gu6p19jxfsnIh8TIVRvNdluYC2wo4qDl5EW5BEZ8OSuJ121ncOT1oRpzXB0cVZ9e5_UVAEr9X3f26_Eomg52-PjrgcRJ_jPIUYbrlo06KjjX2h0fzMr21ZE023Gw")
2138

2239
TransIP also provide a **demo token** to authenticate yourself as the TransIP
2340
demo user in test mode::

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def get_long_description() -> str:
2929
url="https://github.com/roaldnefs/python-transip",
3030
packages=find_packages(),
3131
include_package_data=True,
32-
install_requires=["requests>=2.25.1"],
32+
install_requires=["cryptography>=3.3.1", "requests>=2.25.1"],
3333
python_requires=">=3.6.12",
3434
entry_points={},
3535
classifiers=[

tests/fixtures/auth.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[
2+
{
3+
"method": "POST",
4+
"url": "https://api.transip.nl/v6/auth",
5+
"json": {
6+
"token": "ACCESS_TOKEN"
7+
},
8+
"status": 200,
9+
"content_type": "application/json"
10+
}
11+
]

tests/test_transip.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,55 @@
1818
# along with python-transip. If not, see <https://www.gnu.org/licenses/>.
1919

2020
import unittest
21+
import responses # type: ignore
2122

2223
from transip import TransIP
24+
from tests.utils import load_responses_fixtures
2325

2426

2527
class TransIPTest(unittest.TestCase):
2628
"""Test the TransIP client class."""
2729

2830
client: TransIP
31+
privkey: str
2932

3033
@classmethod
31-
def setUpClass(self) -> None:
32-
"""Set up a minimal TransIP client."""
33-
self.client = TransIP(access_token='ACCESS_TOKEN')
34+
def setUpClass(cls) -> None:
35+
"""Set up a minimal TransIP client and RSA private key."""
36+
cls.client = TransIP(access_token='ACCESS_TOKEN')
37+
cls.privkey: str = ( # type: ignore
38+
"-----BEGIN RSA PRIVATE KEY-----\n"
39+
"MIIEpAIBAAKCAQEAsUSEHsMuB380OUZQWDyyND4q8lEuJAgNnMkO8s5NGwzP8XSi\n"
40+
"2DdFglLGLe9kjpADs3XqZFsk8ZFFn7x0idFydGyh9tbJ2WkR9E+kNUJV5iQDzPOB\n"
41+
"wvyygEREqnl/o1h3c1q8tD2HZKBcjChn9JbMzdWwAaIs3ppcGWrEI0jZFFfSAyIZ\n"
42+
"GkC3k3umOykWIKflQcT/soAfdqW+2P9/KD/wb3AZCer2i6B2hiITiDbHh5q84Hgk\n"
43+
"D/Zg1M4yrYDyxDeGkAJHkGKNaE0tgUPoz3XTGP7uFYIx00qJyhmnzQcyV/Xcw3ZQ\n"
44+
"7DFUj1HQ5wG/kEF9a4F1+AAiO5C5QbGTFYSwBwIDAQABAoIBABbtIZlI7P8TOJHf\n"
45+
"wixnTTTshWlpjmoikIAikMheXiKNeadkylrkaxz7z53JRFwbzB69tV7dWt3TSAns\n"
46+
"ubXJXOAp3JisFtcDe8r5MeeheLKXHda396RcQknMioTxycw6eNh2d8ln28br5oxJ\n"
47+
"/YfoqPxGEsljTCJOHHM9F7johwrWSQ6f+gmiOkABvIHKgTBLa++v0D+vNrUjM6rx\n"
48+
"IE+dBrx8yIgkF4qSg4Dqnr7D0KqCZUGLZ/3K8ShQUtiQYzyHIWKUId3NUecIQcrT\n"
49+
"2Ri2TITKuER0fa7Mr+3LMSh/3+HtP2AoM34ouxr9H98LFz/UXxuFIRFTx7UVRt4N\n"
50+
"3zqhsEECgYEA+TnXanBJmFz3sNYtlQixtKrh496GB0NheuK4xeNEj9/3gJ6J/rtL\n"
51+
"ZHI7VH8r6aqoqw7sO/WJdxkwZTBOz2fe1QJ5BN0HBI5S6jIBQv9Nfqar0TDvNLB+\n"
52+
"pH6eYJZ/IEFIMObv9YmsPohXpGeXynecrpl8SazEIWLb8IzgLY0HpokCgYEAthX5\n"
53+
"1th4Re0P9rzXp21bbEwcvOKcg5dcpSaTtA1eQEILl6qqT3FP7w8/Ed7NRRY9Gcs+\n"
54+
"inAc96YRNAgIGgfT3R1BmxOMWfdFBT1zlCheS6egKKLzVPzKPiMoMP4zu4hy6uH5\n"
55+
"YVqpDLu0YQu1J2L0VYdZ9xAC0//Rx8KRcs6m/g8CgYEA41VDja+HMhf7R67WPU+E\n"
56+
"6YvGKRjdoNpxnKoaaUd5TtO46/WxYk5t4t3gCJ9H6wjkecRO8BJ0pdKwNlzuRno0\n"
57+
"5JAw26LRt/Iq571dMUO36IMXzuWYDLPBkUJ+LRSaOU3TD+hXkd1W5GNxrmFgMCsT\n"
58+
"HKCcooeZD+shPDcEdghipiECgYARBTDbYlSrxKMPX0uRPOmkz+CHz27t5gIk9dws\n"
59+
"omtC+ml2/d75mg/surIci4UIhjGj7Zmk+yHaDE3jXTTUqhKlwoxVYJhn+HMdMEdT\n"
60+
"fAqEa+DOq5yvPwnwkPy6x6gySWjkh8b10LGonQsZXyzJx7grHoHMVFTPWERVtdw+\n"
61+
"rQ5zBQKBgQC0Iwx4eeQYx60OCZpioNEQ3QPaFgqoWYEexmcMlpgQ9ycdnHx3SkE8\n"
62+
"SlMokcPIEJDhdF3632kIAHOOJeA4Tmshf+ol/O2U2PDgbJZL6W6FJlT28sZVUU8j\n"
63+
"IjFmiAiW6IIEqkJxuR1diAjppEiMmSkjPavo7oQs0TZMMUkli1N9dw==\n"
64+
"-----END RSA PRIVATE KEY-----"
65+
)
66+
67+
def setUp(self) -> None:
68+
"""Setup mocked responses for the '/auth' endpoint."""
69+
load_responses_fixtures("auth.json")
3470

3571
def test_base_url(self) -> None:
3672
"""Test the base URL of the API."""
@@ -43,3 +79,19 @@ def test_authorization_header(self) -> None:
4379
auth_header: str = self.client.headers["Authorization"]
4480

4581
assert auth_header == "Bearer ACCESS_TOKEN"
82+
83+
@responses.activate
84+
def test_private_key_authorization_header(self) -> None:
85+
"""
86+
Test if TransIP instances initialized with a private key, results in
87+
the 'Authorization' header containing the access token returned by the
88+
mocked response.
89+
"""
90+
client: TransIP = TransIP(login="testuser", private_key=self.privkey)
91+
auth_header: str = self.client.headers["Authorization"]
92+
93+
# Assert a single request is made to the mocked TransIP API
94+
self.assertEqual(len(responses.calls), 1)
95+
# Assert the 'Authorization' header contains the access token returned
96+
# by the mocked response
97+
self.assertEqual(auth_header, "Bearer ACCESS_TOKEN")

tests/test_utils.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2021 Roald Nefs <info@roaldnefs.com>
4+
#
5+
# This file is part of python-transip.
6+
#
7+
# python-transip is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser General Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# python-transip is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public License
18+
# along with python-transip. If not, see <https://www.gnu.org/licenses/>.
19+
20+
import unittest
21+
import string
22+
23+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
24+
25+
from transip.utils import (
26+
load_rsa_private_key, generate_message_signature, generate_nonce
27+
)
28+
29+
30+
class UtilsTest(unittest.TestCase):
31+
"""Test the transip.utils functions."""
32+
33+
privkey: str
34+
35+
@classmethod
36+
def setUpClass(cls) -> None:
37+
cls.privkey: str = ( # type: ignore
38+
"-----BEGIN RSA PRIVATE KEY-----\n"
39+
"MIIEpAIBAAKCAQEAsUSEHsMuB380OUZQWDyyND4q8lEuJAgNnMkO8s5NGwzP8XSi\n"
40+
"2DdFglLGLe9kjpADs3XqZFsk8ZFFn7x0idFydGyh9tbJ2WkR9E+kNUJV5iQDzPOB\n"
41+
"wvyygEREqnl/o1h3c1q8tD2HZKBcjChn9JbMzdWwAaIs3ppcGWrEI0jZFFfSAyIZ\n"
42+
"GkC3k3umOykWIKflQcT/soAfdqW+2P9/KD/wb3AZCer2i6B2hiITiDbHh5q84Hgk\n"
43+
"D/Zg1M4yrYDyxDeGkAJHkGKNaE0tgUPoz3XTGP7uFYIx00qJyhmnzQcyV/Xcw3ZQ\n"
44+
"7DFUj1HQ5wG/kEF9a4F1+AAiO5C5QbGTFYSwBwIDAQABAoIBABbtIZlI7P8TOJHf\n"
45+
"wixnTTTshWlpjmoikIAikMheXiKNeadkylrkaxz7z53JRFwbzB69tV7dWt3TSAns\n"
46+
"ubXJXOAp3JisFtcDe8r5MeeheLKXHda396RcQknMioTxycw6eNh2d8ln28br5oxJ\n"
47+
"/YfoqPxGEsljTCJOHHM9F7johwrWSQ6f+gmiOkABvIHKgTBLa++v0D+vNrUjM6rx\n"
48+
"IE+dBrx8yIgkF4qSg4Dqnr7D0KqCZUGLZ/3K8ShQUtiQYzyHIWKUId3NUecIQcrT\n"
49+
"2Ri2TITKuER0fa7Mr+3LMSh/3+HtP2AoM34ouxr9H98LFz/UXxuFIRFTx7UVRt4N\n"
50+
"3zqhsEECgYEA+TnXanBJmFz3sNYtlQixtKrh496GB0NheuK4xeNEj9/3gJ6J/rtL\n"
51+
"ZHI7VH8r6aqoqw7sO/WJdxkwZTBOz2fe1QJ5BN0HBI5S6jIBQv9Nfqar0TDvNLB+\n"
52+
"pH6eYJZ/IEFIMObv9YmsPohXpGeXynecrpl8SazEIWLb8IzgLY0HpokCgYEAthX5\n"
53+
"1th4Re0P9rzXp21bbEwcvOKcg5dcpSaTtA1eQEILl6qqT3FP7w8/Ed7NRRY9Gcs+\n"
54+
"inAc96YRNAgIGgfT3R1BmxOMWfdFBT1zlCheS6egKKLzVPzKPiMoMP4zu4hy6uH5\n"
55+
"YVqpDLu0YQu1J2L0VYdZ9xAC0//Rx8KRcs6m/g8CgYEA41VDja+HMhf7R67WPU+E\n"
56+
"6YvGKRjdoNpxnKoaaUd5TtO46/WxYk5t4t3gCJ9H6wjkecRO8BJ0pdKwNlzuRno0\n"
57+
"5JAw26LRt/Iq571dMUO36IMXzuWYDLPBkUJ+LRSaOU3TD+hXkd1W5GNxrmFgMCsT\n"
58+
"HKCcooeZD+shPDcEdghipiECgYARBTDbYlSrxKMPX0uRPOmkz+CHz27t5gIk9dws\n"
59+
"omtC+ml2/d75mg/surIci4UIhjGj7Zmk+yHaDE3jXTTUqhKlwoxVYJhn+HMdMEdT\n"
60+
"fAqEa+DOq5yvPwnwkPy6x6gySWjkh8b10LGonQsZXyzJx7grHoHMVFTPWERVtdw+\n"
61+
"rQ5zBQKBgQC0Iwx4eeQYx60OCZpioNEQ3QPaFgqoWYEexmcMlpgQ9ycdnHx3SkE8\n"
62+
"SlMokcPIEJDhdF3632kIAHOOJeA4Tmshf+ol/O2U2PDgbJZL6W6FJlT28sZVUU8j\n"
63+
"IjFmiAiW6IIEqkJxuR1diAjppEiMmSkjPavo7oQs0TZMMUkli1N9dw==\n"
64+
"-----END RSA PRIVATE KEY-----"
65+
)
66+
67+
def test_load_rsa_private_key(self) -> None:
68+
"""
69+
Test the content of a RSA private key can be converted to a
70+
RSAPrivateKey instance.
71+
"""
72+
self.assertTrue(isinstance(load_rsa_private_key(self.privkey), RSAPrivateKey))
73+
74+
def test_generate_message_signature(self) -> None:
75+
"""
76+
Test message signature generation using the defined RSA private key.
77+
78+
Example message encoded using:
79+
https://8gwifi.org/rsasignverifyfunctions.jsp
80+
"""
81+
message: str = "A message for signing"
82+
encoded: str = ("NFi2v07lhYmyTarOtIfpw50W25ukKWjtqsVzti/Y2RiGKPEzJQtFZ"
83+
"QaYJCFfIn8HfYjdbzOTK5DIFxwL8NCJK3Mb+wxZOkO4NDJC7mVgdO"
84+
"I6VuET4F3Er4ZjO4pkMLSaV6B0Mcm/yj8Wom1lfeRZxItDXPAbkMj"
85+
"47Ywsx7enAEXfrZrYwHy+rWLPN6WWCrCDWAJGu7lz5+YIy7rpLyRx"
86+
"Ff57QkMMJal0VCWyQUx+JBMdoW7rGVN1u+AxRY0yFj+QxWRB1z0JC"
87+
"E0Xmur+gQ+4+rgIEDE6VU2VY0A8+SY7hyRb2JN8yoLAeI+21ODwo5"
88+
"h/x1zw3Bstyzuvzo0QmHp7Mw==")
89+
90+
self.assertTrue(
91+
generate_message_signature(message, self.privkey) == encoded
92+
)
93+
94+
def test_generate_nonce_length(self) -> None:
95+
"""
96+
Test the length of the generated nonce and whether or not an
97+
exception is thrown for invalid lengths.
98+
"""
99+
# Test valid lengths
100+
for length in [1, 2, 32]:
101+
self.assertTrue(len(generate_nonce(length)) == length)
102+
103+
# Test invalid lengths
104+
for length in [0, -1]:
105+
self.assertRaises(ValueError, generate_nonce, length)
106+
107+
def test_generate_nonce_alphabet(self) -> None:
108+
"""
109+
Test if the generated nonce only contains characters from the alphabet.
110+
"""
111+
alphabet: str = 'a'
112+
self.assertTrue(generate_nonce(3, alphabet) == 'aaa')

0 commit comments

Comments
 (0)