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.
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/entitle/SKILL.md +27 -0
- {bt_cli-0.4.42/src/bt_cli/data → bt_cli-0.4.45/.claude}/skills/pra/SKILL.md +3 -0
- {bt_cli-0.4.42/src/bt_cli/data → bt_cli-0.4.45}/CLAUDE.md +1 -1
- {bt_cli-0.4.42 → bt_cli-0.4.45}/PKG-INFO +2 -1
- {bt_cli-0.4.42 → bt_cli-0.4.45}/pyproject.toml +2 -1
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/cli.py +22 -1
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/configure.py +17 -2
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/config.py +14 -4
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/config_file.py +12 -1
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/client/base.py +51 -6
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/requests.py +112 -35
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/client/base.py +217 -6
- bt_cli-0.4.45/src/bt_cli/pra/commands/group_policies.py +531 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/policies.py +4 -72
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/quick.py +213 -0
- bt_cli-0.4.45/src/bt_cli/pra/commands/users.py +235 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/__init__.py +16 -0
- bt_cli-0.4.45/src/bt_cli/pra/models/group_policy.py +45 -0
- bt_cli-0.4.42/src/bt_cli/pra/commands/users.py +0 -87
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.env.example +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/.gitignore +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/README.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/assets/cli-help.png +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/assets/cli-output.png +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/bt-cli.spec +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/bt_entry.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45/src/bt_cli/data}/CLAUDE.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.42/.claude → bt_cli-0.4.45/src/bt_cli/data}/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/conftest.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.42 → bt_cli-0.4.45}/tests/pws-quick-test-plan.md +0 -0
- {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
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bt-cli
|
|
3
|
-
Version: 0.4.
|
|
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.
|
|
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]
|
|
@@ -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]
|
|
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(
|
|
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
|
|
294
|
-
BT_ENTITLE_API_KEY
|
|
295
|
-
|
|
296
|
-
|
|
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": {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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)
|