cloudsmith-cli 1.24.0__tar.gz → 1.25.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudsmith_cli-1.24.0/cloudsmith_cli.egg-info → cloudsmith_cli-1.25.0}/PKG-INFO +2 -2
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/__init__.py +8 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/manage.py +24 -9
- cloudsmith_cli-1.25.0/cloudsmith_cli/cli/commands/credential_helper/pnpm.py +51 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/entitlements.py +17 -5
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/logout.py +6 -3
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/mcp.py +7 -2
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/push.py +30 -1
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/upstream.py +1 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/config.py +1 -1
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/decorators.py +5 -2
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/saml.py +25 -24
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/validators.py +23 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/webserver.py +62 -8
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/keyring.py +38 -1
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/generic.py +16 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/launchers.py +19 -9
- cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/__init__.py +4 -0
- cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/installer.py +264 -0
- cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/rc.py +185 -0
- cloudsmith_cli-1.25.0/cloudsmith_cli/credential_helpers/pnpm/runtime.py +68 -0
- cloudsmith_cli-1.25.0/cloudsmith_cli/data/VERSION +1 -0
- cloudsmith_cli-1.25.0/cloudsmith_cli/templates/auth_two_factor.html +38 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0/cloudsmith_cli.egg-info}/PKG-INFO +2 -2
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/SOURCES.txt +7 -1
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/requires.txt +1 -1
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/pyproject.toml +3 -2
- cloudsmith_cli-1.24.0/cloudsmith_cli/data/VERSION +0 -1
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/LICENSE +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/MANIFEST.in +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/README.md +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/__main__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/command.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/auth.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/check.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/docker.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/credential_helper/generic.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/domains.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/download.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/login.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/main.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metadata.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/move.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/status.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/tokens.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/vulnerabilities.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/commands/whoami.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/dsc_parser.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/exceptions.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/metadata_common.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/table.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/types.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/cli/utils.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/conftest.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/distros.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/files.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/init.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/metadata.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/metrics.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/orgs.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/packages.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/quota.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/rates.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/repos.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/status.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/user.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/version.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/api/vulnerabilities.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/cache_utils.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/config.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/chain.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/models.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/cache.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/aws.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/base.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/generic.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/oidc/exchange.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/provider.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/cli_flag.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/credentials_file.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/env_var.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/keyring_provider.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/credentials/providers/oidc_provider.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/download.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/mcp/data.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/mcp/server.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/pagination.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/ratelimits.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/rest.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/utils.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/core/version.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/backends.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/common.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/custom_domains.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/default_domains.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/docker/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/docker/installer.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/credential_helpers/docker/runtime.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/data/config.ini +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/data/credentials.ini +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/templates/__init__.py +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/templates/auth_error.html +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli/templates/auth_success.html +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
- {cloudsmith_cli-1.24.0 → cloudsmith_cli-1.25.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudsmith-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.25.0
|
|
4
4
|
Summary: Cloudsmith Command-Line Interface (CLI)
|
|
5
5
|
Author-email: Cloudsmith Ltd <support@cloudsmith.io>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,7 +28,7 @@ Requires-Dist: click-configfile>=0.2.3
|
|
|
28
28
|
Requires-Dist: click-didyoumean>=0.0.3
|
|
29
29
|
Requires-Dist: click-spinner>=0.1.7
|
|
30
30
|
Requires-Dist: json5>=0.9.0
|
|
31
|
-
Requires-Dist: cloudsmith-api<3.0,>=2.0.
|
|
31
|
+
Requires-Dist: cloudsmith-api<3.0,>=2.0.31
|
|
32
32
|
Requires-Dist: keyring>=25.4.1
|
|
33
33
|
Requires-Dist: keyrings-alt>=5.0.2
|
|
34
34
|
Requires-Dist: keyrings-cryptfile>=1.3.9
|
|
@@ -12,6 +12,7 @@ from ..main import main
|
|
|
12
12
|
from .docker import docker as docker_cmd
|
|
13
13
|
from .generic import generic as generic_cmd
|
|
14
14
|
from .manage import install_cmd, list_cmd, uninstall_cmd
|
|
15
|
+
from .pnpm import pnpm as pnpm_cmd
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
@click.group()
|
|
@@ -27,11 +28,18 @@ def credential_helper():
|
|
|
27
28
|
# Install Docker credential helper
|
|
28
29
|
$ cloudsmith credential-helper install docker
|
|
29
30
|
|
|
31
|
+
# Install pnpm credential helper
|
|
32
|
+
$ cloudsmith credential-helper install pnpm
|
|
33
|
+
|
|
30
34
|
# Test Docker credential helper directly
|
|
31
35
|
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker
|
|
36
|
+
|
|
37
|
+
# Test pnpm credential helper directly
|
|
38
|
+
$ cloudsmith credential-helper pnpm npm.cloudsmith.io
|
|
32
39
|
"""
|
|
33
40
|
|
|
34
41
|
|
|
42
|
+
credential_helper.add_command(pnpm_cmd, name="pnpm")
|
|
35
43
|
credential_helper.add_command(docker_cmd, name="docker")
|
|
36
44
|
credential_helper.add_command(generic_cmd, name="generic")
|
|
37
45
|
credential_helper.add_command(install_cmd, name="install")
|
|
@@ -12,6 +12,9 @@ import sys
|
|
|
12
12
|
|
|
13
13
|
import click
|
|
14
14
|
|
|
15
|
+
from cloudsmith_cli.credential_helpers.generic import PartialInstallError
|
|
16
|
+
from cloudsmith_cli.credential_helpers.pnpm.installer import PNPMInstaller
|
|
17
|
+
|
|
15
18
|
from ....credential_helpers.docker.installer import DockerInstaller
|
|
16
19
|
from ... import utils
|
|
17
20
|
from ...decorators import (
|
|
@@ -27,6 +30,7 @@ from ...decorators import (
|
|
|
27
30
|
|
|
28
31
|
_INSTALLERS: dict[str, type] = {
|
|
29
32
|
"docker": DockerInstaller,
|
|
33
|
+
"pnpm": PNPMInstaller,
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
|
|
@@ -40,7 +44,7 @@ def _get_installer(name: str):
|
|
|
40
44
|
|
|
41
45
|
Returns
|
|
42
46
|
-------
|
|
43
|
-
|
|
47
|
+
BaseInstaller
|
|
44
48
|
An instance of the appropriate installer class.
|
|
45
49
|
|
|
46
50
|
Raises
|
|
@@ -85,7 +89,7 @@ def _get_installer(name: str):
|
|
|
85
89
|
"--no-discover",
|
|
86
90
|
is_flag=True,
|
|
87
91
|
default=False,
|
|
88
|
-
help="Disable automatic discovery of custom
|
|
92
|
+
help="Disable automatic discovery of custom domains.",
|
|
89
93
|
)
|
|
90
94
|
@click.option(
|
|
91
95
|
"--refresh",
|
|
@@ -110,25 +114,30 @@ def install_cmd(
|
|
|
110
114
|
) -> None:
|
|
111
115
|
"""Install a credential helper launcher and configure the package manager.
|
|
112
116
|
|
|
113
|
-
HELPER is the name of the credential helper to install (e.g. ``docker``).
|
|
117
|
+
HELPER is the name of the credential helper to install (e.g. ``docker``, ``pnpm``).
|
|
118
|
+
|
|
119
|
+
Important for pnpm: The tokenHelper directive is only honored in the
|
|
120
|
+
user-level ~/.npmrc file, not in a project-level .npmrc. This is a pnpm
|
|
121
|
+
security restriction. The absolute path to the launcher is automatically
|
|
122
|
+
calculated and configured.
|
|
114
123
|
|
|
115
124
|
Examples:
|
|
116
125
|
|
|
117
126
|
\b
|
|
118
|
-
# Install
|
|
119
|
-
$ cloudsmith credential-helper install
|
|
127
|
+
# Install credential helper
|
|
128
|
+
$ cloudsmith credential-helper install HELPER
|
|
120
129
|
|
|
121
130
|
\b
|
|
122
131
|
# Install with a custom domain
|
|
123
|
-
$ cloudsmith credential-helper install
|
|
132
|
+
$ cloudsmith credential-helper install HELPER --domain my.registry.example.com
|
|
124
133
|
|
|
125
134
|
\b
|
|
126
135
|
# Preview without making changes
|
|
127
|
-
$ cloudsmith credential-helper install
|
|
136
|
+
$ cloudsmith credential-helper install HELPER --dry-run
|
|
128
137
|
|
|
129
138
|
\b
|
|
130
139
|
# Disable automatic custom-domain discovery
|
|
131
|
-
$ cloudsmith credential-helper install
|
|
140
|
+
$ cloudsmith credential-helper install HELPER --no-discover
|
|
132
141
|
"""
|
|
133
142
|
installer = _get_installer(helper)
|
|
134
143
|
try:
|
|
@@ -146,6 +155,11 @@ def install_cmd(
|
|
|
146
155
|
raise click.ClickException(
|
|
147
156
|
f"Failed to install {helper!r} credential helper: {exc}"
|
|
148
157
|
)
|
|
158
|
+
except PartialInstallError as exc:
|
|
159
|
+
actions = exc.actions
|
|
160
|
+
ec = exc.exit_code
|
|
161
|
+
else:
|
|
162
|
+
ec = 0
|
|
149
163
|
|
|
150
164
|
use_stderr = utils.should_use_stderr(opts)
|
|
151
165
|
warnings = [a for a in actions if a.startswith("WARNING")]
|
|
@@ -157,7 +171,7 @@ def install_cmd(
|
|
|
157
171
|
"warnings": warnings,
|
|
158
172
|
}
|
|
159
173
|
if utils.maybe_print_as_json(opts, data):
|
|
160
|
-
|
|
174
|
+
sys.exit(ec)
|
|
161
175
|
|
|
162
176
|
if dry_run:
|
|
163
177
|
click.echo("Dry run — no changes will be made:", err=use_stderr)
|
|
@@ -165,6 +179,7 @@ def install_cmd(
|
|
|
165
179
|
click.echo(f" {action}" if dry_run else action, err=use_stderr)
|
|
166
180
|
for warning in warnings:
|
|
167
181
|
click.secho(f" {warning}" if dry_run else warning, err=True, fg="yellow")
|
|
182
|
+
sys.exit(ec)
|
|
168
183
|
|
|
169
184
|
|
|
170
185
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""
|
|
3
|
+
pnpm credential helper command.
|
|
4
|
+
|
|
5
|
+
Implements the pnpm credential helper protocol for Cloudsmith registries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from ....credential_helpers.pnpm import execute
|
|
13
|
+
from ...decorators import common_api_auth_options, resolve_credentials
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.argument("repo", required=False, default="npm.cloudsmith.io")
|
|
18
|
+
@common_api_auth_options
|
|
19
|
+
@resolve_credentials
|
|
20
|
+
def pnpm(opts, repo):
|
|
21
|
+
"""
|
|
22
|
+
Input (arg, optional):
|
|
23
|
+
Server URL as plain text (e.g. "npm.cloudsmith.io")
|
|
24
|
+
|
|
25
|
+
Output (stdout):
|
|
26
|
+
Text: <cloudsmith-token>
|
|
27
|
+
|
|
28
|
+
\b
|
|
29
|
+
Exit codes:
|
|
30
|
+
0: Success
|
|
31
|
+
1: Error (no credentials available, not a Cloudsmith registry, etc.)
|
|
32
|
+
|
|
33
|
+
\b
|
|
34
|
+
Environment variables:
|
|
35
|
+
CLOUDSMITH_API_KEY: API key for authentication (optional)
|
|
36
|
+
CLOUDSMITH_ORG: Organisation slug (required for custom domain support)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
exit_code, stdout, stderr = execute(
|
|
40
|
+
repo,
|
|
41
|
+
credential=opts.credential,
|
|
42
|
+
api_host=opts.api_host,
|
|
43
|
+
org=opts.org,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if stdout is not None:
|
|
47
|
+
click.echo(stdout, nl=False)
|
|
48
|
+
if stderr is not None:
|
|
49
|
+
click.echo(stderr, err=True)
|
|
50
|
+
|
|
51
|
+
sys.exit(exit_code)
|
|
@@ -115,7 +115,9 @@ def list_entitlements(ctx, opts, owner_repo, page, page_size, show_tokens, page_
|
|
|
115
115
|
|
|
116
116
|
click.secho("OK", fg="green", err=use_stderr)
|
|
117
117
|
|
|
118
|
-
print_entitlements(
|
|
118
|
+
print_entitlements(
|
|
119
|
+
opts=opts, data=entitlements_, page_info=page_info, page_all=page_all
|
|
120
|
+
)
|
|
119
121
|
|
|
120
122
|
|
|
121
123
|
@entitlements.command(name="list", aliases=["ls"])
|
|
@@ -126,7 +128,7 @@ def list_(*args, **kwargs): # pylint: disable=missing-docstring
|
|
|
126
128
|
return list_entitlements(*args, **kwargs)
|
|
127
129
|
|
|
128
130
|
|
|
129
|
-
def print_entitlements(opts, data, page_info=None, show_list_info=True):
|
|
131
|
+
def print_entitlements(opts, data, page_info=None, show_list_info=True, page_all=False):
|
|
130
132
|
"""Print entitlements as a table or output in another format."""
|
|
131
133
|
if utils.maybe_print_as_json(opts, data, page_info):
|
|
132
134
|
return
|
|
@@ -163,11 +165,16 @@ def print_entitlements(opts, data, page_info=None, show_list_info=True):
|
|
|
163
165
|
|
|
164
166
|
num_results = len(data)
|
|
165
167
|
list_suffix = "entitlement%s" % ("s" if num_results != 1 else "")
|
|
166
|
-
utils.pretty_print_list_info(
|
|
168
|
+
utils.pretty_print_list_info(
|
|
169
|
+
num_results=num_results,
|
|
170
|
+
page_info=None if page_all else page_info,
|
|
171
|
+
suffix=f"{list_suffix} retrieved" if page_all else f"{list_suffix} visible",
|
|
172
|
+
page_all=page_all,
|
|
173
|
+
)
|
|
167
174
|
|
|
168
175
|
|
|
169
176
|
def print_entitlements_with_restrictions(
|
|
170
|
-
opts, data, page_info=None, show_list_info=True
|
|
177
|
+
opts, data, page_info=None, show_list_info=True, page_all=False
|
|
171
178
|
):
|
|
172
179
|
# pylint: disable=too-many-locals
|
|
173
180
|
"""Print entitlements (with restrictions) as a table or output in another format."""
|
|
@@ -276,7 +283,12 @@ def print_entitlements_with_restrictions(
|
|
|
276
283
|
|
|
277
284
|
num_results = len(data)
|
|
278
285
|
list_suffix = "entitlement%s" % ("s" if num_results != 1 else "")
|
|
279
|
-
utils.pretty_print_list_info(
|
|
286
|
+
utils.pretty_print_list_info(
|
|
287
|
+
num_results=num_results,
|
|
288
|
+
page_info=None if page_all else page_info,
|
|
289
|
+
suffix=f"{list_suffix} retrieved" if page_all else f"{list_suffix} visible",
|
|
290
|
+
page_all=page_all,
|
|
291
|
+
)
|
|
280
292
|
|
|
281
293
|
|
|
282
294
|
@entitlements.command(aliases=["new"])
|
|
@@ -7,7 +7,7 @@ import click
|
|
|
7
7
|
import cloudsmith_api
|
|
8
8
|
|
|
9
9
|
from ...core import keyring
|
|
10
|
-
from .. import decorators, utils
|
|
10
|
+
from .. import decorators, utils, validators
|
|
11
11
|
from ..config import CredentialsReader
|
|
12
12
|
from .main import main
|
|
13
13
|
|
|
@@ -117,8 +117,11 @@ def logout(ctx, opts, api_host, keyring_only, config_only, dry_run):
|
|
|
117
117
|
"--keyring-only and --config-only are mutually exclusive."
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
api_host = (
|
|
121
|
+
validators.normalize_api_host(api_host)
|
|
122
|
+
or opts.api_host
|
|
123
|
+
or cloudsmith_api.Configuration().host
|
|
124
|
+
)
|
|
122
125
|
|
|
123
126
|
use_stderr = utils.should_use_stderr(opts)
|
|
124
127
|
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"""Main command/entrypoint."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import json
|
|
4
6
|
import os
|
|
5
7
|
import shutil
|
|
6
8
|
import sys
|
|
7
9
|
import tempfile
|
|
8
10
|
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
9
12
|
|
|
10
13
|
import click
|
|
11
14
|
import json5
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
from ...core.mcp
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ...core.mcp import server
|
|
18
|
+
from ...core.mcp.data import OpenAPITool
|
|
19
|
+
|
|
15
20
|
from .. import command, decorators, utils
|
|
16
21
|
from .main import main
|
|
17
22
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# pylint: disable=too-many-lines
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import math
|
|
6
7
|
import os
|
|
7
8
|
import shlex
|
|
@@ -694,7 +695,7 @@ def wait_for_package_sync(
|
|
|
694
695
|
# When using stderr for logs, avoid an interactive progress bar and just poll for status.
|
|
695
696
|
while True:
|
|
696
697
|
res = get_package_status(owner, repo, slug)
|
|
697
|
-
ok, failed, _,
|
|
698
|
+
ok, failed, _, status_str, stage_str, reason = res
|
|
698
699
|
if ok or failed:
|
|
699
700
|
break
|
|
700
701
|
|
|
@@ -811,6 +812,34 @@ def wait_for_package_sync(
|
|
|
811
812
|
attempts=attempts,
|
|
812
813
|
)
|
|
813
814
|
else:
|
|
815
|
+
if use_stderr:
|
|
816
|
+
# In JSON output mode the human-readable text above went to
|
|
817
|
+
# stderr, so stdout still needs a machine-readable envelope.
|
|
818
|
+
# Mirror the shape produced by ``handle_api_exceptions`` so a
|
|
819
|
+
# sync failure is parseable the same way as an API failure.
|
|
820
|
+
error_data = {
|
|
821
|
+
"detail": reason or "Package failed to synchronise.",
|
|
822
|
+
"help": {
|
|
823
|
+
"context": context_msg,
|
|
824
|
+
"hint": None,
|
|
825
|
+
},
|
|
826
|
+
"meta": {
|
|
827
|
+
"status": status_str,
|
|
828
|
+
"stage": stage_str or "Unknown",
|
|
829
|
+
"seconds": seconds,
|
|
830
|
+
},
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
metadata_context = getattr(opts, "push_metadata_info", None)
|
|
834
|
+
if metadata_context is not None:
|
|
835
|
+
error_data["metadata_attachment"] = metadata_context
|
|
836
|
+
|
|
837
|
+
click.echo(
|
|
838
|
+
json.dumps(
|
|
839
|
+
error_data, indent=4 if opts.output == "pretty_json" else None
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
|
|
814
843
|
ctx.exit(1)
|
|
815
844
|
|
|
816
845
|
|
|
@@ -341,7 +341,7 @@ class Options: # pylint: disable=too-many-public-methods
|
|
|
341
341
|
@api_host.setter
|
|
342
342
|
def api_host(self, value):
|
|
343
343
|
"""Set value for API host."""
|
|
344
|
-
self._set_option("api_host", value)
|
|
344
|
+
self._set_option("api_host", validators.normalize_api_host(value))
|
|
345
345
|
|
|
346
346
|
@property
|
|
347
347
|
def api_key(self):
|
|
@@ -16,7 +16,6 @@ from ..core.credentials.oidc.detectors import (
|
|
|
16
16
|
disabled_detectors_from_env,
|
|
17
17
|
registered_detectors,
|
|
18
18
|
)
|
|
19
|
-
from ..core.mcp import server
|
|
20
19
|
from ..core.rest import create_requests_session as _create_session
|
|
21
20
|
from . import config, utils
|
|
22
21
|
|
|
@@ -312,7 +311,9 @@ def initialise_session(f):
|
|
|
312
311
|
"""Create a shared HTTP session with proxy/SSL/user-agent settings."""
|
|
313
312
|
|
|
314
313
|
@click.option(
|
|
315
|
-
"--api-host",
|
|
314
|
+
"--api-host",
|
|
315
|
+
envvar="CLOUDSMITH_API_HOST",
|
|
316
|
+
help="The API host to connect to",
|
|
316
317
|
)
|
|
317
318
|
@click.option(
|
|
318
319
|
"--api-proxy",
|
|
@@ -627,6 +628,8 @@ def initialise_mcp(f):
|
|
|
627
628
|
@click.pass_context
|
|
628
629
|
@functools.wraps(f)
|
|
629
630
|
def wrapper(ctx, *args, **kwargs):
|
|
631
|
+
from ..core.mcp import server
|
|
632
|
+
|
|
630
633
|
opts = kwargs.get("opts")
|
|
631
634
|
|
|
632
635
|
all_tools = kwargs.pop("all_tools")
|
|
@@ -26,6 +26,28 @@ def create_configured_session(opts):
|
|
|
26
26
|
return session
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def raise_for_api_error(response):
|
|
30
|
+
"""Raise :class:`ApiException` if *response* failed, keeping the API's detail.
|
|
31
|
+
|
|
32
|
+
Without the detail the exception renders as bare status text, so a caller
|
|
33
|
+
reporting it tells the user that something failed but never what.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
except requests.RequestException as exc:
|
|
38
|
+
try:
|
|
39
|
+
body = exc.response.json()
|
|
40
|
+
except ValueError:
|
|
41
|
+
body = None
|
|
42
|
+
|
|
43
|
+
raise ApiException(
|
|
44
|
+
response.status_code,
|
|
45
|
+
detail=body.get("detail") if isinstance(body, dict) else None,
|
|
46
|
+
headers=exc.response.headers,
|
|
47
|
+
body=exc.response.content,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
def get_idp_url(api_host, owner, session):
|
|
30
52
|
org_saml_url = "{api_host}/orgs/{owner}/saml/?{params}".format(
|
|
31
53
|
api_host=api_host,
|
|
@@ -35,14 +57,7 @@ def get_idp_url(api_host, owner, session):
|
|
|
35
57
|
|
|
36
58
|
org_saml_response = session.get(org_saml_url, timeout=30)
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
org_saml_response.raise_for_status()
|
|
40
|
-
except requests.RequestException as exc:
|
|
41
|
-
raise ApiException(
|
|
42
|
-
org_saml_response.status_code,
|
|
43
|
-
headers=exc.response.headers,
|
|
44
|
-
body=exc.response.content,
|
|
45
|
-
)
|
|
60
|
+
raise_for_api_error(org_saml_response)
|
|
46
61
|
|
|
47
62
|
return org_saml_response.json().get("redirect_url")
|
|
48
63
|
|
|
@@ -60,14 +75,7 @@ def exchange_2fa_token(api_host, two_factor_token, totp_token, session):
|
|
|
60
75
|
timeout=30,
|
|
61
76
|
)
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
exchange_response.raise_for_status()
|
|
65
|
-
except requests.RequestException as exc:
|
|
66
|
-
raise ApiException(
|
|
67
|
-
exchange_response.status_code,
|
|
68
|
-
headers=exc.response.headers,
|
|
69
|
-
body=exc.response.content,
|
|
70
|
-
)
|
|
78
|
+
raise_for_api_error(exchange_response)
|
|
71
79
|
|
|
72
80
|
exchange_data = exchange_response.json()
|
|
73
81
|
access_token = exchange_data.get("access_token")
|
|
@@ -89,14 +97,7 @@ def refresh_access_token(api_host, access_token, refresh_token, session):
|
|
|
89
97
|
timeout=30,
|
|
90
98
|
)
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
response.raise_for_status()
|
|
94
|
-
except requests.RequestException as exc:
|
|
95
|
-
raise ApiException(
|
|
96
|
-
response.status_code,
|
|
97
|
-
headers=exc.response.headers,
|
|
98
|
-
body=exc.response.content,
|
|
99
|
-
)
|
|
100
|
+
raise_for_api_error(response)
|
|
100
101
|
|
|
101
102
|
response_data = response.json()
|
|
102
103
|
access_token = response_data.get("access_token")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CLI - Validators."""
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import re
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from urllib.parse import urlsplit
|
|
6
7
|
|
|
@@ -13,6 +14,8 @@ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
|
13
14
|
BAD_API_HEADERS = ("user-agent", "host")
|
|
14
15
|
API_HEADER_TRANSFORMS = {}
|
|
15
16
|
PUBLIC_API_HOST_SUFFIXES = ("cloudsmith.io", "cloudsmith.com")
|
|
17
|
+
DEFAULT_API_HOST_SCHEME = "https"
|
|
18
|
+
API_HOST_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*://")
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class IntOrWildcard(click.ParamType):
|
|
@@ -86,6 +89,26 @@ def validate_api_headers(param, value):
|
|
|
86
89
|
return headers
|
|
87
90
|
|
|
88
91
|
|
|
92
|
+
def normalize_api_host(value):
|
|
93
|
+
"""Normalise a user-supplied API host into a canonical URL.
|
|
94
|
+
|
|
95
|
+
Removes surrounding whitespace and trailing slashes. Adds the https
|
|
96
|
+
scheme when the value does not give one. Returns None for a blank value,
|
|
97
|
+
so a blank value does not replace a host that is already set.
|
|
98
|
+
"""
|
|
99
|
+
if not isinstance(value, str):
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
host = value.strip()
|
|
103
|
+
if not host:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
if not API_HOST_SCHEME_RE.match(host):
|
|
107
|
+
host = f"{DEFAULT_API_HOST_SCHEME}://{host.lstrip('/')}"
|
|
108
|
+
|
|
109
|
+
return host.rstrip("/")
|
|
110
|
+
|
|
111
|
+
|
|
89
112
|
def host_matches_suffixes(url, suffixes):
|
|
90
113
|
"""True if url's hostname equals or is a subdomain of one of the suffixes.
|
|
91
114
|
|
|
@@ -13,6 +13,8 @@ from ..core.credentials.models import CredentialResult
|
|
|
13
13
|
from ..core.keyring import store_sso_tokens
|
|
14
14
|
from .saml import exchange_2fa_token
|
|
15
15
|
|
|
16
|
+
REJECTED_TWO_FACTOR_STATUSES = frozenset({400, 401, 403, 422})
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
def get_template_path(template_name):
|
|
18
20
|
"""Get the absolute path to a template file."""
|
|
@@ -118,6 +120,10 @@ class AuthenticationWebServer(HTTPServer):
|
|
|
118
120
|
|
|
119
121
|
|
|
120
122
|
class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
|
|
123
|
+
# Set once a response is written, so a later failure doesn't write a
|
|
124
|
+
# second one onto a request the browser already considers finished.
|
|
125
|
+
responded = False
|
|
126
|
+
|
|
121
127
|
def __init__(self, request, client_address, server, **kwargs):
|
|
122
128
|
self.owner = kwargs.get("owner")
|
|
123
129
|
self.debug = kwargs.get("debug", False)
|
|
@@ -132,17 +138,64 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
|
|
|
132
138
|
"""Get the API host from the server instance."""
|
|
133
139
|
return self.server_instance.api_host if self.server_instance else None
|
|
134
140
|
|
|
141
|
+
def _prompt_and_exchange_2fa_token(self, two_factor_token):
|
|
142
|
+
"""Prompt for a 2FA code, and prompt again while the API rejects it."""
|
|
143
|
+
click.echo(err=True)
|
|
144
|
+
click.secho(
|
|
145
|
+
"Your organization requires two-factor authentication.",
|
|
146
|
+
fg="yellow",
|
|
147
|
+
bold=True,
|
|
148
|
+
err=True,
|
|
149
|
+
)
|
|
150
|
+
click.echo(
|
|
151
|
+
"Enter the code from your authenticator app to finish signing in.",
|
|
152
|
+
err=True,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
click.echo("Press Ctrl-C to give up.", err=True)
|
|
156
|
+
|
|
157
|
+
while True:
|
|
158
|
+
totp_token = click.prompt(
|
|
159
|
+
"Please enter your 2FA code", hide_input=True, type=str, err=True
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
return exchange_2fa_token(
|
|
164
|
+
self.api_host, two_factor_token, totp_token, session=self.session
|
|
165
|
+
)
|
|
166
|
+
except ApiException as exc:
|
|
167
|
+
if exc.status not in REJECTED_TWO_FACTOR_STATUSES:
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
click.secho(
|
|
171
|
+
f"\nThat 2FA code was rejected ({exc}). Please try again.",
|
|
172
|
+
fg="red",
|
|
173
|
+
err=True,
|
|
174
|
+
)
|
|
175
|
+
|
|
135
176
|
def _return_response(self, status=200, message=None):
|
|
177
|
+
body = (message or "").encode("utf-8")
|
|
178
|
+
|
|
136
179
|
self.send_response(status)
|
|
137
180
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
181
|
+
# Without a length the client can only detect the end of the body when
|
|
182
|
+
# the connection closes, which is after do_GET returns. The 2FA page is
|
|
183
|
+
# sent before a blocking prompt, so it must be readable straight away.
|
|
184
|
+
self.send_header("Content-Length", str(len(body)))
|
|
138
185
|
self.end_headers()
|
|
139
186
|
|
|
140
|
-
self.wfile.write(
|
|
187
|
+
self.wfile.write(body)
|
|
188
|
+
self.wfile.flush()
|
|
189
|
+
self.responded = True
|
|
141
190
|
|
|
142
191
|
def _return_success_response(self):
|
|
143
192
|
html_content = render_template("auth_success.html")
|
|
144
193
|
self._return_response(message=html_content)
|
|
145
194
|
|
|
195
|
+
def _return_two_factor_response(self):
|
|
196
|
+
html_content = render_template("auth_two_factor.html")
|
|
197
|
+
self._return_response(message=html_content)
|
|
198
|
+
|
|
146
199
|
def _return_error_response(self, error_message=None):
|
|
147
200
|
error_details = ""
|
|
148
201
|
if error_message:
|
|
@@ -210,12 +263,13 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
|
|
|
210
263
|
return
|
|
211
264
|
|
|
212
265
|
if two_factor_token:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
266
|
+
# Answer the browser before prompting. The prompt blocks, so a
|
|
267
|
+
# request left open until then shows the user a blank page with
|
|
268
|
+
# no sign that the terminal is waiting on them.
|
|
269
|
+
self._return_two_factor_response()
|
|
216
270
|
|
|
217
|
-
access_token, refresh_token =
|
|
218
|
-
|
|
271
|
+
access_token, refresh_token = self._prompt_and_exchange_2fa_token(
|
|
272
|
+
two_factor_token
|
|
219
273
|
)
|
|
220
274
|
|
|
221
275
|
# Store the access token on the server instance (same as above)
|
|
@@ -232,10 +286,10 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
|
|
|
232
286
|
self.server_instance.refresh_api_config_after_auth()
|
|
233
287
|
|
|
234
288
|
click.secho("\nAuthentication complete", fg="green", err=True)
|
|
235
|
-
self._return_success_response()
|
|
236
289
|
return
|
|
237
290
|
except Exception:
|
|
238
|
-
self.
|
|
291
|
+
if not self.responded:
|
|
292
|
+
self._return_error_response()
|
|
239
293
|
raise
|
|
240
294
|
|
|
241
295
|
click.secho("\nNo valid authentication parameters received", fg="red", err=True)
|