diff --git a/pylxd/models/certificate.py b/pylxd/models/certificate.py index b3dbb62c..48cecd0a 100644 --- a/pylxd/models/certificate.py +++ b/pylxd/models/certificate.py @@ -11,6 +11,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import json +from base64 import b64encode + from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import Encoding @@ -75,6 +78,39 @@ def create( fingerprint = location.split("/")[-1] return cls.get(client, fingerprint) + @classmethod + def create_token( + cls, + client, + name="", + projects=None, + restricted=False, + ): + """Create a new token.""" + data = { + "password": "", + "certificate": "", + "type": "client", + "token": True, + "name": name, + "restricted": restricted, + "projects": projects, + } + response = client.api.certificates.post(json=data) + metadata = response.json()["metadata"]["metadata"] + + # Assemble a token from the returned metadata + token = { + "name": name, + "fingerprint": metadata["fingerprint"], + "addresses": metadata["addresses"], + "secret": metadata["secret"], + } + + # Convert to (compact) JSON and base64 encode it + token = json.dumps(token, separators=(",", ":")) + return b64encode(token.encode()).decode() + @property def api(self): return self.client.api.certificates[self.fingerprint] diff --git a/pylxd/models/tests/test_certificate.py b/pylxd/models/tests/test_certificate.py index 08586ffe..9d7c392c 100644 --- a/pylxd/models/tests/test_certificate.py +++ b/pylxd/models/tests/test_certificate.py @@ -11,6 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import json import os from pylxd import models @@ -46,6 +47,67 @@ def test_create(self): an_certificate.fingerprint, ) + def test_create_token(self): + """A token is returned.""" + self.add_rule( + { + "text": json.dumps( + { + "type": "sync", + "status": "Operation created", + "status_code": 100, + "operation": "/1.0/operations/a1d77f1b-7dfb-44e0-a3a3-1dd18bd5c15a", + "error_code": 0, + "error": "", + "metadata": { + "id": "a1d77f1b-7dfb-44e0-a3a3-1dd18bd5c15a", + "class": "token", + "description": "Executing operation", + "created_at": "2022-06-01T19:22:21.778204449Z", + "updated_at": "2022-06-01T19:22:21.778204449Z", + "status": "Running", + "status_code": 103, + "resources": None, + "metadata": { + "addresses": [ + "127.0.0.1:8443", + "[::1]:8443", + ], + "fingerprint": "eddaa6023f9064f94dd6fadb36c01d9af9de935efff76f4ebada5a2fda4753be", + "request": { + "name": "foo", + "type": "client", + "restricted": True, + "projects": ["default"], + "certificate": "", + "password": "", + "token": True, + }, + "secret": "6efac2f5de066103dc9798414e916996a8ffe3b9818608d4fe3ba175fae618ad", + }, + "may_cancel": True, + "err": "", + "location": "foo", + }, + }, + ), + "method": "POST", + "url": r"^http://pylxd.test/1.0/certificates$", + "headers": { + "location": "/1.0/operations/a1d77f1b-7dfb-44e0-a3a3-1dd18bd5c15a", + }, + }, + ) + + a_token = self.client.certificates.create_token( + name="foo", projects=["default"], restricted=True + ) + + self.assertEqual( + "eyJuYW1lIjoiZm9vIiwiZmluZ2VycHJpbnQiOiJlZGRhYTYwMjNmOTA2NGY5NGRkNmZhZGIzNmMwMWQ5YWY5ZGU5MzVlZmZmNzZmNGViYWRhNWEyZmRhNDc1M2JlIiwiYWRkcmVzc2VzIjpbIjEyNy4wLjAuMTo4NDQzIiwiWzo6MV06ODQ0MyJdLCJzZWNyZXQiOiI2ZWZhYzJmNWRlMDY2MTAzZGM5Nzk4NDE0ZTkxNjk5NmE4ZmZlM2I5ODE4NjA4ZDRmZTNiYTE3NWZhZTYxOGFkIn0=", + a_token, + ) + def test_fetch(self): """A partial object is fully fetched.""" an_certificate = models.Certificate(self.client, fingerprint="an-certificate")