From bf662f0c19797b27b86d48af92a99ab522761b9f Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Sun, 7 Jul 2024 08:52:09 -0700 Subject: [PATCH 01/10] ignore venv and pycharm files. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 17f499ee..df10277b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,9 @@ coverage.xml *.pyc # Editors +.idea/ .vscode/ # Docs build site .venv +venv From 92fb28bd020daf831627101461d909c3cf3f7b7e Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Sun, 7 Jul 2024 21:41:58 -0700 Subject: [PATCH 02/10] Work in progress. --- planet/auth.py | 266 +++++++++++-------------------------------- planet/cli/auth.py | 61 ++++++---- planet/cli/cli.py | 22 +++- planet/cli/cmds.py | 6 +- planet/constants.py | 4 +- planet/exceptions.py | 5 - setup.py | 10 +- 7 files changed, 140 insertions(+), 234 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index 38dda64b..3e74dc75 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -1,5 +1,5 @@ # Copyright 2020 Planet Labs, Inc. -# Copyright 2022 Planet Labs PBC. +# Copyright 2022, 2024 Planet Labs PBC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,28 +15,31 @@ """Manage authentication with Planet APIs""" from __future__ import annotations # https://stackoverflow.com/a/33533514 import abc -import json -import logging -import os import pathlib -import stat import typing -from typing import Optional - +import warnings import httpx -import jwt -from . import http -from .constants import ENV_API_KEY, PLANET_BASE_URL, SECRET_FILE_PATH -from .exceptions import AuthException +from .constants import SECRET_FILE_PATH -LOGGER = logging.getLogger(__name__) +from planet_auth import Auth as PLAuth +from planet_auth import PlanetLegacyAuthClientConfig as PLAuth_PlanetLegacyAuthClientConfig +from planet_auth_config import Production as PLAuthConf_Production -BASE_URL = f'{PLANET_BASE_URL}/v0/auth' AuthType = httpx.Auth +# TODO: planet_auth.Auth potentially entirely supersedes this class. +# But, keeping this here for now for interface stability. +# TODO: Add from_oauth_user_browser / no browser / service account? +# Between confidential and non-confidential clients, user clients +# and m2m clients, and clients with and without browsers and rich +# user interaction, there are a wide variety of ways a customer's +# client may need to obtain OAuth tokens. With time limited +# access tokens and the need to manage refresh activity, the auth +# service interaction model is also necessarily different than +# what this Auth class models. class Auth(metaclass=abc.ABCMeta): """Handle authentication information for use with Planet APIs.""" @@ -47,14 +50,19 @@ def from_key(key: str) -> AuthType: Parameters: key: Planet API key """ - auth = APIKeyAuth(key=key) - LOGGER.debug('Auth obtained from api key.') - return auth + # TODO : document preferred new method in the warning. User OAuth flows should be favored + # for user API access. M2M OAuth flows for other use cases. + warnings.warn("Auth.from_key() will be deprecated.", PendingDeprecationWarning) + plauth_config = { + "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + "api_key": key, + } + pl_authlib_context = PLAuth.initialize_from_config_dict(client_config=plauth_config) + return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod def from_file( - filename: Optional[typing.Union[str, - pathlib.Path]] = None) -> AuthType: + filename: typing.Optional[typing.Union[str, pathlib.Path]] = None) -> AuthType: """Create authentication from secret file. The secret file is named `.planet.json` and is stored in the user @@ -65,21 +73,19 @@ def from_file( filename: Alternate path for the planet secret file. """ - filename = filename or SECRET_FILE_PATH - - try: - secrets = _SecretFile(filename).read() - auth = APIKeyAuth.from_dict(secrets) - except FileNotFoundError: - raise AuthException(f'File {filename} does not exist.') - except (KeyError, json.decoder.JSONDecodeError): - raise AuthException(f'File {filename} is not the correct format.') - - LOGGER.debug(f'Auth read from secret file {filename}.') - return auth + # TODO - Document preferred replacement. A token file from PLAuth's Legacy client + # is formatted to be compatible with this library's .planet.json format files. + warnings.warn("Auth.from_file() will be deprecated.", PendingDeprecationWarning) + plauth_config = { + **PLAuthConf_Production.LEGACY_AUTH_AUTHORITY, + "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + } + pl_authlib_context = PLAuth.initialize_from_config_dict(client_config=plauth_config, + token_file=filename or SECRET_FILE_PATH) + return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod - def from_env(variable_name: Optional[str] = None) -> AuthType: + def from_env(variable_name: typing.Optional[str] = None) -> AuthType: """Create authentication from environment variable. Reads the `PL_API_KEY` environment variable @@ -87,21 +93,16 @@ def from_env(variable_name: Optional[str] = None) -> AuthType: Parameters: variable_name: Alternate environment variable. """ - variable_name = variable_name or ENV_API_KEY - api_key = os.getenv(variable_name, '') - try: - auth = APIKeyAuth(api_key) - LOGGER.debug(f'Auth set from environment variable {variable_name}') - except APIKeyAuthException: - raise AuthException( - f'Environment variable {variable_name} either does not exist ' - 'or is empty.') - return auth + # TODO: document all the new ways the env can set things up for OAuth clients. + if variable_name: + warnings.warn("The variable_name parameter has been deprecated from planet.Auth.from_env().", DeprecationWarning) + pl_authlib_context = PLAuth.initialize_from_env() + return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod def from_login(email: str, password: str, - base_url: Optional[str] = None) -> AuthType: + base_url: typing.Optional[str] = None) -> AuthType: """Create authentication from login email and password. Note: To keep your password secure, the use of `getpass` is @@ -113,159 +114,28 @@ def from_login(email: str, base_url: The base URL to use. Defaults to production authentication API base url. """ - cl = AuthClient(base_url=base_url) - auth_data = cl.login(email, password) - - api_key = auth_data['api_key'] - auth = APIKeyAuth(api_key) - LOGGER.debug('Auth set from login email and password') - return auth - - @classmethod - @abc.abstractmethod - def from_dict(cls, data: dict) -> AuthType: - pass - - @property - @abc.abstractmethod - def value(self): - pass - - @abc.abstractmethod - def to_dict(self) -> dict: - pass - - def store(self, - filename: Optional[typing.Union[str, pathlib.Path]] = None): - """Store authentication information in secret file. - - Parameters: - filename: Alternate path for the planet secret file. - """ - filename = filename or SECRET_FILE_PATH - secret_file = _SecretFile(filename) - secret_file.write(self.to_dict()) - - -class AuthClient: - - def __init__(self, base_url: Optional[str] = None): - """ - Parameters: - base_url: The base URL to use. Defaults to production - authentication API base url. - """ - self._base_url = base_url or BASE_URL - if self._base_url.endswith('/'): - self._base_url = self._base_url[:-1] - - def login(self, email: str, password: str) -> dict: - """Login using email identity and credentials. - - Note: To keep your password secure, the use of `getpass` is - recommended. - - Parameters: - email: Planet account email address. - password: Planet account password. - - Returns: - A JSON object containing an `api_key` property with the user's - API_KEY. - """ - url = f'{self._base_url}/login' - data = {'email': email, 'password': password} - - sess = http.AuthSession() - resp = sess.request(url=url, method='POST', json=data) - return self.decode_response(resp) - - @staticmethod - def decode_response(response): - """Decode the token JWT""" - token = response.json()['token'] - return jwt.decode(token, options={'verify_signature': False}) - - -class APIKeyAuthException(Exception): - """exceptions thrown by APIKeyAuth""" - pass - - -class APIKeyAuth(httpx.BasicAuth, Auth): - """Planet API Key authentication.""" - DICT_KEY = 'key' - - def __init__(self, key: str): - """Initialize APIKeyAuth. - - Parameters: - key: API key. - - Raises: - APIKeyException: If API key is None or empty string. - """ - if not key: - raise APIKeyAuthException('API key cannot be empty.') - self._key = key - super().__init__(self._key, '') - - @classmethod - def from_dict(cls, data: dict) -> APIKeyAuth: - """Instantiate APIKeyAuth from a dict.""" - api_key = data[cls.DICT_KEY] - return cls(api_key) - - def to_dict(self): - """Represent APIKeyAuth as a dict.""" - return {self.DICT_KEY: self._key} - - @property - def value(self): - return self._key - - -class _SecretFile: - - def __init__(self, path: typing.Union[str, pathlib.Path]): - self.path = pathlib.Path(path) - - self.permissions = stat.S_IRUSR | stat.S_IWUSR # user rw - - # in sdk versions <=2.0.0, secret file was created with the wrong - # permissions, fix this automatically as well as catching the unlikely - # cases where the permissions get changed externally - self._enforce_permissions() - - def write(self, contents: dict): - try: - secrets_to_write = self.read() - secrets_to_write.update(contents) - except (FileNotFoundError, KeyError, json.decoder.JSONDecodeError): - secrets_to_write = contents - - self._write(secrets_to_write) - - def _write(self, contents: dict): - LOGGER.debug(f'Writing to {self.path}') - - def opener(path, flags): - return os.open(path, flags, self.permissions) - - with open(self.path, 'w', opener=opener) as fp: - fp.write(json.dumps(contents)) - - def read(self) -> dict: - LOGGER.debug(f'Reading from {self.path}') - with open(self.path, 'r') as fp: - contents = json.loads(fp.read()) - return contents - - def _enforce_permissions(self): - """if the file's permissions are not what they should be, fix them""" - if self.path.exists(): - # in octal, permissions is the last three bits of the mode - file_permissions = self.path.stat().st_mode & 0o777 - if file_permissions != self.permissions: - LOGGER.info('Fixing planet secret file permissions.') - self.path.chmod(self.permissions) + # TODO - PLAuth.login will save the credential if the file is set. Handle this case? + # We need to conditionally set the token file in the initialize call. + # TODO - update warning with directions on what to replace this with. (maybe from_user_oauth_login?) + warnings.warn("Auth.from_login will be deprecated.", PendingDeprecationWarning) + plauth_config = { + **PLAuthConf_Production.LEGACY_AUTH_AUTHORITY, + "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + } + if base_url: + plauth_config["legacy_auth_endpoint"] = base_url + + pl_authlib_context = PLAuth.initialize_from_config_dict(client_config=plauth_config) + pl_authlib_context.login(username=email, password=password, allow_tty_prompt=False) + return _PLAuthLibAuth(plauth=pl_authlib_context) + + +class _PLAuthLibAuth(Auth, AuthType): + # PLAuth uses a "has a" AuthType authenticator pattern. + # This library's Auth class employs a "is a" AuthType authenticator design pattern. + # This class smooths over that design difference. + def __init__(self, plauth: PLAuth): + self._plauth = plauth + + def auth_flow(self, r: httpx._models.Request): + return self._plauth.request_authenticator().auth_flow(r) diff --git a/planet/cli/auth.py b/planet/cli/auth.py index 06033669..a4f8b0f7 100644 --- a/planet/cli/auth.py +++ b/planet/cli/auth.py @@ -14,16 +14,29 @@ """Auth API CLI""" import logging import os - +import warnings import click -import planet -from planet.constants import ENV_API_KEY +from planet_auth import Auth as PLAuth +from planet_auth import PlanetLegacyAuthClientConfig as PLAuth_PlanetLegacyAuthClientConfig +from planet_auth import FileBackedPlanetLegacyApiKey as PLAuth_FileBackedPlanetLegacyApiKey +from planet_auth_config import Production as PLAuthConf_Production +import planet_auth_utils.commands.cli.planet_legacy_auth_cmd + +from planet.constants import ENV_API_KEY, SECRET_FILE_PATH from .cmds import translate_exceptions LOGGER = logging.getLogger(__name__) +def _api_key_env_warning(): + if os.getenv(ENV_API_KEY): + click.echo(f'Warning - Environment variable {ENV_API_KEY} already ' + 'exists. To update, with the new value, use the ' + 'following:') + click.echo(f'export {ENV_API_KEY}=$(planet auth value)') + + @click.group() # type: ignore @click.pass_context @click.option('-u', @@ -32,7 +45,19 @@ help='Assign custom base Auth API URL.') def auth(ctx, base_url): """Commands for working with Planet authentication""" - ctx.obj['BASE_URL'] = base_url + # TODO: should we deprecate this whole command in favor of the functionality of the embedded 'plauth'? + # warnings.warn("'auth' command will be deprecated. Please use 'plauth' to manage user credentials.", PendingDeprecationWarning) + + # Override any newer style planet_auth library auth profiles and + # always wire up the legacy auth implementation to the planet library's + # preferred paths. + _plauth_config = { + **PLAuthConf_Production.LEGACY_AUTH_AUTHORITY, + "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + } + if base_url: + _plauth_config["legacy_auth_endpoint"] = base_url + ctx.obj["AUTH"] = PLAuth.initialize_from_config_dict(client_config=_plauth_config, token_file=SECRET_FILE_PATH) @auth.command() # type: ignore @@ -48,34 +73,26 @@ def auth(ctx, base_url): help=('Account password. Will not be saved.')) def init(ctx, email, password): """Obtain and store authentication information""" - base_url = ctx.obj['BASE_URL'] - plauth = planet.Auth.from_login(email, password, base_url=base_url) - plauth.store() + ctx.invoke(planet_auth_utils.commands.cli.planet_legacy_auth_cmd.pllegacy_do_login, username=email, password=password) click.echo('Initialized') - if os.getenv(ENV_API_KEY): - click.echo(f'Warning - Environment variable {ENV_API_KEY} already ' - 'exists. To update, with the new value, use the following:') - click.echo(f'export {ENV_API_KEY}=$(planet auth value)') - + _api_key_env_warning() @auth.command() # type: ignore +@click.pass_context @translate_exceptions -def value(): +def value(ctx): """Print the stored authentication information""" - click.echo(planet.Auth.from_file().value) + ctx.forward(planet_auth_utils.commands.cli.planet_legacy_auth_cmd.do_print_api_key) @auth.command() # type: ignore +@click.pass_context @translate_exceptions @click.argument('key') -def store(key): +def store(ctx, key): """Store authentication information""" - plauth = planet.Auth.from_key(key) + _token_file = PLAuth_FileBackedPlanetLegacyApiKey(api_key=key, api_key_file=ctx.obj["AUTH"].token_file_path()) if click.confirm('This overrides the stored value. Continue?'): - plauth.store() + _token_file.save() click.echo('Updated') - if os.getenv(ENV_API_KEY): - click.echo(f'Warning - Environment variable {ENV_API_KEY} already ' - 'exists. To update, with the new value, use the ' - 'following:') - click.echo(f'export {ENV_API_KEY}=$(planet auth value)') + _api_key_env_warning() diff --git a/planet/cli/cli.py b/planet/cli/cli.py index 862eb489..c4d1355b 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -18,9 +18,15 @@ import click +from planet_auth_utils import embedded_plauth_cmd_group +from planet_auth_utils import initialize_auth_client_context +from planet_auth_utils import opt_auth_profile +from planet_auth_utils import opt_auth_client_config_file +from planet_auth_utils import opt_token_file + import planet -from . import auth, collect, data, orders, subscriptions +from . import auth, cmds, collect, data, orders, subscriptions LOGGER = logging.getLogger(__name__) @@ -36,7 +42,11 @@ default="warning", help=("Optional: set verbosity level to warning, info, or debug.\ Defaults to warning.")) -def main(ctx, verbosity, quiet): +@opt_auth_profile +# @opt_token_file # TODO - support this? Check compatibility with other commands or legacy file? +# @opt_auth_client_config_file # TODO - support this? Limit it to make interface simpler? +@cmds.translate_exceptions +def main(ctx, verbosity, quiet, auth_profile): """Planet SDK for Python CLI""" _configure_logging(verbosity) @@ -44,7 +54,11 @@ def main(ctx, verbosity, quiet): # by means other than the `if` block below) ctx.ensure_object(dict) ctx.obj['QUIET'] = quiet - + ctx.obj['AUTH'] = initialize_auth_client_context( + auth_profile_opt=auth_profile, + token_file_opt=None, # TODO - support arg? token_file_opt=token_file + auth_client_config_file_opt=None, # TODO - support arg? auth_client_config_file_opt=auth_client_config_file + ) def _configure_logging(verbosity): """configure logging via verbosity level, corresponding @@ -73,6 +87,8 @@ def _configure_logging(verbosity): format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +# main.add_command(cmd=embedded_plauth_cmd_group, name="auth") # type: ignore +main.add_command(cmd=embedded_plauth_cmd_group, name="plauth") # type: ignore main.add_command(auth.auth) # type: ignore main.add_command(data.data) # type: ignore main.add_command(orders.orders) # type: ignore diff --git a/planet/cli/cmds.py b/planet/cli/cmds.py index f25cadc8..3cfba616 100644 --- a/planet/cli/cmds.py +++ b/planet/cli/cmds.py @@ -17,6 +17,8 @@ import click +from planet_auth import AuthException as PLAuth_AuthException + from planet import exceptions @@ -55,10 +57,10 @@ def translate_exceptions(func): def wrapper(*args, **kwargs): try: func(*args, **kwargs) - except exceptions.AuthException: + except PLAuth_AuthException: raise click.ClickException( 'Auth information does not exist or is corrupted. Initialize ' - 'with `planet auth init`.') + 'with `planet auth init`.') # TODO: where do we want to steer users now? `planet plauth`? except exceptions.PlanetError as ex: raise click.ClickException(ex) diff --git a/planet/constants.py b/planet/constants.py index c9b1843b..d1508eb6 100644 --- a/planet/constants.py +++ b/planet/constants.py @@ -20,8 +20,8 @@ DATA_DIR = Path(os.path.dirname(__file__)) / 'data' -ENV_API_KEY = 'PL_API_KEY' +ENV_API_KEY = 'PL_API_KEY' # TODO: belongs to plauth lib PLANET_BASE_URL = 'https://api.planet.com' -SECRET_FILE_PATH = Path(os.path.expanduser('~')) / '.planet.json' +SECRET_FILE_PATH = Path(os.path.expanduser('~')) / '.planet.json' # TODO: deprecate diff --git a/planet/exceptions.py b/planet/exceptions.py index eee852bd..1935e65a 100644 --- a/planet/exceptions.py +++ b/planet/exceptions.py @@ -78,11 +78,6 @@ class ClientError(PlanetError): pass -class AuthException(ClientError): - """Exceptions encountered during authentication""" - pass - - class PagingError(ClientError): """For errors that occur during paging.""" pass diff --git a/setup.py b/setup.py index c8a8c29e..dc3f0a29 100644 --- a/setup.py +++ b/setup.py @@ -28,9 +28,11 @@ 'geojson', 'httpx>=0.23.0', 'jsonschema', - 'pyjwt>=2.1', 'tqdm>=4.56', 'typing-extensions', + 'planet-auth>=1.5.2', + 'planet-auth-util>=1.5.1', + 'planet-auth-config>=0.3.2', # FIXME - This currently has internal endpoints, too. ] test_requires = ['pytest', 'anyio', 'pytest-cov', 'respx>=0.20'] @@ -44,6 +46,10 @@ 'mkdocstrings==0.18.1' ] +dev_requires = [ + 'nox' +] + setup( name='planet', version=version, @@ -80,7 +86,7 @@ 'test': test_requires, 'lint': lint_requires, 'docs': doc_requires, - 'dev': test_requires + lint_requires + doc_requires + 'dev': test_requires + lint_requires + doc_requires + dev_requires }, entry_points={ 'console_scripts': [ From 03beb9f9101617725ac973870030218a73d41376 Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Sun, 25 Aug 2024 16:39:58 -0700 Subject: [PATCH 03/10] working on using planet_auth library --- planet/auth.py | 39 +++++++++++++++++++++---------------- planet/cli/auth.py | 33 ++++++++++++++++++------------- planet/cli/cli.py | 31 ++++++++++++++++------------- planet/cli/cmds.py | 5 +++-- planet/cli/data.py | 2 +- planet/cli/orders.py | 2 +- planet/cli/session.py | 4 ++-- planet/cli/subscriptions.py | 2 +- planet/constants.py | 4 +--- setup.py | 4 ++-- 10 files changed, 70 insertions(+), 56 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index 3e74dc75..3df61d5a 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -20,11 +20,10 @@ import warnings import httpx -from .constants import SECRET_FILE_PATH +import planet_auth +import planet_auth_config -from planet_auth import Auth as PLAuth -from planet_auth import PlanetLegacyAuthClientConfig as PLAuth_PlanetLegacyAuthClientConfig -from planet_auth_config import Production as PLAuthConf_Production +from .constants import SECRET_FILE_PATH AuthType = httpx.Auth @@ -54,10 +53,10 @@ def from_key(key: str) -> AuthType: # for user API access. M2M OAuth flows for other use cases. warnings.warn("Auth.from_key() will be deprecated.", PendingDeprecationWarning) plauth_config = { - "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), "api_key": key, } - pl_authlib_context = PLAuth.initialize_from_config_dict(client_config=plauth_config) + pl_authlib_context = planet_auth.Auth.initialize_from_config_dict(client_config=plauth_config) return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod @@ -77,10 +76,10 @@ def from_file( # is formatted to be compatible with this library's .planet.json format files. warnings.warn("Auth.from_file() will be deprecated.", PendingDeprecationWarning) plauth_config = { - **PLAuthConf_Production.LEGACY_AUTH_AUTHORITY, - "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + **planet_auth_config.Production.LEGACY_AUTH_AUTHORITY, + "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), } - pl_authlib_context = PLAuth.initialize_from_config_dict(client_config=plauth_config, + pl_authlib_context = planet_auth.Auth.initialize_from_config_dict(client_config=plauth_config, token_file=filename or SECRET_FILE_PATH) return _PLAuthLibAuth(plauth=pl_authlib_context) @@ -96,7 +95,7 @@ def from_env(variable_name: typing.Optional[str] = None) -> AuthType: # TODO: document all the new ways the env can set things up for OAuth clients. if variable_name: warnings.warn("The variable_name parameter has been deprecated from planet.Auth.from_env().", DeprecationWarning) - pl_authlib_context = PLAuth.initialize_from_env() + pl_authlib_context = planet_auth.Auth.initialize_from_env() return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod @@ -119,22 +118,28 @@ def from_login(email: str, # TODO - update warning with directions on what to replace this with. (maybe from_user_oauth_login?) warnings.warn("Auth.from_login will be deprecated.", PendingDeprecationWarning) plauth_config = { - **PLAuthConf_Production.LEGACY_AUTH_AUTHORITY, - "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + **planet_auth_config.Production.LEGACY_AUTH_AUTHORITY, + "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), } if base_url: plauth_config["legacy_auth_endpoint"] = base_url - pl_authlib_context = PLAuth.initialize_from_config_dict(client_config=plauth_config) + pl_authlib_context = planet_auth.Auth.initialize_from_config_dict(client_config=plauth_config) pl_authlib_context.login(username=email, password=password, allow_tty_prompt=False) return _PLAuthLibAuth(plauth=pl_authlib_context) + @staticmethod + def from_plauth(pl_authlib_context: planet_auth.Auth): + return _PLAuthLibAuth(plauth=pl_authlib_context) + class _PLAuthLibAuth(Auth, AuthType): - # PLAuth uses a "has a" AuthType authenticator pattern. - # This library's Auth class employs a "is a" AuthType authenticator design pattern. - # This class smooths over that design difference. - def __init__(self, plauth: PLAuth): + # The Planet Auth Library uses a "has a" authenticator pattern for its Auth context. + # This library employs a "is a" authenticator design pattern for user's of + # its Auth context obtained from the constructors above. + # This class smooths over that design difference as we move to using + # the Planet Auth Library. + def __init__(self, plauth: planet_auth.Auth): self._plauth = plauth def auth_flow(self, r: httpx._models.Request): diff --git a/planet/cli/auth.py b/planet/cli/auth.py index a4f8b0f7..35980515 100644 --- a/planet/cli/auth.py +++ b/planet/cli/auth.py @@ -17,24 +17,26 @@ import warnings import click -from planet_auth import Auth as PLAuth -from planet_auth import PlanetLegacyAuthClientConfig as PLAuth_PlanetLegacyAuthClientConfig -from planet_auth import FileBackedPlanetLegacyApiKey as PLAuth_FileBackedPlanetLegacyApiKey -from planet_auth_config import Production as PLAuthConf_Production -import planet_auth_utils.commands.cli.planet_legacy_auth_cmd +import planet_auth +import planet_auth_config +import planet_auth_utils.commands.cli.planet_legacy_auth_cmd # FIXME - Should be top level import -from planet.constants import ENV_API_KEY, SECRET_FILE_PATH +from planet.constants import SECRET_FILE_PATH from .cmds import translate_exceptions LOGGER = logging.getLogger(__name__) +# FIXME: this may need to be expanded to cover all env vars in play +# (PLAUTH has no equivalent warning, maybe it should if/when we add +# some sort of select default profile functionality.) +# FIXME: this warning encourages the use of API keys, that I want to deprecate. def _api_key_env_warning(): - if os.getenv(ENV_API_KEY): - click.echo(f'Warning - Environment variable {ENV_API_KEY} already ' + if os.getenv(planet_auth.EnvironmentVariables.AUTH_API_KEY): + click.echo(f'Warning - Environment variable {planet_auth.EnvironmentVariables.AUTH_API_KEY} already ' 'exists. To update, with the new value, use the ' 'following:') - click.echo(f'export {ENV_API_KEY}=$(planet auth value)') + click.echo(f'export {planet_auth.EnvironmentVariables.AUTH_API_KEY}=$(planet auth value)') @click.group() # type: ignore @@ -45,19 +47,22 @@ def _api_key_env_warning(): help='Assign custom base Auth API URL.') def auth(ctx, base_url): """Commands for working with Planet authentication""" - # TODO: should we deprecate this whole command in favor of the functionality of the embedded 'plauth'? + # TODO: should we deprecate this whole command in favor of the functionality of the embedded 'plauth'. + # TODO: plauth does not have, and maybe should, some sort of "select default" function + # so that it need not always have the user provide --auth-profile. This should be built into the plauth lib + # so that we can have nice default construction that follows the user's selection. # warnings.warn("'auth' command will be deprecated. Please use 'plauth' to manage user credentials.", PendingDeprecationWarning) # Override any newer style planet_auth library auth profiles and # always wire up the legacy auth implementation to the planet library's # preferred paths. _plauth_config = { - **PLAuthConf_Production.LEGACY_AUTH_AUTHORITY, - "client_type": PLAuth_PlanetLegacyAuthClientConfig.meta().get("client_type"), + **planet_auth_config.Production.LEGACY_AUTH_AUTHORITY, + "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), } if base_url: _plauth_config["legacy_auth_endpoint"] = base_url - ctx.obj["AUTH"] = PLAuth.initialize_from_config_dict(client_config=_plauth_config, token_file=SECRET_FILE_PATH) + ctx.obj["AUTH"] = planet_auth.Auth.initialize_from_config_dict(client_config=_plauth_config, token_file=SECRET_FILE_PATH) @auth.command() # type: ignore @@ -91,7 +96,7 @@ def value(ctx): @click.argument('key') def store(ctx, key): """Store authentication information""" - _token_file = PLAuth_FileBackedPlanetLegacyApiKey(api_key=key, api_key_file=ctx.obj["AUTH"].token_file_path()) + _token_file = planet_auth.FileBackedPlanetLegacyApiKey(api_key=key, api_key_file=ctx.obj["AUTH"].token_file_path()) if click.confirm('This overrides the stored value. Continue?'): _token_file.save() click.echo('Updated') diff --git a/planet/cli/cli.py b/planet/cli/cli.py index c4d1355b..cf18b380 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -18,12 +18,7 @@ import click -from planet_auth_utils import embedded_plauth_cmd_group -from planet_auth_utils import initialize_auth_client_context -from planet_auth_utils import opt_auth_profile -from planet_auth_utils import opt_auth_client_config_file -from planet_auth_utils import opt_token_file - +import planet_auth_utils import planet from . import auth, cmds, collect, data, orders, subscriptions @@ -42,9 +37,9 @@ default="warning", help=("Optional: set verbosity level to warning, info, or debug.\ Defaults to warning.")) -@opt_auth_profile -# @opt_token_file # TODO - support this? Check compatibility with other commands or legacy file? -# @opt_auth_client_config_file # TODO - support this? Limit it to make interface simpler? +@planet_auth_utils.opt_auth_profile +# @planet_auth_utils.opt_token_file # TODO - support this? Check compatibility with other commands or legacy file? +# @planet_auth_utils.opt_auth_client_config_file # TODO - support this? Limit it to make interface simpler? @cmds.translate_exceptions def main(ctx, verbosity, quiet, auth_profile): """Planet SDK for Python CLI""" @@ -54,12 +49,22 @@ def main(ctx, verbosity, quiet, auth_profile): # by means other than the `if` block below) ctx.ensure_object(dict) ctx.obj['QUIET'] = quiet - ctx.obj['AUTH'] = initialize_auth_client_context( + + _configure_cli_auth_ctx(ctx, auth_profile) + + +def _configure_cli_auth_ctx(ctx, auth_profile): + # planet-auth library Auth type + ctx.obj['AUTH'] = planet_auth_utils.initialize_auth_client_context( auth_profile_opt=auth_profile, token_file_opt=None, # TODO - support arg? token_file_opt=token_file auth_client_config_file_opt=None, # TODO - support arg? auth_client_config_file_opt=auth_client_config_file ) + # planet SDK Auth type + ctx.obj['PLSDK_AUTH'] = planet.Auth.from_plauth(pl_authlib_context=ctx.obj['AUTH']) + + def _configure_logging(verbosity): """configure logging via verbosity level, corresponding to log levels warning, info and debug respectfully. @@ -87,9 +92,9 @@ def _configure_logging(verbosity): format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') -# main.add_command(cmd=embedded_plauth_cmd_group, name="auth") # type: ignore -main.add_command(cmd=embedded_plauth_cmd_group, name="plauth") # type: ignore -main.add_command(auth.auth) # type: ignore +# main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="auth") # type: ignore +main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="plauth") # type: ignore +main.add_command(auth.auth) # type: ignore # TODO: deprecate entirely and use plauth main.add_command(data.data) # type: ignore main.add_command(orders.orders) # type: ignore main.add_command(subscriptions.subscriptions) # type: ignore diff --git a/planet/cli/cmds.py b/planet/cli/cmds.py index 3cfba616..339a0224 100644 --- a/planet/cli/cmds.py +++ b/planet/cli/cmds.py @@ -17,7 +17,7 @@ import click -from planet_auth import AuthException as PLAuth_AuthException +import planet_auth from planet import exceptions @@ -57,8 +57,9 @@ def translate_exceptions(func): def wrapper(*args, **kwargs): try: func(*args, **kwargs) - except PLAuth_AuthException: + except planet_auth.AuthException as pla_ex: raise click.ClickException( + f'{pla_ex}\n' 'Auth information does not exist or is corrupted. Initialize ' 'with `planet auth init`.') # TODO: where do we want to steer users now? `planet plauth`? except exceptions.PlanetError as ex: diff --git a/planet/cli/data.py b/planet/cli/data.py index 916598ea..05a816e5 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -44,7 +44,7 @@ @asynccontextmanager async def data_client(ctx): - async with CliSession() as sess: + async with CliSession(ctx) as sess: cl = DataClient(sess, base_url=ctx.obj['BASE_URL']) yield cl diff --git a/planet/cli/orders.py b/planet/cli/orders.py index db7734ec..b1e484f9 100644 --- a/planet/cli/orders.py +++ b/planet/cli/orders.py @@ -33,7 +33,7 @@ @asynccontextmanager async def orders_client(ctx): base_url = ctx.obj['BASE_URL'] - async with CliSession() as sess: + async with CliSession(ctx) as sess: cl = OrdersClient(sess, base_url=base_url) yield cl diff --git a/planet/cli/session.py b/planet/cli/session.py index a3b28b4d..58b699a4 100644 --- a/planet/cli/session.py +++ b/planet/cli/session.py @@ -7,6 +7,6 @@ class CliSession(Session): """Session with CLI-specific auth and identifying header""" - def __init__(self): - super().__init__(Auth.from_file()) + def __init__(self, ctx): + super().__init__(ctx.obj['PLSDK_AUTH']) self._client.headers.update({'X-Planet-App': 'python-cli'}) diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py index 8461e2b7..43619230 100644 --- a/planet/cli/subscriptions.py +++ b/planet/cli/subscriptions.py @@ -43,7 +43,7 @@ def check_item_type(ctx, param, item_type) -> Optional[List[dict]]: @asynccontextmanager async def subscriptions_client(ctx): - async with CliSession() as sess: + async with CliSession(ctx) as sess: cl = SubscriptionsClient(sess, base_url=ctx.obj['BASE_URL']) yield cl diff --git a/planet/constants.py b/planet/constants.py index d1508eb6..d6610942 100644 --- a/planet/constants.py +++ b/planet/constants.py @@ -20,8 +20,6 @@ DATA_DIR = Path(os.path.dirname(__file__)) / 'data' -ENV_API_KEY = 'PL_API_KEY' # TODO: belongs to plauth lib - PLANET_BASE_URL = 'https://api.planet.com' -SECRET_FILE_PATH = Path(os.path.expanduser('~')) / '.planet.json' # TODO: deprecate +SECRET_FILE_PATH = Path(os.path.expanduser('~')) / '.planet.json' # TODO: deprecate in favor of planet_auth library diff --git a/setup.py b/setup.py index dc3f0a29..a29bf71b 100644 --- a/setup.py +++ b/setup.py @@ -30,9 +30,9 @@ 'jsonschema', 'tqdm>=4.56', 'typing-extensions', - 'planet-auth>=1.5.2', + 'planet-auth>=2.0.0', 'planet-auth-util>=1.5.1', - 'planet-auth-config>=0.3.2', # FIXME - This currently has internal endpoints, too. + 'planet-auth-config>=2.0.0', # FIXME - This currently has staging endpoints, too. ] test_requires = ['pytest', 'anyio', 'pytest-cov', 'respx>=0.20'] From 25c2583a44b8b63eebfaadb6a326af0c7ab54e45 Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Mon, 26 Aug 2024 13:09:39 -0700 Subject: [PATCH 04/10] update requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a29bf71b..42cc564c 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ 'tqdm>=4.56', 'typing-extensions', 'planet-auth>=2.0.0', - 'planet-auth-util>=1.5.1', + 'planet-auth-util>=2.0.0', 'planet-auth-config>=2.0.0', # FIXME - This currently has staging endpoints, too. ] From 971bac13104da0d7648493ed05112d792893e6a4 Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Tue, 27 Aug 2024 09:00:01 -0700 Subject: [PATCH 05/10] WIP --- Branch-Working-and-Release-Notes.txt | 11 +++++++++++ planet/cli/cli.py | 1 + planet/cli/cmds.py | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 Branch-Working-and-Release-Notes.txt diff --git a/Branch-Working-and-Release-Notes.txt b/Branch-Working-and-Release-Notes.txt new file mode 100644 index 00000000..97ecd463 --- /dev/null +++ b/Branch-Working-and-Release-Notes.txt @@ -0,0 +1,11 @@ +- TODO: Deprecated the legacy secret file. This will require a user migration. (todo : provide instructions for bootstrapping the new file) + Maybe I can jigger from_file to still do the old thing, and just steer new logins +- TODO: deprecate the legacy auth command +- TODO: migrate planet.Auth to planet_auth.Auth as much as possible +- TODO: Don't forget env var behaviors +- TODO: good handing for PL_CLIENT_ID and PL_CLIENT_SECRET + +# TODO: The old code would have a in-memory authenticator here. Verify behavior. +# TODO: we do not have a good way to initialize M2M OAuth tokens from the env (I think? Maybe we do.) This really belongs to plauth +# TODO: we need to allocate a client ID for the `planet` CLI. We are presently leaning on plauth's client ID. (maybe this is OK) +# TODO: finalize message in translate_exceptions steering users to how to initialize auth. diff --git a/planet/cli/cli.py b/planet/cli/cli.py index cf18b380..500daa8c 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -92,6 +92,7 @@ def _configure_logging(verbosity): format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +# TODO - deprecate old auth command # main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="auth") # type: ignore main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="plauth") # type: ignore main.add_command(auth.auth) # type: ignore # TODO: deprecate entirely and use plauth diff --git a/planet/cli/cmds.py b/planet/cli/cmds.py index 339a0224..aa165c62 100644 --- a/planet/cli/cmds.py +++ b/planet/cli/cmds.py @@ -61,8 +61,8 @@ def wrapper(*args, **kwargs): raise click.ClickException( f'{pla_ex}\n' 'Auth information does not exist or is corrupted. Initialize ' - 'with `planet auth init`.') # TODO: where do we want to steer users now? `planet plauth`? - except exceptions.PlanetError as ex: + 'with `planet auth`.') # TODO/FIXME: finalize where we want to steer users now. `planet plauth`? + except (exceptions.PlanetError, FileNotFoundError) as ex: raise click.ClickException(ex) return wrapper From 02c0562148c1061818679f0cdd14c83867c29c56 Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Mon, 2 Sep 2024 21:14:26 -0700 Subject: [PATCH 06/10] WIP --- planet/auth.py | 23 ++++++++--------------- planet/cli/cli.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/planet/auth.py b/planet/auth.py index 3df61d5a..d6afb438 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -29,16 +29,9 @@ AuthType = httpx.Auth -# TODO: planet_auth.Auth potentially entirely supersedes this class. -# But, keeping this here for now for interface stability. -# TODO: Add from_oauth_user_browser / no browser / service account? -# Between confidential and non-confidential clients, user clients -# and m2m clients, and clients with and without browsers and rich -# user interaction, there are a wide variety of ways a customer's -# client may need to obtain OAuth tokens. With time limited -# access tokens and the need to manage refresh activity, the auth -# service interaction model is also necessarily different than -# what this Auth class models. +# planet_auth and planet_auth_utils code more or less entirely +# entirely supersedes this class. But, keeping this here for +# now for interface stability to bridge with the rest of the SDK. class Auth(metaclass=abc.ABCMeta): """Handle authentication information for use with Planet APIs.""" @@ -134,11 +127,11 @@ def from_plauth(pl_authlib_context: planet_auth.Auth): class _PLAuthLibAuth(Auth, AuthType): - # The Planet Auth Library uses a "has a" authenticator pattern for its Auth context. - # This library employs a "is a" authenticator design pattern for user's of - # its Auth context obtained from the constructors above. - # This class smooths over that design difference as we move to using - # the Planet Auth Library. + # The Planet Auth Library uses a "has a" authenticator pattern for its + # planet_auth.Auth context class. This SDK library employs a "is a" + # authenticator design pattern for user's of its Auth context obtained + # from the constructors above. This class smooths over that design + # difference as we move to using the Planet Auth Library. def __init__(self, plauth: planet_auth.Auth): self._plauth = plauth diff --git a/planet/cli/cli.py b/planet/cli/cli.py index 500daa8c..7b0610f6 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -38,10 +38,11 @@ help=("Optional: set verbosity level to warning, info, or debug.\ Defaults to warning.")) @planet_auth_utils.opt_auth_profile +@planet_auth_utils.opt_auth_client_id +@planet_auth_utils.opt_auth_client_secret # @planet_auth_utils.opt_token_file # TODO - support this? Check compatibility with other commands or legacy file? -# @planet_auth_utils.opt_auth_client_config_file # TODO - support this? Limit it to make interface simpler? @cmds.translate_exceptions -def main(ctx, verbosity, quiet, auth_profile): +def main(ctx, verbosity, quiet, auth_profile, auth_client_id, auth_client_secret): """Planet SDK for Python CLI""" _configure_logging(verbosity) @@ -50,15 +51,16 @@ def main(ctx, verbosity, quiet, auth_profile): ctx.ensure_object(dict) ctx.obj['QUIET'] = quiet - _configure_cli_auth_ctx(ctx, auth_profile) + _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secret) -def _configure_cli_auth_ctx(ctx, auth_profile): +def _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secret): # planet-auth library Auth type - ctx.obj['AUTH'] = planet_auth_utils.initialize_auth_client_context( + ctx.obj['AUTH'] = planet_auth_utils.ProfileManager.initialize_auth_client_context( auth_profile_opt=auth_profile, - token_file_opt=None, # TODO - support arg? token_file_opt=token_file - auth_client_config_file_opt=None, # TODO - support arg? auth_client_config_file_opt=auth_client_config_file + token_file_opt=None, # TODO - support arg? token_file_opt=token_file, + auth_client_id_opt=auth_client_id, + auth_client_secret_opt=auth_client_secret, ) # planet SDK Auth type From d4063b50f993c0b7cd6c807693d554a8835322b4 Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Tue, 3 Sep 2024 21:22:44 -0700 Subject: [PATCH 07/10] some better handling of options for the plauth lib --- planet/cli/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/planet/cli/cli.py b/planet/cli/cli.py index 7b0610f6..f7b36690 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -40,9 +40,9 @@ @planet_auth_utils.opt_auth_profile @planet_auth_utils.opt_auth_client_id @planet_auth_utils.opt_auth_client_secret -# @planet_auth_utils.opt_token_file # TODO - support this? Check compatibility with other commands or legacy file? +@planet_auth_utils.opt_auth_api_key @cmds.translate_exceptions -def main(ctx, verbosity, quiet, auth_profile, auth_client_id, auth_client_secret): +def main(ctx, verbosity, quiet, auth_profile, auth_client_id, auth_client_secret, auth_api_key): """Planet SDK for Python CLI""" _configure_logging(verbosity) @@ -51,16 +51,17 @@ def main(ctx, verbosity, quiet, auth_profile, auth_client_id, auth_client_secret ctx.ensure_object(dict) ctx.obj['QUIET'] = quiet - _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secret) + _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secret, auth_api_key) -def _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secret): +def _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secret, auth_api_key): # planet-auth library Auth type ctx.obj['AUTH'] = planet_auth_utils.ProfileManager.initialize_auth_client_context( auth_profile_opt=auth_profile, token_file_opt=None, # TODO - support arg? token_file_opt=token_file, auth_client_id_opt=auth_client_id, auth_client_secret_opt=auth_client_secret, + auth_api_key_opt=auth_api_key, ) # planet SDK Auth type From c7ed86c61ccd5b84d36f68adafe993b34e46b359 Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Wed, 4 Sep 2024 16:01:38 -0700 Subject: [PATCH 08/10] more plauth lib work --- planet/auth.py | 56 +++++++++++++----------- planet/cli/auth.py | 103 --------------------------------------------- planet/cli/cli.py | 8 +--- planet/cli/cmds.py | 2 +- 4 files changed, 34 insertions(+), 135 deletions(-) delete mode 100644 planet/cli/auth.py diff --git a/planet/auth.py b/planet/auth.py index d6afb438..a2d4ad56 100644 --- a/planet/auth.py +++ b/planet/auth.py @@ -15,6 +15,7 @@ """Manage authentication with Planet APIs""" from __future__ import annotations # https://stackoverflow.com/a/33533514 import abc +import os import pathlib import typing import warnings @@ -22,6 +23,7 @@ import planet_auth import planet_auth_config +import planet_auth_utils from .constants import SECRET_FILE_PATH @@ -42,14 +44,14 @@ def from_key(key: str) -> AuthType: Parameters: key: Planet API key """ - # TODO : document preferred new method in the warning. User OAuth flows should be favored - # for user API access. M2M OAuth flows for other use cases. - warnings.warn("Auth.from_key() will be deprecated.", PendingDeprecationWarning) - plauth_config = { - "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), - "api_key": key, - } - pl_authlib_context = planet_auth.Auth.initialize_from_config_dict(client_config=plauth_config) + warnings.warn("Planet API keys will be deprecated at some point." + " Initialize an OAuth client, or create an OAuth service account." + " Proceeding for now.", PendingDeprecationWarning) + pl_authlib_context = planet_auth_utils.ProfileManager.initialize_auth_client_context( + auth_profile_opt=planet_auth_utils.Profiles.BUILTIN_PROFILE_NAME_LEGACY, + auth_api_key_opt=key, + save_token_file=False + ) return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod @@ -65,8 +67,9 @@ def from_file( filename: Alternate path for the planet secret file. """ - # TODO - Document preferred replacement. A token file from PLAuth's Legacy client - # is formatted to be compatible with this library's .planet.json format files. + # There is no direct replacement for "from_file()", which held an API key. + # API keys will be deprecated, and user login will be different from service account + # login under OAuth. warnings.warn("Auth.from_file() will be deprecated.", PendingDeprecationWarning) plauth_config = { **planet_auth_config.Production.LEGACY_AUTH_AUTHORITY, @@ -85,11 +88,13 @@ def from_env(variable_name: typing.Optional[str] = None) -> AuthType: Parameters: variable_name: Alternate environment variable. """ - # TODO: document all the new ways the env can set things up for OAuth clients. - if variable_name: - warnings.warn("The variable_name parameter has been deprecated from planet.Auth.from_env().", DeprecationWarning) - pl_authlib_context = planet_auth.Auth.initialize_from_env() - return _PLAuthLibAuth(plauth=pl_authlib_context) + # There are just too many env vars and ways they interact and combine to continue to + # support this method with the planet auth lib in the future. Especially as we want + # to move away from API keys and towards OAuth methods. + warnings.warn("Auth.from_env() will be deprecated.", PendingDeprecationWarning) + variable_name = variable_name or planet_auth.EnvironmentVariables.AUTH_API_KEY + api_key = os.getenv(variable_name, None) + return Auth.from_key(api_key) @staticmethod def from_login(email: str, @@ -106,23 +111,24 @@ def from_login(email: str, base_url: The base URL to use. Defaults to production authentication API base url. """ - # TODO - PLAuth.login will save the credential if the file is set. Handle this case? - # We need to conditionally set the token file in the initialize call. - # TODO - update warning with directions on what to replace this with. (maybe from_user_oauth_login?) - warnings.warn("Auth.from_login will be deprecated.", PendingDeprecationWarning) - plauth_config = { - **planet_auth_config.Production.LEGACY_AUTH_AUTHORITY, - "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), - } + warnings.warn("Auth.from_login() and password based user login will be deprecated.", PendingDeprecationWarning) if base_url: - plauth_config["legacy_auth_endpoint"] = base_url + warnings.warn("base_url is not longer a supported parameter to Auth.from_login()", DeprecationWarning) - pl_authlib_context = planet_auth.Auth.initialize_from_config_dict(client_config=plauth_config) + pl_authlib_context = planet_auth_utils.ProfileManager.initialize_auth_client_context( + auth_profile_opt=planet_auth_utils.Profiles.BUILTIN_PROFILE_NAME_LEGACY + ) + # Note: login() will save the resulting token pl_authlib_context.login(username=email, password=password, allow_tty_prompt=False) return _PLAuthLibAuth(plauth=pl_authlib_context) @staticmethod def from_plauth(pl_authlib_context: planet_auth.Auth): + """ + Create authentication from the provided Planet Auth Library Authentication Context. + Generally, applications will want to use one of the Auth Library helpers to + construct this context, such as the `initialize_auth_client_context()` method. + """ return _PLAuthLibAuth(plauth=pl_authlib_context) diff --git a/planet/cli/auth.py b/planet/cli/auth.py deleted file mode 100644 index 35980515..00000000 --- a/planet/cli/auth.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2022 Planet Labs PBC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Auth API CLI""" -import logging -import os -import warnings -import click - -import planet_auth -import planet_auth_config -import planet_auth_utils.commands.cli.planet_legacy_auth_cmd # FIXME - Should be top level import - -from planet.constants import SECRET_FILE_PATH -from .cmds import translate_exceptions - -LOGGER = logging.getLogger(__name__) - - -# FIXME: this may need to be expanded to cover all env vars in play -# (PLAUTH has no equivalent warning, maybe it should if/when we add -# some sort of select default profile functionality.) -# FIXME: this warning encourages the use of API keys, that I want to deprecate. -def _api_key_env_warning(): - if os.getenv(planet_auth.EnvironmentVariables.AUTH_API_KEY): - click.echo(f'Warning - Environment variable {planet_auth.EnvironmentVariables.AUTH_API_KEY} already ' - 'exists. To update, with the new value, use the ' - 'following:') - click.echo(f'export {planet_auth.EnvironmentVariables.AUTH_API_KEY}=$(planet auth value)') - - -@click.group() # type: ignore -@click.pass_context -@click.option('-u', - '--base-url', - default=None, - help='Assign custom base Auth API URL.') -def auth(ctx, base_url): - """Commands for working with Planet authentication""" - # TODO: should we deprecate this whole command in favor of the functionality of the embedded 'plauth'. - # TODO: plauth does not have, and maybe should, some sort of "select default" function - # so that it need not always have the user provide --auth-profile. This should be built into the plauth lib - # so that we can have nice default construction that follows the user's selection. - # warnings.warn("'auth' command will be deprecated. Please use 'plauth' to manage user credentials.", PendingDeprecationWarning) - - # Override any newer style planet_auth library auth profiles and - # always wire up the legacy auth implementation to the planet library's - # preferred paths. - _plauth_config = { - **planet_auth_config.Production.LEGACY_AUTH_AUTHORITY, - "client_type": planet_auth.PlanetLegacyAuthClientConfig.meta().get("client_type"), - } - if base_url: - _plauth_config["legacy_auth_endpoint"] = base_url - ctx.obj["AUTH"] = planet_auth.Auth.initialize_from_config_dict(client_config=_plauth_config, token_file=SECRET_FILE_PATH) - - -@auth.command() # type: ignore -@click.pass_context -@translate_exceptions -@click.option( - '--email', - default=None, - prompt=True, - help=('The email address associated with your Planet credentials.')) -@click.password_option('--password', - confirmation_prompt=False, - help=('Account password. Will not be saved.')) -def init(ctx, email, password): - """Obtain and store authentication information""" - ctx.invoke(planet_auth_utils.commands.cli.planet_legacy_auth_cmd.pllegacy_do_login, username=email, password=password) - click.echo('Initialized') - _api_key_env_warning() - -@auth.command() # type: ignore -@click.pass_context -@translate_exceptions -def value(ctx): - """Print the stored authentication information""" - ctx.forward(planet_auth_utils.commands.cli.planet_legacy_auth_cmd.do_print_api_key) - - -@auth.command() # type: ignore -@click.pass_context -@translate_exceptions -@click.argument('key') -def store(ctx, key): - """Store authentication information""" - _token_file = planet_auth.FileBackedPlanetLegacyApiKey(api_key=key, api_key_file=ctx.obj["AUTH"].token_file_path()) - if click.confirm('This overrides the stored value. Continue?'): - _token_file.save() - click.echo('Updated') - _api_key_env_warning() diff --git a/planet/cli/cli.py b/planet/cli/cli.py index f7b36690..3a666974 100644 --- a/planet/cli/cli.py +++ b/planet/cli/cli.py @@ -21,7 +21,7 @@ import planet_auth_utils import planet -from . import auth, cmds, collect, data, orders, subscriptions +from . import cmds, collect, data, orders, subscriptions LOGGER = logging.getLogger(__name__) @@ -58,7 +58,6 @@ def _configure_cli_auth_ctx(ctx, auth_profile, auth_client_id, auth_client_secre # planet-auth library Auth type ctx.obj['AUTH'] = planet_auth_utils.ProfileManager.initialize_auth_client_context( auth_profile_opt=auth_profile, - token_file_opt=None, # TODO - support arg? token_file_opt=token_file, auth_client_id_opt=auth_client_id, auth_client_secret_opt=auth_client_secret, auth_api_key_opt=auth_api_key, @@ -95,10 +94,7 @@ def _configure_logging(verbosity): format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') -# TODO - deprecate old auth command -# main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="auth") # type: ignore -main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="plauth") # type: ignore -main.add_command(auth.auth) # type: ignore # TODO: deprecate entirely and use plauth +main.add_command(cmd=planet_auth_utils.embedded_plauth_cmd_group, name="auth") # type: ignore main.add_command(data.data) # type: ignore main.add_command(orders.orders) # type: ignore main.add_command(subscriptions.subscriptions) # type: ignore diff --git a/planet/cli/cmds.py b/planet/cli/cmds.py index aa165c62..602e7688 100644 --- a/planet/cli/cmds.py +++ b/planet/cli/cmds.py @@ -61,7 +61,7 @@ def wrapper(*args, **kwargs): raise click.ClickException( f'{pla_ex}\n' 'Auth information does not exist or is corrupted. Initialize ' - 'with `planet auth`.') # TODO/FIXME: finalize where we want to steer users now. `planet plauth`? + 'with `planet auth`.') except (exceptions.PlanetError, FileNotFoundError) as ex: raise click.ClickException(ex) From a9ff29439e82fb0bbc45b0fe27b4e35b18a5187d Mon Sep 17 00:00:00 2001 From: carl-adams-planet Date: Wed, 4 Sep 2024 16:09:35 -0700 Subject: [PATCH 09/10] update notes --- Branch-Working-and-Release-Notes.txt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Branch-Working-and-Release-Notes.txt b/Branch-Working-and-Release-Notes.txt index 97ecd463..ed7d2da1 100644 --- a/Branch-Working-and-Release-Notes.txt +++ b/Branch-Working-and-Release-Notes.txt @@ -1,11 +1,8 @@ -- TODO: Deprecated the legacy secret file. This will require a user migration. (todo : provide instructions for bootstrapping the new file) - Maybe I can jigger from_file to still do the old thing, and just steer new logins -- TODO: deprecate the legacy auth command -- TODO: migrate planet.Auth to planet_auth.Auth as much as possible -- TODO: Don't forget env var behaviors -- TODO: good handing for PL_CLIENT_ID and PL_CLIENT_SECRET +- Replaced most user auth code with the planet auth library. This brings with it OAuth for user interactive and M2M flows. +- Deprecated the old auth command. New auth command is implemented with the separate auth library + - TODO: link to lib + - planet auth init -> planet auth oauth login (or, planet --auth-profile legacy auth legacy login) + - planet auth value -> planet plauth oauth print-access-token (or planet --auth-profile legacy auth legacy print-api-key) +- The legacy secret file has largely been deprecated. This will require a user migration. -# TODO: The old code would have a in-memory authenticator here. Verify behavior. -# TODO: we do not have a good way to initialize M2M OAuth tokens from the env (I think? Maybe we do.) This really belongs to plauth # TODO: we need to allocate a client ID for the `planet` CLI. We are presently leaning on plauth's client ID. (maybe this is OK) -# TODO: finalize message in translate_exceptions steering users to how to initialize auth. From 7868a0fa1f8982a528a12088cb4840d0f1586e5f Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Fri, 4 Oct 2024 20:06:21 -0700 Subject: [PATCH 10/10] fix setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42cc564c..f6d4a6de 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ 'tqdm>=4.56', 'typing-extensions', 'planet-auth>=2.0.0', - 'planet-auth-util>=2.0.0', + 'planet-auth-utils>=2.0.0', 'planet-auth-config>=2.0.0', # FIXME - This currently has staging endpoints, too. ]