diff --git a/docs/api-planet-auth-config-injection.md b/docs/api-planet-auth-config-injection.md new file mode 100644 index 0000000..dc512f6 --- /dev/null +++ b/docs/api-planet-auth-config-injection.md @@ -0,0 +1,6 @@ +# ::: planet_auth_config_injection + options: + show_root_full_path: true + inherited_members: true + show_submodules: true + show_if_no_docstring: false diff --git a/mkdocs.yml b/mkdocs.yml index c1899b2..905d8dd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,6 +80,7 @@ nav: - API Reference: - Planet Auth: 'api-planet-auth.md' - Planet Auth Utils: 'api-planet-auth-utils.md' + - Planet Auth Config Injection: 'api-planet-auth-config-injection.md' - Examples: - Installation: 'examples-installation.md' - CLI: 'examples-cli.md' diff --git a/src/planet_auth_config_injection/__init__.py b/src/planet_auth_config_injection/__init__.py new file mode 100644 index 0000000..0c2bf40 --- /dev/null +++ b/src/planet_auth_config_injection/__init__.py @@ -0,0 +1,61 @@ +# Copyright 2025 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. + +""" +# The Planet Authentication Library Configration Injection Package: `planet_auth_config_injection` + +This package provides interfaces and utilities for higher-level applications +to inject configuration into the Planet Authentication Library. + +The Planet Auth Library provides configuration injection to improve the +end-user experience of tools built on top of the Planet Auth Library. +This allows built-in default client configurations to be provided. +Namespacing may also be configured to avoid collisions in the Auth Library's +use of environment variables. Injected configration is primarily consumed +by initialization functionality in planet_auth_utils.PlanetAuthFactory +and the various planet_auth_utils provided `click` commands. + +These concerns belong more to the final end-user application than to a +library that sits between the Planet Auth library and the end-user +application. Such libraries themselves may be used by a variety of +applications in any number of deployment environments, making the +decision of what configuration to inject a difficult one. + +Library writers may provide configuration injection to their developers, +but should be conscious of the fact that multiple libraries within an +application may depend on Planet Auth libraries. Library writers are +advised to provide configuration injection as an option for their users, +and not silently force it into the loaded. + +In order to inject configuration, the application writer must do two things: + +1. They must write a class that implements the + [planet_auth_config_injection.BuiltinConfigurationProviderInterface][] + interface. +2. They must set the environment variable `PL_AUTH_BUILTIN_CONFIG_PROVIDER` to the + fully qualified package, module, and class name of their implementation + _before_ any import of the `planet_auth` or `planet_auth_utils` packages. +""" + +from .builtins_provider import ( + AUTH_BUILTIN_PROVIDER, + BuiltinConfigurationProviderInterface, + EmptyBuiltinProfileConstants, +) + +__all__ = [ + "AUTH_BUILTIN_PROVIDER", + "BuiltinConfigurationProviderInterface", + "EmptyBuiltinProfileConstants", +] diff --git a/src/planet_auth_utils/builtins_provider.py b/src/planet_auth_config_injection/builtins_provider.py similarity index 75% rename from src/planet_auth_utils/builtins_provider.py rename to src/planet_auth_config_injection/builtins_provider.py index 159a8f3..1366bf9 100644 --- a/src/planet_auth_utils/builtins_provider.py +++ b/src/planet_auth_config_injection/builtins_provider.py @@ -16,6 +16,16 @@ from typing import Dict, List, Optional +# Unlike other environment variables, AUTH_BUILTIN_PROVIDER is not name-spaced. +# It is intended for libraries and applications to inject configuration by +# being set within the program. It's not expected to be set by end-users. +AUTH_BUILTIN_PROVIDER = "PL_AUTH_BUILTIN_CONFIG_PROVIDER" +""" +Environment variable to specify a python module and class that implement the +BuiltinConfigurationProviderInterface abstract interface to provide the library +and utility commands with some built-in configurations. +""" + _NOOP_AUTH_CLIENT_CONFIG = { "client_type": "none", } @@ -23,23 +33,28 @@ class BuiltinConfigurationProviderInterface(ABC): """ - Interface to define what profiles are built-in. - - What auth configuration profiles are built-in is - completely pluggable for users of the planet_auth and - planet_auth_utils packages. This is to support reuse - in different deployments, or even support reuse by a - different software stack all together. - - To inject built-in that override the coded in defaults, - set the environment variable PL_AUTH_BUILTIN_CONFIG_PROVIDER - to the module.classname of a class that implements this interface. + Interface to define built-in application configuration. + This includes providing built-in auth client configuration + profiles, pre-defined trust environments for server use, + and namespacing for environment and global configuration + variables. Built-in profile names are expected to be all lowercase. Built-in trust environments are expected to be all uppercase. """ + def namespace(self) -> str: + """ + Application namespace. This will be used as a prefix in various + contexts so that multiple applications may use the Planet auth + libraries in the same environment without collisions. Presently, + this namespace is used as a prefix for environment variables, and + as a prefix for config settings store to the user's `~/.planet.json` + file. + """ + return "" + @abstractmethod def builtin_client_authclient_config_dicts(self) -> Dict[str, dict]: """ diff --git a/src/planet_auth_utils/__init__.py b/src/planet_auth_utils/__init__.py index 11cf1e7..1c93a87 100644 --- a/src/planet_auth_utils/__init__.py +++ b/src/planet_auth_utils/__init__.py @@ -96,14 +96,10 @@ opt_username, opt_yes_no, ) -from .commands.cli.util import recast_exceptions_to_click +from .commands.cli.util import recast_exceptions_to_click, monkeypatch_hide_click_cmd_options from planet_auth_utils.constants import EnvironmentVariables from planet_auth_utils.plauth_factory import PlanetAuthFactory -from planet_auth_utils.builtins import ( - Builtins, - # Easily causes circular dependencies. Intentionally not part of the main package interface for now. - # BuiltinConfigurationProviderInterface, -) +from planet_auth_utils.builtins import Builtins from planet_auth_utils.profile import Profile from planet_auth_utils.plauth_user_config import PlanetAuthUserConfig @@ -162,10 +158,11 @@ "opt_token_file", "opt_username", "opt_yes_no", + # "recast_exceptions_to_click", + "monkeypatch_hide_click_cmd_options", # "Builtins", - # "BuiltinConfigurationProviderInterface", "EnvironmentVariables", "PlanetAuthFactory", "Profile", diff --git a/src/planet_auth_utils/builtins.py b/src/planet_auth_utils/builtins.py index 775db44..90fcb23 100644 --- a/src/planet_auth_utils/builtins.py +++ b/src/planet_auth_utils/builtins.py @@ -17,9 +17,12 @@ from planet_auth import AuthClientConfig from planet_auth_utils.profile import ProfileException -from planet_auth_utils.constants import EnvironmentVariables from planet_auth.logging.auth_logger import getAuthLogger -from .builtins_provider import BuiltinConfigurationProviderInterface, EmptyBuiltinProfileConstants +from planet_auth_config_injection import ( + BuiltinConfigurationProviderInterface, + EmptyBuiltinProfileConstants, + AUTH_BUILTIN_PROVIDER, +) auth_logger = getAuthLogger() @@ -33,6 +36,7 @@ def _load_builtins_worker(builtin_provider_fq_class_name, log_warning=False): return module_name, _, class_name = builtin_provider_fq_class_name.rpartition(".") + auth_logger.debug(msg=f'Loading built-in provider:"{builtin_provider_fq_class_name}".') if module_name and class_name: try: builtin_provider_module = importlib.import_module(module_name) # nosemgrep - WARNING - See below @@ -61,7 +65,7 @@ def _load_builtins() -> BuiltinConfigurationProviderInterface: # Undermining it can undermine client or service security. # It is a convenience for seamless developer experience, but maybe # we should not be so eager to please. - builtin_provider = _load_builtins_worker(os.getenv(EnvironmentVariables.AUTH_BUILTIN_PROVIDER)) + builtin_provider = _load_builtins_worker(os.getenv(AUTH_BUILTIN_PROVIDER)) if builtin_provider: return builtin_provider @@ -86,6 +90,12 @@ class Builtins: def _load_builtin_jit(): if not Builtins._builtin: Builtins._builtin = _load_builtins() + auth_logger.debug(msg=f"Successfully loaded built-in provider: {Builtins._builtin.__class__.__name__}") + + @staticmethod + def namespace() -> str: + Builtins._load_builtin_jit() + return Builtins._builtin.namespace() @staticmethod def is_builtin_profile(profile: str) -> bool: diff --git a/src/planet_auth_utils/commands/cli/main.py b/src/planet_auth_utils/commands/cli/main.py index 17d9e65..5351913 100644 --- a/src/planet_auth_utils/commands/cli/main.py +++ b/src/planet_auth_utils/commands/cli/main.py @@ -84,7 +84,10 @@ def cmd_plauth_embedded(ctx): Embeddable version of the Planet Auth Client root command. The embedded command differs from the stand-alone command in that it expects the context to be instantiated and options to be handled by - the parent command. See [planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context][] + the parent command. The [planet_auth.Auth][] library context _must_ + be saved to the object field `AUTH` in the click context object. + + See [planet_auth_utils.PlanetAuthFactory.initialize_auth_client_context][] for user-friendly auth client context initialization. See [examples](/examples/#embedding-the-click-auth-command). diff --git a/src/planet_auth_utils/commands/cli/util.py b/src/planet_auth_utils/commands/cli/util.py index 929714d..16a907f 100644 --- a/src/planet_auth_utils/commands/cli/util.py +++ b/src/planet_auth_utils/commands/cli/util.py @@ -15,7 +15,7 @@ import click import functools import json -from typing import Optional +from typing import List, Optional import planet_auth from planet_auth.constants import AUTH_CONFIG_FILE_SOPS, AUTH_CONFIG_FILE_PLAIN @@ -26,7 +26,25 @@ from .prompts import prompt_and_change_user_default_profile_if_different +def monkeypatch_hide_click_cmd_options(cmd, hide_options: List[str]): + """ + Monkey patch a click command to hide the specified command options. + Useful when reusing click commands in contexts where you do not + wish to expose all the options. + """ + for hide_option in hide_options: + for param in cmd.params: + if param.name == hide_option: + param.hidden = True + break + + def recast_exceptions_to_click(*exceptions, **params): # pylint: disable=W0613 + """ + Decorator to catch exceptions and raise them as ClickExceptions. + Useful to apply to `click` commands to supress stack traces that + might be otherwise exposed to the end-user. + """ if not exceptions: exceptions = (Exception,) # params.get('some_arg', 'default') diff --git a/src/planet_auth_utils/constants.py b/src/planet_auth_utils/constants.py index c061d1e..dd4f48d 100644 --- a/src/planet_auth_utils/constants.py +++ b/src/planet_auth_utils/constants.py @@ -12,95 +12,143 @@ # See the License for the specific language governing permissions and # limitations under the License. +from planet_auth_utils.builtins import Builtins -class EnvironmentVariables: - """ - Environment Variables used in the planet_auth_utils packages - """ - - AUTH_API_KEY = "PL_API_KEY" - """ - A literal Planet API key. - """ - - AUTH_CLIENT_ID = "PL_AUTH_CLIENT_ID" - """ - Client ID for an OAuth service account - """ - - AUTH_CLIENT_SECRET = "PL_AUTH_CLIENT_SECRET" - """ - Client Secret for an OAuth service account - """ - - AUTH_EXTRA = "PL_AUTH_EXTRA" - """ - List of extra options. Values should be formatted as =. - Multiple options should be whitespace delimited. - """ - AUTH_PROFILE = "PL_AUTH_PROFILE" - """ - Name of a profile to use for auth client configuration. - """ +class classproperty(object): + def __init__(self, method=None): + self.fget = method - AUTH_TOKEN = "PL_AUTH_TOKEN" - """ - Literal token string. - """ + def __get__(self, instance, cls=None): + return self.fget(cls) - AUTH_TOKEN_FILE = "PL_AUTH_TOKEN_FILE" - """ - File path to use for storing tokens. - """ - AUTH_ISSUER = "PL_AUTH_ISSUER" - """ - Issuer to use when requesting or validating OAuth tokens. - """ - - AUTH_AUDIENCE = "PL_AUTH_AUDIENCE" - """ - Audience to use when requesting or validating OAuth tokens. - """ - - AUTH_ORGANIZATION = "PL_AUTH_ORGANIZATION" - """ - Organization to use when performing client authentication. - Only used for some authentication mechanisms. - """ - - AUTH_PROJECT = "PL_AUTH_PROJECT" - """ - Project ID to use when performing authentication. - Not all implementations understand this option. - """ - - AUTH_PASSWORD = "PL_AUTH_PASSWORD" - """ - Password to use when performing client authentication. - Only used for some authentication mechanisms. - """ - - AUTH_SCOPE = "PL_AUTH_SCOPE" - """ - List of scopes to request when requesting OAuth tokens. - Multiple scopes should be whitespace delimited. - """ - - AUTH_USERNAME = "PL_AUTH_USERNAME" - """ - Username to use when performing client authentication. - Only used for some authentication mechanisms. - """ - - AUTH_LOGLEVEL = "PL_LOGLEVEL" +class EnvironmentVariables: """ - Specify the log level. + Environment Variables used in the planet_auth_utils packages """ - AUTH_BUILTIN_PROVIDER = "PL_AUTH_BUILTIN_CONFIG_PROVIDER" - """ - Specify a python module and class that implement the BuiltinConfigurationProviderInterface abstract - interface to provide the library and utility commands with some built-in configurations. - """ + @staticmethod + def _namespace_variable(undecorated_variable: str): + """ + Decorate the variable name with a namespace. + This is done so that multiple applications may use + the Planet auth library without conflicting. + """ + + namespace = Builtins.namespace() + if namespace: + return f"{namespace.upper()}_{undecorated_variable}" + return undecorated_variable + + @classproperty + def AUTH_API_KEY(cls): # pylint: disable=no-self-argument + """ + A literal Planet API key. + """ + return cls._namespace_variable("PL_API_KEY") + + @classproperty + def AUTH_CLIENT_ID(cls): # pylint: disable=no-self-argument + """ + Client ID for an OAuth service account + """ + # traceback.print_stack(file=sys.stdout) + return cls._namespace_variable("PL_AUTH_CLIENT_ID") + + @classproperty + def AUTH_CLIENT_SECRET(cls): # pylint: disable=no-self-argument + """ + Client Secret for an OAuth service account + """ + return cls._namespace_variable("PL_AUTH_CLIENT_SECRET") + + @classproperty + def AUTH_EXTRA(cls): # pylint: disable=no-self-argument + """ + List of extra options. Values should be formatted as =. + Multiple options should be whitespace delimited. + """ + return cls._namespace_variable("PL_AUTH_EXTRA") + + @classproperty + def AUTH_PROFILE(cls): # pylint: disable=no-self-argument + """ + Name of a profile to use for auth client configuration. + """ + return cls._namespace_variable("PL_AUTH_PROFILE") + + @classproperty + def AUTH_TOKEN(cls): # pylint: disable=no-self-argument + """ + Literal token string. + """ + return cls._namespace_variable("PL_AUTH_TOKEN") + + @classproperty + def AUTH_TOKEN_FILE(cls): # pylint: disable=no-self-argument + """ + File path to use for storing tokens. + """ + return cls._namespace_variable("PL_AUTH_TOKEN_FILE") + + @classproperty + def AUTH_ISSUER(cls): # pylint: disable=no-self-argument + """ + Issuer to use when requesting or validating OAuth tokens. + """ + return cls._namespace_variable("PL_AUTH_ISSUER") + + @classproperty + def AUTH_AUDIENCE(cls): # pylint: disable=no-self-argument + """ + Audience to use when requesting or validating OAuth tokens. + """ + return cls._namespace_variable("PL_AUTH_AUDIENCE") + + @classproperty + def AUTH_ORGANIZATION(cls): # pylint: disable=no-self-argument + """ + Organization to use when performing client authentication. + Only used for some authentication mechanisms. + """ + return cls._namespace_variable("PL_AUTH_ORGANIZATION") + + @classproperty + def AUTH_PROJECT(cls): # pylint: disable=no-self-argument + """ + Project ID to use when performing authentication. + Not all implementations understand this option. + """ + return cls._namespace_variable("PL_AUTH_PROJECT") + + @classproperty + def AUTH_PASSWORD(cls): # pylint: disable=no-self-argument + """ + Password to use when performing client authentication. + Only used for some authentication mechanisms. + """ + return cls._namespace_variable("PL_AUTH_PASSWORD") + + @classproperty + def AUTH_SCOPE(cls): # pylint: disable=no-self-argument + """ + List of scopes to request when requesting OAuth tokens. + Multiple scopes should be whitespace delimited. + """ + return cls._namespace_variable("PL_AUTH_SCOPE") + + @classproperty + def AUTH_USERNAME(cls): # pylint: disable=no-self-argument + """ + Username to use when performing client authentication. + Only used for some authentication mechanisms. + """ + return cls._namespace_variable("PL_AUTH_USERNAME") + + @classproperty + def AUTH_LOGLEVEL(cls): # pylint: disable=no-self-argument + """ + Specify the log level. + """ + return cls._namespace_variable("PL_LOGLEVEL") diff --git a/tests/test_planet_auth_utils/unit/auth_utils/builtins_test_impl.py b/tests/test_planet_auth_utils/unit/auth_utils/builtins_test_impl.py index 324e57f..5727cb5 100644 --- a/tests/test_planet_auth_utils/unit/auth_utils/builtins_test_impl.py +++ b/tests/test_planet_auth_utils/unit/auth_utils/builtins_test_impl.py @@ -13,7 +13,7 @@ # limitations under the License. from typing import Dict, List, Optional -from planet_auth_utils.builtins_provider import BuiltinConfigurationProviderInterface +from planet_auth_config_injection import BuiltinConfigurationProviderInterface class MockStagingEnv: diff --git a/tests/test_planet_auth_utils/unit/auth_utils/test_builtins.py b/tests/test_planet_auth_utils/unit/auth_utils/test_builtins.py index 80c130a..ccbe480 100644 --- a/tests/test_planet_auth_utils/unit/auth_utils/test_builtins.py +++ b/tests/test_planet_auth_utils/unit/auth_utils/test_builtins.py @@ -16,8 +16,8 @@ import pytest import unittest +from planet_auth_config_injection import AUTH_BUILTIN_PROVIDER from planet_auth_utils.builtins import Builtins, BuiltinsException -from planet_auth_utils.constants import EnvironmentVariables from tests.test_planet_auth_utils.util import TestWithHomeDirProfiles from tests.test_planet_auth_utils.unit.auth_utils.builtins_test_impl import BuiltinConfigurationProviderMockTestImpl @@ -56,7 +56,7 @@ def test_builtin_all_profile_dicts_are_valid(self): class TestAuthClientContextInitHelpers(TestWithHomeDirProfiles, unittest.TestCase): def setUp(self): - os.environ[EnvironmentVariables.AUTH_BUILTIN_PROVIDER] = ( + os.environ[AUTH_BUILTIN_PROVIDER] = ( "tests.test_planet_auth_utils.unit.auth_utils.builtins_test_impl.BuiltinConfigurationProviderMockTestImpl" ) Builtins._builtin = None # Reset built-in state. diff --git a/tests/test_planet_auth_utils/unit/auth_utils/test_plauth_factory.py b/tests/test_planet_auth_utils/unit/auth_utils/test_plauth_factory.py index 3426bbd..241299f 100644 --- a/tests/test_planet_auth_utils/unit/auth_utils/test_plauth_factory.py +++ b/tests/test_planet_auth_utils/unit/auth_utils/test_plauth_factory.py @@ -20,6 +20,7 @@ import planet_auth.storage_utils from planet_auth.constants import AUTH_CONFIG_FILE_PLAIN +from planet_auth_config_injection import AUTH_BUILTIN_PROVIDER from planet_auth_utils.builtins import Builtins from planet_auth_utils.constants import EnvironmentVariables from planet_auth_utils.plauth_factory import PlanetAuthFactory, MissingArgumentException @@ -37,7 +38,7 @@ class TestAuthClientContextInitHelpers(TestWithHomeDirProfiles, unittest.TestCase): def setUp(self): - os.environ[EnvironmentVariables.AUTH_BUILTIN_PROVIDER] = ( + os.environ[AUTH_BUILTIN_PROVIDER] = ( "tests.test_planet_auth_utils.unit.auth_utils.builtins_test_impl.BuiltinConfigurationProviderMockTestImpl" ) Builtins._builtin = None # Reset built-in state. @@ -391,7 +392,7 @@ def test_save_profile_does_not_save_when_false(self): class TestResourceServerValidatorInitHelper(TestWithHomeDirProfiles, unittest.TestCase): def setUp(self): - os.environ[EnvironmentVariables.AUTH_BUILTIN_PROVIDER] = ( + os.environ[AUTH_BUILTIN_PROVIDER] = ( "tests.test_planet_auth_utils.unit.auth_utils.builtins_test_impl.BuiltinConfigurationProviderMockTestImpl" ) Builtins._builtin = None # Reset built-in state.