Source code for plone_client.credentials
"""Where the password or token of a profile is kept.
``keyring`` (default)
The operating system's secret store, through the :mod:`keyring` package:
macOS Keychain, Windows Credential Locker, or the Secret Service on Linux.
Entries are stored under the service ``plone-client:<profile>``.
``env``
An environment variable, named by the profile's ``secret_env``
(``PLONE_CLIENT_PASSWORD`` by default). Handy in CI.
``command``
The output of a command, named by the profile's ``secret_command``, such
as ``op read op://Private/site/password`` or ``pass show plone/site``.
The command runs without a shell.
``none``
No secret: the profile is anonymous.
"""
from .exceptions import PloneConfigurationError
from typing import Any
from typing import TYPE_CHECKING
import os
import shlex
import subprocess
if TYPE_CHECKING:
from .profiles import Profile
SERVICE_PREFIX = "plone-client"
[docs]
def keyring_service(profile: "Profile") -> str:
"""Keyring service name of a profile.
:param profile: The profile.
:returns: ``plone-client:<profile name>``.
"""
return f"{SERVICE_PREFIX}:{profile.name}"
[docs]
def keyring_account(profile: "Profile") -> str:
"""Keyring account name of a profile.
:param profile: The profile.
:returns: The username, or ``token`` for token profiles without one.
"""
return profile.username or "token"
def _keyring() -> Any:
"""Import :mod:`keyring`.
:returns: The module.
:raises PloneConfigurationError: If it is not installed.
"""
try:
import keyring
except ImportError: # no cov
raise PloneConfigurationError(
"The keyring needs the 'cli' extra: pip install 'plone-client[cli]'"
) from None
return keyring
[docs]
def get_secret(profile: "Profile") -> str:
"""Look up the secret of a profile.
:param profile: The profile.
:returns: The password or token.
:raises PloneConfigurationError: If the secret is missing.
"""
if profile.secret == "none":
return ""
if profile.secret == "env":
value = os.environ.get(profile.secret_env, "")
if not value:
raise PloneConfigurationError(
f"Profile {profile.name!r}: set ${profile.secret_env}"
)
return value
if profile.secret == "command":
return _run_command(profile)
value = _keyring().get_password(keyring_service(profile), keyring_account(profile))
if not value:
raise PloneConfigurationError(
f"Profile {profile.name!r}: no secret in the keyring; "
f"run 'plone-client profile set-secret {profile.name}'"
)
return str(value)
def _run_command(profile: "Profile") -> str:
"""Run the secret command of a profile.
:param profile: The profile.
:returns: The first line of the command's output.
:raises PloneConfigurationError: If the command fails or prints nothing.
"""
try:
result = subprocess.run( # noqa: S603
shlex.split(profile.secret_command),
capture_output=True,
text=True,
check=False,
timeout=60,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise PloneConfigurationError(
f"Profile {profile.name!r}: secret command failed: {exc}"
) from exc
value = result.stdout.splitlines()[0].strip() if result.stdout else ""
if result.returncode or not value:
raise PloneConfigurationError(
f"Profile {profile.name!r}: secret command exited with "
f"{result.returncode} and printed no secret"
)
return value
[docs]
def set_secret(profile: "Profile", value: str) -> None:
"""Store the secret of a profile in the keyring.
:param profile: The profile.
:param value: The password or token.
:raises PloneConfigurationError: If the profile keeps its secret elsewhere.
"""
if profile.secret != "keyring":
raise PloneConfigurationError(
f"Profile {profile.name!r} reads its secret from {profile.secret!r}, "
"not from the keyring"
)
_keyring().set_password(keyring_service(profile), keyring_account(profile), value)
[docs]
def delete_secret(profile: "Profile") -> bool:
"""Remove the secret of a profile from the keyring.
:param profile: The profile.
:returns: ``True`` if there was a secret to remove.
"""
if profile.secret != "keyring":
return False
keyring = _keyring()
try:
keyring.delete_password(keyring_service(profile), keyring_account(profile))
except keyring.errors.PasswordDeleteError:
return False
return True