Skip to content

WX integration feedack - Add namespacing handling to builtins to minimize the problems of collisions in config file and environment variable usage #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/api-planet-auth-config-injection.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions src/planet_auth/oidc/request_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Dict, Optional

import planet_auth.logging.auth_logger
from planet_auth import AuthException
from planet_auth.credential import Credential
from planet_auth.request_authenticator import CredentialRequestAuthenticator
from planet_auth.oidc.auth_client import OidcAuthClient
Expand Down Expand Up @@ -123,6 +124,11 @@ def _refresh_if_needed(self):
msg="Error refreshing auth token. Continuing with old auth token. Refresh error: " + str(e)
)

if not (self._credential and self._credential.is_loaded()):
# "refresh" may also be called to initialize in some cases, as in client credentials flow.
# Continuing with what we have is not an option when we have nothing.
raise AuthException("Failed to load or obtain a valid access token.")

def pre_request_hook(self):
self._refresh_if_needed()
super().pre_request_hook()
Expand Down
60 changes: 60 additions & 0 deletions src/planet_auth_config_injection/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 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, that itself may be used by a variety of applications in
a variety of deployment environments

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",
]
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,45 @@
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 generally expected to be set by
# 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",
}


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 includes being
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]:
"""
Expand Down
7 changes: 4 additions & 3 deletions src/planet_auth_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +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, BuiltinConfigurationProviderInterface
from planet_auth_utils.builtins import Builtins
from planet_auth_utils.profile import Profile
from planet_auth_utils.plauth_user_config import PlanetAuthUserConfig

Expand Down Expand Up @@ -158,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",
Expand Down
16 changes: 13 additions & 3 deletions src/planet_auth_utils/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion src/planet_auth_utils/commands/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 set 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).
Expand Down
20 changes: 19 additions & 1 deletion src/planet_auth_utils/commands/cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand Down
Loading
Loading