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.
Files changed (174) hide show
  1. {bt_cli-0.4.29 → bt_cli-0.4.31}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.29 → bt_cli-0.4.31}/PKG-INFO +1 -1
  3. {bt_cli-0.4.29 → bt_cli-0.4.31}/pyproject.toml +1 -1
  4. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/rest_debug.py +5 -0
  6. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/passwordsafe.py +69 -30
  7. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/credentials.py +24 -10
  8. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/quick.py +37 -16
  9. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/users.py +11 -5
  10. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/common.py +16 -0
  11. {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/bt/SKILL.md +0 -0
  12. {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/entitle/SKILL.md +0 -0
  13. {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/epmw/SKILL.md +0 -0
  14. {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/pra/SKILL.md +0 -0
  15. {bt_cli-0.4.29 → bt_cli-0.4.31}/.claude/skills/pws/SKILL.md +0 -0
  16. {bt_cli-0.4.29 → bt_cli-0.4.31}/.env.example +0 -0
  17. {bt_cli-0.4.29 → bt_cli-0.4.31}/.github/workflows/ci.yml +0 -0
  18. {bt_cli-0.4.29 → bt_cli-0.4.31}/.github/workflows/release.yml +0 -0
  19. {bt_cli-0.4.29 → bt_cli-0.4.31}/.gitignore +0 -0
  20. {bt_cli-0.4.29 → bt_cli-0.4.31}/README.md +0 -0
  21. {bt_cli-0.4.29 → bt_cli-0.4.31}/assets/cli-help.png +0 -0
  22. {bt_cli-0.4.29 → bt_cli-0.4.31}/assets/cli-output.png +0 -0
  23. {bt_cli-0.4.29 → bt_cli-0.4.31}/bt-cli.spec +0 -0
  24. {bt_cli-0.4.29 → bt_cli-0.4.31}/bt_entry.py +0 -0
  25. {bt_cli-0.4.29 → bt_cli-0.4.31}/scripts/bt_entry.py +0 -0
  26. {bt_cli-0.4.29 → bt_cli-0.4.31}/scripts/sync-package-data.sh +0 -0
  27. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/cli.py +0 -0
  28. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/__init__.py +0 -0
  29. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/configure.py +0 -0
  30. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/learn.py +0 -0
  31. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/commands/quick.py +0 -0
  32. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/__init__.py +0 -0
  33. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/auth.py +0 -0
  34. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/client.py +0 -0
  35. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/config.py +0 -0
  36. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/config_file.py +0 -0
  37. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/csv_utils.py +0 -0
  38. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/errors.py +0 -0
  39. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/output.py +0 -0
  40. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/core/prompts.py +0 -0
  41. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/CLAUDE.md +0 -0
  42. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/__init__.py +0 -0
  43. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  44. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  45. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  46. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  47. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  48. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/__init__.py +0 -0
  49. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/client/__init__.py +0 -0
  50. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/client/base.py +0 -0
  51. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/__init__.py +0 -0
  52. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/accounts.py +0 -0
  53. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/applications.py +0 -0
  54. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/auth.py +0 -0
  55. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/bundles.py +0 -0
  56. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/integrations.py +0 -0
  57. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/permissions.py +0 -0
  58. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/policies.py +0 -0
  59. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/resources.py +0 -0
  60. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/roles.py +0 -0
  61. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/users.py +0 -0
  62. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/commands/workflows.py +0 -0
  63. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/__init__.py +0 -0
  64. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/bundle.py +0 -0
  65. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/common.py +0 -0
  66. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/integration.py +0 -0
  67. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/permission.py +0 -0
  68. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/policy.py +0 -0
  69. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/resource.py +0 -0
  70. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/role.py +0 -0
  71. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/user.py +0 -0
  72. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/entitle/models/workflow.py +0 -0
  73. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/__init__.py +0 -0
  74. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/client/__init__.py +0 -0
  75. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/client/base.py +0 -0
  76. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/__init__.py +0 -0
  77. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/audits.py +0 -0
  78. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/auth.py +0 -0
  79. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/computers.py +0 -0
  80. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/events.py +0 -0
  81. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/groups.py +0 -0
  82. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/policies.py +0 -0
  83. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/quick.py +0 -0
  84. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/requests.py +0 -0
  85. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/roles.py +0 -0
  86. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/tasks.py +0 -0
  87. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/commands/users.py +0 -0
  88. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/epmw/models/__init__.py +0 -0
  89. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/__init__.py +0 -0
  90. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/client/__init__.py +0 -0
  91. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/client/base.py +0 -0
  92. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/__init__.py +0 -0
  93. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/auth.py +0 -0
  94. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/import_export.py +0 -0
  95. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  96. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  97. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jump_items.py +0 -0
  98. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  99. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/policies.py +0 -0
  100. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/quick.py +0 -0
  101. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/teams.py +0 -0
  102. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/users.py +0 -0
  103. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/commands/vault.py +0 -0
  104. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/__init__.py +0 -0
  105. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/common.py +0 -0
  106. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jump_client.py +0 -0
  107. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jump_group.py +0 -0
  108. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jump_item.py +0 -0
  109. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/jumpoint.py +0 -0
  110. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/team.py +0 -0
  111. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/user.py +0 -0
  112. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pra/models/vault.py +0 -0
  113. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/__init__.py +0 -0
  114. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/__init__.py +0 -0
  115. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/base.py +0 -0
  116. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  117. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/__init__.py +0 -0
  118. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/accounts.py +0 -0
  119. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/assets.py +0 -0
  120. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/attributes.py +0 -0
  121. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/auth.py +0 -0
  122. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/clouds.py +0 -0
  123. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/config.py +0 -0
  124. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/databases.py +0 -0
  125. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/directories.py +0 -0
  126. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/functional.py +0 -0
  127. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/import_export.py +0 -0
  128. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/platforms.py +0 -0
  129. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/search.py +0 -0
  130. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/secrets.py +0 -0
  131. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/systems.py +0 -0
  132. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/commands/workgroups.py +0 -0
  133. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/config.py +0 -0
  134. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/__init__.py +0 -0
  135. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/account.py +0 -0
  136. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/asset.py +0 -0
  137. {bt_cli-0.4.29 → bt_cli-0.4.31}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.29 → bt_cli-0.4.31}/tests/pws-smoke-test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.29**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.31**
4
4
 
5
5
  ## Setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.29
3
+ Version: 0.4.31
4
4
  Summary: BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM
5
5
  Author-email: Dave Grendysz <dgrendysz@beyondtrust.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.29"
7
+ version = "0.4.31"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.29"
3
+ __version__ = "0.4.31"
@@ -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(self: "PasswordSafeClient", request_id: int) -> dict[str, Any]:
382
- """Get the credential (password) for an approved request.
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 object with Password field
396
+ Credential dict with key based on type (Password, PrivateKey, or Passphrase)
389
397
  """
390
- result = self.get(f"/Credentials/{request_id}")
391
- # API returns password as plain string, wrap in dict for consistency
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 {"Password": result}
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
- Args:
810
- search: Optional name filter
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
- params = {}
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
- params["name"] = search
818
- return self.paginate("/UserGroups", params=params)
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
- Args:
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
- return self.paginate(f"/UserGroups/{group_id}/Users")
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
- Args:
856
- search: Optional username search filter (client-side filtering)
857
- limit: Maximum number of results (None for all)
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
- # API doesn't support server-side search, so we fetch all and filter client-side
863
- users = self.paginate("/Users")
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 password (for scripts)"),
108
+ raw: bool = typer.Option(False, "--raw", "-r", help="Output only the credential value (for scripts)"),
103
109
  ) -> None:
104
- """Get the password for an approved credential request.
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 # Just the password for scripts
109
- PASSWORD=$(bt pws credentials show 12345 --raw)
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
- # Output just the password with no formatting (for scripts)
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
- password = credential.get("Password", "N/A")
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"Password: [bold green]{password}[/bold green]",
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
- raw: bool = typer.Option(False, "--raw", help="Output only the password (for scripts)"),
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 password
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
- credential = client.get_credential(request_id)
119
- password = credential.get("Password", "")
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(password, end="")
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
- "password": password,
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"Password: [bold green]{password}[/bold green]\n\n"
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
- auto_checkin: bool = typer.Option(True, "--auto-checkin/--no-auto-checkin", help="Auto checkin after showing password"),
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 password quickly - checkout, show, and optionally auto-checkin.
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 password
365
- credential = client.get_credential(request_id)
366
- password = credential.get("Password", "")
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 password
369
- console.print(f"\n[bold green]{password}[/bold green]\n")
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 (may be slow)"),
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(search=search, limit=None if fetch_all else limit)
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