bt-cli 0.4.55__tar.gz → 0.4.57__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bt_cli-0.4.55 → bt_cli-0.4.57}/CLAUDE.md +3 -3
- {bt_cli-0.4.55 → bt_cli-0.4.57}/PKG-INFO +1 -1
- {bt_cli-0.4.55 → bt_cli-0.4.57}/pyproject.toml +1 -1
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/configure.py +66 -4
- {bt_cli-0.4.55 → 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.55 → 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.55/src/bt_cli/pf/client/base.py +0 -133
- bt_cli-0.4.55/tests/pf/test_client.py +0 -79
- bt_cli-0.4.55/tests/pf/test_commands.py +0 -121
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.env.example +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/.gitignore +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/README.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/assets/cli-help.png +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/assets/cli-output.png +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/bt-cli.spec +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/bt_entry.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/conftest.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.55 → bt_cli-0.4.57}/tests/secrets/test_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BT-CLI
|
|
2
2
|
|
|
3
|
-
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.
|
|
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
|
|
|
@@ -57,6 +57,15 @@ def configure_callback(
|
|
|
57
57
|
help="Entitle user-context API key, only required for `bt entitle requests create` "
|
|
58
58
|
"(visible in shell history/process list — prefer interactive `bt configure`)",
|
|
59
59
|
),
|
|
60
|
+
site_id: Optional[str] = typer.Option(
|
|
61
|
+
None, "--site-id", help="Site UUID (pf, secrets, epml)"
|
|
62
|
+
),
|
|
63
|
+
pat: Optional[str] = typer.Option(
|
|
64
|
+
None,
|
|
65
|
+
"--pat",
|
|
66
|
+
help="Personal Access Token (pf, secrets, epml) — visible in shell "
|
|
67
|
+
"history/process list; prefer interactive `bt configure`",
|
|
68
|
+
),
|
|
60
69
|
show_input: bool = typer.Option(
|
|
61
70
|
False,
|
|
62
71
|
"--show-input",
|
|
@@ -84,18 +93,25 @@ def configure_callback(
|
|
|
84
93
|
return
|
|
85
94
|
|
|
86
95
|
# Check if any non-interactive flags were provided
|
|
87
|
-
has_flags = any([api_url, client_id, client_secret, api_key, user_api_key])
|
|
96
|
+
has_flags = any([api_url, client_id, client_secret, api_key, user_api_key, site_id, pat])
|
|
88
97
|
|
|
89
98
|
if has_flags and product:
|
|
90
99
|
# Non-interactive mode with flags
|
|
91
100
|
_configure_with_flags(
|
|
92
|
-
product, profile, api_url, client_id, client_secret, api_key, user_api_key
|
|
101
|
+
product, profile, api_url, client_id, client_secret, api_key, user_api_key,
|
|
102
|
+
site_id, pat,
|
|
93
103
|
)
|
|
94
104
|
else:
|
|
95
105
|
# Interactive mode
|
|
96
106
|
_configure_interactive(product, profile, show_input=show_input)
|
|
97
107
|
|
|
98
108
|
|
|
109
|
+
def _confirm_received(value: str) -> None:
|
|
110
|
+
"""Echo a masked confirmation so the user knows a hidden paste landed."""
|
|
111
|
+
hint = "****" + value[-4:] if len(value) > 4 else "****"
|
|
112
|
+
console.print(f" [dim]Received {len(value)} characters ({hint})[/dim]")
|
|
113
|
+
|
|
114
|
+
|
|
99
115
|
def _configure_interactive(
|
|
100
116
|
product: Optional[str] = None,
|
|
101
117
|
profile: Optional[str] = None,
|
|
@@ -196,6 +212,8 @@ def _configure_interactive(
|
|
|
196
212
|
else:
|
|
197
213
|
hint = "****" + existing_val[-4:] if len(existing_val) > 4 else "****"
|
|
198
214
|
console.print(f" [dim](current: {hint}, press Enter to keep)[/dim]")
|
|
215
|
+
if not show_input:
|
|
216
|
+
console.print(" [dim](input hidden — nothing echoes while you type/paste)[/dim]")
|
|
199
217
|
value = Prompt.ask(
|
|
200
218
|
prompt_text,
|
|
201
219
|
password=not show_input,
|
|
@@ -203,6 +221,19 @@ def _configure_interactive(
|
|
|
203
221
|
)
|
|
204
222
|
if not value and default:
|
|
205
223
|
value = default
|
|
224
|
+
elif value:
|
|
225
|
+
_confirm_received(value)
|
|
226
|
+
elif field_info.get("required") and not show_input:
|
|
227
|
+
# Hidden prompts don't receive pastes on some terminals (notably
|
|
228
|
+
# Windows consoles) — offer a visible retry instead of failing.
|
|
229
|
+
if Confirm.ask(
|
|
230
|
+
" Nothing received — paste may not work with hidden input. "
|
|
231
|
+
"Retry with input visible?",
|
|
232
|
+
default=True,
|
|
233
|
+
):
|
|
234
|
+
value = Prompt.ask(prompt_text, default="")
|
|
235
|
+
if value:
|
|
236
|
+
_confirm_received(value)
|
|
206
237
|
# Handle regular fields
|
|
207
238
|
else:
|
|
208
239
|
value = Prompt.ask(
|
|
@@ -255,6 +286,8 @@ def _configure_with_flags(
|
|
|
255
286
|
client_secret: Optional[str],
|
|
256
287
|
api_key: Optional[str],
|
|
257
288
|
user_api_key: Optional[str] = None,
|
|
289
|
+
site_id: Optional[str] = None,
|
|
290
|
+
pat: Optional[str] = None,
|
|
258
291
|
) -> None:
|
|
259
292
|
"""Configure using command-line flags (non-interactive)."""
|
|
260
293
|
if product not in PRODUCTS:
|
|
@@ -281,6 +314,17 @@ def _configure_with_flags(
|
|
|
281
314
|
print_error("--user-api-key is only valid for product=entitle")
|
|
282
315
|
raise typer.Exit(2)
|
|
283
316
|
new_config["user_api_key"] = user_api_key
|
|
317
|
+
pat_products = ("pf", "secrets", "epml")
|
|
318
|
+
if site_id is not None:
|
|
319
|
+
if product not in pat_products:
|
|
320
|
+
print_error(f"--site-id is only valid for product={'|'.join(pat_products)}")
|
|
321
|
+
raise typer.Exit(2)
|
|
322
|
+
new_config["site_id"] = site_id
|
|
323
|
+
if pat is not None:
|
|
324
|
+
if product not in pat_products:
|
|
325
|
+
print_error(f"--pat is only valid for product={'|'.join(pat_products)}")
|
|
326
|
+
raise typer.Exit(2)
|
|
327
|
+
new_config["pat"] = pat
|
|
284
328
|
|
|
285
329
|
# Infer auth method
|
|
286
330
|
if api_key:
|
|
@@ -330,6 +374,24 @@ def _test_connection(product: str, profile: str) -> None:
|
|
|
330
374
|
client.get("/Computers", params={"pageSize": 1})
|
|
331
375
|
print_success("EPM Windows connection successful!")
|
|
332
376
|
|
|
377
|
+
elif product == "epml":
|
|
378
|
+
from ..epml.client import get_client
|
|
379
|
+
with get_client() as client:
|
|
380
|
+
client.get_settings()
|
|
381
|
+
print_success("EPM Linux connection successful!")
|
|
382
|
+
|
|
383
|
+
elif product == "secrets":
|
|
384
|
+
from ..secrets.client import get_client
|
|
385
|
+
with get_client() as client:
|
|
386
|
+
client.list_folders()
|
|
387
|
+
print_success("Secrets API connection successful!")
|
|
388
|
+
|
|
389
|
+
elif product == "pf":
|
|
390
|
+
from ..pf.client import get_client
|
|
391
|
+
with get_client() as client:
|
|
392
|
+
info = client.get_user_info() or {}
|
|
393
|
+
print_success(f"Pathfinder connection successful! ({info.get('email', '?')})")
|
|
394
|
+
|
|
333
395
|
except Exception as e:
|
|
334
396
|
print_error(f"Connection failed: {e}")
|
|
335
397
|
|
|
@@ -375,7 +437,7 @@ def show_config(
|
|
|
375
437
|
first = True
|
|
376
438
|
for key, value in settings.items():
|
|
377
439
|
# Mask secrets
|
|
378
|
-
if not show_secrets and any(s in key for s in ["secret", "key", "password"]):
|
|
440
|
+
if not show_secrets and (key == "pat" or any(s in key for s in ["secret", "key", "password"])):
|
|
379
441
|
if isinstance(value, str) and value.startswith("keyring://"):
|
|
380
442
|
display_value = "[dim]<stored in keyring>[/dim]"
|
|
381
443
|
else:
|
|
@@ -599,7 +661,7 @@ def show_effective_config(
|
|
|
599
661
|
def mask_secret(key: str, value: str) -> str:
|
|
600
662
|
if not value:
|
|
601
663
|
return "[dim]not set[/dim]"
|
|
602
|
-
if not show_secrets and any(s in key.lower() for s in ["secret", "key", "password"]):
|
|
664
|
+
if not show_secrets and (key.lower() == "pat" or any(s in key.lower() for s in ["secret", "key", "password"])):
|
|
603
665
|
return "****" + value[-4:] if len(value) > 4 else "****"
|
|
604
666
|
return value
|
|
605
667
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pf
|
|
3
|
-
description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, or registered machine identities.
|
|
3
|
+
description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry, user invites and product/site access grants. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, user onboarding, or registered machine identities.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Pathfinder Platform (`bt pf`)
|
|
@@ -45,6 +45,37 @@ bt pf machines list # registered machine IDs
|
|
|
45
45
|
bt pf machines get <uuid> # one machine's record (secret hash never shown)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## User onboarding & access assignment (`bt pf users`)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Invite a user to the org (sends a real email; confirms unless --yes).
|
|
52
|
+
# Default site = BT_PF_SITE_ID; -p grants products on that site at invite time.
|
|
53
|
+
bt pf users invite -e new.user@corp.com --first-name New --last-name User \
|
|
54
|
+
-p pra -p epml
|
|
55
|
+
|
|
56
|
+
bt pf users cancel-invite new.user@corp.com # cancel a pending invite
|
|
57
|
+
|
|
58
|
+
# Per-user product access on a site (product slugs are lowercase:
|
|
59
|
+
# epmwm, pra, psc, rs, entitle, epml, insights, secrets)
|
|
60
|
+
bt pf users grant -u <user-uuid> -p epml # default site
|
|
61
|
+
bt pf users revoke -u <user-uuid> -p epml -s <site-uuid>
|
|
62
|
+
|
|
63
|
+
# Site membership (optionally granting products in the same call)
|
|
64
|
+
bt pf users add-to-site -u <user-uuid> -s <site-uuid> -p pra -p psc
|
|
65
|
+
bt pf users remove-from-site -u <user-uuid> -s <site-uuid>
|
|
66
|
+
|
|
67
|
+
bt pf users delete <user-uuid> [...] # bulk delete (org admin; confirms)
|
|
68
|
+
bt pf users list # org-level — 403 over PAT, exits 3 with hint
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Write-path caveat (verified 2026-06-11):** all of these routes are
|
|
72
|
+
*deployed* on the PAT gateway (GET returns 405 = POST/DELETE exist), but PAT
|
|
73
|
+
principals are **not authorized** — a live productaccess grant POST returned
|
|
74
|
+
the same empty-body 403 as the org-level GETs. Every `bt pf users` write
|
|
75
|
+
currently exits 3 with the cookie-auth hint. The commands are correct and
|
|
76
|
+
will start working the moment the gateway grants PATs the org-level
|
|
77
|
+
permission (or a cookie-auth mode lands).
|
|
78
|
+
|
|
48
79
|
## What works over PAT vs what doesn't (verified 2026-06-11)
|
|
49
80
|
|
|
50
81
|
| Endpoint | PAT result |
|
|
@@ -52,7 +83,10 @@ bt pf machines get <uuid> # one machine's record (secret hash never shown)
|
|
|
52
83
|
| `auth/UserInfo`, `auth/User/Access`, `auth/User/Token`, `auth/User/McpToken` | 200 — any valid PAT |
|
|
53
84
|
| `auth/Machine`, `auth/Machine/{id}` | 200 — only with a PAT from a product-bearing site (Administration-site PATs get 403) |
|
|
54
85
|
| `auth/Users`, `auth/SAMLProviders`, `auth/Auditing` | 403 — org-level, blocked for PAT principals regardless of scopes seen so far |
|
|
55
|
-
| `auth/
|
|
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)
|