bt-cli 0.4.56__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.
- {bt_cli-0.4.56 → bt_cli-0.4.57}/CLAUDE.md +3 -3
- {bt_cli-0.4.56 → bt_cli-0.4.57}/PKG-INFO +1 -1
- {bt_cli-0.4.56 → bt_cli-0.4.57}/pyproject.toml +1 -1
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/pf/SKILL.md +46 -6
- bt_cli-0.4.57/src/bt_cli/pf/client/base.py +225 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/commands/__init__.py +2 -0
- bt_cli-0.4.57/src/bt_cli/pf/commands/users.py +298 -0
- bt_cli-0.4.57/tests/pf/test_client.py +159 -0
- bt_cli-0.4.57/tests/pf/test_commands.py +266 -0
- bt_cli-0.4.56/src/bt_cli/pf/client/base.py +0 -133
- bt_cli-0.4.56/tests/pf/test_client.py +0 -79
- bt_cli-0.4.56/tests/pf/test_commands.py +0 -121
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.env.example +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/.gitignore +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/README.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/assets/cli-help.png +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/assets/cli-output.png +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/bt-cli.spec +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/bt_entry.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/conftest.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.57}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.56 → 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.
|
|
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
|
|
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
|
---
|
|
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/
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
SignIn → /session/login flow —
|
|
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)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Pathfinder user onboarding and access-assignment commands.
|
|
2
|
+
|
|
3
|
+
Route status over the PAT gateway (probed 2026-06-11): invitation POSTs,
|
|
4
|
+
Users/delete, and the productaccess/siteaccess grant/revoke routes are all
|
|
5
|
+
deployed, but PAT principals are NOT authorized — a live productaccess
|
|
6
|
+
grant POST returned the same empty-body 403 as the org-level GETs. These
|
|
7
|
+
operations currently require cookie auth on app.beyondtrust.io; the
|
|
8
|
+
commands stay wired for when the gateway opens up and exit 3 with a hint
|
|
9
|
+
in the meantime.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from typing import Iterator, List, Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from bt_cli.core.output import (
|
|
19
|
+
OutputFormat,
|
|
20
|
+
confirm_action,
|
|
21
|
+
print_api_error,
|
|
22
|
+
print_detail,
|
|
23
|
+
print_error,
|
|
24
|
+
print_json,
|
|
25
|
+
print_success,
|
|
26
|
+
print_table,
|
|
27
|
+
print_warning,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
users_app = typer.Typer(no_args_is_help=True, help="Platform users (invite, access, delete)")
|
|
31
|
+
|
|
32
|
+
PRODUCTS = ("epmwm", "pra", "psc", "rs", "entitle", "epml", "insights", "secrets")
|
|
33
|
+
PRODUCT_SLUGS = ", ".join(PRODUCTS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _slug(value: str) -> str:
|
|
37
|
+
"""Normalize a product slug, rejecting unknown ones before any API call.
|
|
38
|
+
|
|
39
|
+
A typo'd slug would otherwise surface as a bare 403 from the gateway.
|
|
40
|
+
"""
|
|
41
|
+
slug = value.lower()
|
|
42
|
+
if slug not in PRODUCTS:
|
|
43
|
+
print_error(f"Unknown product '{value}' — valid slugs: {PRODUCT_SLUGS}")
|
|
44
|
+
raise typer.Exit(2)
|
|
45
|
+
return slug
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _confirm(message: str, yes: bool) -> None:
|
|
49
|
+
"""Confirm or exit. EOF (non-interactive stdin without --yes) exits cleanly."""
|
|
50
|
+
if yes:
|
|
51
|
+
return
|
|
52
|
+
try:
|
|
53
|
+
confirmed = confirm_action(message)
|
|
54
|
+
except EOFError:
|
|
55
|
+
print_error("Confirmation required — pass --yes/-y for non-interactive use")
|
|
56
|
+
raise typer.Exit(2)
|
|
57
|
+
if not confirmed:
|
|
58
|
+
raise typer.Exit(0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@contextmanager
|
|
62
|
+
def _pf_call(operation: str) -> Iterator["PFClient"]: # noqa: F821
|
|
63
|
+
"""Open a PF client and translate failures into CLI output and exit codes.
|
|
64
|
+
|
|
65
|
+
An empty-body 403 is the PAT gateway's org-level block signature
|
|
66
|
+
(verified 2026-06-11) → exit 3 with the cookie-auth hint. A 403 with a
|
|
67
|
+
body is a real authorization error and falls through to the generic
|
|
68
|
+
handler, as do config errors from get_client() (e.g. missing
|
|
69
|
+
BT_PF_SITE_ID) → exit 1.
|
|
70
|
+
"""
|
|
71
|
+
from bt_cli.pf.client import get_client
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with get_client() as client:
|
|
75
|
+
yield client
|
|
76
|
+
except typer.Exit:
|
|
77
|
+
raise
|
|
78
|
+
except httpx.HTTPStatusError as e:
|
|
79
|
+
if e.response.status_code == 403 and not e.response.content:
|
|
80
|
+
print_warning(
|
|
81
|
+
"403 Forbidden — this is an org-level operation the PAT gateway "
|
|
82
|
+
"blocks for PAT principals; it currently requires cookie auth "
|
|
83
|
+
"on app.beyondtrust.io."
|
|
84
|
+
)
|
|
85
|
+
raise typer.Exit(3)
|
|
86
|
+
print_api_error(e, operation)
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print_api_error(e, operation)
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@users_app.command("list")
|
|
94
|
+
def list_users(
|
|
95
|
+
email: Optional[str] = typer.Option(None, "--email", help="Filter by email"),
|
|
96
|
+
first_name: Optional[str] = typer.Option(None, "--first-name", help="Filter by first name"),
|
|
97
|
+
last_name: Optional[str] = typer.Option(None, "--last-name", help="Filter by last name"),
|
|
98
|
+
active: Optional[str] = typer.Option(None, "--active", help="Filter by status"),
|
|
99
|
+
site_id: Optional[str] = typer.Option(None, "--site-id", "-s", help="Filter by site ID"),
|
|
100
|
+
user_id: Optional[str] = typer.Option(None, "--user-id", "-u", help="Filter by user ID"),
|
|
101
|
+
page: Optional[int] = typer.Option(None, "--page", help="Page number (1-based)"),
|
|
102
|
+
page_size: Optional[int] = typer.Option(None, "--page-size", help="Page size"),
|
|
103
|
+
output: OutputFormat = typer.Option(
|
|
104
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
105
|
+
),
|
|
106
|
+
):
|
|
107
|
+
"""List organization users (org-level — 403 over PAT as of 2026-06-11)."""
|
|
108
|
+
params = {
|
|
109
|
+
k: v
|
|
110
|
+
for k, v in {
|
|
111
|
+
"email": email,
|
|
112
|
+
"firstName": first_name,
|
|
113
|
+
"lastName": last_name,
|
|
114
|
+
"active": active,
|
|
115
|
+
"siteId": site_id,
|
|
116
|
+
"userId": user_id,
|
|
117
|
+
"page": page,
|
|
118
|
+
"pageSize": page_size,
|
|
119
|
+
}.items()
|
|
120
|
+
if v is not None
|
|
121
|
+
}
|
|
122
|
+
with _pf_call("list users") as client:
|
|
123
|
+
users = client.list_users(params or None) or []
|
|
124
|
+
|
|
125
|
+
if output == OutputFormat.JSON:
|
|
126
|
+
print_json(users)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
rows = [
|
|
130
|
+
{
|
|
131
|
+
"id": u.get("id") or u.get("userId"),
|
|
132
|
+
"email": u.get("email"),
|
|
133
|
+
"name": f"{u.get('firstName') or ''} {u.get('lastName') or ''}".strip(),
|
|
134
|
+
"active": u.get("active"),
|
|
135
|
+
"mfa": u.get("mfaEnabled"),
|
|
136
|
+
}
|
|
137
|
+
for u in users
|
|
138
|
+
]
|
|
139
|
+
print_table(rows, [
|
|
140
|
+
("ID", "id"),
|
|
141
|
+
("Email", "email"),
|
|
142
|
+
("Name", "name"),
|
|
143
|
+
("Active", "active"),
|
|
144
|
+
("MFA", "mfa"),
|
|
145
|
+
], title=f"Platform users ({len(rows)})")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@users_app.command("invite")
|
|
149
|
+
def invite_user(
|
|
150
|
+
email: str = typer.Option(..., "--email", "-e", help="Email address to invite"),
|
|
151
|
+
first_name: Optional[str] = typer.Option(None, "--first-name", help="First name"),
|
|
152
|
+
last_name: Optional[str] = typer.Option(None, "--last-name", help="Last name"),
|
|
153
|
+
site_id: Optional[str] = typer.Option(
|
|
154
|
+
None, "--site-id", "-s", help="Site to grant (default: configured BT_PF_SITE_ID)"
|
|
155
|
+
),
|
|
156
|
+
site_name: Optional[str] = typer.Option(None, "--site-name", help="Site display name"),
|
|
157
|
+
residency: Optional[str] = typer.Option(None, "--residency", help="Site residency (e.g. US)"),
|
|
158
|
+
products: List[str] = typer.Option(
|
|
159
|
+
[], "--product", "-p", help=f"Product slug to grant on the site ({PRODUCT_SLUGS}); repeatable"
|
|
160
|
+
),
|
|
161
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
162
|
+
output: OutputFormat = typer.Option(
|
|
163
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
164
|
+
),
|
|
165
|
+
):
|
|
166
|
+
"""Invite a user to the organization — sends a real invitation email."""
|
|
167
|
+
slugs = [_slug(p) for p in products]
|
|
168
|
+
|
|
169
|
+
with _pf_call("invite user") as client:
|
|
170
|
+
site_id = site_id or client.site_id
|
|
171
|
+
site_meta = {"siteId": site_id}
|
|
172
|
+
if site_name:
|
|
173
|
+
site_meta["name"] = site_name
|
|
174
|
+
if residency:
|
|
175
|
+
site_meta["residency"] = residency
|
|
176
|
+
|
|
177
|
+
payload = {
|
|
178
|
+
"email": email,
|
|
179
|
+
"siteMetaData": [site_meta],
|
|
180
|
+
"productMetaData": [
|
|
181
|
+
{"name": s, "siteId": site_id, **({"siteName": site_name} if site_name else {})}
|
|
182
|
+
for s in slugs
|
|
183
|
+
],
|
|
184
|
+
}
|
|
185
|
+
if first_name:
|
|
186
|
+
payload["firstName"] = first_name
|
|
187
|
+
if last_name:
|
|
188
|
+
payload["lastName"] = last_name
|
|
189
|
+
|
|
190
|
+
summary = f"Invite {email} to site {site_id}"
|
|
191
|
+
if slugs:
|
|
192
|
+
summary += f" with products: {', '.join(slugs)}"
|
|
193
|
+
_confirm(f"{summary} — sends an invitation email. Continue?", yes)
|
|
194
|
+
|
|
195
|
+
result = client.invite_user(payload) or {}
|
|
196
|
+
|
|
197
|
+
if output == OutputFormat.JSON:
|
|
198
|
+
print_json(result)
|
|
199
|
+
return
|
|
200
|
+
print_success(f"Invited {email}")
|
|
201
|
+
if isinstance(result, dict):
|
|
202
|
+
print_detail(result, [
|
|
203
|
+
("Invite ID", "inviteId"),
|
|
204
|
+
("User ID", "userId"),
|
|
205
|
+
("Organization", "organizationId"),
|
|
206
|
+
("Federated", "isFederated"),
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@users_app.command("cancel-invite")
|
|
211
|
+
def cancel_invite(
|
|
212
|
+
email: str = typer.Argument(..., help="Email address of the pending invite"),
|
|
213
|
+
):
|
|
214
|
+
"""Cancel a pending user invitation."""
|
|
215
|
+
with _pf_call("cancel invitation") as client:
|
|
216
|
+
client.cancel_invitation(email)
|
|
217
|
+
print_success(f"Cancelled pending invite for {email}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@users_app.command("delete")
|
|
221
|
+
def delete_users(
|
|
222
|
+
user_ids: List[str] = typer.Argument(..., help="User ID(s) to delete"),
|
|
223
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
224
|
+
):
|
|
225
|
+
"""Delete platform users (org admin; irreversible)."""
|
|
226
|
+
_confirm(f"Permanently delete {len(user_ids)} user(s): {', '.join(user_ids)}?", yes)
|
|
227
|
+
with _pf_call("delete users") as client:
|
|
228
|
+
client.delete_users(user_ids)
|
|
229
|
+
print_success(f"Deleted {len(user_ids)} user(s)")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@users_app.command("grant")
|
|
233
|
+
def grant_product(
|
|
234
|
+
user_id: str = typer.Option(..., "--user-id", "-u", help="User to grant access to"),
|
|
235
|
+
product: str = typer.Option(..., "--product", "-p", help=f"Product slug ({PRODUCT_SLUGS})"),
|
|
236
|
+
site_id: Optional[str] = typer.Option(
|
|
237
|
+
None, "--site-id", "-s", help="Site (default: configured BT_PF_SITE_ID)"
|
|
238
|
+
),
|
|
239
|
+
):
|
|
240
|
+
"""Grant a user access to a product on a site."""
|
|
241
|
+
slug = _slug(product)
|
|
242
|
+
with _pf_call("grant product access") as client:
|
|
243
|
+
site_id = site_id or client.site_id
|
|
244
|
+
client.grant_product_access(slug, site_id, user_id)
|
|
245
|
+
print_success(f"Granted {slug} on site {site_id} to user {user_id}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@users_app.command("revoke")
|
|
249
|
+
def revoke_product(
|
|
250
|
+
user_id: str = typer.Option(..., "--user-id", "-u", help="User to revoke access from"),
|
|
251
|
+
product: str = typer.Option(..., "--product", "-p", help=f"Product slug ({PRODUCT_SLUGS})"),
|
|
252
|
+
site_id: Optional[str] = typer.Option(
|
|
253
|
+
None, "--site-id", "-s", help="Site (default: configured BT_PF_SITE_ID)"
|
|
254
|
+
),
|
|
255
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
256
|
+
):
|
|
257
|
+
"""Revoke a user's access to a product on a site."""
|
|
258
|
+
slug = _slug(product)
|
|
259
|
+
with _pf_call("revoke product access") as client:
|
|
260
|
+
site_id = site_id or client.site_id
|
|
261
|
+
_confirm(f"Revoke {slug} on site {site_id} from user {user_id}?", yes)
|
|
262
|
+
client.revoke_product_access(slug, site_id, user_id)
|
|
263
|
+
print_success(f"Revoked {slug} on site {site_id} from user {user_id}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@users_app.command("add-to-site")
|
|
267
|
+
def add_to_site(
|
|
268
|
+
user_id: str = typer.Option(..., "--user-id", "-u", help="User to add"),
|
|
269
|
+
site_id: Optional[str] = typer.Option(
|
|
270
|
+
None, "--site-id", "-s", help="Site (default: configured BT_PF_SITE_ID)"
|
|
271
|
+
),
|
|
272
|
+
products: List[str] = typer.Option(
|
|
273
|
+
[], "--product", "-p", help="Product slug(s) to grant on the site too; repeatable"
|
|
274
|
+
),
|
|
275
|
+
):
|
|
276
|
+
"""Grant a user access to a site (optionally with products)."""
|
|
277
|
+
slugs = [_slug(p) for p in products]
|
|
278
|
+
with _pf_call("grant site access") as client:
|
|
279
|
+
site_id = site_id or client.site_id
|
|
280
|
+
client.grant_site_access(site_id, user_id, slugs)
|
|
281
|
+
granted = f" with {', '.join(slugs)}" if slugs else ""
|
|
282
|
+
print_success(f"Added user {user_id} to site {site_id}{granted}")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@users_app.command("remove-from-site")
|
|
286
|
+
def remove_from_site(
|
|
287
|
+
user_id: str = typer.Option(..., "--user-id", "-u", help="User to remove"),
|
|
288
|
+
site_id: Optional[str] = typer.Option(
|
|
289
|
+
None, "--site-id", "-s", help="Site (default: configured BT_PF_SITE_ID)"
|
|
290
|
+
),
|
|
291
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
292
|
+
):
|
|
293
|
+
"""Revoke a user's access to a site."""
|
|
294
|
+
with _pf_call("revoke site access") as client:
|
|
295
|
+
site_id = site_id or client.site_id
|
|
296
|
+
_confirm(f"Remove user {user_id} from site {site_id}?", yes)
|
|
297
|
+
client.revoke_site_access(site_id, user_id)
|
|
298
|
+
print_success(f"Removed user {user_id} from site {site_id}")
|