bt-cli 0.4.36__tar.gz → 0.4.38__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.36/src/bt_cli/data → bt_cli-0.4.38/.claude}/skills/epml/SKILL.md +16 -4
- {bt_cli-0.4.36 → bt_cli-0.4.38}/CLAUDE.md +1 -1
- {bt_cli-0.4.36 → bt_cli-0.4.38}/PKG-INFO +1 -1
- {bt_cli-0.4.36 → bt_cli-0.4.38}/pyproject.toml +1 -1
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/data/CLAUDE.md +1 -1
- {bt_cli-0.4.36/.claude → bt_cli-0.4.38/src/bt_cli/data}/skills/epml/SKILL.md +16 -4
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/client/base.py +77 -8
- bt_cli-0.4.38/src/bt_cli/epml/commands/rbp_roles.py +342 -0
- bt_cli-0.4.36/src/bt_cli/epml/commands/rbp_roles.py +0 -173
- {bt_cli-0.4.36/src/bt_cli/data → bt_cli-0.4.38/.claude}/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.36/src/bt_cli/data → bt_cli-0.4.38/.claude}/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.36/src/bt_cli/data → bt_cli-0.4.38/.claude}/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.36/src/bt_cli/data → bt_cli-0.4.38/.claude}/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.36/src/bt_cli/data → bt_cli-0.4.38/.claude}/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/.env.example +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/.gitignore +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/README.md +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/assets/cli-help.png +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/assets/cli-output.png +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/bt-cli.spec +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/bt_entry.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.36/tests/pws → bt_cli-0.4.38/src/bt_cli/data}/__init__.py +0 -0
- {bt_cli-0.4.36/.claude → bt_cli-0.4.38/src/bt_cli/data}/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.36/.claude → bt_cli-0.4.38/src/bt_cli/data}/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.36/.claude → bt_cli-0.4.38/src/bt_cli/data}/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.36/.claude → bt_cli-0.4.38/src/bt_cli/data}/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.36/.claude → bt_cli-0.4.38/src/bt_cli/data}/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/conftest.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.36/src/bt_cli/data → bt_cli-0.4.38/tests/pws}/__init__.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.36 → bt_cli-0.4.38}/tests/pws-smoke-test.sh +0 -0
|
@@ -132,13 +132,22 @@ bt epml rbp tmdategrps tmdates replace <tmdategrp_id> --file tmdates.json
|
|
|
132
132
|
|
|
133
133
|
# Roles + assignments
|
|
134
134
|
bt epml rbp roles list
|
|
135
|
-
bt epml rbp roles create --name "Helpdesk Role"
|
|
135
|
+
bt epml rbp roles create --name "Helpdesk Role" --action A \
|
|
136
|
+
--description "..." --comment "..." --tag "AdminAccess" \
|
|
137
|
+
--risk 6 --rpt 1 \
|
|
138
|
+
--iolog '/iologs/%date%/%uniqueid%.iolog' \
|
|
139
|
+
--banner-text "Helpdesk Role" # builds standard ###-framed banner
|
|
140
|
+
# or --message "raw multi-line message"
|
|
141
|
+
# or --banner # banner with %rbprole% template
|
|
142
|
+
bt epml rbp roles update <id> --tag NewTag --risk 9 --rpt 1 # in-place edit
|
|
136
143
|
bt epml rbp roles duplicate <role_id>
|
|
137
|
-
bt epml rbp roles cmdgrps add
|
|
144
|
+
bt epml rbp roles cmdgrps add <role_id> --ids 1,2 # cmdgrps & tmdategrps: just IDs
|
|
145
|
+
bt epml rbp roles tmdategrps add <role_id> --ids 1
|
|
146
|
+
bt epml rbp roles hostgrps add <role_id> --ids 1 --kind B # B = both Submit and Run-as
|
|
147
|
+
bt epml rbp roles usergrps add <role_id> --ids 4 --kind S # S = Submit (who requests)
|
|
148
|
+
bt epml rbp roles usergrps add <role_id> --ids 3 --kind R # R = Run-as (whose identity)
|
|
138
149
|
bt epml rbp roles cmdgrps remove <role_id> <cmdgrp_id>
|
|
139
150
|
bt epml rbp roles hostgrps list <role_id>
|
|
140
|
-
bt epml rbp roles usergrps add <role_id> --ids 5
|
|
141
|
-
bt epml rbp roles tmdategrps add <role_id> --ids 1
|
|
142
151
|
|
|
143
152
|
# Entitlement report ('who can do what')
|
|
144
153
|
bt epml rbp entitlement run
|
|
@@ -200,6 +209,9 @@ bt epml quick tests-then-deploy --suite <suite_id> # commits if pass; rollb
|
|
|
200
209
|
- **`POST /usergrps/multiple`** (bulk create): body is `{"usergroups": [...]}` — undocumented wrapper key.
|
|
201
210
|
- **POST on child collections is additive**, not replacing. Calling `commands add` twice with the same command will get you a duplicate.
|
|
202
211
|
- **Children share the parent's `id`** in GET responses — the listed `id` field is the cmdgrp/hostgrp/usergrp ID, not unique per child. The actual identifier is the `cmd`/`host`/`user` text.
|
|
212
|
+
- **Role assignments take a single object per request, not an array**. The CLI loops under the hood; the wire body is e.g. `{"cmds": 35}`, `{"hosts": 1, "type": "S"}`, `{"users": 4, "type": "R"}`, `{"tmdates": 1}`.
|
|
213
|
+
- **Hostgrp / usergrp assignments require a `type` field** (`S` = Submit / who requests, `R` = Run-as / whose identity the command runs under). Without `type` the request 400s with "RBP role type not in: [S,R]". CLI: `--kind S|R|B` on `roles hostgrps add` and `roles usergrps add`. `B` (default) creates both an S row and an R row for the same id — appropriate when the same group plays both roles. For "Admin requests, runs as root", do `--ids 4 --kind S` and `--ids 3 --kind R` separately.
|
|
214
|
+
- **Roles need `rpt: 1`** to appear in `bt epml rbp entitlement run`. The role still functions for policy evaluation either way, but only `rpt=1` roles are surfaced in the report. Not yet exposed by the CLI — file-level edit via export/import is the workaround.
|
|
203
215
|
|
|
204
216
|
## Known gaps (TODO)
|
|
205
217
|
|
|
@@ -132,13 +132,22 @@ bt epml rbp tmdategrps tmdates replace <tmdategrp_id> --file tmdates.json
|
|
|
132
132
|
|
|
133
133
|
# Roles + assignments
|
|
134
134
|
bt epml rbp roles list
|
|
135
|
-
bt epml rbp roles create --name "Helpdesk Role"
|
|
135
|
+
bt epml rbp roles create --name "Helpdesk Role" --action A \
|
|
136
|
+
--description "..." --comment "..." --tag "AdminAccess" \
|
|
137
|
+
--risk 6 --rpt 1 \
|
|
138
|
+
--iolog '/iologs/%date%/%uniqueid%.iolog' \
|
|
139
|
+
--banner-text "Helpdesk Role" # builds standard ###-framed banner
|
|
140
|
+
# or --message "raw multi-line message"
|
|
141
|
+
# or --banner # banner with %rbprole% template
|
|
142
|
+
bt epml rbp roles update <id> --tag NewTag --risk 9 --rpt 1 # in-place edit
|
|
136
143
|
bt epml rbp roles duplicate <role_id>
|
|
137
|
-
bt epml rbp roles cmdgrps add
|
|
144
|
+
bt epml rbp roles cmdgrps add <role_id> --ids 1,2 # cmdgrps & tmdategrps: just IDs
|
|
145
|
+
bt epml rbp roles tmdategrps add <role_id> --ids 1
|
|
146
|
+
bt epml rbp roles hostgrps add <role_id> --ids 1 --kind B # B = both Submit and Run-as
|
|
147
|
+
bt epml rbp roles usergrps add <role_id> --ids 4 --kind S # S = Submit (who requests)
|
|
148
|
+
bt epml rbp roles usergrps add <role_id> --ids 3 --kind R # R = Run-as (whose identity)
|
|
138
149
|
bt epml rbp roles cmdgrps remove <role_id> <cmdgrp_id>
|
|
139
150
|
bt epml rbp roles hostgrps list <role_id>
|
|
140
|
-
bt epml rbp roles usergrps add <role_id> --ids 5
|
|
141
|
-
bt epml rbp roles tmdategrps add <role_id> --ids 1
|
|
142
151
|
|
|
143
152
|
# Entitlement report ('who can do what')
|
|
144
153
|
bt epml rbp entitlement run
|
|
@@ -200,6 +209,9 @@ bt epml quick tests-then-deploy --suite <suite_id> # commits if pass; rollb
|
|
|
200
209
|
- **`POST /usergrps/multiple`** (bulk create): body is `{"usergroups": [...]}` — undocumented wrapper key.
|
|
201
210
|
- **POST on child collections is additive**, not replacing. Calling `commands add` twice with the same command will get you a duplicate.
|
|
202
211
|
- **Children share the parent's `id`** in GET responses — the listed `id` field is the cmdgrp/hostgrp/usergrp ID, not unique per child. The actual identifier is the `cmd`/`host`/`user` text.
|
|
212
|
+
- **Role assignments take a single object per request, not an array**. The CLI loops under the hood; the wire body is e.g. `{"cmds": 35}`, `{"hosts": 1, "type": "S"}`, `{"users": 4, "type": "R"}`, `{"tmdates": 1}`.
|
|
213
|
+
- **Hostgrp / usergrp assignments require a `type` field** (`S` = Submit / who requests, `R` = Run-as / whose identity the command runs under). Without `type` the request 400s with "RBP role type not in: [S,R]". CLI: `--kind S|R|B` on `roles hostgrps add` and `roles usergrps add`. `B` (default) creates both an S row and an R row for the same id — appropriate when the same group plays both roles. For "Admin requests, runs as root", do `--ids 4 --kind S` and `--ids 3 --kind R` separately.
|
|
214
|
+
- **Roles need `rpt: 1`** to appear in `bt epml rbp entitlement run`. The role still functions for policy evaluation either way, but only `rpt=1` roles are surfaced in the report. Not yet exposed by the CLI — file-level edit via export/import is the workaround.
|
|
203
215
|
|
|
204
216
|
## Known gaps (TODO)
|
|
205
217
|
|
|
@@ -476,6 +476,29 @@ class EPMLClient:
|
|
|
476
476
|
body.setdefault("action", "A")
|
|
477
477
|
return self.post(f"/api/pbul/{h}/rbp/roles", json=body)
|
|
478
478
|
|
|
479
|
+
def update_role(self, role_id: int, partial: Dict[str, Any], host_id: Optional[int] = None) -> Any:
|
|
480
|
+
"""Update an existing role (read-modify-write).
|
|
481
|
+
|
|
482
|
+
The API's create/update endpoint OVERWRITES on `id` match — fields not
|
|
483
|
+
in the body get zeroed/cleared. To make an update feel like a partial
|
|
484
|
+
modification, this helper fetches the current role, merges in your
|
|
485
|
+
changes, and posts the merged record. Avoids accidentally clobbering
|
|
486
|
+
`action`, `name`, `iolog`, etc. when you only meant to change `tag`.
|
|
487
|
+
|
|
488
|
+
Caveat: the v1 API has no GET-single-role, so we list and filter.
|
|
489
|
+
"""
|
|
490
|
+
h = self.host(host_id)
|
|
491
|
+
roles = self.list_roles(host_id=host_id) or []
|
|
492
|
+
current = next((r for r in roles if r.get("id") == role_id), None)
|
|
493
|
+
if current is None:
|
|
494
|
+
raise ValueError(f"role id {role_id} not found")
|
|
495
|
+
# role child relations (rolecmds/roleusers/etc.) get filtered out so we
|
|
496
|
+
# don't accidentally rewrite assignments — those have their own endpoints.
|
|
497
|
+
merged = {k: v for k, v in current.items() if not k.startswith("role")}
|
|
498
|
+
merged.update(partial)
|
|
499
|
+
merged["id"] = role_id
|
|
500
|
+
return self.post(f"/api/pbul/{h}/rbp/roles", json=merged)
|
|
501
|
+
|
|
479
502
|
def delete_roles(self, ids: List[int], host_id: Optional[int] = None) -> None:
|
|
480
503
|
h = self.host(host_id)
|
|
481
504
|
for rid in ids:
|
|
@@ -489,9 +512,13 @@ class EPMLClient:
|
|
|
489
512
|
h = self.host(host_id)
|
|
490
513
|
return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/cmdgrps")
|
|
491
514
|
|
|
492
|
-
def add_role_cmdgrps(self, role_id: int, cmdgrp_ids: List[int], host_id: Optional[int] = None) -> Any:
|
|
515
|
+
def add_role_cmdgrps(self, role_id: int, cmdgrp_ids: List[int], host_id: Optional[int] = None) -> List[Any]:
|
|
516
|
+
"""Add cmdgrps to a role. The API accepts ONE assignment per request
|
|
517
|
+
as a bare object (`{cmds: <id>}`) — not an array. Loop here.
|
|
518
|
+
"""
|
|
493
519
|
h = self.host(host_id)
|
|
494
|
-
|
|
520
|
+
path = f"/api/pbul/{h}/rbp/roles/{role_id}/cmdgrps"
|
|
521
|
+
return [self.post(path, json={"cmds": cid}) for cid in cmdgrp_ids]
|
|
495
522
|
|
|
496
523
|
def remove_role_cmdgrp(self, role_id: int, cmdgrp_id: int, host_id: Optional[int] = None) -> None:
|
|
497
524
|
h = self.host(host_id)
|
|
@@ -501,9 +528,31 @@ class EPMLClient:
|
|
|
501
528
|
h = self.host(host_id)
|
|
502
529
|
return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/hostgrps")
|
|
503
530
|
|
|
504
|
-
def add_role_hostgrps(
|
|
531
|
+
def add_role_hostgrps(
|
|
532
|
+
self,
|
|
533
|
+
role_id: int,
|
|
534
|
+
hostgrp_ids: List[int],
|
|
535
|
+
kind: str = "B",
|
|
536
|
+
host_id: Optional[int] = None,
|
|
537
|
+
) -> List[Any]:
|
|
538
|
+
"""Add hostgrps to a role. Each assignment carries a `type`:
|
|
539
|
+
S = Submit (where the request comes from)
|
|
540
|
+
R = Run-as (where the command actually runs)
|
|
541
|
+
|
|
542
|
+
For a typical "users on these hosts can run on these same hosts" rule,
|
|
543
|
+
you usually want BOTH — pass `kind="B"` (default) and the client posts
|
|
544
|
+
twice, once with type S and once with type R.
|
|
545
|
+
"""
|
|
505
546
|
h = self.host(host_id)
|
|
506
|
-
|
|
547
|
+
path = f"/api/pbul/{h}/rbp/roles/{role_id}/hostgrps"
|
|
548
|
+
types = ("S", "R") if kind.upper() == "B" else (kind.upper(),)
|
|
549
|
+
if not all(t in ("S", "R") for t in types):
|
|
550
|
+
raise ValueError(f"hostgrp kind must be S, R, or B (both); got {kind!r}")
|
|
551
|
+
results = []
|
|
552
|
+
for hid in hostgrp_ids:
|
|
553
|
+
for t in types:
|
|
554
|
+
results.append(self.post(path, json={"hosts": hid, "type": t}))
|
|
555
|
+
return results
|
|
507
556
|
|
|
508
557
|
def remove_role_hostgrp(self, role_id: int, hostgrp_id: int, host_id: Optional[int] = None) -> None:
|
|
509
558
|
h = self.host(host_id)
|
|
@@ -513,9 +562,27 @@ class EPMLClient:
|
|
|
513
562
|
h = self.host(host_id)
|
|
514
563
|
return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/usergrps")
|
|
515
564
|
|
|
516
|
-
def add_role_usergrps(
|
|
565
|
+
def add_role_usergrps(
|
|
566
|
+
self,
|
|
567
|
+
role_id: int,
|
|
568
|
+
usergrp_ids: List[int],
|
|
569
|
+
kind: str = "B",
|
|
570
|
+
host_id: Optional[int] = None,
|
|
571
|
+
) -> List[Any]:
|
|
572
|
+
"""Add usergrps to a role. Same S/R/B `type` semantics as hostgrps.
|
|
573
|
+
S = Submit user (who requests)
|
|
574
|
+
R = Run-as user (whose identity the command runs under)
|
|
575
|
+
"""
|
|
517
576
|
h = self.host(host_id)
|
|
518
|
-
|
|
577
|
+
path = f"/api/pbul/{h}/rbp/roles/{role_id}/usergrps"
|
|
578
|
+
types = ("S", "R") if kind.upper() == "B" else (kind.upper(),)
|
|
579
|
+
if not all(t in ("S", "R") for t in types):
|
|
580
|
+
raise ValueError(f"usergrp kind must be S, R, or B (both); got {kind!r}")
|
|
581
|
+
results = []
|
|
582
|
+
for uid in usergrp_ids:
|
|
583
|
+
for t in types:
|
|
584
|
+
results.append(self.post(path, json={"users": uid, "type": t}))
|
|
585
|
+
return results
|
|
519
586
|
|
|
520
587
|
def remove_role_usergrp(self, role_id: int, usergrp_id: int, host_id: Optional[int] = None) -> None:
|
|
521
588
|
h = self.host(host_id)
|
|
@@ -525,9 +592,11 @@ class EPMLClient:
|
|
|
525
592
|
h = self.host(host_id)
|
|
526
593
|
return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/tmdategrps")
|
|
527
594
|
|
|
528
|
-
def add_role_tmdategrps(self, role_id: int, tmdategrp_ids: List[int], host_id: Optional[int] = None) -> Any:
|
|
595
|
+
def add_role_tmdategrps(self, role_id: int, tmdategrp_ids: List[int], host_id: Optional[int] = None) -> List[Any]:
|
|
596
|
+
"""Add tmdategrps to a role. Single object per request, key is `tmdates`."""
|
|
529
597
|
h = self.host(host_id)
|
|
530
|
-
|
|
598
|
+
path = f"/api/pbul/{h}/rbp/roles/{role_id}/tmdategrps"
|
|
599
|
+
return [self.post(path, json={"tmdates": tid}) for tid in tmdategrp_ids]
|
|
531
600
|
|
|
532
601
|
def remove_role_tmdategrp(self, role_id: int, tmdategrp_id: int, host_id: Optional[int] = None) -> None:
|
|
533
602
|
h = self.host(host_id)
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""RBP roles + role/group assignments (`/api/pbul/{hostid}/rbp/roles`)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from bt_cli.core.output import OutputFormat, print_api_error, print_json, print_table
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True, help="RBP roles")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _host_opt():
|
|
14
|
+
return typer.Option(None, "--host", "-H", help="PMUL host id (default: BT_EPML_DEFAULT_HOST)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_BAR = "#" * 60
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_banner(title: Optional[str] = None) -> str:
|
|
21
|
+
"""Build a banner-style policy message.
|
|
22
|
+
|
|
23
|
+
Server substitutes `%rbprole%` and `%event%` at session time. Pass a
|
|
24
|
+
title string to replace the role-name line with literal text.
|
|
25
|
+
"""
|
|
26
|
+
title_line = f" Policy: {title}\r\n" if title else " Policy: %rbprole%\r\n"
|
|
27
|
+
return (
|
|
28
|
+
"\r\n"
|
|
29
|
+
+ _BAR + "\r\n"
|
|
30
|
+
+ title_line
|
|
31
|
+
+ " Status: %event%\r\n"
|
|
32
|
+
+ " Session Recorded: Yes\r\n"
|
|
33
|
+
+ _BAR + "\r\n"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_message(message: Optional[str], banner: bool, banner_text: Optional[str]) -> Optional[str]:
|
|
38
|
+
"""Pick the final message: explicit --message wins; else build a banner if asked."""
|
|
39
|
+
if message is not None:
|
|
40
|
+
return message
|
|
41
|
+
if banner_text is not None:
|
|
42
|
+
return _build_banner(banner_text)
|
|
43
|
+
if banner:
|
|
44
|
+
return _build_banner()
|
|
45
|
+
return None # leave message unset
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("list")
|
|
49
|
+
def list_roles(
|
|
50
|
+
host: Optional[int] = _host_opt(),
|
|
51
|
+
output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
|
|
52
|
+
):
|
|
53
|
+
"""List roles."""
|
|
54
|
+
from bt_cli.epml.client import get_client
|
|
55
|
+
try:
|
|
56
|
+
with get_client() as c:
|
|
57
|
+
data = c.list_roles(host_id=host)
|
|
58
|
+
if output == OutputFormat.JSON:
|
|
59
|
+
print_json(data)
|
|
60
|
+
else:
|
|
61
|
+
rows = data if isinstance(data, list) else (data.get("data", []) if isinstance(data, dict) else [])
|
|
62
|
+
print_table(rows, [
|
|
63
|
+
("ID", "id"),
|
|
64
|
+
("Name", "name"),
|
|
65
|
+
("Description", "description"),
|
|
66
|
+
("Disabled", "disabled"),
|
|
67
|
+
("Type", "type"),
|
|
68
|
+
], title="Roles")
|
|
69
|
+
except httpx.HTTPStatusError as e:
|
|
70
|
+
print_api_error(e, "list roles"); raise typer.Exit(1)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print_api_error(e, "list roles"); raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("create")
|
|
76
|
+
def create_role(
|
|
77
|
+
name: str = typer.Option(..., "--name", "-n"),
|
|
78
|
+
description: str = typer.Option("", "--description", "-d"),
|
|
79
|
+
comment: Optional[str] = typer.Option(None, "--comment", "-c", help="Internal comment (defaults to description if omitted)"),
|
|
80
|
+
tag: Optional[str] = typer.Option(None, "--tag", help="Free-form tag for filtering/reporting"),
|
|
81
|
+
risk: Optional[int] = typer.Option(None, "--risk", "-r", help="Risk score 0-9 (Postgres-style example uses 6)"),
|
|
82
|
+
rpt: Optional[int] = typer.Option(None, "--rpt", help="Entitlement reporting: 1 to surface in `entitlement run`, 0 to hide"),
|
|
83
|
+
action: str = typer.Option("A", "--action", "-a", help="Role verdict: A=Allow, R=Reject"),
|
|
84
|
+
iolog: Optional[str] = typer.Option(
|
|
85
|
+
None, "--iolog",
|
|
86
|
+
help="I/O log path template (e.g. /iologs/%date%/%uniqueid%.iolog). Omit to disable.",
|
|
87
|
+
),
|
|
88
|
+
message: Optional[str] = typer.Option(None, "--message", "-m", help="Raw message shown to the requesting user (multi-line OK; verbatim)"),
|
|
89
|
+
banner: bool = typer.Option(False, "--banner", help="Use a standard ###-framed banner with %rbprole% and %event% template variables"),
|
|
90
|
+
banner_text: Optional[str] = typer.Option(None, "--banner-text", help="Like --banner but use the given title text instead of %rbprole% substitution"),
|
|
91
|
+
disabled: bool = typer.Option(False, "--disabled", help="Create the role disabled"),
|
|
92
|
+
host: Optional[int] = _host_opt(),
|
|
93
|
+
):
|
|
94
|
+
"""Create a role.
|
|
95
|
+
|
|
96
|
+
The API requires `action` to be exactly `A` (Allow) or `R` (Reject) —
|
|
97
|
+
not 'Allow'/'Reject'. Defaults to A.
|
|
98
|
+
|
|
99
|
+
`iolog` accepts the appliance's path template syntax — typical value is
|
|
100
|
+
`/iologs/%date%/%uniqueid%.iolog`. Omit to disable I/O logging on this role.
|
|
101
|
+
|
|
102
|
+
Three ways to set the user-visible message (in priority order):
|
|
103
|
+
--message "raw text" verbatim
|
|
104
|
+
--banner-text "Custom Title" builds the standard banner with this title
|
|
105
|
+
--banner builds the standard banner using %rbprole%
|
|
106
|
+
|
|
107
|
+
The standard banner format mirrors the appliance's existing roles:
|
|
108
|
+
##############################
|
|
109
|
+
Policy: <title or %rbprole%>
|
|
110
|
+
Status: %event%
|
|
111
|
+
Session Recorded: Yes
|
|
112
|
+
##############################
|
|
113
|
+
"""
|
|
114
|
+
from bt_cli.epml.client import get_client
|
|
115
|
+
if action not in ("A", "R"):
|
|
116
|
+
typer.echo(f"--action must be 'A' or 'R', got {action!r}", err=True)
|
|
117
|
+
raise typer.Exit(2)
|
|
118
|
+
|
|
119
|
+
body = {"name": name, "description": description, "action": action, "disabled": disabled}
|
|
120
|
+
if comment is not None:
|
|
121
|
+
body["comment"] = comment
|
|
122
|
+
elif description:
|
|
123
|
+
body["comment"] = description # mirror description into comment (matches existing roles)
|
|
124
|
+
if tag is not None:
|
|
125
|
+
body["tag"] = tag
|
|
126
|
+
if risk is not None:
|
|
127
|
+
body["risk"] = risk
|
|
128
|
+
if rpt is not None:
|
|
129
|
+
body["rpt"] = rpt
|
|
130
|
+
if iolog is not None:
|
|
131
|
+
body["iolog"] = iolog
|
|
132
|
+
|
|
133
|
+
final_message = _resolve_message(message, banner, banner_text)
|
|
134
|
+
if final_message is not None:
|
|
135
|
+
body["message"] = final_message
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
with get_client() as c:
|
|
139
|
+
result = c.create_role(body, host_id=host)
|
|
140
|
+
print_json(result)
|
|
141
|
+
except httpx.HTTPStatusError as e:
|
|
142
|
+
print_api_error(e, "create role"); raise typer.Exit(1)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print_api_error(e, "create role"); raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command("update")
|
|
148
|
+
def update_role(
|
|
149
|
+
role_id: int = typer.Argument(..., help="Role ID to update"),
|
|
150
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Rename the role"),
|
|
151
|
+
description: Optional[str] = typer.Option(None, "--description", "-d"),
|
|
152
|
+
comment: Optional[str] = typer.Option(None, "--comment", "-c"),
|
|
153
|
+
tag: Optional[str] = typer.Option(None, "--tag"),
|
|
154
|
+
risk: Optional[int] = typer.Option(None, "--risk", "-r"),
|
|
155
|
+
rpt: Optional[int] = typer.Option(None, "--rpt"),
|
|
156
|
+
action: Optional[str] = typer.Option(None, "--action", "-a", help="A=Allow / R=Reject"),
|
|
157
|
+
iolog: Optional[str] = typer.Option(None, "--iolog", help="I/O log template; pass '' to disable"),
|
|
158
|
+
message: Optional[str] = typer.Option(None, "--message", "-m"),
|
|
159
|
+
banner: bool = typer.Option(False, "--banner"),
|
|
160
|
+
banner_text: Optional[str] = typer.Option(None, "--banner-text"),
|
|
161
|
+
disabled: Optional[bool] = typer.Option(None, "--disabled/--enabled"),
|
|
162
|
+
host: Optional[int] = _host_opt(),
|
|
163
|
+
):
|
|
164
|
+
"""Update a role's metadata in place. Only fields you pass are modified.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
bt epml rbp roles update 126 --tag "RootAccess" --risk 9 --rpt 1
|
|
168
|
+
bt epml rbp roles update 126 --banner-text "Root Shell Access"
|
|
169
|
+
bt epml rbp roles update 126 --message "Custom session header..."
|
|
170
|
+
"""
|
|
171
|
+
from bt_cli.epml.client import get_client
|
|
172
|
+
if action is not None and action not in ("A", "R"):
|
|
173
|
+
typer.echo(f"--action must be 'A' or 'R', got {action!r}", err=True)
|
|
174
|
+
raise typer.Exit(2)
|
|
175
|
+
|
|
176
|
+
body: dict = {}
|
|
177
|
+
for key, val in (
|
|
178
|
+
("name", name), ("description", description), ("comment", comment),
|
|
179
|
+
("tag", tag), ("risk", risk), ("rpt", rpt), ("action", action),
|
|
180
|
+
("iolog", iolog), ("disabled", disabled),
|
|
181
|
+
):
|
|
182
|
+
if val is not None:
|
|
183
|
+
body[key] = val
|
|
184
|
+
|
|
185
|
+
final_message = _resolve_message(message, banner, banner_text)
|
|
186
|
+
if final_message is not None:
|
|
187
|
+
body["message"] = final_message
|
|
188
|
+
|
|
189
|
+
if not body:
|
|
190
|
+
typer.echo("Nothing to update — pass at least one field flag.", err=True)
|
|
191
|
+
raise typer.Exit(2)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
with get_client() as c:
|
|
195
|
+
result = c.update_role(role_id, body, host_id=host)
|
|
196
|
+
print_json(result)
|
|
197
|
+
except httpx.HTTPStatusError as e:
|
|
198
|
+
print_api_error(e, "update role"); raise typer.Exit(1)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print_api_error(e, "update role"); raise typer.Exit(1)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command("delete")
|
|
204
|
+
def delete_role(
|
|
205
|
+
ids: str = typer.Argument(..., help="Comma-separated role IDs"),
|
|
206
|
+
host: Optional[int] = _host_opt(),
|
|
207
|
+
):
|
|
208
|
+
"""Delete one or more roles by ID."""
|
|
209
|
+
from bt_cli.epml.client import get_client
|
|
210
|
+
try:
|
|
211
|
+
id_list = [int(x.strip()) for x in ids.split(",") if x.strip()]
|
|
212
|
+
with get_client() as c:
|
|
213
|
+
c.delete_roles(id_list, host_id=host)
|
|
214
|
+
typer.echo(f"Deleted {len(id_list)} role(s)")
|
|
215
|
+
except httpx.HTTPStatusError as e:
|
|
216
|
+
print_api_error(e, "delete role"); raise typer.Exit(1)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print_api_error(e, "delete role"); raise typer.Exit(1)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command("duplicate")
|
|
222
|
+
def duplicate_role(
|
|
223
|
+
role_id: int = typer.Argument(..., help="Role ID to duplicate"),
|
|
224
|
+
host: Optional[int] = _host_opt(),
|
|
225
|
+
):
|
|
226
|
+
"""Duplicate a role."""
|
|
227
|
+
from bt_cli.epml.client import get_client
|
|
228
|
+
try:
|
|
229
|
+
with get_client() as c:
|
|
230
|
+
result = c.duplicate_role(role_id, host_id=host)
|
|
231
|
+
print_json(result)
|
|
232
|
+
except httpx.HTTPStatusError as e:
|
|
233
|
+
print_api_error(e, "duplicate role"); raise typer.Exit(1)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print_api_error(e, "duplicate role"); raise typer.Exit(1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---- assignments: cmdgrps / hostgrps / usergrps / tmdategrps ----
|
|
239
|
+
|
|
240
|
+
def _make_assignment_app(label: str, list_fn: str, add_fn: str, remove_fn: str, has_kind: bool = False):
|
|
241
|
+
"""Build a sub-typer for managing one role-child resource type.
|
|
242
|
+
|
|
243
|
+
has_kind: if True, expose --kind S|R|B (Submit / Run-as / Both). Used for
|
|
244
|
+
hostgrps and usergrps where each assignment carries a type. Cmdgrps and
|
|
245
|
+
tmdategrps don't take a kind.
|
|
246
|
+
"""
|
|
247
|
+
sub = typer.Typer(no_args_is_help=True, help=f"Manage {label} on a role")
|
|
248
|
+
|
|
249
|
+
@sub.command("list")
|
|
250
|
+
def _list(
|
|
251
|
+
role_id: int = typer.Argument(..., help="Role ID"),
|
|
252
|
+
host: Optional[int] = _host_opt(),
|
|
253
|
+
output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
|
|
254
|
+
):
|
|
255
|
+
f"""List {label} assigned to a role."""
|
|
256
|
+
from bt_cli.epml.client import get_client
|
|
257
|
+
try:
|
|
258
|
+
with get_client() as c:
|
|
259
|
+
data = getattr(c, list_fn)(role_id, host_id=host)
|
|
260
|
+
if output == OutputFormat.JSON:
|
|
261
|
+
print_json(data)
|
|
262
|
+
else:
|
|
263
|
+
rows = data if isinstance(data, list) else (data.get("data", []) if isinstance(data, dict) else [])
|
|
264
|
+
# Best-effort table: include `type` column when present
|
|
265
|
+
if rows and isinstance(rows[0], dict) and "type" in rows[0]:
|
|
266
|
+
cols = [(k.upper(), k) for k in rows[0].keys()]
|
|
267
|
+
else:
|
|
268
|
+
cols = [("ID", "id"), ("Name", "name")]
|
|
269
|
+
print_table(rows, cols, title=f"{label} on role {role_id}")
|
|
270
|
+
except httpx.HTTPStatusError as e:
|
|
271
|
+
print_api_error(e, f"list role {label}"); raise typer.Exit(1)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print_api_error(e, f"list role {label}"); raise typer.Exit(1)
|
|
274
|
+
|
|
275
|
+
if has_kind:
|
|
276
|
+
@sub.command("add")
|
|
277
|
+
def _add(
|
|
278
|
+
role_id: int = typer.Argument(..., help="Role ID"),
|
|
279
|
+
ids: str = typer.Option(..., "--ids", help=f"Comma-separated {label} IDs to add"),
|
|
280
|
+
kind: str = typer.Option(
|
|
281
|
+
"B", "--kind", "-k",
|
|
282
|
+
help="Assignment type: S=Submit, R=Run-as, B=Both (creates two assignments per id)",
|
|
283
|
+
),
|
|
284
|
+
host: Optional[int] = _host_opt(),
|
|
285
|
+
):
|
|
286
|
+
f"""Add {label} to a role."""
|
|
287
|
+
from bt_cli.epml.client import get_client
|
|
288
|
+
if kind.upper() not in ("S", "R", "B"):
|
|
289
|
+
typer.echo(f"--kind must be S, R, or B; got {kind!r}", err=True)
|
|
290
|
+
raise typer.Exit(2)
|
|
291
|
+
try:
|
|
292
|
+
id_list = [int(x.strip()) for x in ids.split(",") if x.strip()]
|
|
293
|
+
with get_client() as c:
|
|
294
|
+
result = getattr(c, add_fn)(role_id, id_list, kind=kind.upper(), host_id=host)
|
|
295
|
+
print_json(result)
|
|
296
|
+
except httpx.HTTPStatusError as e:
|
|
297
|
+
print_api_error(e, f"add role {label}"); raise typer.Exit(1)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
print_api_error(e, f"add role {label}"); raise typer.Exit(1)
|
|
300
|
+
else:
|
|
301
|
+
@sub.command("add")
|
|
302
|
+
def _add(
|
|
303
|
+
role_id: int = typer.Argument(..., help="Role ID"),
|
|
304
|
+
ids: str = typer.Option(..., "--ids", help=f"Comma-separated {label} IDs to add"),
|
|
305
|
+
host: Optional[int] = _host_opt(),
|
|
306
|
+
):
|
|
307
|
+
f"""Add {label} to a role."""
|
|
308
|
+
from bt_cli.epml.client import get_client
|
|
309
|
+
try:
|
|
310
|
+
id_list = [int(x.strip()) for x in ids.split(",") if x.strip()]
|
|
311
|
+
with get_client() as c:
|
|
312
|
+
result = getattr(c, add_fn)(role_id, id_list, host_id=host)
|
|
313
|
+
print_json(result)
|
|
314
|
+
except httpx.HTTPStatusError as e:
|
|
315
|
+
print_api_error(e, f"add role {label}"); raise typer.Exit(1)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print_api_error(e, f"add role {label}"); raise typer.Exit(1)
|
|
318
|
+
|
|
319
|
+
@sub.command("remove")
|
|
320
|
+
def _remove(
|
|
321
|
+
role_id: int = typer.Argument(..., help="Role ID"),
|
|
322
|
+
item_id: int = typer.Argument(..., help=f"{label} ID to remove"),
|
|
323
|
+
host: Optional[int] = _host_opt(),
|
|
324
|
+
):
|
|
325
|
+
f"""Remove a {label} from a role."""
|
|
326
|
+
from bt_cli.epml.client import get_client
|
|
327
|
+
try:
|
|
328
|
+
with get_client() as c:
|
|
329
|
+
getattr(c, remove_fn)(role_id, item_id, host_id=host)
|
|
330
|
+
typer.echo(f"Removed {label} {item_id} from role {role_id}")
|
|
331
|
+
except httpx.HTTPStatusError as e:
|
|
332
|
+
print_api_error(e, f"remove role {label}"); raise typer.Exit(1)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
print_api_error(e, f"remove role {label}"); raise typer.Exit(1)
|
|
335
|
+
|
|
336
|
+
return sub
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
app.add_typer(_make_assignment_app("cmdgrps", "list_role_cmdgrps", "add_role_cmdgrps", "remove_role_cmdgrp"), name="cmdgrps")
|
|
340
|
+
app.add_typer(_make_assignment_app("hostgrps", "list_role_hostgrps", "add_role_hostgrps", "remove_role_hostgrp", has_kind=True), name="hostgrps")
|
|
341
|
+
app.add_typer(_make_assignment_app("usergrps", "list_role_usergrps", "add_role_usergrps", "remove_role_usergrp", has_kind=True), name="usergrps")
|
|
342
|
+
app.add_typer(_make_assignment_app("tmdategrps", "list_role_tmdategrps", "add_role_tmdategrps", "remove_role_tmdategrp"), name="tmdategrps")
|