bt-cli 0.4.29__tar.gz → 0.4.31__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.
- {bt_cli-0.4.29 → bt_cli-0.4.31}/CLAUDE.md +1 -1
- {bt_cli-0.4.29 → bt_cli-0.4.31}/PKG-INFO +1 -1
- {bt_cli-0.4.29 → bt_cli-0.4.31}/pyproject.toml +1 -1
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/rest_debug.py +5 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/passwordsafe.py +69 -30
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/credentials.py +24 -10
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/quick.py +37 -16
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/users.py +11 -5
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/common.py +16 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.env.example +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/.gitignore +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/README.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/assets/cli-help.png +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/assets/cli-output.png +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/bt-cli.spec +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/bt_entry.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/conftest.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws-smoke-test.sh +0 -0
|
@@ -74,6 +74,7 @@ def _sanitize_body(body: Any) -> Any:
|
|
|
74
74
|
"client_secret", "client-secret", "clientsecret",
|
|
75
75
|
"authorization", "bearer", "credential", "credentials",
|
|
76
76
|
"access_token", "refresh_token", "id_token",
|
|
77
|
+
"private_key", "privatekey", "ssh_key", "sshkey", "passphrase",
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if isinstance(body, dict):
|
|
@@ -118,6 +119,10 @@ def _truncate_body(body: Any, max_length: int = 500, sanitize: bool = True) -> s
|
|
|
118
119
|
body = json.dumps(body, indent=2)
|
|
119
120
|
|
|
120
121
|
body_str = str(body)
|
|
122
|
+
|
|
123
|
+
# Detect PEM private key material in string responses
|
|
124
|
+
if sanitize and "-----BEGIN" in body_str and "PRIVATE KEY" in body_str:
|
|
125
|
+
return "[REDACTED - private key material]"
|
|
121
126
|
if len(body_str) > max_length:
|
|
122
127
|
return body_str[:max_length] + f"\n... ({len(body_str) - max_length} more chars)"
|
|
123
128
|
return body_str
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Optional, TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from ..models.common import CredentialType, CREDENTIAL_TYPE_META
|
|
6
|
+
|
|
5
7
|
if TYPE_CHECKING:
|
|
6
8
|
from .base import PasswordSafeClient
|
|
7
9
|
|
|
@@ -378,19 +380,32 @@ class PasswordSafeMixin:
|
|
|
378
380
|
access_type=access_type,
|
|
379
381
|
)
|
|
380
382
|
|
|
381
|
-
def get_credential(
|
|
382
|
-
""
|
|
383
|
+
def get_credential(
|
|
384
|
+
self: "PasswordSafeClient",
|
|
385
|
+
request_id: int,
|
|
386
|
+
credential_type: Optional[str] = None,
|
|
387
|
+
) -> dict[str, Any]:
|
|
388
|
+
"""Get the credential for an approved request.
|
|
383
389
|
|
|
384
390
|
Args:
|
|
385
391
|
request_id: Request ID
|
|
392
|
+
credential_type: Type of credential to retrieve:
|
|
393
|
+
password (default), dsskey (SSH private key), passphrase
|
|
386
394
|
|
|
387
395
|
Returns:
|
|
388
|
-
Credential
|
|
396
|
+
Credential dict with key based on type (Password, PrivateKey, or Passphrase)
|
|
389
397
|
"""
|
|
390
|
-
|
|
391
|
-
|
|
398
|
+
# Only send type param for non-default types (preserves backward compat)
|
|
399
|
+
params = {"type": credential_type} if credential_type and credential_type != "password" else None
|
|
400
|
+
result = self.get(f"/Credentials/{request_id}", params=params)
|
|
401
|
+
|
|
402
|
+
# Determine the response dict key based on credential type
|
|
403
|
+
ctype = CredentialType(credential_type) if credential_type else CredentialType.PASSWORD
|
|
404
|
+
meta = CREDENTIAL_TYPE_META[ctype]
|
|
405
|
+
|
|
406
|
+
# API returns credential as plain string, wrap in dict for consistency
|
|
392
407
|
if isinstance(result, str):
|
|
393
|
-
return {"
|
|
408
|
+
return {meta["key"]: result}
|
|
394
409
|
return result
|
|
395
410
|
|
|
396
411
|
def checkin_request(
|
|
@@ -806,16 +821,29 @@ class PasswordSafeMixin:
|
|
|
806
821
|
) -> list[dict[str, Any]]:
|
|
807
822
|
"""List user groups.
|
|
808
823
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
Returns:
|
|
813
|
-
List of user group objects
|
|
824
|
+
/UserGroups does not support pagination. The ``name`` server-side
|
|
825
|
+
param is an exact match, so filtering is handled client-side.
|
|
814
826
|
"""
|
|
815
|
-
|
|
827
|
+
result = self.get("/UserGroups")
|
|
828
|
+
if isinstance(result, list):
|
|
829
|
+
groups = result
|
|
830
|
+
elif isinstance(result, dict):
|
|
831
|
+
if "GroupID" in result:
|
|
832
|
+
groups = [result]
|
|
833
|
+
else:
|
|
834
|
+
groups = result.get("Data", result.get("results", []))
|
|
835
|
+
else:
|
|
836
|
+
groups = []
|
|
837
|
+
|
|
816
838
|
if search:
|
|
817
|
-
|
|
818
|
-
|
|
839
|
+
search_lower = search.lower()
|
|
840
|
+
groups = [
|
|
841
|
+
g for g in groups
|
|
842
|
+
if search_lower in (g.get("Name", "") or "").lower()
|
|
843
|
+
or search_lower in (g.get("Description", "") or "").lower()
|
|
844
|
+
]
|
|
845
|
+
|
|
846
|
+
return groups
|
|
819
847
|
|
|
820
848
|
def get_user_group(self: "PasswordSafeClient", group_id: int) -> dict[str, Any]:
|
|
821
849
|
"""Get a user group by ID.
|
|
@@ -833,13 +861,16 @@ class PasswordSafeMixin:
|
|
|
833
861
|
) -> list[dict[str, Any]]:
|
|
834
862
|
"""Get users in a user group.
|
|
835
863
|
|
|
836
|
-
|
|
837
|
-
group_id: User group ID
|
|
838
|
-
|
|
839
|
-
Returns:
|
|
840
|
-
List of user objects (includes ClientID, AccessPolicyID for API users)
|
|
864
|
+
/UserGroups/{id}/Users does not support pagination.
|
|
841
865
|
"""
|
|
842
|
-
|
|
866
|
+
result = self.get(f"/UserGroups/{group_id}/Users")
|
|
867
|
+
if isinstance(result, list):
|
|
868
|
+
return result
|
|
869
|
+
if isinstance(result, dict):
|
|
870
|
+
if "UserID" in result:
|
|
871
|
+
return [result]
|
|
872
|
+
return result.get("Data", result.get("results", []))
|
|
873
|
+
return []
|
|
843
874
|
|
|
844
875
|
# =========================================================================
|
|
845
876
|
# Users
|
|
@@ -849,20 +880,29 @@ class PasswordSafeMixin:
|
|
|
849
880
|
self: "PasswordSafeClient",
|
|
850
881
|
search: Optional[str] = None,
|
|
851
882
|
limit: Optional[int] = None,
|
|
883
|
+
include_inactive: bool = False,
|
|
852
884
|
) -> list[dict[str, Any]]:
|
|
853
885
|
"""List users.
|
|
854
886
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
Returns:
|
|
860
|
-
List of user objects
|
|
887
|
+
/Users does not support pagination. The ``username`` server-side
|
|
888
|
+
param is an exact match (returns a single user dict or 404), so
|
|
889
|
+
searching is handled client-side across username/first/last/email.
|
|
861
890
|
"""
|
|
862
|
-
|
|
863
|
-
|
|
891
|
+
params: dict[str, Any] = {}
|
|
892
|
+
if include_inactive:
|
|
893
|
+
params["includeInactive"] = "true"
|
|
894
|
+
|
|
895
|
+
result = self.get("/Users", params=params)
|
|
896
|
+
if isinstance(result, list):
|
|
897
|
+
users = result
|
|
898
|
+
elif isinstance(result, dict):
|
|
899
|
+
if "UserID" in result:
|
|
900
|
+
users = [result]
|
|
901
|
+
else:
|
|
902
|
+
users = result.get("Data", result.get("results", []))
|
|
903
|
+
else:
|
|
904
|
+
users = []
|
|
864
905
|
|
|
865
|
-
# Apply client-side search filter
|
|
866
906
|
if search:
|
|
867
907
|
search_lower = search.lower()
|
|
868
908
|
users = [
|
|
@@ -873,7 +913,6 @@ class PasswordSafeMixin:
|
|
|
873
913
|
or search_lower in (u.get("EmailAddress", "") or "").lower()
|
|
874
914
|
]
|
|
875
915
|
|
|
876
|
-
# Apply limit
|
|
877
916
|
if limit is not None:
|
|
878
917
|
users = users[:limit]
|
|
879
918
|
|
|
@@ -6,11 +6,13 @@ import json
|
|
|
6
6
|
import httpx
|
|
7
7
|
import typer
|
|
8
8
|
from rich.console import Console
|
|
9
|
+
from rich.markup import escape as rich_escape
|
|
9
10
|
from rich.table import Table
|
|
10
11
|
from rich.panel import Panel
|
|
11
12
|
|
|
12
13
|
from ...core.output import print_error, print_api_error
|
|
13
14
|
from ..client.base import get_client
|
|
15
|
+
from ..models.common import CredentialType, CREDENTIAL_TYPE_META
|
|
14
16
|
|
|
15
17
|
app = typer.Typer(no_args_is_help=True, help="Checkout and manage credentials in Password Safe")
|
|
16
18
|
console = Console()
|
|
@@ -77,7 +79,8 @@ def checkout_credential(
|
|
|
77
79
|
f"System: {system}\n"
|
|
78
80
|
f"Account: {account}\n"
|
|
79
81
|
f"Duration: {duration} minutes\n\n"
|
|
80
|
-
f"[dim]Use 'pws credentials show {request_id}' to get the password[/dim]"
|
|
82
|
+
f"[dim]Use 'pws credentials show {request_id}' to get the password[/dim]\n"
|
|
83
|
+
f"[dim]For SSH keys: 'pws credentials show {request_id} --credential-type dsskey'[/dim]",
|
|
81
84
|
title="Checkout Request",
|
|
82
85
|
))
|
|
83
86
|
|
|
@@ -98,31 +101,42 @@ def checkout_credential(
|
|
|
98
101
|
@app.command("show")
|
|
99
102
|
def show_credential(
|
|
100
103
|
request_id: int = typer.Argument(..., help="Request ID"),
|
|
104
|
+
credential_type: str = typer.Option(
|
|
105
|
+
"password", "--credential-type", help="Credential type: password, dsskey, passphrase"
|
|
106
|
+
),
|
|
101
107
|
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
102
|
-
raw: bool = typer.Option(False, "--raw", "-r", help="Output only the
|
|
108
|
+
raw: bool = typer.Option(False, "--raw", "-r", help="Output only the credential value (for scripts)"),
|
|
103
109
|
) -> None:
|
|
104
|
-
"""Get the
|
|
110
|
+
"""Get the credential for an approved request.
|
|
105
111
|
|
|
106
112
|
Example:
|
|
107
113
|
pws credentials show 12345
|
|
108
|
-
pws credentials show 12345 --raw
|
|
109
|
-
|
|
114
|
+
pws credentials show 12345 --raw
|
|
115
|
+
pws credentials show 12345 --credential-type dsskey --raw
|
|
116
|
+
KEY=$(bt pws credentials show 12345 --credential-type dsskey --raw)
|
|
110
117
|
"""
|
|
118
|
+
try:
|
|
119
|
+
ctype = CredentialType(credential_type)
|
|
120
|
+
except ValueError:
|
|
121
|
+
print_error(f"Invalid credential type: {credential_type}. Must be: password, dsskey, passphrase")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
meta = CREDENTIAL_TYPE_META[ctype]
|
|
125
|
+
|
|
111
126
|
try:
|
|
112
127
|
with get_client() as client:
|
|
113
128
|
client.authenticate()
|
|
114
|
-
credential = client.get_credential(request_id)
|
|
129
|
+
credential = client.get_credential(request_id, credential_type=credential_type)
|
|
115
130
|
|
|
116
131
|
if raw:
|
|
117
|
-
|
|
118
|
-
print(credential.get("Password", ""), end="")
|
|
132
|
+
print(credential.get(meta["key"], ""), end="")
|
|
119
133
|
elif output == "json":
|
|
120
134
|
console.print_json(json.dumps(credential, default=str))
|
|
121
135
|
else:
|
|
122
|
-
|
|
136
|
+
value = rich_escape(credential.get(meta["key"], "N/A"))
|
|
123
137
|
console.print(Panel(
|
|
124
138
|
f"Request ID: [bold cyan]{request_id}[/bold cyan]\n"
|
|
125
|
-
f"
|
|
139
|
+
f"{meta['label']}: [bold green]{value}[/bold green]",
|
|
126
140
|
title="Credential",
|
|
127
141
|
))
|
|
128
142
|
|
|
@@ -6,12 +6,14 @@ import json
|
|
|
6
6
|
import httpx
|
|
7
7
|
import typer
|
|
8
8
|
from rich.console import Console
|
|
9
|
+
from rich.markup import escape as rich_escape
|
|
9
10
|
from rich.panel import Panel
|
|
10
11
|
from rich.table import Table
|
|
11
12
|
|
|
12
13
|
from ...core.output import print_api_error, print_error, print_success, print_warning
|
|
13
14
|
from ...core.prompts import prompt_if_missing, prompt_from_list, prompt_choice
|
|
14
15
|
from ..client.base import get_client
|
|
16
|
+
from ..models.common import CredentialType, CREDENTIAL_TYPE_META
|
|
15
17
|
|
|
16
18
|
app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
|
|
17
19
|
console = Console()
|
|
@@ -23,12 +25,13 @@ def quick_checkout(
|
|
|
23
25
|
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
24
26
|
duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
|
|
25
27
|
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for checkout"),
|
|
26
|
-
|
|
28
|
+
credential_type: str = typer.Option("password", "--credential-type", help="Credential type: password, dsskey, passphrase"),
|
|
29
|
+
raw: bool = typer.Option(False, "--raw", help="Output only the credential value (for scripts)"),
|
|
27
30
|
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
28
31
|
) -> None:
|
|
29
|
-
"""Checkout credentials and show password in one step.
|
|
32
|
+
"""Checkout credentials and show password/SSH key in one step.
|
|
30
33
|
|
|
31
|
-
Combines: find system -> find account -> checkout -> show
|
|
34
|
+
Combines: find system -> find account -> checkout -> show credential
|
|
32
35
|
|
|
33
36
|
If system or account not provided, prompts interactively.
|
|
34
37
|
|
|
@@ -37,6 +40,7 @@ def quick_checkout(
|
|
|
37
40
|
bt pws quick checkout -s "axion-finapp-01" -a "root"
|
|
38
41
|
bt pws quick checkout -s axion -a root --duration 30
|
|
39
42
|
PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
43
|
+
KEY=$(bt pws quick checkout -s server -a admin --credential-type dsskey --raw)
|
|
40
44
|
"""
|
|
41
45
|
try:
|
|
42
46
|
with get_client() as client:
|
|
@@ -115,12 +119,19 @@ def quick_checkout(
|
|
|
115
119
|
request_id = request.get("RequestID")
|
|
116
120
|
|
|
117
121
|
# Step 4: Get the credential
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
try:
|
|
123
|
+
ctype = CredentialType(credential_type)
|
|
124
|
+
except ValueError:
|
|
125
|
+
print_error(f"Invalid credential type: {credential_type}. Must be: password, dsskey, passphrase")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
meta = CREDENTIAL_TYPE_META[ctype]
|
|
128
|
+
|
|
129
|
+
credential = client.get_credential(request_id, credential_type=credential_type)
|
|
130
|
+
value = credential.get(meta["key"], "")
|
|
120
131
|
|
|
121
132
|
# Output
|
|
122
133
|
if raw:
|
|
123
|
-
print(
|
|
134
|
+
print(value, end="")
|
|
124
135
|
elif output == "json":
|
|
125
136
|
result = {
|
|
126
137
|
"request_id": request_id,
|
|
@@ -128,7 +139,8 @@ def quick_checkout(
|
|
|
128
139
|
"system_id": system_id,
|
|
129
140
|
"account": account_name,
|
|
130
141
|
"account_id": account_id,
|
|
131
|
-
"
|
|
142
|
+
"credential_type": credential_type,
|
|
143
|
+
meta["key"]: value,
|
|
132
144
|
"duration_minutes": duration,
|
|
133
145
|
}
|
|
134
146
|
console.print_json(json.dumps(result))
|
|
@@ -139,7 +151,7 @@ def quick_checkout(
|
|
|
139
151
|
f"Account: [cyan]{account_name}[/cyan] (ID: {account_id})\n"
|
|
140
152
|
f"Request ID: [bold yellow]{request_id}[/bold yellow]\n"
|
|
141
153
|
f"Duration: {duration} minutes\n\n"
|
|
142
|
-
f"
|
|
154
|
+
f"{meta['label']}: [bold green]{rich_escape(value)}[/bold green]\n\n"
|
|
143
155
|
f"[dim]Checkin: bt pws credentials checkin {request_id}[/dim]",
|
|
144
156
|
title="Quick Checkout",
|
|
145
157
|
))
|
|
@@ -288,16 +300,18 @@ def quick_password(
|
|
|
288
300
|
system: Optional[str] = typer.Option(None, "--system", "-s", help="System name"),
|
|
289
301
|
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
290
302
|
duration: int = typer.Option(5, "--duration", "-d", help="Duration in minutes (default: 5)"),
|
|
291
|
-
|
|
303
|
+
credential_type: str = typer.Option("password", "--credential-type", help="Credential type: password, dsskey, passphrase"),
|
|
304
|
+
auto_checkin: bool = typer.Option(True, "--auto-checkin/--no-auto-checkin", help="Auto checkin after showing credential"),
|
|
292
305
|
) -> None:
|
|
293
|
-
"""Get a
|
|
306
|
+
"""Get a credential quickly - checkout, show, and optionally auto-checkin.
|
|
294
307
|
|
|
295
|
-
Ideal for quick lookups where you just need to see/copy the password.
|
|
308
|
+
Ideal for quick lookups where you just need to see/copy the password or SSH key.
|
|
296
309
|
If system or account not provided, prompts interactively.
|
|
297
310
|
|
|
298
311
|
Examples:
|
|
299
312
|
bt pws quick password # Interactive mode
|
|
300
313
|
bt pws quick password -s server -a root
|
|
314
|
+
bt pws quick password -s server -a root --credential-type dsskey
|
|
301
315
|
bt pws quick password -s db-server -a admin --no-auto-checkin
|
|
302
316
|
"""
|
|
303
317
|
try:
|
|
@@ -361,12 +375,19 @@ def quick_password(
|
|
|
361
375
|
)
|
|
362
376
|
request_id = request.get("RequestID")
|
|
363
377
|
|
|
364
|
-
# Get
|
|
365
|
-
|
|
366
|
-
|
|
378
|
+
# Get credential
|
|
379
|
+
try:
|
|
380
|
+
ctype = CredentialType(credential_type)
|
|
381
|
+
except ValueError:
|
|
382
|
+
print_error(f"Invalid credential type: {credential_type}. Must be: password, dsskey, passphrase")
|
|
383
|
+
raise typer.Exit(1)
|
|
384
|
+
meta = CREDENTIAL_TYPE_META[ctype]
|
|
385
|
+
|
|
386
|
+
credential = client.get_credential(request_id, credential_type=credential_type)
|
|
387
|
+
value = credential.get(meta["key"], "")
|
|
367
388
|
|
|
368
|
-
# Show
|
|
369
|
-
console.print(f"\n[bold green]{
|
|
389
|
+
# Show credential
|
|
390
|
+
console.print(f"\n[bold green]{rich_escape(value)}[/bold green]\n")
|
|
370
391
|
console.print(f"[dim]{account_name}@{system_name} (Request: {request_id})[/dim]")
|
|
371
392
|
|
|
372
393
|
# Auto checkin
|
|
@@ -179,23 +179,29 @@ def print_roles_table(roles: list[dict], title: str = "Roles") -> None:
|
|
|
179
179
|
|
|
180
180
|
@app.command("list")
|
|
181
181
|
def list_users(
|
|
182
|
-
search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by username"),
|
|
182
|
+
search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by username (also matches first/last/email)"),
|
|
183
183
|
limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
|
|
184
|
-
fetch_all: bool = typer.Option(False, "--all", help="Fetch all results
|
|
184
|
+
fetch_all: bool = typer.Option(False, "--all", help="Fetch all results"),
|
|
185
|
+
include_inactive: bool = typer.Option(False, "--include-inactive", help="Include inactive users"),
|
|
185
186
|
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
186
187
|
) -> None:
|
|
187
188
|
"""List all users.
|
|
188
189
|
|
|
189
190
|
Examples:
|
|
190
|
-
bt pws users list # First 50 users
|
|
191
|
-
bt pws users list --all # All users
|
|
191
|
+
bt pws users list # First 50 active users
|
|
192
|
+
bt pws users list --all # All active users
|
|
193
|
+
bt pws users list --include-inactive # Include inactive users
|
|
192
194
|
bt pws users list -s "admin" # Search by username
|
|
193
195
|
bt pws users list -o json # JSON output
|
|
194
196
|
"""
|
|
195
197
|
try:
|
|
196
198
|
with get_client() as client:
|
|
197
199
|
client.authenticate()
|
|
198
|
-
users = client.list_users(
|
|
200
|
+
users = client.list_users(
|
|
201
|
+
search=search,
|
|
202
|
+
limit=None if fetch_all else limit,
|
|
203
|
+
include_inactive=include_inactive,
|
|
204
|
+
)
|
|
199
205
|
|
|
200
206
|
if output == "json":
|
|
201
207
|
console.print_json(json.dumps(users, default=str))
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
"""Common models and types shared across the API."""
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from typing import Any, Generic, Optional, TypeVar
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from pydantic import BaseModel, ConfigDict
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
class CredentialType(str, Enum):
|
|
10
|
+
"""Type of credential to retrieve from Password Safe."""
|
|
11
|
+
|
|
12
|
+
PASSWORD = "password"
|
|
13
|
+
DSSKEY = "dsskey"
|
|
14
|
+
PASSPHRASE = "passphrase"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CREDENTIAL_TYPE_META = {
|
|
18
|
+
CredentialType.PASSWORD: {"key": "Password", "label": "Password"},
|
|
19
|
+
CredentialType.DSSKEY: {"key": "PrivateKey", "label": "Private Key"},
|
|
20
|
+
CredentialType.PASSPHRASE: {"key": "Passphrase", "label": "Passphrase"},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
8
24
|
# Type variable for generic paginated responses
|
|
9
25
|
T = TypeVar("T", bound=BaseModel)
|
|
10
26
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|