From 5e198825d30c8833c5f7583fb0c40e8668de051a Mon Sep 17 00:00:00 2001 From: Caner Derici Date: Mon, 8 Jan 2024 15:30:47 -0700 Subject: [PATCH 1/5] User accounts.yaml as a backup to user provided credentials For both model and controller connections, accounts.yaml is required to have both username and password for connection to be established. This is not always true, for example, juju change-user-password actually removes the password from the accounts.yaml file. --- juju/client/connector.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/juju/client/connector.py b/juju/client/connector.py index e51f610cc..2ffcf0384 100644 --- a/juju/client/connector.py +++ b/juju/client/connector.py @@ -80,6 +80,15 @@ async def connect(self, **kwargs): else: if self._connection: await self._connection.close() + + account = kwargs.pop('account', {}) + # Prioritize the username and password that user provided + # If not enough, try to patch it with info from accounts.yaml + if 'username' not in kwargs and account.get('user'): + kwargs.update(username=account.get('user')) + if 'password' not in kwargs and account.get('password'): + kwargs.update(password=account.get('password')) + self._connection = await Connection.connect(**kwargs) if not self.controller_name: @@ -103,7 +112,7 @@ async def disconnect(self): await self._log_connection.close() self._log_connection = None - async def connect_controller(self, controller_name=None, specified_facades=None): + async def connect_controller(self, controller_name=None, specified_facades=None, **kwargs): """Connect to a controller by name. If the name is empty, it connect to the current controller. """ @@ -118,16 +127,15 @@ async def connect_controller(self, controller_name=None, specified_facades=None) proxy = proxy_from_config(controller.get('proxy-config', None)) - await self.connect( - endpoint=endpoints, - uuid=None, - username=accounts.get('user'), - password=accounts.get('password'), - cacert=controller.get('ca-cert'), - bakery_client=self.bakery_client_for_controller(controller_name), - specified_facades=specified_facades, - proxy=proxy, - ) + kwargs.update(endpoint=endpoints, + uuid=None, + account=accounts, + cacert=controller.get('ca-cert'), + bakery_client=self.bakery_client_for_controller(controller_name), + specified_facades=specified_facades, + proxy=proxy, + ) + await self.connect(**kwargs) self.controller_name = controller_name self.controller_uuid = controller["uuid"] @@ -176,8 +184,7 @@ async def connect_model(self, model_name=None, **kwargs): # JujuData. kwargs.update(endpoint=endpoints, uuid=model_uuid, - username=account.get('user'), - password=account.get('password'), + account=account, cacert=controller.get('ca-cert'), bakery_client=self.bakery_client_for_controller(controller_name), proxy=proxy) From 29901399b078be5b398e569e80f57a3991f4ac2f Mon Sep 17 00:00:00 2001 From: Caner Derici Date: Mon, 8 Jan 2024 15:33:12 -0700 Subject: [PATCH 2/5] Ask for credentials if some missing before connect Fixes #1001 --- juju/client/connector.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/juju/client/connector.py b/juju/client/connector.py index 2ffcf0384..a5998c6a3 100644 --- a/juju/client/connector.py +++ b/juju/client/connector.py @@ -89,6 +89,9 @@ async def connect(self, **kwargs): if 'password' not in kwargs and account.get('password'): kwargs.update(password=account.get('password')) + if not ({'username', 'password'}.issubset(kwargs)): + required = {'username', 'password'}.difference(kwargs) + raise ValueError(f'Some authentication parameters are required : {",".join(required)}') self._connection = await Connection.connect(**kwargs) if not self.controller_name: From f31ef1682cbe9cb9698e0187ede04b35189f13c8 Mon Sep 17 00:00:00 2001 From: Caner Derici Date: Mon, 8 Jan 2024 15:52:38 -0700 Subject: [PATCH 3/5] Update endpoints only if controller model can be accessed Fixes #998 ControllerAPIInfoForModels call requires the uuid of the controller model, and if the user doesn't have at least read access, they won't be able to have that, and the update_endpoint call at the end of a regular connection fails. --- juju/controller.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/juju/controller.py b/juju/controller.py index d1c4607af..b153355b9 100644 --- a/juju/controller.py +++ b/juju/controller.py @@ -139,11 +139,14 @@ async def connect(self, *args, **kwargs): await self.update_endpoints() async def update_endpoints(self): - info = await self.info() - self._connector._connection.endpoints = [ - (e, info.results[0].cacert) - for e in info.results[0].addresses - ] + try: + info = await self.info() + self._connector._connection.endpoints = [ + (e, info.results[0].cacert) + for e in info.results[0].addresses + ] + except errors.JujuPermissionError: + pass async def connect_current(self): """ @@ -281,6 +284,8 @@ async def info(self): """ log.debug('Getting information') uuids = await self.model_uuids() + if 'controller' not in uuids: + raise errors.JujuPermissionError('Requires access to controller model.') controller_facade = client.ControllerFacade.from_connection(self.connection()) params = [client.Entity(tag.model(uuids["controller"]))] return await controller_facade.ControllerAPIInfoForModels(entities=params) From df3ec4276afdeaa79ac2df8811a840873a6a3b03 Mon Sep 17 00:00:00 2001 From: Caner Derici Date: Mon, 8 Jan 2024 15:57:45 -0700 Subject: [PATCH 4/5] Emit a warning in update_endpoints on failure --- juju/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/juju/controller.py b/juju/controller.py index b153355b9..2e46e22ef 100644 --- a/juju/controller.py +++ b/juju/controller.py @@ -146,6 +146,7 @@ async def update_endpoints(self): for e in info.results[0].addresses ] except errors.JujuPermissionError: + log.warning("This user doesn't have at least read access to the controller model, so endpoints are not updated after connection.") pass async def connect_current(self): From c7e4f1333abf885a9efe6e075b7807d6b93a68f9 Mon Sep 17 00:00:00 2001 From: Caner Derici Date: Wed, 10 Jan 2024 13:16:08 -0700 Subject: [PATCH 5/5] Add JujuPermissionError in juju/errors --- juju/errors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/juju/errors.py b/juju/errors.py index 61026dcee..a246aaa9c 100644 --- a/juju/errors.py +++ b/juju/errors.py @@ -82,6 +82,10 @@ class JujuUnitError(JujuError): pass +class JujuPermissionError(JujuError): + pass + + class JujuBackupError(JujuError): pass