bt-cli 0.4.42__tar.gz → 0.4.45__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 (213) hide show
  1. {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/entitle/SKILL.md +27 -0
  2. {bt_cli-0.4.42/src/bt_cli/data → bt_cli-0.4.45/.claude}/skills/pra/SKILL.md +3 -0
  3. {bt_cli-0.4.42/src/bt_cli/data → bt_cli-0.4.45}/CLAUDE.md +1 -1
  4. {bt_cli-0.4.42 → bt_cli-0.4.45}/PKG-INFO +2 -1
  5. {bt_cli-0.4.42 → bt_cli-0.4.45}/pyproject.toml +2 -1
  6. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/__init__.py +1 -1
  7. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/cli.py +22 -1
  8. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/configure.py +17 -2
  9. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/config.py +14 -4
  10. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/config_file.py +12 -1
  11. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/client/base.py +51 -6
  12. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/requests.py +112 -35
  13. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/client/base.py +217 -6
  14. bt_cli-0.4.45/src/bt_cli/pra/commands/group_policies.py +531 -0
  15. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/policies.py +4 -72
  16. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/quick.py +213 -0
  17. bt_cli-0.4.45/src/bt_cli/pra/commands/users.py +235 -0
  18. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/__init__.py +16 -0
  19. bt_cli-0.4.45/src/bt_cli/pra/models/group_policy.py +45 -0
  20. bt_cli-0.4.42/src/bt_cli/pra/commands/users.py +0 -87
  21. {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/bt/SKILL.md +0 -0
  22. {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/epml/SKILL.md +0 -0
  23. {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/epmw/SKILL.md +0 -0
  24. {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/pws/SKILL.md +0 -0
  25. {bt_cli-0.4.42 → bt_cli-0.4.45}/.env.example +0 -0
  26. {bt_cli-0.4.42 → bt_cli-0.4.45}/.github/workflows/ci.yml +0 -0
  27. {bt_cli-0.4.42 → bt_cli-0.4.45}/.github/workflows/release.yml +0 -0
  28. {bt_cli-0.4.42 → bt_cli-0.4.45}/.gitignore +0 -0
  29. {bt_cli-0.4.42 → bt_cli-0.4.45}/README.md +0 -0
  30. {bt_cli-0.4.42 → bt_cli-0.4.45}/assets/cli-help.png +0 -0
  31. {bt_cli-0.4.42 → bt_cli-0.4.45}/assets/cli-output.png +0 -0
  32. {bt_cli-0.4.42 → bt_cli-0.4.45}/bt-cli.spec +0 -0
  33. {bt_cli-0.4.42 → bt_cli-0.4.45}/bt_entry.py +0 -0
  34. {bt_cli-0.4.42 → bt_cli-0.4.45}/epml-clients-server-side-filters-plan.md +0 -0
  35. {bt_cli-0.4.42 → bt_cli-0.4.45}/epml-implementation-plan.md +0 -0
  36. {bt_cli-0.4.42 → bt_cli-0.4.45}/pf-implementation-plan.md +0 -0
  37. {bt_cli-0.4.42 → bt_cli-0.4.45}/scripts/bt_entry.py +0 -0
  38. {bt_cli-0.4.42 → bt_cli-0.4.45}/scripts/pf_onboard.py +0 -0
  39. {bt_cli-0.4.42 → bt_cli-0.4.45}/scripts/sync-package-data.sh +0 -0
  40. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/__init__.py +0 -0
  41. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/learn.py +0 -0
  42. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/quick.py +0 -0
  43. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/__init__.py +0 -0
  44. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/auth.py +0 -0
  45. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/client.py +0 -0
  46. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/csv_utils.py +0 -0
  47. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/errors.py +0 -0
  48. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/output.py +0 -0
  49. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/prompts.py +0 -0
  50. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/rest_debug.py +0 -0
  51. {bt_cli-0.4.42 → bt_cli-0.4.45/src/bt_cli/data}/CLAUDE.md +0 -0
  52. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/__init__.py +0 -0
  53. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  54. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  55. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
  56. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  57. {bt_cli-0.4.42/.claude → bt_cli-0.4.45/src/bt_cli/data}/skills/pra/SKILL.md +0 -0
  58. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  59. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/__init__.py +0 -0
  60. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/client/__init__.py +0 -0
  61. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/__init__.py +0 -0
  62. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/accounts.py +0 -0
  63. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/applications.py +0 -0
  64. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/auth.py +0 -0
  65. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/bundles.py +0 -0
  66. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/integrations.py +0 -0
  67. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/permissions.py +0 -0
  68. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/policies.py +0 -0
  69. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/resources.py +0 -0
  70. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/roles.py +0 -0
  71. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/users.py +0 -0
  72. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/workflows.py +0 -0
  73. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/__init__.py +0 -0
  74. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/bundle.py +0 -0
  75. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/common.py +0 -0
  76. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/integration.py +0 -0
  77. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/permission.py +0 -0
  78. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/policy.py +0 -0
  79. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/resource.py +0 -0
  80. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/role.py +0 -0
  81. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/user.py +0 -0
  82. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/workflow.py +0 -0
  83. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/__init__.py +0 -0
  84. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/client/__init__.py +0 -0
  85. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/client/base.py +0 -0
  86. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/__init__.py +0 -0
  87. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/audit.py +0 -0
  88. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/auth.py +0 -0
  89. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  90. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/clients.py +0 -0
  91. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/external_apis.py +0 -0
  92. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/hosts.py +0 -0
  93. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/iolog.py +0 -0
  94. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/license.py +0 -0
  95. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/quick.py +0 -0
  96. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  97. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  98. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  99. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  100. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
  101. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  102. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  103. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  104. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  105. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/settings.py +0 -0
  106. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/siems.py +0 -0
  107. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/users.py +0 -0
  108. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/models/__init__.py +0 -0
  109. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/__init__.py +0 -0
  110. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/client/__init__.py +0 -0
  111. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/client/base.py +0 -0
  112. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/__init__.py +0 -0
  113. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/audits.py +0 -0
  114. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/auth.py +0 -0
  115. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/computers.py +0 -0
  116. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/events.py +0 -0
  117. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/groups.py +0 -0
  118. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/policies.py +0 -0
  119. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/quick.py +0 -0
  120. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/requests.py +0 -0
  121. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/roles.py +0 -0
  122. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/tasks.py +0 -0
  123. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/users.py +0 -0
  124. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/models/__init__.py +0 -0
  125. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/__init__.py +0 -0
  126. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/client/__init__.py +0 -0
  127. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/__init__.py +0 -0
  128. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/auth.py +0 -0
  129. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/import_export.py +0 -0
  130. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  131. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  132. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jump_items.py +0 -0
  133. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  134. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/teams.py +0 -0
  135. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/vault.py +0 -0
  136. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/common.py +0 -0
  137. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jump_client.py +0 -0
  138. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jump_group.py +0 -0
  139. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jump_item.py +0 -0
  140. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jumpoint.py +0 -0
  141. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/team.py +0 -0
  142. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/user.py +0 -0
  143. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/vault.py +0 -0
  144. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/__init__.py +0 -0
  145. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/__init__.py +0 -0
  146. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/base.py +0 -0
  147. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  148. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  149. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/__init__.py +0 -0
  150. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/accounts.py +0 -0
  151. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/assets.py +0 -0
  152. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/attributes.py +0 -0
  153. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/auth.py +0 -0
  154. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/clouds.py +0 -0
  155. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/config.py +0 -0
  156. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/credentials.py +0 -0
  157. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/databases.py +0 -0
  158. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/directories.py +0 -0
  159. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/functional.py +0 -0
  160. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/import_export.py +0 -0
  161. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/platforms.py +0 -0
  162. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/quick.py +0 -0
  163. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/search.py +0 -0
  164. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/secrets.py +0 -0
  165. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/systems.py +0 -0
  166. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/users.py +0 -0
  167. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/workgroups.py +0 -0
  168. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/config.py +0 -0
  169. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/__init__.py +0 -0
  170. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/account.py +0 -0
  171. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/asset.py +0 -0
  172. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/common.py +0 -0
  173. {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/system.py +0 -0
  174. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/__init__.py +0 -0
  175. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/conftest.py +0 -0
  176. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/__init__.py +0 -0
  177. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_auth.py +0 -0
  178. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_config.py +0 -0
  179. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_errors.py +0 -0
  180. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_rest_debug.py +0 -0
  181. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle/__init__.py +0 -0
  182. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle/test_client.py +0 -0
  183. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle/test_commands.py +0 -0
  184. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle-smoke-test.sh +0 -0
  185. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epml/__init__.py +0 -0
  186. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epml/test_client.py +0 -0
  187. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epml/test_commands.py +0 -0
  188. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw/__init__.py +0 -0
  189. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw/test_client.py +0 -0
  190. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw/test_commands.py +0 -0
  191. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw-quick-test-plan.md +0 -0
  192. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/fixtures/__init__.py +0 -0
  193. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/fixtures/responses.py +0 -0
  194. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/__init__.py +0 -0
  195. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/conftest.py +0 -0
  196. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/helpers.py +0 -0
  197. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_entitle_integration.py +0 -0
  198. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_epmw_integration.py +0 -0
  199. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_epmw_lifecycle.py +0 -0
  200. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pra_integration.py +0 -0
  201. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pra_lifecycle.py +0 -0
  202. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pws_integration.py +0 -0
  203. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pws_lifecycle.py +0 -0
  204. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra/__init__.py +0 -0
  205. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra/test_client.py +0 -0
  206. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra/test_commands.py +0 -0
  207. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra-smoke-test.sh +0 -0
  208. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra-test-plan.md +0 -0
  209. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws/__init__.py +0 -0
  210. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws/test_client.py +0 -0
  211. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws/test_commands.py +0 -0
  212. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws-quick-test-plan.md +0 -0
  213. {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws-smoke-test.sh +0 -0
@@ -5,6 +5,33 @@ description: Entitle commands for JIT access, bundles, workflows, and permission
5
5
 
6
6
  # Entitle Commands (`bt entitle`)
7
7
 
8
+ ## Two API tokens — read vs. write-on-behalf-of-user
9
+
10
+ Entitle issues two distinct kinds of bearer token. The CLI uses both:
11
+
12
+ | Token | Config field | Env var | Used for |
13
+ |---|---|---|---|
14
+ | **Org / admin** | `entitle.api_key` | `BT_ENTITLE_API_KEY` | All read-only commands (`list`, `get`) and admin writes (revoke, delete) |
15
+ | **User-context** | `entitle.user_api_key` | `BT_ENTITLE_USER_API_KEY` | On-behalf-of-a-user writes — currently `bt entitle requests create` |
16
+
17
+ The org/admin token **cannot** create access requests because requests are
18
+ made *as* a person, not on behalf of the org. If only the admin token is
19
+ configured, `bt entitle requests create` exits early with a panel explaining
20
+ how to add a user token.
21
+
22
+ ```yaml
23
+ # ~/.bt-cli/config.yaml
24
+ profiles:
25
+ default:
26
+ entitle:
27
+ api_url: https://api.us.entitle.io
28
+ api_key: <ORG_TOKEN> # broad read access
29
+ user_api_key: <USER_TOKEN> # personal token, for `requests create`
30
+ ```
31
+
32
+ The user token is generated from `app.entitle.io` → user profile → API tokens
33
+ → **Create user token** (different page from the org token).
34
+
8
35
  ## IMPORTANT: Destructive Operations
9
36
 
10
37
  **ALWAYS confirm with the user before:**
@@ -13,6 +13,9 @@ description: Privileged Remote Access commands for jump items, vault accounts, a
13
13
  - `bt pra jump-items tunnel delete` - Deletes protocol tunnel
14
14
  - `bt pra jump-groups delete` - Deletes jump group
15
15
  - `bt pra vault accounts delete` - Deletes vault account
16
+ - `bt pra users delete` - Deletes a user (non-admins only)
17
+ - `bt pra policies group delete` - Deletes a Group Policy
18
+ - `bt pra policies group {jumpoints|members|jump-groups|vault-accounts|vault-account-groups|teams} remove` - Detaches a sub-resource from a Group Policy
16
19
 
17
20
  List affected resources first, then ask for explicit confirmation.
18
21
 
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.41**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.45**
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.42
3
+ Version: 0.4.45
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
@@ -23,6 +23,7 @@ Requires-Dist: httpx>=0.27.0
23
23
  Requires-Dist: pydantic>=2.0.0
24
24
  Requires-Dist: python-dotenv>=1.0.0
25
25
  Requires-Dist: pyyaml>=6.0.0
26
+ Requires-Dist: questionary>=2.0.0
26
27
  Requires-Dist: rich<14.0.0,>=13.7.0
27
28
  Requires-Dist: shellingham>=1.5.0
28
29
  Requires-Dist: truststore>=0.8.0; python_version >= '3.10'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.42"
7
+ version = "0.4.45"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -44,6 +44,7 @@ dependencies = [
44
44
  "pyyaml>=6.0.0",
45
45
  "shellingham>=1.5.0",
46
46
  "truststore>=0.8.0;python_version>='3.10'", # Use OS certificate store
47
+ "questionary>=2.0.0",
47
48
  ]
48
49
 
49
50
  [project.optional-dependencies]
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.42"
3
+ __version__ = "0.4.45"
@@ -340,7 +340,16 @@ def tree_command(
340
340
  vault.add("accounts list|get|create|delete|checkout|checkin|get-user-data|get-public-key")
341
341
  vault.add("groups list|get")
342
342
 
343
- pra.add("[green]quick[/green] shell-jump|rdp-jump")
343
+ pra.add("[green]users[/green] list|get|update|delete|group-policies|provision")
344
+ pra.add("[green]teams[/green] list|get")
345
+ pol = pra.add("[green]policies[/green]")
346
+ pol.add("jump list|get")
347
+ pol.add("session list")
348
+ pol.add("group list|get|create|update|delete|copy|provision")
349
+ pol.add(" + jumpoints|members|jump-groups|vault-accounts|")
350
+ pol.add(" vault-account-groups|teams (list|add|remove)")
351
+
352
+ pra.add("[green]quick[/green] shell-jump|rdp-jump|change-group-policy-update")
344
353
 
345
354
  # Entitle
346
355
  if not product or product == "entitle":
@@ -467,6 +476,18 @@ def _get_all_commands() -> list[tuple[str, str]]:
467
476
  ("bt pra vault accounts checkout", "Checkout vault credentials"),
468
477
  ("bt pra vault accounts get-user-data", "Generate EC2 user-data for SSH CA"),
469
478
  ("bt pra vault accounts get-public-key", "Get SSH CA public key"),
479
+ ("bt pra users list", "List PRA users"),
480
+ ("bt pra users update", "Update a user (--unlock to clear failed_logins)"),
481
+ ("bt pra users group-policies", "Show GPs a user is a member of"),
482
+ ("bt pra users provision -u <id>", "Recompute permissions for a user"),
483
+ ("bt pra policies group list", "List Group Policies"),
484
+ ("bt pra policies group create --name X", "Create a Group Policy"),
485
+ ("bt pra policies group copy <id> --name Y", "Duplicate a Group Policy"),
486
+ ("bt pra policies group members list <id>", "List members of a GP"),
487
+ ("bt pra policies group jump-groups add <id>", "Attach a Jump Group to a GP"),
488
+ ("bt pra policies group vault-accounts add <id>", "Attach a Vault Account to a GP"),
489
+ ("bt pra policies group teams add <id>", "Attach a Team to a GP"),
490
+ ("bt pra quick change-group-policy-update", "Interactive: toggle a user's GP memberships"),
470
491
  # Entitle
471
492
  ("bt entitle auth test", "Test Entitle connection"),
472
493
  ("bt entitle integrations list", "List integrations"),
@@ -43,6 +43,11 @@ def configure_callback(
43
43
  client_id: Optional[str] = typer.Option(None, "--client-id", help="OAuth Client ID"),
44
44
  client_secret: Optional[str] = typer.Option(None, "--client-secret", help="OAuth Client Secret"),
45
45
  api_key: Optional[str] = typer.Option(None, "--api-key", help="API Key"),
46
+ user_api_key: Optional[str] = typer.Option(
47
+ None,
48
+ "--user-api-key",
49
+ help="Entitle user-context API key (only required for `bt entitle requests create`)",
50
+ ),
46
51
  ) -> None:
47
52
  """Configure bt-cli interactively or via flags.
48
53
 
@@ -65,11 +70,13 @@ def configure_callback(
65
70
  return
66
71
 
67
72
  # Check if any non-interactive flags were provided
68
- has_flags = any([api_url, client_id, client_secret, api_key])
73
+ has_flags = any([api_url, client_id, client_secret, api_key, user_api_key])
69
74
 
70
75
  if has_flags and product:
71
76
  # Non-interactive mode with flags
72
- _configure_with_flags(product, profile, api_url, client_id, client_secret, api_key)
77
+ _configure_with_flags(
78
+ product, profile, api_url, client_id, client_secret, api_key, user_api_key
79
+ )
73
80
  else:
74
81
  # Interactive mode
75
82
  _configure_interactive(product, profile)
@@ -219,6 +226,7 @@ def _configure_with_flags(
219
226
  client_id: Optional[str],
220
227
  client_secret: Optional[str],
221
228
  api_key: Optional[str],
229
+ user_api_key: Optional[str] = None,
222
230
  ) -> None:
223
231
  """Configure using command-line flags (non-interactive)."""
224
232
  if product not in PRODUCTS:
@@ -240,6 +248,11 @@ def _configure_with_flags(
240
248
  new_config["client_secret"] = client_secret
241
249
  if api_key:
242
250
  new_config["api_key"] = api_key
251
+ if user_api_key is not None:
252
+ if product != "entitle":
253
+ print_error("--user-api-key is only valid for product=entitle")
254
+ raise typer.Exit(2)
255
+ new_config["user_api_key"] = user_api_key
243
256
 
244
257
  # Infer auth method
245
258
  if api_key:
@@ -481,6 +494,7 @@ def import_from_env(
481
494
  "entitle": {
482
495
  "api_url": "BT_ENTITLE_API_URL",
483
496
  "api_key": "BT_ENTITLE_API_KEY",
497
+ "user_api_key": "BT_ENTITLE_USER_API_KEY",
484
498
  "verify_ssl": "BT_ENTITLE_VERIFY_SSL",
485
499
  "timeout": "BT_ENTITLE_TIMEOUT",
486
500
  },
@@ -582,6 +596,7 @@ def show_effective_config(
582
596
  "Entitle": {
583
597
  "api_url": "BT_ENTITLE_API_URL",
584
598
  "api_key": "BT_ENTITLE_API_KEY",
599
+ "user_api_key": "BT_ENTITLE_USER_API_KEY",
585
600
  "verify_ssl": "BT_ENTITLE_VERIFY_SSL",
586
601
  "timeout": "BT_ENTITLE_TIMEOUT",
587
602
  },
@@ -60,6 +60,10 @@ class EntitleConfig(ProductConfig):
60
60
  """
61
61
 
62
62
  api_key: str = ""
63
+ # Optional separate token issued for a real user (not the org/admin token).
64
+ # Required for write operations performed *on behalf of* a user, like
65
+ # `bt entitle requests create`. Not required for read-only admin work.
66
+ user_api_key: str = ""
63
67
 
64
68
  def validate(self) -> None:
65
69
  """Validate configuration."""
@@ -290,10 +294,13 @@ def load_entitle_config(env_file: Optional[str] = None, profile: Optional[str] =
290
294
  2. Config file (~/.bt-cli/config.yaml)
291
295
 
292
296
  Environment variables:
293
- BT_ENTITLE_API_URL - API endpoint URL (default: https://api.us.entitle.io)
294
- BT_ENTITLE_API_KEY - API key for Bearer authentication (required)
295
- BT_ENTITLE_VERIFY_SSL - SSL verification (default: true)
296
- BT_ENTITLE_TIMEOUT - Request timeout in seconds (default: 30)
297
+ BT_ENTITLE_API_URL - API endpoint URL (default: https://api.us.entitle.io)
298
+ BT_ENTITLE_API_KEY - Org/admin Bearer token (required for read ops)
299
+ BT_ENTITLE_USER_API_KEY - User-context Bearer token (optional; required
300
+ for `bt entitle requests create` since access
301
+ requests are made on behalf of a real user)
302
+ BT_ENTITLE_VERIFY_SSL - SSL verification (default: true)
303
+ BT_ENTITLE_TIMEOUT - Request timeout in seconds (default: 30)
297
304
  """
298
305
  if env_file:
299
306
  load_dotenv(env_file)
@@ -307,10 +314,13 @@ def load_entitle_config(env_file: Optional[str] = None, profile: Optional[str] =
307
314
  # Resolve any keyring references
308
315
  if "api_key" in layered:
309
316
  layered["api_key"] = _resolve_value(layered["api_key"])
317
+ if "user_api_key" in layered:
318
+ layered["user_api_key"] = _resolve_value(layered["user_api_key"])
310
319
 
311
320
  config = EntitleConfig(
312
321
  api_url=layered.get("api_url") or os.getenv("BT_ENTITLE_API_URL", "https://api.us.entitle.io"),
313
322
  api_key=layered.get("api_key") or os.getenv("BT_ENTITLE_API_KEY", ""),
323
+ user_api_key=layered.get("user_api_key") or os.getenv("BT_ENTITLE_USER_API_KEY", ""),
314
324
  verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_ENTITLE_VERIFY_SSL")),
315
325
  timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_ENTITLE_TIMEOUT"), 30.0),
316
326
  )
@@ -51,7 +51,16 @@ PRODUCTS = {
51
51
  "default": "https://api.us.entitle.io",
52
52
  "example": "https://api.us.entitle.io or https://api.eu.entitle.io",
53
53
  },
54
- "api_key": {"prompt": "API Key", "required": True, "secret": True},
54
+ "api_key": {
55
+ "prompt": "Org/Admin API Key (read + admin writes)",
56
+ "required": True,
57
+ "secret": True,
58
+ },
59
+ "user_api_key": {
60
+ "prompt": "User-context API Key (only for `requests create`; leave blank to skip)",
61
+ "required": False,
62
+ "secret": True,
63
+ },
55
64
  "verify_ssl": {"prompt": "Verify SSL", "required": False, "secret": False, "default": True},
56
65
  "timeout": {"prompt": "Timeout (seconds)", "required": False, "secret": False, "default": 30},
57
66
  },
@@ -341,6 +350,8 @@ def _get_env_mappings(product: str) -> dict[str, str]:
341
350
  if product == "pws":
342
351
  mappings["run_as"] = f"{prefix}_RUN_AS"
343
352
  mappings["api_version"] = f"{prefix}_API_VERSION"
353
+ elif product == "entitle":
354
+ mappings["user_api_key"] = f"{prefix}_USER_API_KEY"
344
355
  elif product == "epml":
345
356
  mappings["site_id"] = f"{prefix}_SITE_ID"
346
357
  mappings["pat"] = f"{prefix}_PAT"
@@ -13,23 +13,55 @@ from ...core.client import _warn_ssl_disabled
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
+ class MissingUserTokenError(RuntimeError):
17
+ """Raised when a user-context op is requested but no user token is set."""
18
+
19
+
16
20
  class EntitleClient:
17
21
  """HTTP client for BeyondTrust Entitle API.
18
22
 
19
- Uses simple Bearer token authentication with API key.
23
+ Entitle issues two distinct kinds of bearer tokens:
24
+
25
+ * **Org / admin token** — broad read access across the whole tenant. Used
26
+ for `list`, `get`, and admin-only writes. Stored as `api_key`.
27
+ * **User-context token** — issued for an individual user; access requests
28
+ and other on-behalf-of writes must use this one. Stored as
29
+ `user_api_key`. If absent, `MissingUserTokenError` is raised at
30
+ construction time.
31
+
32
+ Pass ``use_user_token=True`` to opt into the user-context token.
20
33
  """
21
34
 
22
- def __init__(self, config: EntitleConfig):
35
+ def __init__(self, config: EntitleConfig, use_user_token: bool = False):
23
36
  """Initialize the Entitle client.
24
37
 
25
38
  Args:
26
- config: Configuration with API URL and API key
39
+ config: Configuration with API URL and one or both API keys
40
+ use_user_token: If True, authenticate with `user_api_key` instead
41
+ of `api_key`. Required for `requests create`.
42
+
43
+ Raises:
44
+ MissingUserTokenError: if `use_user_token=True` but no user token
45
+ is configured.
27
46
  """
28
47
  self.config = config
29
48
  # Entitle API uses /public/v1 suffix
30
49
  self.base_url = f"{config.api_url.rstrip('/')}/public/v1"
31
50
  self._client: Optional[httpx.Client] = None
32
- self._auth = BearerTokenAuth(config.api_key)
51
+
52
+ if use_user_token:
53
+ if not config.user_api_key:
54
+ raise MissingUserTokenError(
55
+ "No user-context token configured. Set "
56
+ "BT_ENTITLE_USER_API_KEY or `entitle.user_api_key` in "
57
+ "~/.bt-cli/config.yaml. The current admin/org token "
58
+ "cannot create access requests on behalf of a user."
59
+ )
60
+ token = config.user_api_key
61
+ else:
62
+ token = config.api_key
63
+ self._auth = BearerTokenAuth(token)
64
+ self._using_user_token = use_user_token
33
65
 
34
66
  def __enter__(self) -> "EntitleClient":
35
67
  """Context manager entry - create HTTP client."""
@@ -477,11 +509,24 @@ class EntitleClient:
477
509
  return self.get(f"/accessRequests/{request_id}")
478
510
 
479
511
 
480
- def get_client() -> EntitleClient:
512
+ def get_client(user_token: bool = False) -> EntitleClient:
481
513
  """Create a configured Entitle client.
482
514
 
515
+ Args:
516
+ user_token: If True, build a client authenticated with the
517
+ user-context token (`config.user_api_key`). Required for
518
+ on-behalf-of-user writes such as creating access requests.
519
+
483
520
  Returns:
484
521
  EntitleClient instance
485
522
  """
486
523
  config = load_entitle_config()
487
- return EntitleClient(config)
524
+ return EntitleClient(config, use_user_token=user_token)
525
+
526
+
527
+ def has_user_token() -> bool:
528
+ """True if a user-context token is configured (without raising)."""
529
+ try:
530
+ return bool(load_entitle_config().user_api_key)
531
+ except Exception:
532
+ return False
@@ -2,14 +2,30 @@
2
2
 
3
3
  Hybrid UX: pass --bundle or --role + --duration + --justification for a fully
4
4
  non-interactive run, or omit any of those to drop into a filter-then-pick menu.
5
+
6
+ Auth note
7
+ ---------
8
+ Entitle issues two kinds of API tokens:
9
+
10
+ * **Org / admin** token — used for read-only listings (the default for
11
+ `bt entitle ...`).
12
+ * **User-context** token — required for any operation made *on behalf of*
13
+ a real user. `bt entitle requests create` is one of those, so it always
14
+ uses the user token (`config.user_api_key` /
15
+ `BT_ENTITLE_USER_API_KEY`). If that token is missing the command bails
16
+ out with a friendly explanation rather than silently submitting under the
17
+ wrong identity.
5
18
  """
6
19
 
7
20
  from typing import Any, Optional
8
21
 
9
22
  import httpx
10
23
  import typer
24
+ from rich.panel import Panel
25
+ from rich.table import Table
26
+ from rich.text import Text
11
27
 
12
- from ..client.base import EntitleClient, get_client
28
+ from ..client.base import EntitleClient, MissingUserTokenError, get_client, has_user_token
13
29
  from ...core.output import console, print_json, print_success, print_api_error
14
30
  from ...core.prompts import prompt_choice, prompt_filtered_pick
15
31
 
@@ -131,6 +147,42 @@ def _prompt_justification() -> str:
131
147
  console.print("[red]Justification must be 1..2048 characters[/red]")
132
148
 
133
149
 
150
+ def _abort_missing_user_token() -> None:
151
+ """Render a friendly panel explaining how to add a user-context token."""
152
+ body = Text.from_markup(
153
+ "[bold]No user-context token configured.[/bold]\n\n"
154
+ "Creating an access request is an [italic]on-behalf-of-a-user[/italic] "
155
+ "action and requires a personal Entitle token, not the org/admin "
156
+ "token used for listings.\n\n"
157
+ "[bold]Add one:[/bold]\n\n"
158
+ " • [cyan]Env var[/cyan]\n"
159
+ " [dim]export[/dim] BT_ENTITLE_USER_API_KEY=eyJhb...\n"
160
+ " • [cyan]~/.bt-cli/config.yaml[/cyan]\n"
161
+ " [dim]profiles:\n"
162
+ " default:\n"
163
+ " entitle:\n"
164
+ " user_api_key:[/dim] eyJhb...\n\n"
165
+ "Get the token from [link]https://app.entitle.io[/link] → "
166
+ "your profile → API tokens → [bold]Create user token[/bold]. "
167
+ "(Org/admin tokens are issued from a different page and won't work "
168
+ "here.)"
169
+ )
170
+ console.print(Panel(body, title="[red]Cannot create access request[/red]", border_style="red"))
171
+
172
+
173
+ def _summary_panel(target_type: str, target_label: str, target_id: str,
174
+ duration: int, justification: str) -> Panel:
175
+ table = Table.grid(padding=(0, 2))
176
+ table.add_column(style="dim", justify="right")
177
+ table.add_column()
178
+ table.add_row("Target type", target_type)
179
+ table.add_row("Target", target_label)
180
+ table.add_row("Target ID", f"[dim]{target_id}[/dim]")
181
+ table.add_row("Duration", f"{_fmt_duration(duration)} [dim]({duration}s)[/dim]")
182
+ table.add_row("Justification", justification)
183
+ return Panel(table, title="[bold]Access Request Summary[/bold]", border_style="cyan")
184
+
185
+
134
186
  @app.command("create")
135
187
  def create_access_request(
136
188
  bundle: Optional[str] = typer.Option(None, "--bundle", "-b", help="Bundle ID to request"),
@@ -147,52 +199,63 @@ def create_access_request(
147
199
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip the pre-submit confirmation"),
148
200
  output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
149
201
  ) -> None:
150
- """Create a new access request.
202
+ """Create a new access request (uses the user-context token).
203
+
204
+ Pass --bundle OR --role plus --duration and --justification for
205
+ non-interactive use. Omit any of them to drop into a filter-then-pick menu.
151
206
 
152
- Pass --bundle OR --role plus --duration and --justification for non-interactive
153
- use. Omit any of them to drop into a filter-then-pick menu.
207
+ Authentication: this command always uses `BT_ENTITLE_USER_API_KEY` /
208
+ `entitle.user_api_key`, NOT the org/admin token. If that token is not
209
+ configured, the command exits before contacting the API.
154
210
  """
155
211
  if bundle and role:
156
212
  console.print("[red]Pass either --bundle or --role, not both[/red]")
157
213
  raise typer.Exit(1)
158
214
 
215
+ if not has_user_token():
216
+ _abort_missing_user_token()
217
+ raise typer.Exit(2)
218
+
219
+ # Read-only target/duration discovery uses the admin/org token (broad read
220
+ # access). The actual create POST uses the user-context token.
159
221
  try:
160
- with get_client() as client:
161
- # Resolve target
222
+ with get_client(user_token=False) as reader:
162
223
  if bundle:
163
224
  target_type, target_id, target_label = "bundle", bundle, bundle
164
225
  elif role:
165
226
  target_type, target_id, target_label = "role", role, role
166
227
  else:
167
- target_type, target_id, target_label = _pick_target_interactive(client)
228
+ console.rule("[bold cyan]Pick what to request[/bold cyan]")
229
+ target_type, target_id, target_label = _pick_target_interactive(reader)
168
230
 
169
- # Resolve duration
170
231
  if duration is None:
171
- duration = _pick_duration_interactive(client, target_type, target_id)
232
+ console.rule("[bold cyan]Pick a duration[/bold cyan]")
233
+ duration = _pick_duration_interactive(reader, target_type, target_id)
172
234
  elif duration < 1:
173
235
  console.print("[red]--duration must be >= 1 second[/red]")
174
236
  raise typer.Exit(1)
175
237
 
176
- # Resolve justification
177
- if justification is None:
178
- justification = _prompt_justification()
179
- else:
180
- if not (1 <= len(justification) <= 2048):
181
- console.print("[red]--justification must be 1..2048 characters[/red]")
182
- raise typer.Exit(1)
183
-
184
- # Pre-submit summary
185
- console.print("\n[bold]Access request summary[/bold]")
186
- console.print(f" Target type : {target_type}")
187
- console.print(f" Target : {target_label}")
188
- console.print(f" Target ID : {target_id}")
189
- console.print(f" Duration : {_fmt_duration(duration)} ({duration}s)")
190
- console.print(f" Justification : {justification}")
191
-
192
- if not yes:
193
- typer.confirm("\nSubmit this request?", default=True, abort=True)
194
-
195
- result = client.create_access_request(
238
+ if justification is None:
239
+ console.rule("[bold cyan]Justification[/bold cyan]")
240
+ justification = _prompt_justification()
241
+ else:
242
+ if not (1 <= len(justification) <= 2048):
243
+ console.print("[red]--justification must be 1..2048 characters[/red]")
244
+ raise typer.Exit(1)
245
+
246
+ # Pre-submit panel
247
+ console.print()
248
+ console.print(_summary_panel(target_type, target_label, target_id, duration, justification))
249
+ console.print(
250
+ "[dim]Submitting under the [bold]user-context[/bold] Entitle token "
251
+ "(BT_ENTITLE_USER_API_KEY).[/dim]"
252
+ )
253
+
254
+ if not yes:
255
+ typer.confirm("Submit this request?", default=True, abort=True)
256
+
257
+ with get_client(user_token=True) as writer:
258
+ result = writer.create_access_request(
196
259
  target_type=target_type,
197
260
  target_id=target_id,
198
261
  duration=duration,
@@ -204,16 +267,30 @@ def create_access_request(
204
267
  if isinstance(result, dict) and "result" in result
205
268
  else (result.get("id") if isinstance(result, dict) else None)
206
269
  )
207
- print_success("Access request submitted")
208
- if request_id:
209
- console.print(f"[dim]Follow up with:[/dim] bt entitle requests get {request_id}")
210
270
 
211
271
  if output == "json":
212
272
  print_json(result)
213
- else:
214
- print_json(result) # response shape isn't documented; show full payload
273
+ return
274
+
275
+ success_table = Table.grid(padding=(0, 2))
276
+ success_table.add_column(style="dim", justify="right")
277
+ success_table.add_column()
278
+ if request_id:
279
+ success_table.add_row("Request ID", f"[bold]{request_id}[/bold]")
280
+ success_table.add_row("Target", target_label)
281
+ success_table.add_row("Duration", _fmt_duration(duration))
282
+ if request_id:
283
+ success_table.add_row(
284
+ "Follow up", f"[cyan]bt entitle requests get {request_id}[/cyan]"
285
+ )
286
+ console.print(Panel(success_table, title="[green]Submitted[/green]", border_style="green"))
287
+
288
+ except MissingUserTokenError as e:
289
+ console.print(f"[red]{e}[/red]")
290
+ raise typer.Exit(2)
215
291
  except typer.Abort:
216
- raise
292
+ console.print("[yellow]Aborted — nothing submitted.[/yellow]")
293
+ raise typer.Exit(1)
217
294
  except httpx.HTTPStatusError as e:
218
295
  print_api_error(e, "create access request")
219
296
  raise typer.Exit(1)