bt-cli 0.4.55__tar.gz → 0.4.57__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 (246) hide show
  1. {bt_cli-0.4.55 → bt_cli-0.4.57}/CLAUDE.md +3 -3
  2. {bt_cli-0.4.55 → bt_cli-0.4.57}/PKG-INFO +1 -1
  3. {bt_cli-0.4.55 → bt_cli-0.4.57}/pyproject.toml +1 -1
  4. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/configure.py +66 -4
  6. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/pf/SKILL.md +46 -6
  7. bt_cli-0.4.57/src/bt_cli/pf/client/base.py +225 -0
  8. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/__init__.py +2 -0
  9. bt_cli-0.4.57/src/bt_cli/pf/commands/users.py +298 -0
  10. bt_cli-0.4.57/tests/pf/test_client.py +159 -0
  11. bt_cli-0.4.57/tests/pf/test_commands.py +266 -0
  12. bt_cli-0.4.55/src/bt_cli/pf/client/base.py +0 -133
  13. bt_cli-0.4.55/tests/pf/test_client.py +0 -79
  14. bt_cli-0.4.55/tests/pf/test_commands.py +0 -121
  15. {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/bt/SKILL.md +0 -0
  16. {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/entitle/SKILL.md +0 -0
  17. {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/epml/SKILL.md +0 -0
  18. {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/epmw/SKILL.md +0 -0
  19. {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/pra/SKILL.md +0 -0
  20. {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/pws/SKILL.md +0 -0
  21. {bt_cli-0.4.55 → bt_cli-0.4.57}/.env.example +0 -0
  22. {bt_cli-0.4.55 → bt_cli-0.4.57}/.github/workflows/ci.yml +0 -0
  23. {bt_cli-0.4.55 → bt_cli-0.4.57}/.github/workflows/release.yml +0 -0
  24. {bt_cli-0.4.55 → bt_cli-0.4.57}/.gitignore +0 -0
  25. {bt_cli-0.4.55 → bt_cli-0.4.57}/README.md +0 -0
  26. {bt_cli-0.4.55 → bt_cli-0.4.57}/assets/cli-help.png +0 -0
  27. {bt_cli-0.4.55 → bt_cli-0.4.57}/assets/cli-output.png +0 -0
  28. {bt_cli-0.4.55 → bt_cli-0.4.57}/bt-cli.spec +0 -0
  29. {bt_cli-0.4.55 → bt_cli-0.4.57}/bt_entry.py +0 -0
  30. {bt_cli-0.4.55 → bt_cli-0.4.57}/epml-clients-server-side-filters-plan.md +0 -0
  31. {bt_cli-0.4.55 → bt_cli-0.4.57}/epml-implementation-plan.md +0 -0
  32. {bt_cli-0.4.55 → bt_cli-0.4.57}/pf-implementation-plan.md +0 -0
  33. {bt_cli-0.4.55 → bt_cli-0.4.57}/scripts/bt_entry.py +0 -0
  34. {bt_cli-0.4.55 → bt_cli-0.4.57}/scripts/pf_onboard.py +0 -0
  35. {bt_cli-0.4.55 → bt_cli-0.4.57}/scripts/sync-package-data.sh +0 -0
  36. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/cli.py +0 -0
  37. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/__init__.py +0 -0
  38. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/learn.py +0 -0
  39. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/quick.py +0 -0
  40. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/__init__.py +0 -0
  41. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/auth.py +0 -0
  42. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/client.py +0 -0
  43. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/config.py +0 -0
  44. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/config_file.py +0 -0
  45. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/csv_utils.py +0 -0
  46. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/errors.py +0 -0
  47. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/output.py +0 -0
  48. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/prompts.py +0 -0
  49. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/rest_debug.py +0 -0
  50. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/CLAUDE.md +0 -0
  51. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/__init__.py +0 -0
  52. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  53. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  54. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
  55. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  56. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  57. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  58. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
  59. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/__init__.py +0 -0
  60. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/client/__init__.py +0 -0
  61. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/client/base.py +0 -0
  62. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/__init__.py +0 -0
  63. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/accounts.py +0 -0
  64. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/applications.py +0 -0
  65. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/auth.py +0 -0
  66. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/bundles.py +0 -0
  67. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/integrations.py +0 -0
  68. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/permissions.py +0 -0
  69. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/policies.py +0 -0
  70. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/requests.py +0 -0
  71. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/resources.py +0 -0
  72. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/roles.py +0 -0
  73. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/users.py +0 -0
  74. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/workflows.py +0 -0
  75. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/__init__.py +0 -0
  76. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/bundle.py +0 -0
  77. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/common.py +0 -0
  78. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/integration.py +0 -0
  79. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/permission.py +0 -0
  80. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/policy.py +0 -0
  81. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/resource.py +0 -0
  82. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/role.py +0 -0
  83. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/user.py +0 -0
  84. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/workflow.py +0 -0
  85. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/__init__.py +0 -0
  86. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/client/__init__.py +0 -0
  87. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/client/base.py +0 -0
  88. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/__init__.py +0 -0
  89. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/audit.py +0 -0
  90. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/auth.py +0 -0
  91. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  92. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/clients.py +0 -0
  93. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/external_apis.py +0 -0
  94. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/hosts.py +0 -0
  95. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/iolog.py +0 -0
  96. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/license.py +0 -0
  97. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/quick.py +0 -0
  98. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  99. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  100. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  101. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  102. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
  103. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  104. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  105. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  106. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  107. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/settings.py +0 -0
  108. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/siems.py +0 -0
  109. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/users.py +0 -0
  110. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/models/__init__.py +0 -0
  111. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/__init__.py +0 -0
  112. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/client/__init__.py +0 -0
  113. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/client/base.py +0 -0
  114. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/__init__.py +0 -0
  115. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/audits.py +0 -0
  116. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/auth.py +0 -0
  117. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/computers.py +0 -0
  118. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/events.py +0 -0
  119. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/groups.py +0 -0
  120. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/policies.py +0 -0
  121. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/quick.py +0 -0
  122. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/requests.py +0 -0
  123. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/roles.py +0 -0
  124. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/tasks.py +0 -0
  125. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/users.py +0 -0
  126. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/models/__init__.py +0 -0
  127. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/__init__.py +0 -0
  128. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/client/__init__.py +0 -0
  129. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/auth.py +0 -0
  130. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/machines.py +0 -0
  131. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/tokens.py +0 -0
  132. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/user.py +0 -0
  133. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/__init__.py +0 -0
  134. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/client/__init__.py +0 -0
  135. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/client/base.py +0 -0
  136. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/__init__.py +0 -0
  137. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/auth.py +0 -0
  138. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/group_policies.py +0 -0
  139. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/import_export.py +0 -0
  140. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  141. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  142. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_items.py +0 -0
  143. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  144. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/policies.py +0 -0
  145. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/quick.py +0 -0
  146. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/teams.py +0 -0
  147. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/users.py +0 -0
  148. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/vault.py +0 -0
  149. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/__init__.py +0 -0
  150. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/common.py +0 -0
  151. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/group_policy.py +0 -0
  152. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_client.py +0 -0
  153. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_group.py +0 -0
  154. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_item.py +0 -0
  155. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jumpoint.py +0 -0
  156. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/team.py +0 -0
  157. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/user.py +0 -0
  158. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/vault.py +0 -0
  159. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/__init__.py +0 -0
  160. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/__init__.py +0 -0
  161. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/base.py +0 -0
  162. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  163. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  164. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/__init__.py +0 -0
  165. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/accounts.py +0 -0
  166. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/assets.py +0 -0
  167. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/attributes.py +0 -0
  168. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/auth.py +0 -0
  169. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/clouds.py +0 -0
  170. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/config.py +0 -0
  171. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/credentials.py +0 -0
  172. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/databases.py +0 -0
  173. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/directories.py +0 -0
  174. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/functional.py +0 -0
  175. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/import_export.py +0 -0
  176. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/platforms.py +0 -0
  177. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/quick.py +0 -0
  178. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/search.py +0 -0
  179. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/secrets.py +0 -0
  180. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/systems.py +0 -0
  181. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/users.py +0 -0
  182. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/workgroups.py +0 -0
  183. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/config.py +0 -0
  184. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/__init__.py +0 -0
  185. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/account.py +0 -0
  186. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/asset.py +0 -0
  187. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/common.py +0 -0
  188. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/system.py +0 -0
  189. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/__init__.py +0 -0
  190. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/client/__init__.py +0 -0
  191. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/client/base.py +0 -0
  192. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/__init__.py +0 -0
  193. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/_hints.py +0 -0
  194. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/auth.py +0 -0
  195. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/dynamic.py +0 -0
  196. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/folders.py +0 -0
  197. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/integrations.py +0 -0
  198. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/leases.py +0 -0
  199. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/static.py +0 -0
  200. {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/models/__init__.py +0 -0
  201. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/__init__.py +0 -0
  202. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/conftest.py +0 -0
  203. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/__init__.py +0 -0
  204. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_auth.py +0 -0
  205. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_config.py +0 -0
  206. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_config_file.py +0 -0
  207. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_errors.py +0 -0
  208. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_output.py +0 -0
  209. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_rest_debug.py +0 -0
  210. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle/__init__.py +0 -0
  211. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle/test_client.py +0 -0
  212. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle/test_commands.py +0 -0
  213. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle-smoke-test.sh +0 -0
  214. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epml/__init__.py +0 -0
  215. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epml/test_client.py +0 -0
  216. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epml/test_commands.py +0 -0
  217. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw/__init__.py +0 -0
  218. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw/test_client.py +0 -0
  219. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw/test_commands.py +0 -0
  220. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw-quick-test-plan.md +0 -0
  221. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/fixtures/__init__.py +0 -0
  222. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/fixtures/responses.py +0 -0
  223. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/__init__.py +0 -0
  224. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/conftest.py +0 -0
  225. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/helpers.py +0 -0
  226. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_entitle_integration.py +0 -0
  227. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_epmw_integration.py +0 -0
  228. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_epmw_lifecycle.py +0 -0
  229. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pra_integration.py +0 -0
  230. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pra_lifecycle.py +0 -0
  231. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pws_integration.py +0 -0
  232. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pws_lifecycle.py +0 -0
  233. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pf/__init__.py +0 -0
  234. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra/__init__.py +0 -0
  235. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra/test_client.py +0 -0
  236. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra/test_commands.py +0 -0
  237. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra-smoke-test.sh +0 -0
  238. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra-test-plan.md +0 -0
  239. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws/__init__.py +0 -0
  240. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws/test_client.py +0 -0
  241. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws/test_commands.py +0 -0
  242. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws-quick-test-plan.md +0 -0
  243. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws-smoke-test.sh +0 -0
  244. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/secrets/__init__.py +0 -0
  245. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/secrets/test_client.py +0 -0
  246. {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/secrets/test_commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.55**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.57**
4
4
 
5
5
  ## Setup
6
6
 
@@ -45,7 +45,7 @@ Use these slash commands for detailed product guidance:
45
45
  | EPM Windows | `bt epmw` | `computers`, `groups`, `policies`, `requests`, `quick` |
46
46
  | EPM Linux | `bt epml` | `rbp` (cmdgrps/hostgrps/usergrps/tmdategrps/roles/policy/tests/tx), `settings`, `users`, `audit`, `siems`, `quick` |
47
47
  | Secrets API | `bt secrets` | `folders`, `static`, `dynamic` (incl. `generate`), `leases`, `integrations` |
48
- | Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines` |
48
+ | Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines`, `users` (invite/grant/revoke/site membership) |
49
49
 
50
50
  ## Common Patterns
51
51
 
@@ -91,7 +91,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
91
91
  - **EPML role banner format**: `bt epml rbp roles create --banner-text "Title"` builds a `############`-framed message using `%rbprole%`/`%event%` server substitutions, mirroring the appliance's existing roles. See the worked role example in the EPM-L SKILL.md.
92
92
  - **Secrets dynamic path is `/dynamic`** (NOT `/dynamic-secrets` as the doc claims); version header is `2026-04-28` (NOT 2026-01-02; server also rejects every earlier dated header — bumps are mandatory, not optional); `dynamic/{name}/generate` returns 201; `/leases/revoke` is scope-gated and 403 is the expected lab response — `bt secrets leases revoke` exits 3 with a clean warning, not generic failure. Documented in `src/bt_cli/data/skills/secrets/SKILL.md`.
93
93
  - **Secrets `generate` mints real AWS STS credentials and is audited** — don't loop it in dev. Use `dynamic list`/`get` + `leases list` for browsing.
94
- - **PF URL mapping**: Nomine swagger paths `/api/auth/X` → gateway `https://api.beyondtrust.io/site/<site-id>/platform/auth/X`. **PATs are site-bound** (minted on the active portal site; other sites → `401 Access denied for this site`). Org-level endpoints (`auth/Users`, `auth/Auditing`, `auth/SAMLProviders`) are 403 for PAT principals; user onboarding & token create/revoke need cookie auth on app.beyondtrust.io. Details in `src/bt_cli/data/skills/pf/SKILL.md`.
94
+ - **PF URL mapping**: Nomine swagger paths `/api/auth/X` → gateway `https://api.beyondtrust.io/site/<site-id>/platform/auth/X`. **PATs are site-bound** (minted on the active portal site; other sites → `401 Access denied for this site`). Org-level GETs (`auth/Users`, `auth/Auditing`, `auth/SAMLProviders`, `auth/*access/org`) are 403 for PAT principals; token create/revoke still needs cookie auth on app.beyondtrust.io. Onboarding/access writes (`UserInvitation*`, `Users/delete`, `productaccess`/`siteaccess` grant/revoke) are deployed on the PAT gateway (`bt pf users ...`) but PAT principals get the same org-level 403 on POST (live-verified 2026-06-11) — commands exit 3 with a cookie-auth hint until the gateway opens up. Details in `src/bt_cli/data/skills/pf/SKILL.md`.
95
95
 
96
96
  ## Functional vs Managed Accounts
97
97
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.55
3
+ Version: 0.4.57
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.55"
7
+ version = "0.4.57"
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.55"
3
+ __version__ = "0.4.57"
@@ -57,6 +57,15 @@ def configure_callback(
57
57
  help="Entitle user-context API key, only required for `bt entitle requests create` "
58
58
  "(visible in shell history/process list — prefer interactive `bt configure`)",
59
59
  ),
60
+ site_id: Optional[str] = typer.Option(
61
+ None, "--site-id", help="Site UUID (pf, secrets, epml)"
62
+ ),
63
+ pat: Optional[str] = typer.Option(
64
+ None,
65
+ "--pat",
66
+ help="Personal Access Token (pf, secrets, epml) — visible in shell "
67
+ "history/process list; prefer interactive `bt configure`",
68
+ ),
60
69
  show_input: bool = typer.Option(
61
70
  False,
62
71
  "--show-input",
@@ -84,18 +93,25 @@ def configure_callback(
84
93
  return
85
94
 
86
95
  # Check if any non-interactive flags were provided
87
- has_flags = any([api_url, client_id, client_secret, api_key, user_api_key])
96
+ has_flags = any([api_url, client_id, client_secret, api_key, user_api_key, site_id, pat])
88
97
 
89
98
  if has_flags and product:
90
99
  # Non-interactive mode with flags
91
100
  _configure_with_flags(
92
- product, profile, api_url, client_id, client_secret, api_key, user_api_key
101
+ product, profile, api_url, client_id, client_secret, api_key, user_api_key,
102
+ site_id, pat,
93
103
  )
94
104
  else:
95
105
  # Interactive mode
96
106
  _configure_interactive(product, profile, show_input=show_input)
97
107
 
98
108
 
109
+ def _confirm_received(value: str) -> None:
110
+ """Echo a masked confirmation so the user knows a hidden paste landed."""
111
+ hint = "****" + value[-4:] if len(value) > 4 else "****"
112
+ console.print(f" [dim]Received {len(value)} characters ({hint})[/dim]")
113
+
114
+
99
115
  def _configure_interactive(
100
116
  product: Optional[str] = None,
101
117
  profile: Optional[str] = None,
@@ -196,6 +212,8 @@ def _configure_interactive(
196
212
  else:
197
213
  hint = "****" + existing_val[-4:] if len(existing_val) > 4 else "****"
198
214
  console.print(f" [dim](current: {hint}, press Enter to keep)[/dim]")
215
+ if not show_input:
216
+ console.print(" [dim](input hidden — nothing echoes while you type/paste)[/dim]")
199
217
  value = Prompt.ask(
200
218
  prompt_text,
201
219
  password=not show_input,
@@ -203,6 +221,19 @@ def _configure_interactive(
203
221
  )
204
222
  if not value and default:
205
223
  value = default
224
+ elif value:
225
+ _confirm_received(value)
226
+ elif field_info.get("required") and not show_input:
227
+ # Hidden prompts don't receive pastes on some terminals (notably
228
+ # Windows consoles) — offer a visible retry instead of failing.
229
+ if Confirm.ask(
230
+ " Nothing received — paste may not work with hidden input. "
231
+ "Retry with input visible?",
232
+ default=True,
233
+ ):
234
+ value = Prompt.ask(prompt_text, default="")
235
+ if value:
236
+ _confirm_received(value)
206
237
  # Handle regular fields
207
238
  else:
208
239
  value = Prompt.ask(
@@ -255,6 +286,8 @@ def _configure_with_flags(
255
286
  client_secret: Optional[str],
256
287
  api_key: Optional[str],
257
288
  user_api_key: Optional[str] = None,
289
+ site_id: Optional[str] = None,
290
+ pat: Optional[str] = None,
258
291
  ) -> None:
259
292
  """Configure using command-line flags (non-interactive)."""
260
293
  if product not in PRODUCTS:
@@ -281,6 +314,17 @@ def _configure_with_flags(
281
314
  print_error("--user-api-key is only valid for product=entitle")
282
315
  raise typer.Exit(2)
283
316
  new_config["user_api_key"] = user_api_key
317
+ pat_products = ("pf", "secrets", "epml")
318
+ if site_id is not None:
319
+ if product not in pat_products:
320
+ print_error(f"--site-id is only valid for product={'|'.join(pat_products)}")
321
+ raise typer.Exit(2)
322
+ new_config["site_id"] = site_id
323
+ if pat is not None:
324
+ if product not in pat_products:
325
+ print_error(f"--pat is only valid for product={'|'.join(pat_products)}")
326
+ raise typer.Exit(2)
327
+ new_config["pat"] = pat
284
328
 
285
329
  # Infer auth method
286
330
  if api_key:
@@ -330,6 +374,24 @@ def _test_connection(product: str, profile: str) -> None:
330
374
  client.get("/Computers", params={"pageSize": 1})
331
375
  print_success("EPM Windows connection successful!")
332
376
 
377
+ elif product == "epml":
378
+ from ..epml.client import get_client
379
+ with get_client() as client:
380
+ client.get_settings()
381
+ print_success("EPM Linux connection successful!")
382
+
383
+ elif product == "secrets":
384
+ from ..secrets.client import get_client
385
+ with get_client() as client:
386
+ client.list_folders()
387
+ print_success("Secrets API connection successful!")
388
+
389
+ elif product == "pf":
390
+ from ..pf.client import get_client
391
+ with get_client() as client:
392
+ info = client.get_user_info() or {}
393
+ print_success(f"Pathfinder connection successful! ({info.get('email', '?')})")
394
+
333
395
  except Exception as e:
334
396
  print_error(f"Connection failed: {e}")
335
397
 
@@ -375,7 +437,7 @@ def show_config(
375
437
  first = True
376
438
  for key, value in settings.items():
377
439
  # Mask secrets
378
- if not show_secrets and any(s in key for s in ["secret", "key", "password"]):
440
+ if not show_secrets and (key == "pat" or any(s in key for s in ["secret", "key", "password"])):
379
441
  if isinstance(value, str) and value.startswith("keyring://"):
380
442
  display_value = "[dim]<stored in keyring>[/dim]"
381
443
  else:
@@ -599,7 +661,7 @@ def show_effective_config(
599
661
  def mask_secret(key: str, value: str) -> str:
600
662
  if not value:
601
663
  return "[dim]not set[/dim]"
602
- if not show_secrets and any(s in key.lower() for s in ["secret", "key", "password"]):
664
+ if not show_secrets and (key.lower() == "pat" or any(s in key.lower() for s in ["secret", "key", "password"])):
603
665
  return "****" + value[-4:] if len(value) > 4 else "****"
604
666
  return value
605
667
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pf
3
- description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, or registered machine identities.
3
+ description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry, user invites and product/site access grants. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, user onboarding, or registered machine identities.
4
4
  ---
5
5
 
6
6
  # Pathfinder Platform (`bt pf`)
@@ -45,6 +45,37 @@ bt pf machines list # registered machine IDs
45
45
  bt pf machines get <uuid> # one machine's record (secret hash never shown)
46
46
  ```
47
47
 
48
+ ## User onboarding & access assignment (`bt pf users`)
49
+
50
+ ```bash
51
+ # Invite a user to the org (sends a real email; confirms unless --yes).
52
+ # Default site = BT_PF_SITE_ID; -p grants products on that site at invite time.
53
+ bt pf users invite -e new.user@corp.com --first-name New --last-name User \
54
+ -p pra -p epml
55
+
56
+ bt pf users cancel-invite new.user@corp.com # cancel a pending invite
57
+
58
+ # Per-user product access on a site (product slugs are lowercase:
59
+ # epmwm, pra, psc, rs, entitle, epml, insights, secrets)
60
+ bt pf users grant -u <user-uuid> -p epml # default site
61
+ bt pf users revoke -u <user-uuid> -p epml -s <site-uuid>
62
+
63
+ # Site membership (optionally granting products in the same call)
64
+ bt pf users add-to-site -u <user-uuid> -s <site-uuid> -p pra -p psc
65
+ bt pf users remove-from-site -u <user-uuid> -s <site-uuid>
66
+
67
+ bt pf users delete <user-uuid> [...] # bulk delete (org admin; confirms)
68
+ bt pf users list # org-level — 403 over PAT, exits 3 with hint
69
+ ```
70
+
71
+ **Write-path caveat (verified 2026-06-11):** all of these routes are
72
+ *deployed* on the PAT gateway (GET returns 405 = POST/DELETE exist), but PAT
73
+ principals are **not authorized** — a live productaccess grant POST returned
74
+ the same empty-body 403 as the org-level GETs. Every `bt pf users` write
75
+ currently exits 3 with the cookie-auth hint. The commands are correct and
76
+ will start working the moment the gateway grants PATs the org-level
77
+ permission (or a cookie-auth mode lands).
78
+
48
79
  ## What works over PAT vs what doesn't (verified 2026-06-11)
49
80
 
50
81
  | Endpoint | PAT result |
@@ -52,7 +83,10 @@ bt pf machines get <uuid> # one machine's record (secret hash never shown)
52
83
  | `auth/UserInfo`, `auth/User/Access`, `auth/User/Token`, `auth/User/McpToken` | 200 — any valid PAT |
53
84
  | `auth/Machine`, `auth/Machine/{id}` | 200 — only with a PAT from a product-bearing site (Administration-site PATs get 403) |
54
85
  | `auth/Users`, `auth/SAMLProviders`, `auth/Auditing` | 403 — org-level, blocked for PAT principals regardless of scopes seen so far |
55
- | `auth/UserInvitation` | 405 on GET (POST-only; reachable — onboarding over PAT untested) |
86
+ | `auth/productaccess/org`, `auth/access/org`, `auth/siteaccess/org` | 403 — same org-level block |
87
+ | `auth/productaccess` (collection GET) | 404 — not deployed on the gateway (also 404 on the app host) |
88
+ | `auth/UserInvitation`, `auth/UserInvitation/Product`, `auth/UserInvitation/Cancel`, `auth/Users/delete` | 405 on GET — POST routes deployed, but PAT writes hit the org-level 403 (see below) |
89
+ | `auth/productaccess/{p}/site/{s}/user/{u}`, `auth/siteaccess/{s}/user/{u}` | 405 on GET — routes deployed; live grant POST → empty-body 403 (org-level block applies to writes too) |
56
90
 
57
91
  PAT `access` scopes are the lowercase product IDs of the mint site (visible in
58
92
  `bt pf pats list`). The org-level 403s may require a scope no UI-minted PAT
@@ -70,7 +104,13 @@ but never activated.
70
104
 
71
105
  ## Not exposed via PAT (cookie-auth only, app.beyondtrust.io/api/auth/*)
72
106
 
73
- User onboarding (UserInvitation POST + Accept), PAT/MCP token creation and
74
- revocation, user listing, auditing, SAML providers. These need the `access`
75
- session cookie (browser JWT ~5min TTL, or the programmatic
76
- SignIn → /session/login flow — password-auth users only, not federated).
107
+ Confirmed PAT-blocked (403): user listing, auditing, SAML providers, the
108
+ org-default access-rule reads. Still cookie-only: PAT/MCP token creation and
109
+ revocation, invitation Accept. These need the `access` session cookie
110
+ (browser JWT ~5min TTL, or the programmatic SignIn → /session/login flow —
111
+ password-auth users only, not federated).
112
+
113
+ Invite body shape (spec `InviteOrganizationUserInput`): `{email, firstName,
114
+ lastName, siteMetaData: [{siteId, name, residency}], productMetaData:
115
+ [{name, siteId, siteName}]}` — `bt pf users invite` builds this, defaulting
116
+ the site to `BT_PF_SITE_ID` and omitting name/residency unless given.
@@ -0,0 +1,225 @@
1
+ """Pathfinder platform API client (PAT bearer auth, /site/<id>/platform/auth/... URLs)."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+ from urllib.parse import quote as _urlquote
6
+
7
+ import httpx
8
+
9
+ from bt_cli.core.client import _warn_ssl_disabled
10
+ from bt_cli.core.config import PFConfig, load_pf_config
11
+ from bt_cli.core.rest_debug import get_event_hooks
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PFClient:
17
+ """Client for the Pathfinder platform API (internal name: Nomine).
18
+
19
+ URL composition:
20
+
21
+ {api_url}/site/{site_id}/platform/auth/<path>
22
+
23
+ The Nomine swagger writes paths as `/api/auth/X`; on the gateway the
24
+ product segment is `platform` and the path keeps the `auth/` prefix —
25
+ so spec `/api/auth/UserInfo` is gateway `/site/<id>/platform/auth/UserInfo`.
26
+
27
+ Auth: `Authorization: Bearer <PAT>`. PATs are site-bound — minted while
28
+ a site is active in the portal, valid only for that site's paths
29
+ (`401 Access denied for this site` elsewhere). A just-minted PAT can
30
+ take ~1s to propagate to the edge validator (transient
31
+ `401 Personal access token not found`).
32
+
33
+ Gateway-blocked for PAT principals (verified 2026-06-11): the entire
34
+ org-admin surface — reads (`auth/Users`, `auth/SAMLProviders`,
35
+ `auth/Auditing`, `auth/*access/org`) AND writes (`auth/UserInvitation*`,
36
+ `auth/Users/delete`, the productaccess/siteaccess grant/revoke routes).
37
+ The block's signature is an empty-body 403; the routes themselves are
38
+ deployed (GET returns 405). `auth/Machine` works only with a PAT for a
39
+ product-bearing site (not the Administration site).
40
+ """
41
+
42
+ def __init__(self, config: PFConfig):
43
+ self.config = config
44
+ self.base_url = config.api_url.rstrip("/")
45
+ self.site_id = config.site_id
46
+ self._base = f"{self.base_url}/site/{self.site_id}/platform"
47
+
48
+ if not config.verify_ssl:
49
+ _warn_ssl_disabled()
50
+
51
+ self._client = httpx.Client(
52
+ verify=config.verify_ssl,
53
+ timeout=config.timeout,
54
+ event_hooks=get_event_hooks(),
55
+ )
56
+
57
+ # ---- lifecycle ------------------------------------------------------
58
+
59
+ def close(self) -> None:
60
+ self._client.close()
61
+
62
+ def __enter__(self) -> "PFClient":
63
+ return self
64
+
65
+ def __exit__(self, *args: Any) -> None:
66
+ self.close()
67
+
68
+ # ---- low-level ------------------------------------------------------
69
+
70
+ @staticmethod
71
+ def _q(value: Any) -> str:
72
+ """URL-quote a path segment to prevent traversal / query injection."""
73
+ return _urlquote(str(value), safe="")
74
+
75
+ def _url(self, path: str) -> str:
76
+ if not path.startswith("/"):
77
+ path = "/" + path
78
+ return f"{self._base}{path}"
79
+
80
+ def _headers(self) -> Dict[str, str]:
81
+ return {
82
+ "Authorization": f"Bearer {self.config.pat}",
83
+ "Accept": "application/json",
84
+ "Content-Type": "application/json",
85
+ }
86
+
87
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
88
+ """GET a platform path. Returns parsed JSON (None for empty bodies)."""
89
+ response = self._client.get(self._url(path), headers=self._headers(), params=params)
90
+ response.raise_for_status()
91
+ return self._parse(response)
92
+
93
+ def post(self, path: str, json: Any = None) -> Any:
94
+ """POST a platform path. Returns parsed JSON (None for empty bodies)."""
95
+ response = self._client.post(self._url(path), headers=self._headers(), json=json)
96
+ response.raise_for_status()
97
+ return self._parse(response)
98
+
99
+ def delete(self, path: str) -> Any:
100
+ """DELETE a platform path. Returns parsed JSON (None for empty bodies)."""
101
+ response = self._client.delete(self._url(path), headers=self._headers())
102
+ response.raise_for_status()
103
+ return self._parse(response)
104
+
105
+ @staticmethod
106
+ def _parse(response: httpx.Response) -> Any:
107
+ if not response.content:
108
+ return None
109
+ try:
110
+ return response.json()
111
+ except ValueError:
112
+ return response.text
113
+
114
+ # ---- verified endpoints ----------------------------------------------
115
+
116
+ def get_user_info(self) -> Dict[str, Any]:
117
+ """GET /auth/UserInfo — the PAT owner's profile.
118
+
119
+ Returns `{sub, given_name, family_name, email, role, groups,
120
+ isFederated, mfaEnabled}`.
121
+ """
122
+ return self.get("/auth/UserInfo")
123
+
124
+ def get_user_access(self) -> Dict[str, Any]:
125
+ """GET /auth/User/Access — `{siteAccessRules: [...], productAccessRules: [...]}`."""
126
+ return self.get("/auth/User/Access")
127
+
128
+ def list_pats(self) -> Dict[str, Any]:
129
+ """GET /auth/User/Token — `{personalAccessTokens: [...], success, ...}`."""
130
+ return self.get("/auth/User/Token")
131
+
132
+ def list_mcp_tokens(self) -> Dict[str, Any]:
133
+ """GET /auth/User/McpToken — `{mcpTokens: [...], success, ...}`."""
134
+ return self.get("/auth/User/McpToken")
135
+
136
+ def list_machines(self) -> Dict[str, Any]:
137
+ """GET /auth/Machine — `{machines: [uuid, ...], success, ...}`.
138
+
139
+ Only reachable with a PAT minted on a product-bearing site; an
140
+ Administration-site PAT gets 403.
141
+ """
142
+ return self.get("/auth/Machine")
143
+
144
+ def get_machine(self, machine_id: str) -> Dict[str, Any]:
145
+ """GET /auth/Machine/{id} — details for one registered machine."""
146
+ return self.get(f"/auth/Machine/{self._q(machine_id)}")
147
+
148
+ # ---- users & onboarding ----------------------------------------------
149
+ #
150
+ # Route status over PAT (probed 2026-06-11): the invitation POSTs,
151
+ # Users/delete POST, and the productaccess/siteaccess grant/revoke
152
+ # routes all exist (GET returns 405), but PAT principals are NOT
153
+ # authorized — a live productaccess grant POST returned the same
154
+ # empty-body 403 as the org-level GETs (`auth/Users`,
155
+ # `auth/productaccess/org`, `auth/access/org`, `auth/siteaccess/org`).
156
+ # The bare `auth/productaccess` collection GET is 404 (not deployed).
157
+ # These methods stay wired for cookie-auth support or a future gateway
158
+ # policy change; commands surface the 403 as exit 3 with a hint.
159
+
160
+ def list_users(self, params: Optional[Dict[str, Any]] = None) -> Any:
161
+ """GET /auth/Users — org user list (plain array).
162
+
163
+ Filters: email, firstName, lastName, active, siteId, userId,
164
+ mfaEnabled, page (1-based), pageSize. 403 for PAT principals as of
165
+ 2026-06-11 — works over cookie auth on app.beyondtrust.io.
166
+ """
167
+ return self.get("/auth/Users", params=params)
168
+
169
+ def invite_user(self, payload: Dict[str, Any]) -> Dict[str, Any]:
170
+ """POST /auth/UserInvitation — invite a user to the organization.
171
+
172
+ Payload: `{email, firstName, lastName, siteMetaData: [{siteId,
173
+ name, residency}], productMetaData: [{name, siteId, siteName}]}`.
174
+ Sends a real invitation email.
175
+ """
176
+ return self.post("/auth/UserInvitation", json=payload)
177
+
178
+ def cancel_invitation(self, email: str) -> Dict[str, Any]:
179
+ """POST /auth/UserInvitation/Cancel — cancel a pending invite."""
180
+ return self.post("/auth/UserInvitation/Cancel", json={"email": email})
181
+
182
+ def delete_users(self, user_ids: list) -> Dict[str, Any]:
183
+ """POST /auth/Users/delete — bulk-delete platform users (org admin)."""
184
+ return self.post("/auth/Users/delete", json={"userIds": list(user_ids)})
185
+
186
+ def grant_product_access(self, product_id: str, site_id: str, user_id: str) -> Dict[str, Any]:
187
+ """POST /auth/productaccess/{productId}/site/{siteId}/user/{userId}.
188
+
189
+ Product IDs are lowercase slugs (epmwm, pra, psc, rs, entitle,
190
+ epml, insights, secrets); normalized here so the contract holds
191
+ for every caller.
192
+ """
193
+ return self.post(
194
+ f"/auth/productaccess/{self._q(str(product_id).lower())}/site/{self._q(site_id)}"
195
+ f"/user/{self._q(user_id)}"
196
+ )
197
+
198
+ def revoke_product_access(self, product_id: str, site_id: str, user_id: str) -> Dict[str, Any]:
199
+ """DELETE /auth/productaccess/{productId}/site/{siteId}/user/{userId}."""
200
+ return self.delete(
201
+ f"/auth/productaccess/{self._q(str(product_id).lower())}/site/{self._q(site_id)}"
202
+ f"/user/{self._q(user_id)}"
203
+ )
204
+
205
+ def grant_site_access(self, site_id: str, user_id: str,
206
+ product_ids: Optional[list] = None) -> Dict[str, Any]:
207
+ """POST /auth/siteaccess/{siteId}/user/{userId} — add a user to a site.
208
+
209
+ Optional body: array of lowercase product slugs to grant on that
210
+ site too (the spec marks the body itself optional).
211
+ """
212
+ return self.post(
213
+ f"/auth/siteaccess/{self._q(site_id)}/user/{self._q(user_id)}",
214
+ json=[str(p).lower() for p in product_ids] if product_ids else None,
215
+ )
216
+
217
+ def revoke_site_access(self, site_id: str, user_id: str) -> Dict[str, Any]:
218
+ """DELETE /auth/siteaccess/{siteId}/user/{userId} — remove a user from a site."""
219
+ return self.delete(f"/auth/siteaccess/{self._q(site_id)}/user/{self._q(user_id)}")
220
+
221
+
222
+ def get_client() -> PFClient:
223
+ """Get a Pathfinder client with configuration from environment / config file."""
224
+ config = load_pf_config()
225
+ return PFClient(config)
@@ -7,6 +7,7 @@ from .machines import app as machines_app
7
7
  from .tokens import mcp_tokens_app, pats_app
8
8
  from .user import access as access_command
9
9
  from .user import whoami as whoami_command
10
+ from .users import users_app
10
11
 
11
12
  app = typer.Typer(
12
13
  name="pf",
@@ -18,6 +19,7 @@ app.add_typer(auth_app, name="auth")
18
19
  app.add_typer(pats_app, name="pats")
19
20
  app.add_typer(mcp_tokens_app, name="mcp-tokens")
20
21
  app.add_typer(machines_app, name="machines")
22
+ app.add_typer(users_app, name="users")
21
23
 
22
24
  app.command("whoami")(whoami_command)
23
25
  app.command("access")(access_command)