diff --git a/CHANGELOG.md b/CHANGELOG.md index a696774..a8ac7d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ # Change Log (v2.8.1+) +## v4.0.0 [2025-01-17] + +__What's New:__ + +* Reorganized codebase to align with UI orginizational structure. +* Decoupled `my_requests` and `my_approvals` from `my_access`. +* Added `brokers` and `pools` functionality for `access_broker`. +* Added `firewall` settings functionality. +* Added Britive `managed_permissions` functionality. +* Britive exceptions by type and error code. +* `my_resources` improvements. + +__Enhancements:__ + +* Added `add_favorite` and `delete_favorite` to `my_resources`. +* Added checkout approvals to `my_resources`. +* Added ITSM to checkout approvals. +* Added `(create|list|update|delete)_filter`) to `my_access`. +* Added `response_templates` functionality for `access_broker` credentials. +* Added `request_approval[_by_name]|withdraw_approval_request[_by_name]` to `my_resources`. +* Added `my_access.list` to retrieve access details with new `type=sdk` option. + +__Bug Fixes:__ + +* Fixed missing `param_values` option for resource creation. +* `my_approvals.list` now includes `my_resources` requests. +* Make `get` call in helper method instead `list_approvals`. +* Catch `requests.exceptions.JSONDecodeError` in `handle_response`. + +__Dependencies:__ + +* `requests >= 2.32.0` + +__Other:__ + +* Python 3.8 is EOL, so support is dropped. +* Method assignments dropped: + +| Dropped | New location | +| -------------------------------------- | --------------------------------------------- | +| `access_builder` | `application_management.access_builder` | +| `accounts` | `application_management.accounts` | +| `applications` | `application_management.applications` | +| `audit_logs` | `audit_logs.logs` | +| `environment_groups` | `application_management.environment_groups` | +| `environments` | `application_management.environments` | +| `groups` | `application_management.groups` | +| `identity_attributes` | `identity_management.identity_attributes` | +| `identity_providers` | `identity_management.identity_providers` | +| `notification_mediums` | `global_settings.notification_mediums` | +| `notifications` | `workflows.notifications` | +| `permissions` | `application_management.permissions` | +| `profiles` | `application_management.profiles` | +| `saml` | `security.saml` | +| `scans` | `application_management.scans` | +| `security_policies` | `security.security_policies` | +| `service_identities` | `identity_management.service_identities` | +| `service_identity_tokens` | `identity_management.service_identity_tokens` | +| `settings` | `global_settings` | +| `step_up` | `security.step_up_auth` | +| `tags` | `identity_management.tags` | +| `task_services` | `workflows.task_services` | +| `tasks` | `workflows.tasks` | +| `users` | `identity_management.users` | +| `workload` | `identity_management.workload` | +| `my_access.approval_request_status` | `my_requests.approval_request_status` | +| `my_access.approve_request` | `my_approvals.approve_request` | +| `my_access.list_approvals` | `my_approvals.list` | +| `my_access.reject_request` | `my_approvals.reject_request` | + ## v3.1.0 [2024-10-07] __What's New:__ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f21252c..8e201f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ can consume a native Python library. ## Python Version Support -_CURRENT SUPPORTED VERSION(S):_ `>= 3.8` +_CURRENT SUPPORTED VERSION(S):_ `>= 3.9` We use [typing](https://docs.python.org/3/library/typing.html) and dictionary unpacking, e.g. `{**dict1, **dict2}`, which requires Python 3.5 or greater. @@ -117,48 +117,52 @@ to an internal end-to-end process vs. integrating with a cloud service provider. Then run these in order or as required. ```sh -pytest tests/test_005-identity_attributes.py -v -pytest tests/test_010-users.py -v -pytest tests/test_020-tags.py -v -pytest tests/test_030-service_identities.py -v -pytest tests/test_040-service_identity_tokens.py -v -pytest tests/test_050-applications.py -v -pytest tests/test_060-environment_groups.py -v -pytest tests/test_070-environments.py -v -pytest tests/test_080-scans.py -v # WARNING - this one will take a while since it initiates a real scan -pytest tests/test_090-accounts.py -v # NOTE - a scan must first be completed -pytest tests/test_100-permissions.py -v # NOTE - a scan must first be completed -pytest tests/test_110-groups.py -v # NOTE - a scan must first be completed -pytest tests/test_130-profiles.py -v -pytest tests/test_140-task_services.py -v -pytest tests/test_150-tasks.py -v -pytest tests/test_160-security_policies.py -v -pytest tests/test_170-saml.py -v -pytest tests/test_180-api_tokens.py -v -pytest tests/test_190-audit_logs.py -v -pytest tests/test_200-reports.py -v -pytest tests/test_210-identity_providers.py -v -pytest tests/test_215-workload.py -v -pytest tests/test_220-my_access.py -v -pytest tests/test_230-notifications.py -v -pytest tests/test_240-secrets_manager.py -v -pytest tests/test_250-my_secrets.py -v -pytest tests/test_260-notification_mediums.py -v -pytest tests/test_270-system_policies.py -v -pytest tests/test_280_system_actions.py -v -pytest tests/test_290_system_consumers.py -v -pytest tests/test_300-system_roles.py -v -pytest tests/test_310-system_permissions.py -v -pytest tests/test_320-settings_banner.py -v -pytest tests/test_330-response_templates.py -v -pytest tests/test_340-resource_types.py -v -pytest tests/test_350-resource_labels.py -v -pytest tests/test_360-resource.py -v -pytest tests/test_370-resource_permissions.py -v -pytest tests/test_380-access_broker_profiles.py -v -pytest tests/test_390-access_broker_profiles_policies.py -v -pytest tests/test_400-access_broker_permissions.py -v -pytest tests/test_990-delete_all_resources.py -v +pytest tests/000-global_settings-01-identity_attributes.py -v +pytest tests/000-global_settings-02-notification_mediums.py -v +pytest tests/000-global_settings-03-banner.py -v +pytest tests/100-identity_management-01-users.py -v +pytest tests/100-identity_management-02-tags.py -v +pytest tests/100-identity_management-03-service_identities.py -v +pytest tests/100-identity_management-04-service_identity_tokens.py -v +pytest tests/100-identity_management-05-identity_providers.py -v +pytest tests/100-identity_management-06-workload.py -v +pytest tests/150-secrets_manager-01-secrets_manager.py -v +pytest tests/200-application_management-01-applications.py -v +pytest tests/200-application_management-02-environment_groups.py -v +pytest tests/200-application_management-03-environments.py -v +pytest tests/200-application_management-04-scans.py -v # WARNING - this one will take a while since it initiates a real scan +pytest tests/200-application_management-05-accounts.py -v # NOTE - a scan must first be completed +pytest tests/200-application_management-06-permissions.py -v # NOTE - a scan must first be completed +pytest tests/200-application_management-07-groups.py -v # NOTE - a scan must first be completed +pytest tests/200-application_management-08-profiles.py -v +pytest tests/200-application_management-09-access_builder.py -v +pytest tests/250-system-01-policies.py -v +pytest tests/250-system-02-actions.py -v +pytest tests/250-system-03-consumers.py -v +pytest tests/250-system-04-roles.py -v +pytest tests/250-system-05-permissions.py -v +pytest tests/300-workflows-01-task_services.py -v +pytest tests/300-workflows-02-tasks.py -v +pytest tests/300-workflows-03-notifications.py -v +pytest tests/350-access_broker-01-response_templates.py -v +pytest tests/350-access_broker-02-resource_types.py -v +pytest tests/350-access_broker-03-resource_labels.py -v +pytest tests/350-access_broker-04-resource.py -v +pytest tests/350-access_broker-05-resource_permissions.py -v +pytest tests/350-access_broker-06-profiles.py -v +pytest tests/350-access_broker-07-profiles_policies.py -v +pytest tests/350-access_broker-08-permissions.py -v +pytest tests/400-security-01-policies.py -v +pytest tests/400-security-02-saml.py -v +pytest tests/400-security-03-api_tokens.py -v +pytest tests/500-audit_logs-01-logs.py -v +pytest tests/500-audit_logs-02-webhooks.py -v +pytest tests/550-reports-01-reports.py -v +pytest tests/600-britive-01-my_access.py -v +pytest tests/600-britive-02-my_secrets.py -v +pytest tests/600-britive-03-my_requests.py -v +pytest tests/600-britive-04-my_approvals.py -v +pytest tests/999-cleanup-01-delete_all_resources.py -v ``` Or you can simply run `pytest -v` to test everything all at once. The above commands however allow you to halt testing diff --git a/DEPRECATION.md b/DEPRECATION.md index 0216935..4b57d69 100644 --- a/DEPRECATION.md +++ b/DEPRECATION.md @@ -1,6 +1,48 @@ # Deprecation Notices -This document holds the items which are deprecated and will be retired in the next major release. +This document holds the items which are deprecated and/or warrant specific call out with each major release. + + +## Moved methods in `v4.0.0` + +### `my_access` methods have moved: + +| Old location | New location | +| -------------------------------------- | --------------------------------------------- | +| `my_access.approval_request_status` | `my_requests.approval_request_status` | +| `my_access.approve_request` | `my_approvals.approve_request` | +| `my_access.list_approvals` | `my_approvals.list` | +| `my_access.reject_request` | `my_approvals.reject_request` | + +### `britive` methods have moved: + +| Old location | New location | +| -------------------------------------- | --------------------------------------------- | +| `access_builder` | `application_management.access_builder` | +| `accounts` | `application_management.accounts` | +| `applications` | `application_management.applications` | +| `audit_logs` | `audit_logs.logs` | +| `environment_groups` | `application_management.environment_groups` | +| `environments` | `application_management.environments` | +| `groups` | `application_management.groups` | +| `identity_attributes` | `identity_management.identity_attributes` | +| `identity_providers` | `identity_management.identity_providers` | +| `notification_mediums` | `global_settings.notification_mediums` | +| `notifications` | `workflows.notifications` | +| `permissions` | `application_management.permissions` | +| `profiles` | `application_management.profiles` | +| `saml` | `security.saml` | +| `scans` | `application_management.scans` | +| `security_policies` | `security.security_policies` | +| `service_identities` | `identity_management.service_identities` | +| `service_identity_tokens` | `identity_management.service_identity_tokens` | +| `settings` | `global_settings` | +| `step_up` | `security.step_up_auth` | +| `tags` | `identity_management.tags` | +| `task_services` | `workflows.task_services` | +| `tasks` | `workflows.tasks` | +| `users` | `identity_management.users` | +| `workload` | `identity_management.workload` | ## Removed in Major Release 3.0.0 diff --git a/LICENSE b/LICENSE index 71e404d..9d27d11 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Britive, Inc +Copyright (c) 2025 Britive, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3f8cd63..a28212f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This package aims to wrap the Britive API for usage in Python development. For t the developer/end user experience. Some APIs may also be combined into one Python method with a parameter if and where it makes more sense to present the API that way. -This package supports Python versions `>= 3.8`. +This package supports Python versions `>= 3.9`. ## Installation @@ -88,6 +88,8 @@ they exist. * Identity Attributes * Identity Providers * My Access (access granted to the given identity (user or service)) +* My Approvals +* My Requests * My Resources (access granted to the given identity (user or service)) * My Secrets (access granted to the given identity (user or service)) * Notifications @@ -194,7 +196,7 @@ import json britive = Britive() # source needed data from environment variables -print(json.dumps(britive.users.list(), indent=2, default=str)) +print(json.dumps(britive.identity_management.users.list(), indent=2, default=str)) ``` ### Provide Needed Authentication Information in the Script @@ -205,7 +207,7 @@ import json britive = Britive(tenant='example', token='...') # source token and tenant locally (not from environment variables) -print(json.dumps(britive.users.list(), indent=2, default=str)) +print(json.dumps(britive.identity_management.users.list(), indent=2, default=str)) ``` ### Create API Token for a Service Identity @@ -216,7 +218,7 @@ import json britive = Britive() # source needed data from environment variables -print(json.dumps(britive.service_identity_tokens.create(service_identity_id='abc123'), indent=2, default=str)) +print(json.dumps(britive.identity_management.service_identity_tokens.create(service_identity_id='abc123'), indent=2, default=str)) ``` ### Run a Report (JSON and CSV output) @@ -243,7 +245,7 @@ from britive.britive import Britive b = Britive() -policy = b.profiles.policies.build( +policy = b.application_management.profiles.policies.build( name='example', users=['user@domain.com'], approval_notification_medium='Email', @@ -251,5 +253,5 @@ policy = b.profiles.policies.build( time_to_approve=10 ) -b.profiles.policies.create(profile_id='...', policy=policy) +b.application_management.profiles.policies.create(profile_id='...', policy=policy) ``` diff --git a/pyproject.toml b/pyproject.toml index 45f51f0..5c047fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,22 +14,25 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Security", ] license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ - "requests>=2.31.0" + "requests>=2.32.0" ] dynamic = ["version"] keywords = ["britive", "cpam", "identity", "jit"] +[project.optional-dependencies] +azure = ["azure-identity"] + [project.urls] Homepage = "https://www.britive.com" Documentation = "https://docs.britive.com/v1/docs/en/overview-britive-apis" @@ -92,7 +95,7 @@ select = [ [tool.ruff.lint.pylint] allow-magic-value-types = ["int", "str"] -max-args = 10 +max-args = 12 max-branches = 30 max-returns = 8 max-statements = 72 diff --git a/requirements/common.txt b/requirements/common.txt index 3446c13..12dce18 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,3 +1,3 @@ # can likely change this to earlier versions - will deal with that later jmespath -requests>=2.31.0 \ No newline at end of file +requests>=2.32.0 \ No newline at end of file diff --git a/src/britive/__init__.py b/src/britive/__init__.py index 7f5601d..d6497a8 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.1.0' +__version__ = '4.0.0' diff --git a/src/britive/access_broker/__init__.py b/src/britive/access_broker/__init__.py index e69de29..12b684f 100644 --- a/src/britive/access_broker/__init__.py +++ b/src/britive/access_broker/__init__.py @@ -0,0 +1,14 @@ +from .brokers import Brokers +from .pools import Pools +from .profiles import Profiles +from .resources import Resources +from .response_templates import ResponseTemplates + + +class AccessBroker: + def __init__(self, britive) -> None: + self.profiles = Profiles(britive) + self.resources = Resources(britive) + self.response_templates = ResponseTemplates(britive) + self.brokers = Brokers(britive) + self.pools = Pools(britive) diff --git a/src/britive/access_broker/access_broker.py b/src/britive/access_broker/access_broker.py deleted file mode 100644 index 367445c..0000000 --- a/src/britive/access_broker/access_broker.py +++ /dev/null @@ -1,11 +0,0 @@ -from .profiles.profiles import Profile -from .resources.resources import Resources -from .response_templates import ResponseTemplates - - -class AccessBroker: - def __init__(self, britive): - self.britive = britive - self.profiles = Profile(britive) - self.resources = Resources(britive) - self.response_templates = ResponseTemplates(britive) diff --git a/src/britive/access_broker/brokers.py b/src/britive/access_broker/brokers.py new file mode 100644 index 0000000..8893d33 --- /dev/null +++ b/src/britive/access_broker/brokers.py @@ -0,0 +1,21 @@ +class Brokers: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/resource-manager/remote-broker/brokers' + + def list(self, status: str = '') -> list: + """ + List brokers with ability to filter by status. + + :param status: Filter brokers by a list of statuses, combining the statuses using an OR condition. + The statuses are case-sensitive. + Possible values are `active`, `inactive`, and `disconnected`. + Provide values as a comma-separated list to apply an OR filter (e.g., `status=active,inactive`). + :return: List of brokers. + """ + + params = { + 'status': status, + } + + return self.britive.get(self.base_url, params=params)['data'] diff --git a/src/britive/access_broker/pools.py b/src/britive/access_broker/pools.py new file mode 100644 index 0000000..fcf772c --- /dev/null +++ b/src/britive/access_broker/pools.py @@ -0,0 +1,212 @@ +class Pools: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/resource-manager/remote-broker/pools' + + def create(self, name: str, description: str = '', keep_alive: int = 60, disconnect: int = 86400) -> dict: + """ + Create a new broker pool. + + :param name: Name of the new broker pool. + :param description: Description of the new broker pool. Default: '' + :param keep_alive: Keep Alive Seconds of the new broker pool. Default: 60, Minimum: 60, Maximum: 1200 + :param disconnect: Disconnect Seconds of the new broker pool. Default: 86400 + :return: Details of the new broker pool. + """ + + params = { + 'name': name, + 'description': description, + 'keep-alive-seconds': keep_alive, + 'disconnect-seconds': disconnect, + } + + return self.britive.post(self.base_url, json=params) + + def list(self, name_filter: str = '') -> list: + """ + List broker pools with ability to filter by name. + + :param name_filter: Filter broker pools by starts with. + :return: List of broker pools. + """ + + params = { + 'name': name_filter, + } + + return self.britive.get(self.base_url, params=params)['data'] + + def get(self, pool_id: str) -> dict: + """ + Retrieve broker pool by ID. + + :param pool_id: Broker pool ID. + :return: Details of the broker pool. + """ + + return self.britive.get(f'{self.base_url}/{pool_id}') + + def update( + self, pool_id: str, name: str = None, description: str = None, keep_alive: int = None, disconnect: int = None + ) -> dict: + """ + Update broker pool by ID. + + :param pool_id: Broker pool ID. + :param name: Updated name of the broker pool. + :param description: Updated description of the broker pool. + :param keep_alive: Updated Keep Alive Seconds of the broker pool. + :param disconnect: Updated Disconnect Seconds of the broker pool. + :return: Details of the updated broker pool. + """ + + params = self.get(pool_id=pool_id) + + if name: + params['name'] = name + if description: + params['description'] = description + if keep_alive: + params['keep-alive-seconds'] = keep_alive + if disconnect: + params['disconnect-seconds'] = disconnect + + return self.britive.put(f'{self.base_url}/{params.pop("pool-id", pool_id)}', json=params) + + def delete(self, pool_id: str) -> None: + """ + Delete a broker pool by ID - this operation deletes all tokens, labels, and resource associations with the pool. + + :param pool_id: Broker pool ID. + :return: None. + """ + + return self.britive.delete(f'{self.base_url}/{pool_id}') + + def list_brokers(self, pool_id: str) -> list: + """ + Retrieve brokers for the broker pool. + + :param pool_id: Broker pool ID. + :return: List of brokers for the broker pool. + """ + + return self.britive.get(f'{self.base_url}/{pool_id}/brokers') + + def list_resources(self, pool_id: str, search_text: str = '') -> list: + """ + Retrieve resources for the broker pool. + + :param pool_id: Broker pool ID. + :param search_text: Filter resources by search text. + :return: List of resources for the broker pool. + """ + + params = {'filter': f'brokerPool eq {pool_id}', 'searchText': search_text} + + return self.britive.get(f'{self.britive.base_url}/resource-manager/resources', params=params) + + def create_token(self, pool_id, name: str, description: str = '') -> dict: + """ + Create new token for the broker pool - newly created tokens have an `INACTIVE` status. + + :param pool_id: Broker pool ID. + :param name: Name of the new token. + :param description: Description of the token. Default: '' + :return: Details of the new token. + """ + + params = {'token-name': name, 'description': description} + + return self.britive.post(f'{self.base_url}/{pool_id}/tokens', json=params) + + def list_tokens(self, pool_id: str) -> list: + """ + Retrieve authentication tokens for the broker pool. + + :param pool_id: Broker pool ID. + :return: List of authentication tokens. + """ + + return self.britive.get(f'{self.base_url}/{pool_id}/tokens') + + def update_token(self, pool_id: str, name: str, description: str = None, status: str = None) -> dict: + """ + Update a token by name. + + :param pool_id: Broker pool ID. + :param name: Name of the token to update. + :param description: Updated description of the token. + :param status: Updated status of the token. Options: [ACTIVE, INACTIVE] + :return: Details of the updated token. + """ + + params = {} + + if description: + params['description'] = description + if status: + params['status'] = status.upper() + + return self.britive.patch(f'{self.base_url}/{pool_id}/tokens/{name}', json=params) + + def delete_token(self, pool_id: str, name: str) -> None: + """ + Delete a token by name. + + :param pool_id: Broker pool ID. + :param name: Name of the token. + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{pool_id}/tokens/{name}') + + def list_labels(self, pool_id: str) -> list: + """ + Retrieve resource labels for the broker pool. + + :param pool_id: Broker pool ID. + :return: List of resource labels for the broker pool. + """ + + return self.britive.get(f'{self.base_url}/{pool_id}/labels') + + def add_label(self, pool_id: str, key: str, values: list) -> dict: + """ + Add a resource label to the broker pool. + + :param pool_id: Broker pool ID. + :param key: The key of the resource label. + :param values: The list of desired values of the resource label. + :return: Details of the added resource label. + """ + + params = {'key': key, 'label-values': values} + + return self.britive.post(f'{self.base_url}/{pool_id}/labels/', json=params) + + def update_label(self, pool_id: str, key: str, values: list) -> dict: + """ + Add a resource label to the broker pool. + + :param pool_id: Broker pool ID. + :param key: The key of the resource label. + :param values: The list of desired values of the resource label. + :return: Details of the updated resource label. + """ + + params = {'label-values': values} + + return self.britive.post(f'{self.base_url}/{pool_id}/labels/{key}', json=params) + + def delete_label(self, pool_id: str, key: str) -> None: + """ + Remove a resource label from the broker pool. + + :param pool_id: Broker pool ID. + :param key: The key of the resource label. + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{pool_id}/labels/{key}') diff --git a/src/britive/access_broker/profiles/profiles.py b/src/britive/access_broker/profiles/__init__.py similarity index 99% rename from src/britive/access_broker/profiles/profiles.py rename to src/britive/access_broker/profiles/__init__.py index ae06a3d..6ef24f8 100644 --- a/src/britive/access_broker/profiles/profiles.py +++ b/src/britive/access_broker/profiles/__init__.py @@ -2,7 +2,7 @@ from .policies import Policies -class Profile: +class Profiles: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/resource-manager/profiles' diff --git a/src/britive/access_broker/profiles/policies.py b/src/britive/access_broker/profiles/policies.py index f5b358b..36707fb 100644 --- a/src/britive/access_broker/profiles/policies.py +++ b/src/britive/access_broker/profiles/policies.py @@ -82,7 +82,7 @@ def get(self, profile_id: str, policy_id: str) -> dict: return self.britive.get(f'{self.base_url}/{profile_id}/policies/{policy_id}') - def update( # noqa: PLR0913 + def update( self, profile_id: str, policy_id: str, diff --git a/src/britive/access_broker/resources/resources.py b/src/britive/access_broker/resources/__init__.py similarity index 93% rename from src/britive/access_broker/resources/resources.py rename to src/britive/access_broker/resources/__init__.py index da2af3c..7ab5f47 100644 --- a/src/britive/access_broker/resources/resources.py +++ b/src/britive/access_broker/resources/__init__.py @@ -27,20 +27,25 @@ def list(self, filter_expression: str = '', search_text: str = '') -> list: return self.britive.get(f'{self.base_url}', params=params) - def create(self, name: str, resource_type_id: str, description: str = '') -> dict: + def create(self, name: str, resource_type_id: str, description: str = '', param_values: dict = None) -> dict: """ Create a new resource. :param name: Name of the resource. :param description: Description of the resource. :param resource_type_id: ID of the resource type. + :param param_values: Dict of param values. :return: Created resource. """ + if param_values is None: + param_values = {} + params = { 'name': name, 'description': description, 'resourceType': {'id': resource_type_id}, + 'paramValues': param_values, } return self.britive.post(self.base_url, json=params) diff --git a/src/britive/access_broker/resources/types.py b/src/britive/access_broker/resources/types.py index 48f3028..c9f8fbe 100644 --- a/src/britive/access_broker/resources/types.py +++ b/src/britive/access_broker/resources/types.py @@ -1,5 +1,5 @@ class Types: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/resource-manager/resource-types' diff --git a/src/britive/access_broker/response_templates.py b/src/britive/access_broker/response_templates.py index 47f0913..b041374 100644 --- a/src/britive/access_broker/response_templates.py +++ b/src/britive/access_broker/response_templates.py @@ -9,6 +9,7 @@ def create( name: str, description: str = '', is_console_enabled: bool = False, + show_on_ui: bool = False, ) -> dict: """ Create a new response template. @@ -17,6 +18,7 @@ def create( :param description: Description of the response template. :param template_data: Template data. :param is_console_enabled: Is console enabled. + :param show_on_ui: Show on UI. :return: Created response template. """ @@ -24,7 +26,8 @@ def create( 'name': name, 'description': description, 'template_data': template_data, - 'is_console_enabled': is_console_enabled, + 'isConsoleAccessEnabled': is_console_enabled, + 'show_on_ui': show_on_ui, } return self.britive.post(self.base_url, json=params) diff --git a/src/britive/application_management/__init__.py b/src/britive/application_management/__init__.py new file mode 100644 index 0000000..0317e79 --- /dev/null +++ b/src/britive/application_management/__init__.py @@ -0,0 +1,22 @@ +from .access_builder import AccessBuilderSettings +from .accounts import Accounts +from .applications import Applications +from .environment_groups import EnvironmentGroups +from .environments import Environments +from .groups import Groups +from .permissions import Permissions +from .profiles import Profiles +from .scans import Scans + + +class ApplicationManagement: + def __init__(self, britive) -> None: + self.access_builder = AccessBuilderSettings(britive) + self.accounts = Accounts(britive) + self.applications = Applications(britive) + self.environment_groups = EnvironmentGroups(britive) + self.environments = Environments(britive) + self.groups = Groups(britive) + self.permissions = Permissions(britive) + self.profiles = Profiles(britive) + self.scans = Scans(britive) diff --git a/src/britive/access_builder.py b/src/britive/application_management/access_builder.py similarity index 81% rename from src/britive/access_builder.py rename to src/britive/application_management/access_builder.py index 18eb4d6..5a45334 100644 --- a/src/britive/access_builder.py +++ b/src/britive/application_management/access_builder.py @@ -1,5 +1,5 @@ class AccessBuilderSettings: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/apps' @@ -50,7 +50,7 @@ def disable(self, application_id: str) -> None: class AccessBuilderApproversGroups: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/apps' @@ -134,7 +134,7 @@ def delete(self, application_id: str, group_id: str) -> None: class AccessBuilderAssociations: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/apps' @@ -227,7 +227,7 @@ def delete(self, application_id: str, association_id: str) -> None: class AccessBuilderNotifications: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/apps' @@ -274,7 +274,7 @@ def update(self, application_id: str, notification_mediums: list) -> None: class AccessBuilderRequesters: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/apps' @@ -319,3 +319,75 @@ def update(self, application_id: str, user_tag_members: list) -> None: data = {'memberRules': user_tag_members} return self.britive.patch(f'{self.base_url}/{application_id}/access-request-settings', json=data) + + +class AccessBuilderManagedPermissions: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/profile-requests/apps' + + def create( + self, + application_id: str, + name: str, + permissions: list, + description: str = '', + type: str = 'role', + tags: list = None, + ) -> dict: + """ + Add managed permission to the application, from Access Builder. + + :param application_id: The ID of the application. + :param name: The name of the new managed permission. + :param permissions: The policies of the new managed permission. + :param description: The description of the new managed permission. + :param type: The type of the new managed permission. + :param tags: The tags of the new managed permission. + :return: Dict containing details of the new managed permission. + """ + + data = { + 'childPermissions': permissions, + 'description': description, + 'name': name, + 'tags': [] if tags is None else tags, + 'type': type, + } + + return self.britive.get(f'{self.base_url}/{application_id}/britive-managed/permissions', json=data) + + def get(self, application_id: str, permission_id: str) -> dict: + """ + Return details of the managed permission, from Access Builder. + + :param application_id: The ID of the application. + :param permission_id: The ID of the managed permission. + :return: Dict containing details of the managed permission. + """ + + return self.britive.get(f'{self.base_url}/{application_id}/britive-managed/permissions/{permission_id}') + + def validate_policy(self, application_id: str, policy: dict) -> dict: + """ + Validate the provided permission policy, from Access Builder. + + :param application_id: + :param policy: The policy, in JSON format, to validate. + :return: Dict of findings. + """ + + return self.britive.post(f'{self.base_url}/{application_id}/britive-managed/permissions/validate', json=policy) + + def findings(self, application_id: str, permission_id: str) -> dict: + """ + Permission and policy validation findings, from Access Builder. + + :param application_id: The ID of the application. + :param permission_id: The ID of the managed permission. + :return: Dict of findings. + """ + + return self.britive.get( + f'{self.base_url}/{application_id}/britive-managed/permissions/{permission_id}/findings' + ) diff --git a/src/britive/accounts.py b/src/britive/application_management/accounts.py similarity index 99% rename from src/britive/accounts.py rename to src/britive/application_management/accounts.py index 1cd3a6a..ca86ece 100644 --- a/src/britive/accounts.py +++ b/src/britive/application_management/accounts.py @@ -107,8 +107,7 @@ def map( if map_user_to_account_in_all_application_environments: url += f'/{user_id}' return self.britive.post(url, json={'saveToAllEnvs': True}) - else: - return self.britive.post(url, json=[user_id]) + return self.britive.post(url, json=[user_id]) def unmap( self, diff --git a/src/britive/applications.py b/src/britive/application_management/applications.py similarity index 97% rename from src/britive/applications.py rename to src/britive/application_management/applications.py index ba6e576..248e95d 100644 --- a/src/britive/applications.py +++ b/src/britive/application_management/applications.py @@ -137,7 +137,7 @@ def scan(self, application_id: str) -> dict: the accounts in the org and their place in the OU structure. Scans are asynchronous operations. The response will include a `taskId` which can be used to make calls - to `britive.scans.status()` to obtain the current status of the scan. + to `britive.application_management.scans.status()` to obtain the current status of the scan. Note that scans can also be initiated from the Scans class. The same type of scan will be performed no matter where it is initiated. @@ -146,7 +146,7 @@ def scan(self, application_id: str) -> dict: :return: Details of the scan that was initiated. """ - return self.britive.scans.scan(application_id=application_id) + return self.britive.application_management.scans.scan(application_id=application_id) def delete(self, application_id: str) -> None: """ diff --git a/src/britive/environment_groups.py b/src/britive/application_management/environment_groups.py similarity index 100% rename from src/britive/environment_groups.py rename to src/britive/application_management/environment_groups.py diff --git a/src/britive/environments.py b/src/britive/application_management/environments.py similarity index 92% rename from src/britive/environments.py rename to src/britive/application_management/environments.py index 1694f2a..9717935 100644 --- a/src/britive/environments.py +++ b/src/britive/application_management/environments.py @@ -22,7 +22,7 @@ def create(self, application_id: str, name: str, description: str = '', parent_g 'type': 'environment', 'description': description, 'parentGroupId': parent_group_id - or self.britive.environment_groups.get_or_create_root(application_id=application_id), + or self.britive.application_management.environment_groups.get_or_create_root(application_id=application_id), } return self.britive.post(f'{self.base_url}/{application_id}/root-environment-group/environments', json=data) @@ -91,7 +91,7 @@ def scan(self, application_id: str, environment_id: str) -> dict: For all other application types the application will be scanned and not the underlying environment(s). Scans are asynchronous operations. The response will include a `taskId` which can be used to make calls - to `britive.scans.status()` to obtain the current status of the scan. + to `britive.application_management.scans.status()` to obtain the current status of the scan. Note that scans can also be initiated from the Scans class. The same type of scan will be performed no matter where it is initiated. @@ -101,7 +101,9 @@ def scan(self, application_id: str, environment_id: str) -> dict: :return: Details of the scan that was initiated. """ - return self.britive.scans.scan(application_id=application_id, environment_id=environment_id) + return self.britive.application_management.scans.scan( + application_id=application_id, environment_id=environment_id + ) def delete(self, application_id: str, environment_id: str) -> None: """ diff --git a/src/britive/groups.py b/src/britive/application_management/groups.py similarity index 97% rename from src/britive/groups.py rename to src/britive/application_management/groups.py index ec06821..eed31c6 100644 --- a/src/britive/groups.py +++ b/src/britive/application_management/groups.py @@ -11,7 +11,7 @@ def list( filter_expression: str = None, ) -> list: """ - Returnsdetails of all the groups associated with a given application and environment. + Returns details of all the groups associated with a given application and environment. :param application_id: The ID of the application. :param environment_id: Optionally the ID of the environment. Required only for applications which have diff --git a/src/britive/application_management/managed_permissions.py b/src/britive/application_management/managed_permissions.py new file mode 100644 index 0000000..d2464ef --- /dev/null +++ b/src/britive/application_management/managed_permissions.py @@ -0,0 +1,81 @@ +class ManagedPermissions: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/apps' + + def create( + self, + application_id: str, + name: str, + permissions: list, + description: str = '', + type: str = 'role', + tags: list = None, + ) -> dict: + """ + Create a new managed permission for use with the specified application. + + :param application_id: The ID of the application. + :param name: The name of the new managed permission. + :param permissions: The policies of the new managed permission. + :param description: The description of the new managed permission. + :param type: The type of the new managed permission. + :param tags: The tags of the new managed permission. + :return: Dict containing details of the new managed permission. + """ + + data = { + 'childPermissions': permissions, + 'description': description, + 'name': name, + 'tags': [] if tags is None else tags, + 'type': type, + } + + return self.britive.get(f'{self.base_url}/{application_id}/britive-managed/permissions', json=data) + + def list(self, application_id: str, search_text: str = None) -> list: + """ + Return the details of all managed permissions associated the specific application. + + :param application_id: The ID of the application. + :param search_text: Filter based on string match. + :return: List containing details of each managed permission. + """ + + params = {} if search_text is None else {'searchText': search_text} + + return self.britive.get(f'{self.base_url}/{application_id}/britive-managed/permissions', params=params) + + def get(self, application_id: str, permission_id: str) -> dict: + """ + Return details of the managed permission. + + :param application_id: The ID of the application. + :param permission_id: The ID of the managed permission. + :return: Dict containing details of the managed permission. + """ + + return self.britive.get(f'{self.base_url}/{application_id}/britive-managed/permissions/{permission_id}') + + def validate_policy(self, application_id: str, policy: dict) -> dict: + """ + Validate the provided permission policy. + + :param application_id: + :param policy: The policy, in JSON format, to validate. + :return: Dict of findings. + """ + + return self.britive.post(f'{self.base_url}/{application_id}/britive-managed/permissions/validate', json=policy) + + def delete(self, application_id: str, permission_id: str) -> None: + """ + Delete the managed permission. + + :param application_id: The ID of the application. + :param permission_id: The ID of the managed permission. + :return: None. + """ + + return self.britive.delete(f'{self.base_url}/{application_id}/britive-managed/permissions/{permission_id}') diff --git a/src/britive/permissions.py b/src/britive/application_management/permissions.py similarity index 100% rename from src/britive/permissions.py rename to src/britive/application_management/permissions.py diff --git a/src/britive/profiles.py b/src/britive/application_management/profiles.py similarity index 96% rename from src/britive/profiles.py rename to src/britive/application_management/profiles.py index 057a38b..d86b77b 100644 --- a/src/britive/profiles.py +++ b/src/britive/application_management/profiles.py @@ -1,7 +1,7 @@ import json from typing import Union -from . import exceptions +from britive import exceptions creation_defaults = { 'expirationDuration': 3600000, @@ -127,7 +127,7 @@ def get(self, application_id: str, profile_id: str, summary: bool = None) -> dic for profile in self.list(application_id=application_id): if profile['papId'] == profile_id: return profile - raise exceptions.ProfileNotFound() + raise exceptions.ProfileNotFound params = {} if summary: params['view'] = 'summary' @@ -382,7 +382,7 @@ def lint_condition( :returns: Results of the lint operation. """ - url = f'{self.base_url}/{profile_id}/permissions/{permission_name}/' f'{permission_type}/constraints/condition' + url = f'{self.base_url}/{profile_id}/permissions/{permission_name}/{permission_type}/constraints/condition' params = {'operation': 'validate'} @@ -485,8 +485,8 @@ def add_dynamic(self, profile_id: str, identity_attribute_id: str, tag_name: str The value will be sourced from the identity attribute specified. :param profile_id: The ID of the profile. - :param identity_attribute_id: The ID of the identity attribute. Call `britive.identity_attributes.list()` - for details on which attributes can be provided. + :param identity_attribute_id: The ID of the identity attribute. + Call `britive.identity_management.identity_attributes.list()` for which attributes can be provided. :param tag_name: The name of the session tag to include in the AssumeRoleWithSAML call. The value will be dynamically determined based on the value of the specified identity attribute. :param transitive: Set to True to mark the session tag as transitive. Review AWS documentation on why you @@ -538,8 +538,8 @@ def update_dynamic( :param profile_id: The ID of the profile. :param attribute_id: The ID of the session attribute to update. - :param identity_attribute_id: The ID of the identity attribute. Call `britive.identity_attributes.list()` - for details on which attributes can be provided. + :param identity_attribute_id: The ID of the identity attribute. + Call `britive.identity_management.identity_attributes.list()` for which attributes can be provided. :param tag_name: The name of the session tag to include in the AssumeRoleWithSAML call. The value will be dynamically determined based on the value of the specified identity attribute. :param transitive: Set to True to mark the session tag as transitive. Review AWS documentation on why you @@ -699,9 +699,6 @@ def list(self, profile_id: str) -> list: """ List all policies associated with the provided profile. - Only applicable to tenants using version 2 of profiles. If the tenant is on version 1 of profiles then use - `britive.profiles.tags.*` and `britive.profiles.identities.*` instead. - :param profile_id: The ID of the profile. :return: List of policies. """ @@ -712,9 +709,6 @@ def get(self, profile_id: str, policy_id: str, condition_as_dict: bool = False) """ Retrieve details about a specific policy which is associated with the provided profile. - Only applicable to tenants using version 2 of profiles. If the tenant is on version 1 of profiles then use - `britive.profiles.tags.*` and `britive.profiles.identities.*` instead. - :param profile_id: The ID of the profile. :param policy_id: The ID of the policy. :param condition_as_dict: Prior to version 2.22.0 a policy condition block was always returned as stringified @@ -741,9 +735,6 @@ def create(self, profile_id: str, policy: dict) -> dict: """ Create a policy associated with the provided profile. - Only applicable to tenants using version 2 of profiles. If the tenant is on version 1 of profiles then use - `britive.profiles.tags.*` and `britive.profiles.identities.*` instead. - :param profile_id: The ID of the profile. :param policy: The policy contents to create. :return: Details of the newly created policy. @@ -755,9 +746,6 @@ def update(self, profile_id: str, policy_id: str, policy: dict) -> dict: """ Update the contents of the provided policy associated with the provided profile. - Only applicable to tenants using version 2 of profiles. If the tenant is on version 1 of profiles then use - `britive.profiles.tags.*` and `britive.profiles.identities.*` instead. - :param profile_id: The ID of the profile. :param policy_id: The ID of the policy. :param policy: The policy to update. @@ -770,9 +758,6 @@ def delete(self, profile_id: str, policy_id: str) -> None: """ Delete the provided policy associated with the provided profile. - Only applicable to tenants using version 2 of profiles. If the tenant is on version 1 of profiles then use - `britive.profiles.tags.*` and `britive.profiles.identities.*` instead. - :param profile_id: The ID of the profile. :param policy_id: The ID of the policy. :return: None. diff --git a/src/britive/scans.py b/src/britive/application_management/scans.py similarity index 97% rename from src/britive/scans.py rename to src/britive/application_management/scans.py index b50c646..3864d2a 100644 --- a/src/britive/scans.py +++ b/src/britive/application_management/scans.py @@ -23,8 +23,7 @@ def scan(self, application_id: str, environment_id: str = None) -> dict: if environment_id: return self.britive.post(f'{self.base_url}/{application_id}/environments/{environment_id}/scans') - else: - return self.britive.post(f'{self.base_url}/{application_id}/scan') + return self.britive.post(f'{self.base_url}/{application_id}/scan') def status(self, task_id: str) -> dict: """ diff --git a/src/britive/audit_logs/__init__.py b/src/britive/audit_logs/__init__.py new file mode 100644 index 0000000..c1cd5b2 --- /dev/null +++ b/src/britive/audit_logs/__init__.py @@ -0,0 +1,8 @@ +from .logs import Logs +from .webhooks import Webhooks + + +class AuditLogs: + def __init__(self, britive) -> None: + self.logs = Logs(britive) + self.webhooks = Webhooks(britive) diff --git a/src/britive/audit_logs.py b/src/britive/audit_logs/logs.py similarity index 54% rename from src/britive/audit_logs.py rename to src/britive/audit_logs/logs.py index b98000e..0435acc 100644 --- a/src/britive/audit_logs.py +++ b/src/britive/audit_logs/logs.py @@ -1,16 +1,11 @@ -import contextlib from datetime import datetime, timedelta, timezone from typing import Any -import jmespath -from jmespath.exceptions import EmptyExpressionError, ParseError - -class AuditLogs: +class Logs: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/logs' - self.webhooks = AuditLogsWebhooks(britive) def fields(self) -> dict: """ @@ -48,7 +43,7 @@ def query( `datetime.datetime.utcnow()`. `to_time` will be interpreted as if in UTC timezone so it is up to the caller to ensure that the datetime object represents UTC. No timezone manipulation will occur. :param filter_expression: The expression used to filter the results. A list of available fields and operators - can be found by querying `britive.audit_logs.fields()` and `britive.audit_logs.operators`, respectively. + can be found using `britive.audit_logs.logs.fields` and `britive.audit_logs.logs.operators`, respectively. Multiple filter expressions must be joined together by `and`. No other join operator is support. Example: actor.displayName co "bob" and event.displayName eq "application" :param csv: Will result in a CSV string of the audit events being returned instead of a python list of events. @@ -63,8 +58,8 @@ def query( raise ValueError('from_time must occur before to_time.') params = { - 'from': from_time.isoformat(sep='T', timespec='seconds') + 'Z', - 'to': to_time.isoformat(sep='T', timespec='seconds') + 'Z', + 'from': from_time.isoformat(sep='T', timespec='seconds').split('+')[0] + 'Z', + 'to': to_time.isoformat(sep='T', timespec='seconds').split('+')[0] + 'Z', } if filter_expression: params['filter'] = filter_expression @@ -72,62 +67,3 @@ def query( params['size'] = 200 return self.britive.get(f'{self.base_url}{"/csv" if csv else ""}', params=params) - - -class AuditLogsWebhooks: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/logs/webhooks' - - def create_or_update(self, notification_medium_id: str, jmespath_filter: str = '', description: str = '') -> dict: - """ - Return details of a created, or updated if the notificationMediumId already exists, audit log webhook. - - :param notification_medium_id: the notificationMediumId webhook to create or update. - :param jmespath_filter: a JMESPath filter to apply to log entries before sending to the webhook. - :param description: the description of the audit log webhook. - :return: Dict of field keys to field names. - """ - - try: - with contextlib.suppress(EmptyExpressionError): - jmespath.compile(jmespath_filter) - except ParseError as e: - raise ValueError('Invalid JMESPath.') from e - - params = { - 'notificationMediumId': notification_medium_id, - 'filter': jmespath_filter, - 'description': description, - } - - return self.britive.post(f'{self.base_url}', json=params) - - def get(self, notification_medium_id: str) -> dict: - """ - Return audit log webhook details specified by notificationMediumId. - - :param notification_medium_id: the notificationMediumId webhook to retrieve. - :return: Dict of field keys to field names. - """ - - return self.britive.get(f'{self.base_url}/{notification_medium_id}') - - def list(self) -> list: - """ - Return a list of audit log webhook details for the tenant. - - :return: List of field keys to field names. - """ - - return self.britive.get(f'{self.base_url}') - - def delete(self, notification_medium_id: str) -> None: - """ - Delete an audit log webhook specified by notificationMediumId. - - :param notification_medium_id: the notificationMediumId webhook to delete. - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{notification_medium_id}') diff --git a/src/britive/audit_logs/webhooks.py b/src/britive/audit_logs/webhooks.py new file mode 100644 index 0000000..f9bcce3 --- /dev/null +++ b/src/britive/audit_logs/webhooks.py @@ -0,0 +1,63 @@ +import contextlib + +import jmespath +from jmespath.exceptions import EmptyExpressionError, ParseError + + +class Webhooks: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/logs/webhooks' + + def create_or_update(self, notification_medium_id: str, jmespath_filter: str = '', description: str = '') -> dict: + """ + Return details of a created, or updated if the notificationMediumId already exists, audit log webhook. + + :param notification_medium_id: the notificationMediumId webhook to create or update. + :param jmespath_filter: a JMESPath filter to apply to log entries before sending to the webhook. + :param description: the description of the audit log webhook. + :return: Dict of field keys to field names. + """ + + try: + with contextlib.suppress(EmptyExpressionError): + jmespath.compile(jmespath_filter) + except ParseError as e: + raise ValueError('Invalid JMESPath.') from e + + params = { + 'notificationMediumId': notification_medium_id, + 'filter': jmespath_filter, + 'description': description, + } + + return self.britive.post(f'{self.base_url}', json=params) + + def get(self, notification_medium_id: str) -> dict: + """ + Return audit log webhook details specified by notificationMediumId. + + :param notification_medium_id: the notificationMediumId webhook to retrieve. + :return: Dict of field keys to field names. + """ + + return self.britive.get(f'{self.base_url}/{notification_medium_id}') + + def list(self) -> list: + """ + Return a list of audit log webhook details for the tenant. + + :return: List of field keys to field names. + """ + + return self.britive.get(f'{self.base_url}') + + def delete(self, notification_medium_id: str) -> None: + """ + Delete an audit log webhook specified by notificationMediumId. + + :param notification_medium_id: the notificationMediumId webhook to delete. + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{notification_medium_id}') diff --git a/src/britive/britive.py b/src/britive/britive.py index 71f9b27..6f3067c 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -1,53 +1,39 @@ -import json as native_json import os -import socket import time import requests -from .access_broker.access_broker import AccessBroker -from .access_builder import AccessBuilderSettings -from .accounts import Accounts -from .api_tokens import ApiTokens -from .applications import Applications +from . import __version__ +from .access_broker import AccessBroker +from .application_management import ApplicationManagement from .audit_logs import AuditLogs -from .environment_groups import EnvironmentGroups -from .environments import Environments from .exceptions import ( - InvalidFederationProvider, RootEnvironmentGroupNotFound, TenantMissingError, TenantUnderMaintenance, TokenMissingError, - allowed_exceptions, ) -from .groups import Groups -from .helpers import federation_providers as fp -from .helpers import methods as helper_methods -from .identity_attributes import IdentityAttributes -from .identity_providers import IdentityProviders +from .global_settings import GlobalSettings +from .helpers.utils import ( + check_response_for_error, + handle_response, + pagination_type, + parse_tenant, + response_has_no_content, + source_federation_token, + tenant_is_under_maintenance, +) +from .identity_management import IdentityManagement from .my_access import MyAccess +from .my_approvals import MyApprovals +from .my_requests import MyRequests from .my_resources import MyResources from .my_secrets import MySecrets -from .notification_mediums import NotificationMediums -from .notifications import Notifications -from .permissions import Permissions -from .profiles import Profiles from .reports import Reports -from .saml import Saml -from .scans import Scans from .secrets_manager import SecretsManager -from .security_policies import SecurityPolicies -from .service_identities import ServiceIdentities -from .service_identity_tokens import ServiceIdentityTokens -from .settings.settings import Settings -from .step_up import StepUpAuth -from .system.system import System -from .tags import Tags -from .task_services import TaskServices -from .tasks import Tasks -from .users import Users -from .workload import Workload +from .security import ApiTokens, Security +from .system import System +from .workflows import Workflows class Britive: @@ -91,8 +77,8 @@ def __init__( token: str = None, query_features: bool = True, token_federation_provider: str = None, - token_federation_provider_duration_seconds: int = 900, - ): + token_duration: int = 900, + ) -> None: """ Instantiate an authenticated interface that can be used to communicate with the Britive API. @@ -105,238 +91,92 @@ def __init__( want to wait for that API call. Querying for features will help instruct the SDK as to what API calls are allowed to be used based on the features enabled, vs. attempting to make the API call and getting an error. :param token_federation_provider: The federation provider to use to source the token. Details of what can be - provided can be found in the documentation for the Britive.source_federation_token_from method. + provided can be found in the documentation for the Britive.helpers.utils.source_federation_token method. :param token_federation_provider_duration_seconds: Only applicable for the AWS provider. Specify the number of seconds for which the generated token is valid. Defaults to 900 seconds (15 minutes). :raises: TenantMissingError, TokenMissingError """ self.tenant = tenant or os.getenv('BRITIVE_TENANT') - if not self.tenant: - raise TenantMissingError( - 'Tenant not explicitly provided and could not be sourced from environment variable BRITIVE_TENANT' - ) - - if token_federation_provider: - self.__token = self.source_federation_token_from( - provider=token_federation_provider, - tenant=self.tenant, - duration_seconds=token_federation_provider_duration_seconds, - ) - else: - self.__token = token or os.getenv('BRITIVE_API_TOKEN') - - if not self.__token: - raise TokenMissingError( - 'Token not explicitly provided and could not be sourced from environment variable BRITIVE_API_TOKEN' - ) - - # clean up and apply logic to the passed in tenant (for backwards compatibility with no domain being required) - self.tenant = self.parse_tenant(self.tenant) - - self.base_url = f'https://{self.tenant}/api' - self.session = requests.Session() - self.retry_max_times = 5 + raise TenantMissingError('Tenant not provided and cannot be sourced from environment.') + + self.__token = self._initialize_token(token, token_federation_provider, token_duration) + self.base_url = f'https://{parse_tenant(self.tenant)}/api' + self.session = self._setup_session() + self.retry_backoff_factor = 1 - self.retry_response_status = [429, 500, 502, 503, 504] + self.retry_max_times = 5 + self.retry_response_status = {429, 500, 502, 503, 504} - # if PYBRITIVE_CA_BUNDLE set, in pybritive most likely, use it - britive_ca_bundle = os.getenv('PYBRITIVE_CA_BUNDLE') - if britive_ca_bundle: - self.session.verify = britive_ca_bundle + self._initialize_components(query_features) - # allow the disabling of TLS/SSL verification for testing in development (mostly local development) - if os.getenv('BRITIVE_NO_VERIFY_SSL') and '.dev.' in self.tenant: - # turn off ssl verification - self.session.verify = False - # wipe these due to this bug: https://github.com/psf/requests/issues/3829 - os.environ['CURL_CA_BUNDLE'] = '' - os.environ['REQUESTS_CA_BUNDLE'] = '' - # disable the warning message - import urllib3 + def _initialize_token(self, token: str, provider: str, duration: int) -> str: + if provider: + return source_federation_token(provider, self.tenant, duration) + return token or os.getenv('BRITIVE_API_TOKEN') or TokenMissingError('Token not provided.') - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + def _setup_session(self) -> requests.Session: + session = requests.Session() - token_type = 'TOKEN' if len(self.__token) < 50 else 'Bearer' - if len(self.__token.split('::')) > 1: - token_type = 'WorkloadToken' + # if PYBRITIVE_CA_BUNDLE set, in pybritive most likely, use it + if britive_ca_bundle := os.getenv('PYBRITIVE_CA_BUNDLE'): + session.verify = britive_ca_bundle - try: - import britive + # allow the disabling of TLS/SSL verification for testing in development (mostly local development) + if os.getenv('BRITIVE_NO_VERIFY_SSL') and '.dev.' in self.tenant: + session.verify = False + self._disable_ssl_verification_warnings() - version = britive.__version__ - except Exception: - version = 'unknown' + token_type = self._determine_token_type() + version = __version__ - self.session.headers.update( + session.headers.update( { 'Authorization': f'{token_type} {self.__token}', 'Content-Type': 'application/json', 'User-Agent': f'britive-python-sdk/{version} {requests.utils.default_user_agent()}', } ) + return session + + def _disable_ssl_verification_warnings(self) -> None: + # wipe these due to this bug: https://github.com/psf/requests/issues/3829 + os.environ['CURL_CA_BUNDLE'] = '' + os.environ['REQUESTS_CA_BUNDLE'] = '' + import urllib3 - self.access_builder = AccessBuilderSettings(self) - self.accounts = Accounts(self) + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + def _determine_token_type(self) -> str: + if len(self.__token) < 50: + return 'TOKEN' + if len(self.__token.split('::')) > 1: + return 'WorkloadToken' + return 'Bearer' + + def _initialize_components(self, query_features: bool) -> None: + self.access_broker = AccessBroker(self) self.api_tokens = ApiTokens(self) - self.applications = Applications(self) - self.audit_logs = AuditLogs(self) - self.environment_groups = EnvironmentGroups(self) - self.environments = Environments(self) self.feature_flags = self.features() if query_features else {} - self.groups = Groups(self) - self.identity_attributes = IdentityAttributes(self) - self.identity_providers = IdentityProviders(self) self.my_access = MyAccess(self) + self.my_approvals = MyApprovals(self) + self.my_requests = MyRequests(self) self.my_resources = MyResources(self) self.my_secrets = MySecrets(self) - self.notification_mediums = NotificationMediums(self) - self.notifications = Notifications(self) - self.permissions = Permissions(self) - self.profiles = Profiles(self) self.reports = Reports(self) - self.saml = Saml(self) - self.scans = Scans(self) self.secrets_manager = SecretsManager(self) - self.security_policies = SecurityPolicies(self) - self.service_identities = ServiceIdentities(self) - self.service_identity_tokens = ServiceIdentityTokens(self) - self.settings = Settings(self) - self.step_up = StepUpAuth(self) self.system = System(self) - self.tags = Tags(self) - self.task_services = TaskServices(self) - self.tasks = Tasks(self) - self.users = Users(self) - self.workload = Workload(self) - - # depends on my_access - self.access_broker = AccessBroker(self) - - @staticmethod - def source_federation_token_from(provider: str, tenant: str = None, duration_seconds: int = 900) -> str: - """ - Returns a token from the specified federation provider. - - The caller must persist this token if required. New tokens can be generated on each invocation - of this class as well. - - This method only works when running within the context of the specified provider. - It is meant to abstract away the complexities of obtaining a federation token - from common federation providers. Other provider federation tokens can still be - sourced outside of this SDK and provided as input via the standard token presentation - options. - - Six federation providers are currently supported by this method. - - * AWS IAM/STS, with optional profile specified - (aws) - * Github Actions (github) - * Bitbucket Pipelines (bitbucket) - * Azure System Assigned Managed Identities (azuresmi) - * Azure User Assigned Managed Identities (azureumi) - * spacelift.io (spacelift) - * Gitlab (gitlab) - - Any other OIDC federation provider can be used and tokens can be provided to this class for authentication - to a Britive tenant. Details of how to construct these tokens can be found at https://docs.britive.com. - - :param provider: The name of the federation provider. Valid options are `aws`, `github`, `bitbucket`, - `azuresmi`, `azureumi`, `spacelift`, and `gitlab`. - - For the AWS provider it is possible to provide a profile via value `aws-profile`. If no profile is provided - then the boto3 `Session.get_credentials()` method will be used to obtain AWS credentials, which follows - the order provided here: - https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#configuring-credentials - - For the Github provider it is possible to provide an OIDC audience value via `github-`. If no - audience is provided the default Github audience value will be used. - - For Azure User Assigned Managed Identities (azureumi) a client id is required. It must be - provided in the form `azureumi-`. From the Azure documentation...a user-assigned identity's - client ID or, when using Pod Identity, the client ID of an Azure AD app registration. This argument - is supported in all hosting environments. - - For both Azure Managed Identity options it is possible to provide an OIDC audience value via - `azuresmi-` and `azureumi-|`. If no audience is provided the default audience - of `https://management.azure.com/` will be used. - - For the Gitlab provider a token environment variable name can optionally be specified via `gitlab-ENV_VAR`. - Anything after `gitlab-` will be interpreted to represent the name of the environment variable specified - in the YAML file for the ID token. If not provided it will default to `BRITIVE_OIDC_TOKEN`. - - :param tenant: The name of the tenant. This field is optional but if not provided then the tenant will be - sourced from environment variable BRITIVE_TENANT. Knowing the actual tenant is required for the AWS - federation provider. This field can be ignored for non AWS federation providers. - :param duration_seconds: Only applicable for the AWS provider. Specify the number of seconds for which the - generated token is valid. Defaults to 900 seconds (15 minutes). - :return: A federation token that can be used to authenticate to a Britive tenant. - """ - - helper = provider.split('-', maxsplit=1) - provider = helper[0] - - if provider == 'aws': - profile = helper_methods.safe_list_get(helper, 1, None) - return fp.AwsFederationProvider(profile=profile, tenant=tenant, duration=duration_seconds).get_token() - - if provider == 'github': - audience = helper_methods.safe_list_get(helper, 1, None) - return fp.GithubFederationProvider(audience=audience).get_token() - - if provider == 'bitbucket': - return fp.BitbucketFederationProvider().get_token() - - if provider == 'azuresmi': - audience = helper_methods.safe_list_get(helper, 1, None) - return fp.AzureSystemAssignedManagedIdentityFederationProvider(audience=audience).get_token() - - if provider == 'azureumi': - additional_attributes_str = helper_methods.safe_list_get(helper, 1, None) - if not additional_attributes_str: - raise ValueError('client id is required via azurumi-') - additional_attributes = additional_attributes_str.split('|') - client_id = additional_attributes[0] - audience = helper_methods.safe_list_get(additional_attributes, 1, None) - return fp.AzureUserAssignedManagedIdentityFederationProvider( - client_id=client_id, audience=audience - ).get_token() - - if provider == 'spacelift': - return fp.SpaceliftFederationProvider().get_token() - - if provider == 'gitlab': - token_name = helper_methods.safe_list_get(helper, 1, None) - return fp.GitlabFederationProvider(token_env_var=token_name).get_token() - - raise InvalidFederationProvider(f'federation provider {provider} not supported') - - @staticmethod - def parse_tenant(tenant: str) -> str: - domain = tenant.replace('https://', '').replace('http://', '') # remove scheme - domain = domain.split('/')[0] # remove any paths as they will not be needed - try: - domain_helper = domain.split(':') - port = 443 - if len(domain_helper) > 1: - port = domain_helper[1] - domain_without_port = domain_helper[0] - socket.getaddrinfo(host=domain_without_port, port=port) # if success then a full domain was provided - return domain - except socket.gaierror: # assume just the tenant name was provided (originally the only supported method) - domain = f'{tenant}.britive-app.com' - try: - socket.getaddrinfo(host=domain, port=443) # validate the hostname is real - return domain # and if so set the tenant accordingly - except socket.gaierror as se: - raise Exception(f'Invalid tenant provided: {tenant}. DNS resolution failed.') from se + self.application_management = ApplicationManagement(self) + self.audit_logs = AuditLogs(self) + self.identity_management = IdentityManagement(self) + self.global_settings = GlobalSettings(self) + self.security = Security(self) + self.workflows = Workflows(self) def features(self) -> dict: - features = {} - for feature in self.get(f'{self.base_url}/features'): - features[feature['name']] = feature['enabled'] - return features + return {feature['name']: feature['enabled'] for feature in self.get(f'{self.base_url}/features')} def banner(self) -> dict: return self.get(f'{self.base_url}/banner') @@ -374,163 +214,88 @@ def patch_upload(self, url, file_content_as_str, content_type, filename) -> dict files = {filename: (f'{filename}.xml', file_content_as_str, content_type)} response = self.session.patch(url, files=files, headers={'Content-Type': None}) - try: - return response.json() - except native_json.decoder.JSONDecodeError: # if we cannot decode json then the response isn't json - return response.content.decode('utf-8') + return handle_response(response) # note - this method is only used to upload a file when creating a secret def post_upload(self, url, params=None, files=None) -> dict: """Internal use only.""" + response = self.session.post(url, params=params, files=files, headers={'Content-Type': None}) - try: - return response.json() - except native_json.decoder.JSONDecodeError: # if we cannot decode json then the response isn't json - return response.content.decode('utf-8') - - @staticmethod - def __check_response_for_error(response) -> None: - if response.status_code in allowed_exceptions: - try: - content = native_json.loads(response.content.decode('utf-8')) - message = ( - f"{response.status_code} - " - f"{content.get('errorCode') or 'E0000'} - " - f"{content.get('message') or 'no message available'}" - ) - if content.get('details'): - message += f" - {content.get('details')}" - raise allowed_exceptions[response.status_code](message) - except native_json.decoder.JSONDecodeError as je: - content = response.content.decode('utf-8') - message = f'{response.status_code} - {content}' - raise allowed_exceptions[response.status_code](message) from je - - @staticmethod - def __response_has_no_content(response) -> bool: - # handle 204 No Content response - if response.status_code == 204: - return True - - # handle empty 200 response - return response.status_code == 200 and len(response.content) == 0 - - @staticmethod - def __pagination_type(headers, result) -> str: - is_dict = isinstance(result, dict) - has_next_page_header = 'next-page' in headers - - if is_dict and all(x in result for x in ['count', 'page', 'size', 'data']): - return 'inline' - if is_dict and has_next_page_header and all(x in result for x in ['data', 'reportId']): # reports - return 'report' - if has_next_page_header: # this interesting way of paginating is how audit_logs.query() does it - return 'audit' - if is_dict and all(x in result for x in ['result', 'pagination']): - return 'secmgr' - return 'none' - - @staticmethod - def __tenant_is_under_maintenance(response) -> bool: - try: - return response.status_code == 503 and response.json().get('errorCode') == 'MAINT0001' - except Exception: - return False + return handle_response(response) def __request_with_exponential_backoff_and_retry(self, method, url, params, data, json) -> dict: num_retries = 0 - response = {} + while num_retries <= self.retry_max_times: response = self.session.request(method, url, params=params, data=data, json=json) # handle the use case of a tenant being in maintenance mode # which means we should break out of this loop early and # not perform the backoff and retry logic - if self.__tenant_is_under_maintenance(response=response): + if tenant_is_under_maintenance(response): raise TenantUnderMaintenance(response.json().get('message')) - # check for error - if so perform exponential backoff if response.status_code in self.retry_response_status: - wait_time = (2**num_retries) * self.retry_backoff_factor - time.sleep(wait_time) + time.sleep((2**num_retries) * self.retry_backoff_factor) num_retries += 1 - else: # we got a "good" response so break out of while loop + else: + check_response_for_error(response.status_code, handle_response(response)) break - self.__check_response_for_error(response) # handle an error response + return response def __request(self, method, url, params=None, data=None, json=None) -> dict: return_data = [] - num_iterations = 1 - pagination_type = None - while True: # infinite loop in case of pagination - we will break the loop when needed - response = self.__request_with_exponential_backoff_and_retry( - method=method, url=url, params=params, data=data, json=json - ) - if self.__response_has_no_content(response): # handle no content responses + _pagination_type = None + + while True: + response = self.__request_with_exponential_backoff_and_retry(method, url, params, data, json) + if response_has_no_content(response): return None # handle secrets file download - lowercase_headers = {h.lower(): v.lower() for h, v in response.headers.items()} - content_disposition = lowercase_headers.get('content-disposition', '') + content_disposition = response.headers.get('content-disposition', '').lower() if 'attachment' in content_disposition and 'downloadfile' in url: - filename = response.headers.get('content-disposition').split('=')[1].replace('"', '').strip() + filename = response.headers['content-disposition'].split('=')[1].replace('"', '').strip() return {'filename': filename, 'content_bytes': bytes(response.content)} # load the result as a dict - try: - result = response.json() - except ValueError: # includes simplejson.decoder.JSONDecodeError and native_json.decoder.JSONDecodeError - return response.content.decode('utf-8') + result = handle_response(response) + _pagination_type = _pagination_type or pagination_type(response.headers, result) # check on the pagination and iterate if required - we only need to check on this after the first # request - checking it each time can screw up the logic when dealing with pagination coming from - # the response headers as the header won't exist which will mean pagination_type will change to 'none' + # the response headers as the header won't exist which will mean _pagination_type will change to 'none' # which means we drop into the else block below and assign just the LAST page as the result, which # is obviously not what we want to be doing. - if num_iterations == 1: - pagination_type = self.__pagination_type(response.headers, result) - - if pagination_type == 'inline': + if _pagination_type == 'inline': return_data += result['data'] - count = result['count'] - page = result['page'] - size = result['size'] - if size * (page + 1) >= count: # if we have reached the max number of records time to break the loop + if result['size'] * (result['page'] + 1) >= result['count']: break - params['page'] = page + 1 - elif pagination_type == 'audit': - return_data += result # result is already a list + params['page'] = result['page'] + 1 + elif _pagination_type in ('audit', 'report'): + return_data += result if _pagination_type == 'audit' else result['data'] if 'next-page' not in response.headers: break url = response.headers['next-page'] - params = {} # the next-page header has all the URL parameters we need so unset them here - elif pagination_type == 'report': - return_data += result['data'] - if 'next-page' not in response.headers: - break - url = response.headers['next-page'] - params = {} # the next-page header has all the URL parameters we need so unset them here - elif pagination_type == 'secmgr': + params = {} + elif _pagination_type == 'secmgr': return_data += result['result'] url = result['pagination'].get('next', '') - if url == '': + if not url: break - else: # we are not dealing with pagination so just return the response as-is + else: return_data = result break - num_iterations += 1 - - # finally return the response data return return_data def get_root_environment_group(self, application_id: str) -> str: """Internal use only.""" - app = self.applications.get(application_id=application_id) - root_env_group = app.get('rootEnvironmentGroup') or {} - for group in root_env_group.get('environmentGroups', []): - if group['parentId'] == '': + app = self.application_management.applications.get(application_id=application_id) + root_env_group = app.get('rootEnvironmentGroup', {}).get('environmentGroups', []) + for group in root_env_group: + if not group['parentId']: return group['id'] - raise RootEnvironmentGroupNotFound() + raise RootEnvironmentGroupNotFound diff --git a/src/britive/exceptions.py b/src/britive/exceptions.py deleted file mode 100644 index abf0edc..0000000 --- a/src/britive/exceptions.py +++ /dev/null @@ -1,162 +0,0 @@ -class AccessDenied(Exception): - pass - - -class ApiTokenNotFound(Exception): - pass - - -class ApprovalRequiredButNoJustificationProvided(Exception): - pass - - -class StepUpAuthRequiredButNotProvided(Exception): - pass - - -class StepUpAuthFailed(Exception): - pass - - -class StepUpAuthOTPNotProvided(Exception): - pass - - -class ApprovalWorkflowRejected(Exception): - pass - - -class ApprovalWorkflowTimedOut(Exception): - pass - - -class ForbiddenRequest(Exception): - pass - - -class InternalServerError(Exception): - pass - - -class InvalidFederationProvider(Exception): - pass - - -class InvalidRequest(Exception): - pass - - -class MethodNotAllowed(Exception): - pass - - -class NoSecretsVaultFound(Exception): - pass - - -class NotExecutingInAzureEnvironment(Exception): - pass - - -class NotExecutingInBitbucketEnvironment(Exception): - pass - - -class NotExecutingInGithubEnvironment(Exception): - pass - - -class NotExecutingInSpaceliftEnvironment(Exception): - pass - - -class NotExecutingInGitlabEnvironment(Exception): - pass - - -class NotFound(Exception): - pass - - -class ProfileApprovalMaxBlockTimeExceeded(Exception): - pass - - -class ProfileApprovalRejected(Exception): - pass - - -class ProfileApprovalTimedOut(Exception): - pass - - -class ProfileApprovalWithdrawn(Exception): - pass - - -class ProfileCheckoutAlreadyApproved(Exception): - pass - - -class ProfileNotFound(Exception): - pass - - -class RootEnvironmentGroupNotFound(Exception): - pass - - -class ServiceUnavailable(Exception): - pass - - -class TenantMissingError(Exception): - pass - - -class TokenMissingError(Exception): - pass - - -class TooManyServiceIdentitiesFound(Exception): - pass - - -class TooManyUsersFound(Exception): - pass - - -class TransactionNotFound(Exception): - pass - - -class UnauthorizedRequest(Exception): - pass - - -class UserDoesNotHaveMFAEnabled(Exception): - pass - - -class UserNotAllowedToChangePassword(Exception): - pass - - -class UserNotAssociatedWithDefaultIdentityProvider(Exception): - pass - - -class TenantUnderMaintenance(Exception): - pass - - -# from https://docs.britive.com/docs/restapi-status-codes -allowed_exceptions = { - 400: InvalidRequest, - 401: UnauthorizedRequest, - 403: ForbiddenRequest, - 404: NotFound, - 405: MethodNotAllowed, - 500: InternalServerError, - 503: ServiceUnavailable, -} diff --git a/src/britive/exceptions/__init__.py b/src/britive/exceptions/__init__.py new file mode 100644 index 0000000..ff8d271 --- /dev/null +++ b/src/britive/exceptions/__init__.py @@ -0,0 +1,170 @@ +class BritiveException(Exception): + pass + + +class AccessDenied(BritiveException): + pass + + +class ApiTokenNotFound(BritiveException): + pass + + +class ApprovalRequiredButNoJustificationProvided(BritiveException): + pass + + +class ApprovalWorkflowRejected(BritiveException): + pass + + +class ApprovalWorkflowTimedOut(BritiveException): + pass + + +class ForbiddenRequest(BritiveException): + pass + + +class InternalServerError(BritiveException): + pass + + +class InvalidFederationProvider(BritiveException): + pass + + +class InvalidRequest(BritiveException): + pass + + +class MethodNotAllowed(BritiveException): + pass + + +class MissingAzureDependency(BritiveException): + pass + + +class NoSecretsVaultFound(BritiveException): + pass + + +class NotExecutingInAzureEnvironment(BritiveException): + pass + + +class NotExecutingInBitbucketEnvironment(BritiveException): + pass + + +class NotExecutingInGithubEnvironment(BritiveException): + pass + + +class NotExecutingInSpaceliftEnvironment(BritiveException): + pass + + +class NotExecutingInGitlabEnvironment(BritiveException): + pass + + +class NotFound(BritiveException): + pass + + +class ProfileApprovalMaxBlockTimeExceeded(BritiveException): + pass + + +class ProfileApprovalRejected(BritiveException): + pass + + +class ProfileApprovalTimedOut(BritiveException): + pass + + +class ProfileApprovalWithdrawn(BritiveException): + pass + + +class ProfileCheckoutAlreadyApproved(BritiveException): + pass + + +class ProfileNotFound(BritiveException): + pass + + +class RootEnvironmentGroupNotFound(BritiveException): + pass + + +class ServiceUnavailable(BritiveException): + pass + + +class StepUpAuthFailed(BritiveException): + pass + + +class StepUpAuthOTPNotProvided(BritiveException): + pass + + +class StepUpAuthRequiredButNotProvided(BritiveException): + pass + + +class TenantMissingError(BritiveException): + pass + + +class TenantUnderMaintenance(BritiveException): + pass + + +class TokenMissingError(BritiveException): + pass + + +class TooManyServiceIdentitiesFound(BritiveException): + pass + + +class TooManyUsersFound(BritiveException): + pass + + +class TransactionNotFound(BritiveException): + pass + + +class UnauthorizedRequest(BritiveException): + pass + + +class UserDoesNotHaveMFAEnabled(BritiveException): + pass + + +class UserNotAllowedToChangePassword(BritiveException): + pass + + +class UserNotAssociatedWithDefaultIdentityProvider(BritiveException): + pass + + +# from https://docs.britive.com/docs/restapi-status-codes +allowed_exceptions = { + 400: InvalidRequest, + 401: UnauthorizedRequest, + 403: ForbiddenRequest, + 404: NotFound, + 405: MethodNotAllowed, + 500: InternalServerError, + 503: ServiceUnavailable, +} diff --git a/src/britive/exceptions/badrequest.py b/src/britive/exceptions/badrequest.py new file mode 100644 index 0000000..fe5bb6b --- /dev/null +++ b/src/britive/exceptions/badrequest.py @@ -0,0 +1,528 @@ +from . import BritiveException + + +class BritiveBadRequestException(BritiveException): + pass + + +class ApplicationCreationError(BritiveBadRequestException): + pass + + +class ApplicationPropertiesReadError(BritiveBadRequestException): + pass + + +class ApplicationUpdateError(BritiveBadRequestException): + pass + + +class ApplicationDeletionError(BritiveBadRequestException): + pass + + +class ApplicationSavePropertiesError(BritiveBadRequestException): + pass + + +class MissingJustificationError(BritiveBadRequestException): + pass + + +class InvalidJustificationError(BritiveBadRequestException): + pass + + +class MissingTicketError(BritiveBadRequestException): + pass + + +class MissingTicketTypeError(BritiveBadRequestException): + pass + + +class MissingTicketIdError(BritiveBadRequestException): + pass + + +class InvalidTicketTypeError(BritiveBadRequestException): + pass + + +class InvalidTicketIdError(BritiveBadRequestException): + pass + + +class ApiTokenCreationError(BritiveBadRequestException): + pass + + +class ApiTokenUpdateError(BritiveBadRequestException): + pass + + +class ApiTokenDeletionError(BritiveBadRequestException): + pass + + +class AwsProfileCheckInError(BritiveBadRequestException): + pass + + +class AzureProfileCheckInError(BritiveBadRequestException): + pass + + +class GcpProfileCheckInError(BritiveBadRequestException): + pass + + +class OracleProfileCheckInError(BritiveBadRequestException): + pass + + +class SalesForceProfileCheckInError(BritiveBadRequestException): + pass + + +class ServiceNowProfileCheckInError(BritiveBadRequestException): + pass + + +class OktaProfileCheckInError(BritiveBadRequestException): + pass + + +class SnowflakeProfileCheckInError(BritiveBadRequestException): + pass + + +class BritiveProfileCheckInError(BritiveBadRequestException): + pass + + +class AwsIdentityCenterProfileCheckInError(BritiveBadRequestException): + pass + + +class AwsProfileCheckOutError(BritiveBadRequestException): + pass + + +class AzureProfileCheckOutError(BritiveBadRequestException): + pass + + +class GcpProfileCheckOutError(BritiveBadRequestException): + pass + + +class OracleProfileCheckOutError(BritiveBadRequestException): + pass + + +class SalesForceProfileCheckOutError(BritiveBadRequestException): + pass + + +class ServiceNowProfileCheckOutError(BritiveBadRequestException): + pass + + +class OktaProfileCheckOutError(BritiveBadRequestException): + pass + + +class SnowflakeProfileCheckOutError(BritiveBadRequestException): + pass + + +class BritiveProfileCheckOutError(BritiveBadRequestException): + pass + + +class AwsIdentityCenterProfileCheckOutError(BritiveBadRequestException): + pass + + +class ProfileCheckInError(BritiveBadRequestException): + pass + + +class ProfileCheckOutError(BritiveBadRequestException): + pass + + +class ProfileStatusReadError(BritiveBadRequestException): + pass + + +class ProfileStatusUpdateError(BritiveBadRequestException): + pass + + +class ProfileSharedAccountsReadError(BritiveBadRequestException): + pass + + +class ProfileSnapshotDeletionError(BritiveBadRequestException): + pass + + +class ProfileSnapshotCreationError(BritiveBadRequestException): + pass + + +class ProfileAlreadyCheckedInError(BritiveBadRequestException): + pass + + +class ApprovalJustificationRequiredError(BritiveBadRequestException): + pass + + +class ProfileApprovalRequiredError(BritiveBadRequestException): + pass + + +class PendingProfileApprovalRequestError(BritiveBadRequestException): + pass + + +class ProfileCreationError(BritiveBadRequestException): + pass + + +class ProfileReadError(BritiveBadRequestException): + pass + + +class ProfileUpdateError(BritiveBadRequestException): + pass + + +class ProfileDeletionError(BritiveBadRequestException): + pass + + +class ProfileValidationError(BritiveBadRequestException): + pass + + +class ProfileFavoriteSaveError(BritiveBadRequestException): + pass + + +class ProfileFavoriteDeleteError(BritiveBadRequestException): + pass + + +class ProfileFavoriteReadError(BritiveBadRequestException): + pass + + +class ProfileRequestError(BritiveBadRequestException): + pass + + +class ProfilePolicyPermissionsError(BritiveBadRequestException): + pass + + +class ProfilePolicyInvalidTokenError(BritiveBadRequestException): + pass + + +class ProfilePolicyCreationError(BritiveBadRequestException): + pass + + +class ProfilePolicyUpdateError(BritiveBadRequestException): + pass + + +class ProfilePolicyGenericError(BritiveBadRequestException): + pass + + +class ProfilePolicyCreationUpdateError(BritiveBadRequestException): + pass + + +class ResourceFavoriteRemovalError(BritiveBadRequestException): + pass + + +class ResourceProfileCheckOutError(BritiveBadRequestException): + pass + + +class ResourceProfileCheckInNotAllowedError(BritiveBadRequestException): + pass + + +class ResourceInvalidTransactionError(BritiveBadRequestException): + pass + + +class ResourceUnauthorizedCheckInError(BritiveBadRequestException): + pass + + +class ResourceMissingAssociationsError(BritiveBadRequestException): + pass + + +class ResourceMissingPermissionVariableError(BritiveBadRequestException): + pass + + +class ResourcePermissionNotExistError(BritiveBadRequestException): + pass + + +class ResourceLabelKeyMissingError(BritiveBadRequestException): + pass + + +class ResourceProfileMissingError(BritiveBadRequestException): + pass + + +class ResourceTypeMissingError(BritiveBadRequestException): + pass + + +class ResponseTemplateMissingError(BritiveBadRequestException): + pass + + +class ResourceVariablePermissionMissingError(BritiveBadRequestException): + pass + + +class ResourceProfilePermissionExistsError(BritiveBadRequestException): + pass + + +class ResourceProfilePermissionTypeMismatchError(BritiveBadRequestException): + pass + + +class ResourcePermissionNotAddableError(BritiveBadRequestException): + pass + + +class UsernameFormatValidationError(BritiveBadRequestException): + pass + + +class UserPropertiesReadError(BritiveBadRequestException): + pass + + +class UserModificationError(BritiveBadRequestException): + pass + + +class UserCreationError(BritiveBadRequestException): + pass + + +class UserUpdateError(BritiveBadRequestException): + pass + + +class UserDeletionError(BritiveBadRequestException): + pass + + +class UserDisableError(BritiveBadRequestException): + pass + + +class UserEnableError(BritiveBadRequestException): + pass + + +class UserAlreadyExistsError(BritiveBadRequestException): + pass + + +class UserPasswordResetError(BritiveBadRequestException): + pass + + +class UserMfaResetError(BritiveBadRequestException): + pass + + +class UserTagCreationError(BritiveBadRequestException): + pass + + +class UserTagModificationError(BritiveBadRequestException): + pass + + +class UserTagReadError(BritiveBadRequestException): + pass + + +class UserTagAddError(BritiveBadRequestException): + pass + + +class UserTagAddDuplicateError(BritiveBadRequestException): + pass + + +class UserTagRemoveError(BritiveBadRequestException): + pass + + +class UserTagDisableError(BritiveBadRequestException): + pass + + +class UserTagEnableError(BritiveBadRequestException): + pass + + +class UserTagDeletionError(BritiveBadRequestException): + pass + + +class UserTagUpdateError(BritiveBadRequestException): + pass + + +class ReportLoadError(BritiveBadRequestException): + pass + + +class MfaDisableAllUsersError(BritiveBadRequestException): + pass + + +class GenericError(BritiveBadRequestException): + pass + + +bad_request_code_map = { + # Application related + 'A-0001': ApplicationCreationError, + 'A-0002': ApplicationPropertiesReadError, + 'A-0003': ApplicationUpdateError, + 'A-0004': ApplicationDeletionError, + 'A-0005': ApplicationSavePropertiesError, + # Advanced settings related + 'AS-0001': MissingJustificationError, + 'AS-0002': InvalidJustificationError, + 'AS-0003': MissingTicketError, + 'AS-0004': MissingTicketTypeError, + 'AS-0005': MissingTicketIdError, + 'AS-0006': InvalidTicketTypeError, + 'AS-0007': InvalidTicketIdError, + # API token related + 'AT-0001': ApiTokenCreationError, + 'AT-0002': ApiTokenUpdateError, + 'AT-0003': ApiTokenDeletionError, + # Application-specific profile check-in related + 'CI-0001': AwsProfileCheckInError, + 'CI-0002': AzureProfileCheckInError, + 'CI-0003': GcpProfileCheckInError, + 'CI-0004': OracleProfileCheckInError, + 'CI-0005': SalesForceProfileCheckInError, + 'CI-0006': ServiceNowProfileCheckInError, + 'CI-0007': OktaProfileCheckInError, + 'CI-0008': SnowflakeProfileCheckInError, + 'CI-0009': BritiveProfileCheckInError, + 'CI-0010': AwsIdentityCenterProfileCheckInError, + # Application-specific profile checkout related + 'CO-0001': AwsProfileCheckOutError, + 'CO-0002': AzureProfileCheckOutError, + 'CO-0003': GcpProfileCheckOutError, + 'CO-0004': OracleProfileCheckOutError, + 'CO-0005': SalesForceProfileCheckOutError, + 'CO-0006': ServiceNowProfileCheckOutError, + 'CO-0007': OktaProfileCheckOutError, + 'CO-0008': SnowflakeProfileCheckOutError, + 'CO-0009': BritiveProfileCheckOutError, + 'CO-0010': AwsIdentityCenterProfileCheckOutError, + # My Access related + 'MA-0001': ProfileCheckInError, + 'MA-0002': ProfileCheckOutError, + 'MA-0003': ProfileStatusReadError, + 'MA-0004': ProfileStatusUpdateError, + 'MA-0005': ProfileSharedAccountsReadError, + 'MA-0006': ProfileSnapshotDeletionError, + 'MA-0007': ProfileSnapshotCreationError, + 'MA-0008': ProfileAlreadyCheckedInError, + 'MA-0009': ApprovalJustificationRequiredError, + 'MA-0010': ProfileApprovalRequiredError, + 'MA-0011': PendingProfileApprovalRequestError, + # Profile management related + 'P-0001': ProfileCreationError, + 'P-0002': ProfileReadError, + 'P-0003': ProfileUpdateError, + 'P-0004': ProfileDeletionError, + 'P-0005': ProfileValidationError, + # Profile Favorites related + 'PF-0001': ProfileFavoriteSaveError, + 'PF-0002': ProfileFavoriteDeleteError, + 'PF-0003': ProfileFavoriteReadError, + # Profile Policy related + 'PP-001': ProfilePolicyPermissionsError, + 'PP-002': ProfilePolicyInvalidTokenError, + 'PP-003': ProfilePolicyCreationError, + 'PP-004': ProfilePolicyUpdateError, + 'PP-005': ProfilePolicyGenericError, + 'PP-006': ProfilePolicyCreationUpdateError, + # Profile request related + 'PR-0001': ProfileRequestError, + # Access Broker related + 'RM-0001': ResourceFavoriteRemovalError, + 'RM-0002': ResourceProfileCheckOutError, + 'RM-0003': ResourceProfileCheckInNotAllowedError, + 'RM-0004': ResourceInvalidTransactionError, + 'RM-0005': ResourceUnauthorizedCheckInError, + 'RM-0006': ResourceMissingAssociationsError, + 'RM-0007': ResourceMissingPermissionVariableError, + 'RM-0008': ResourcePermissionNotExistError, + 'RM-0009': ResourceLabelKeyMissingError, + 'RM-0010': ResourceProfileMissingError, + 'RM-0011': ResourceTypeMissingError, + 'RM-0012': ResponseTemplateMissingError, + 'RM-0013': ResourceVariablePermissionMissingError, + 'RM-0014': ResourceProfilePermissionExistsError, + 'RM-0015': ResourceProfilePermissionTypeMismatchError, + 'RM-0016': ResourcePermissionNotAddableError, + # Users related + 'U-0001': UsernameFormatValidationError, + 'U-0002': UserPropertiesReadError, + 'U-0003': UserModificationError, + 'U-0004': UserCreationError, + 'U-0005': UserUpdateError, + 'U-0006': UserDeletionError, + 'U-0007': UserDisableError, + 'U-0008': UserEnableError, + 'U-0009': UserAlreadyExistsError, + 'U-0010': UserPasswordResetError, + 'U-0011': UserMfaResetError, + # User tags related + 'UT-0001': UserTagCreationError, + 'UT-0002': UserTagModificationError, + 'UT-0003': UserTagReadError, + 'UT-0004': UserTagAddError, + 'UT-0005': UserTagAddDuplicateError, + 'UT-0006': UserTagRemoveError, + 'UT-0007': UserTagDisableError, + 'UT-0008': UserTagEnableError, + 'UT-0009': UserTagDeletionError, + 'UT-0010': UserTagUpdateError, + # Miscellaneous + 'R-0001': ReportLoadError, + 'IDP-0001': MfaDisableAllUsersError, + 'G-0001': GenericError, +} diff --git a/src/britive/exceptions/generic.py b/src/britive/exceptions/generic.py new file mode 100644 index 0000000..443103a --- /dev/null +++ b/src/britive/exceptions/generic.py @@ -0,0 +1,90 @@ +from . import BritiveException + + +class BritiveGenericException(BritiveException): + pass + + +class BritiveGenericError(BritiveGenericException): + pass + + +class ValidationError(BritiveGenericException): + pass + + +class DoesNotExistError(BritiveGenericException): + pass + + +class DuplicateError(BritiveGenericException): + pass + + +class UserInactiveError(BritiveGenericException): + pass + + +class UserAssignedToProfileDirectlyError(BritiveGenericException): + pass + + +class UserHasAccessViaTagError(BritiveGenericException): + pass + + +class UserAccountLinkAlreadyExists(BritiveGenericException): + pass + + +class ProfileWithMultipleConflictingTagsError(BritiveGenericException): + pass + + +class UnsupportedFeatureError(BritiveGenericException): + pass + + +class CannotUnmapManuallyAutomaticallyMappedAccounts(BritiveGenericException): + pass + + +class InvalidEmailError(BritiveGenericException): + pass + + +class EvaluationError(BritiveGenericException): + pass + + +class ApprovalPendingError(BritiveGenericException): + pass + + +class ApprovalRequiredError(BritiveGenericException): + pass + + +class StepUpAuthenticationRequiredError(BritiveGenericException): + pass + + +generic_code_map = { + 'E1000': BritiveGenericException, + 'E1001': BritiveGenericError, + 'E1003': ValidationError, + 'E1004': DoesNotExistError, + 'E1005': DuplicateError, + 'E1006': UserInactiveError, + 'E1008': UserAssignedToProfileDirectlyError, + 'E1009': UserHasAccessViaTagError, + 'E1010': UserAccountLinkAlreadyExists, + 'E1011': ProfileWithMultipleConflictingTagsError, + 'E1012': UnsupportedFeatureError, + 'E1013': CannotUnmapManuallyAutomaticallyMappedAccounts, + 'E1014': InvalidEmailError, + 'PE-0002': EvaluationError, + 'PE-0010': ApprovalPendingError, + 'PE-0011': ApprovalRequiredError, + 'PE-0028': StepUpAuthenticationRequiredError, +} diff --git a/src/britive/exceptions/unauthorized.py b/src/britive/exceptions/unauthorized.py new file mode 100644 index 0000000..0d2a6d7 --- /dev/null +++ b/src/britive/exceptions/unauthorized.py @@ -0,0 +1,120 @@ +from . import BritiveException + + +class BritiveUnauthorizedException(BritiveException): + pass + + +class AuthenticationFailureError(BritiveUnauthorizedException): + pass + + +class InvalidChallengeParameterError(BritiveUnauthorizedException): + pass + + +class InvalidSsoProviderError(BritiveUnauthorizedException): + pass + + +class UserNotFoundError(BritiveUnauthorizedException): + pass + + +class UserDisabledError(BritiveUnauthorizedException): + pass + + +class ServiceIdentityUserError(BritiveUnauthorizedException): + pass + + +class UserValidationError(BritiveUnauthorizedException): + pass + + +class InvalidOAuthTokenError(BritiveUnauthorizedException): + pass + + +class SelfPasswordChangeError(BritiveUnauthorizedException): + pass + + +class OtpValidationError(BritiveUnauthorizedException): + pass + + +class UserMfaEnrollmentError(BritiveUnauthorizedException): + pass + + +class AuthenticatedTempPasswordChangeError(BritiveUnauthorizedException): + pass + + +class ForgottenPasswordError(BritiveUnauthorizedException): + pass + + +class SsoLoginUserValidationError(BritiveUnauthorizedException): + pass + + +class InvalidTenantError(BritiveUnauthorizedException): + pass + + +class ApiTokenNotFoundError(BritiveUnauthorizedException): + pass + + +class ApiTokenRevokedError(BritiveUnauthorizedException): + pass + + +class ApiTokenExpiredError(BritiveUnauthorizedException): + pass + + +class InvalidApiTokenError(BritiveUnauthorizedException): + pass + + +class CliAuthenticationError(BritiveUnauthorizedException): + pass + + +class InvalidCliTokenError(BritiveUnauthorizedException): + pass + + +class AccountLockoutError(BritiveUnauthorizedException): + pass + + +unauthorized_code_map = { + # Error Codes for the Status Code 401 Unauthorized + 'AU-0000': AuthenticationFailureError, + 'AU-0001': InvalidChallengeParameterError, + 'AU-0002': InvalidSsoProviderError, + 'AU-0003': UserNotFoundError, + 'AU-0004': UserDisabledError, + 'AU-0005': ServiceIdentityUserError, + 'AU-0006': UserValidationError, + 'AU-0007': InvalidOAuthTokenError, + 'AU-0008': SelfPasswordChangeError, + 'AU-0009': OtpValidationError, + 'AU-0010': UserMfaEnrollmentError, + 'AU-0011': AuthenticatedTempPasswordChangeError, + 'AU-0012': ForgottenPasswordError, + 'AU-0013': SsoLoginUserValidationError, + 'AU-0014': InvalidTenantError, + 'AU-0015': ApiTokenNotFoundError, + 'AU-0016': ApiTokenRevokedError, + 'AU-0017': ApiTokenExpiredError, + 'AU-0018': InvalidApiTokenError, + 'AU-0019': CliAuthenticationError, + 'AU-0020': InvalidCliTokenError, + 'AU-0021': AccountLockoutError, +} diff --git a/src/britive/federation_providers/__init__.py b/src/britive/federation_providers/__init__.py new file mode 100644 index 0000000..1577514 --- /dev/null +++ b/src/britive/federation_providers/__init__.py @@ -0,0 +1,20 @@ +from .aws import AwsFederationProvider +from .azure_system_assigned_managed_identity import AzureSystemAssignedManagedIdentityFederationProvider +from .azure_user_assigned_managed_identity import AzureUserAssignedManagedIdentityFederationProvider +from .bitbucket import BitbucketFederationProvider +from .federation_provider import FederationProvider +from .github import GithubFederationProvider +from .gitlab import GitlabFederationProvider +from .spacelift import SpaceliftFederationProvider + + +class FederationProviders: + def __init__(self, britive) -> None: + self.aws = AwsFederationProvider(britive) + self.azure_system_assigned_managed_identity = AzureSystemAssignedManagedIdentityFederationProvider(britive) + self.azure_user_assigned_managed_identity = AzureUserAssignedManagedIdentityFederationProvider(britive) + self.bitbucket = BitbucketFederationProvider(britive) + self.generic = FederationProvider(britive) + self.github = GithubFederationProvider(britive) + self.gitlab = GitlabFederationProvider(britive) + self.spacelift = SpaceliftFederationProvider(britive) diff --git a/src/britive/helpers/federation_providers.py b/src/britive/federation_providers/aws.py similarity index 51% rename from src/britive/helpers/federation_providers.py rename to src/britive/federation_providers/aws.py index ab5a053..f1ddec3 100644 --- a/src/britive/helpers/federation_providers.py +++ b/src/britive/federation_providers/aws.py @@ -5,123 +5,22 @@ import json import os -from .. import exceptions +from britive.exceptions import TenantMissingError - -class FederationProvider: - def __init__(self) -> None: - pass - - def get_token(self) -> None: - raise NotImplementedError() - - -class AzureSystemAssignedManagedIdentityFederationProvider(FederationProvider): - def __init__(self, audience: str = None) -> None: - self.audience = audience if audience else 'https://management.azure.com/' - super().__init__() - - def get_token(self) -> str: - # azure-identity is not a hard requirement of this SDK but is required for the - # azure provider so checking to ensure it exists - try: - from azure.identity._exceptions import CredentialUnavailableError - except ImportError as e: - raise Exception( - 'azure-identity required - please install azure-identity package to use the azure managed ' - 'identity federation provider' - ) from e - - try: - from azure.identity import ManagedIdentityCredential - - token = ManagedIdentityCredential().get_token(self.audience).token - return f'OIDC::{token}' - except ImportError as e: - raise Exception( - 'azure-identity required - please install azure-identity package to use the azure managed ' - 'identity federation provider' - ) from e - except CredentialUnavailableError as e: - msg = ( - 'the codebase is not executing in a azure environment or some other issue is causing the ' - 'managed identity credentials to be unavailable' - ) - raise exceptions.NotExecutingInAzureEnvironment(msg) from e - - -class AzureUserAssignedManagedIdentityFederationProvider(FederationProvider): - def __init__(self, client_id: str, audience: str = None) -> None: - self.audience = audience if audience else 'https://management.azure.com/' - self.client_id = client_id - super().__init__() - - def get_token(self) -> str: - # azure-identity is not a hard requirement of this SDK but is required for the - # azure provider so checking to ensure it exists - try: - from azure.identity._exceptions import CredentialUnavailableError - except ImportError as e: - raise Exception( - 'azure-identity required - please install azure-identity package to use the azure managed ' - 'identity federation provider' - ) from e - - try: - from azure.identity import ManagedIdentityCredential - - token = ManagedIdentityCredential(client_id=self.client_id).get_token(self.audience).token - return f'OIDC::{token}' - except ImportError as e: - raise Exception( - 'azure-identity required - please install azure-identity package to use the azure managed ' - 'identity federation provider' - ) from e - except CredentialUnavailableError as e: - msg = ( - 'the codebase is not executing in a azure environment or some other issue is causing the ' - 'managed identity credentials to be unavailable' - ) - raise exceptions.NotExecutingInAzureEnvironment(msg) from e - - -class GithubFederationProvider(FederationProvider): - def __init__(self, audience: str = None) -> None: - self.audience = audience - super().__init__() - - def get_token(self) -> str: - import requests - - url = os.environ.get('ACTIONS_ID_TOKEN_REQUEST_URL') - bearer_token = os.environ.get('ACTIONS_ID_TOKEN_REQUEST_TOKEN') - - if not url or not bearer_token: - msg = ( - 'the codebase is not executing in a github environment and/or the action is ' - 'is not set to use oidc permissions' - ) - raise exceptions.NotExecutingInGithubEnvironment(msg) - - headers = {'User-Agent': 'actions/oidc-client', 'Authorization': f'Bearer {bearer_token}'} - - if self.audience: - url += f'&audience={self.audience}' - - response = requests.get(url, headers=headers) - return f'OIDC::{response.json()["value"]}' +from .federation_provider import FederationProvider class AwsFederationProvider(FederationProvider): def __init__(self, profile: str, tenant: str, duration: int = 900) -> None: - from ..britive import Britive # doing import here to avoid circular dependency + from britive.britive import Britive # doing import here to avoid circular dependency self.profile = profile self.duration = duration temp_tenant = tenant or os.getenv('BRITIVE_TENANT') if not temp_tenant: - print('Error: the aws federation provider requires the britive tenant as part of the signing algorithm') - raise exceptions.TenantMissingError() + raise TenantMissingError( + 'Error: the aws federation provider requires the britive tenant as part of the signing algorithm' + ) self.tenant = Britive.parse_tenant(temp_tenant).split(':')[0] # remove the port if it exists super().__init__() @@ -134,8 +33,7 @@ def get_signature_key(key, date_stamp, region_name, service_name) -> str: k_date = AwsFederationProvider.sign(('AWS4' + key).encode('utf-8'), date_stamp) k_region = AwsFederationProvider.sign(k_date, region_name) k_service = AwsFederationProvider.sign(k_region, service_name) - k_signing = AwsFederationProvider.sign(k_service, 'aws4_request') - return k_signing + return AwsFederationProvider.sign(k_service, 'aws4_request') def get_token(self) -> str: # boto3 is not a hard requirement of this SDK but is required for the @@ -151,7 +49,7 @@ def get_token(self) -> str: try: session = boto3.Session(profile_name=self.profile) except botoexceptions.ProfileNotFound as e: - raise Exception(f'Error: {str(e)}') from e + raise Exception(f'Error: {e!s}') from e creds = session.get_credentials() access_key_id = creds.access_key @@ -249,48 +147,3 @@ def get_token(self) -> str: token_encoded = base64.urlsafe_b64encode(json.dumps(token).encode('utf-8')) return f'AWS::{token_encoded.decode("utf-8")}' - - -class BitbucketFederationProvider(FederationProvider): - def __init__(self) -> None: - super().__init__() - - # https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/ - def get_token(self) -> str: - id_token = os.environ.get('BITBUCKET_STEP_OIDC_TOKEN') - if not id_token: - msg = ( - 'the codebase is not executing in a bitbucket environment and/or the `oidc` flag ' - 'is not set on the pipeline step' - ) - raise exceptions.NotExecutingInBitbucketEnvironment(msg) - return f'OIDC::{id_token}' - - -class SpaceliftFederationProvider(FederationProvider): - def __init__(self) -> None: - super().__init__() - - # https://docs.spacelift.io/integrations/cloud-providers/oidc/ - def get_token(self) -> str: - id_token = os.environ.get('SPACELIFT_OIDC_TOKEN') - if not id_token: - msg = 'the codebase is not executing in a spacelift.io environment or not using a paid account' - raise exceptions.NotExecutingInSpaceliftEnvironment(msg) - return f'OIDC::{id_token}' - - -class GitlabFederationProvider(FederationProvider): - def __init__(self, token_env_var: str = 'BRITIVE_OIDC_TOKEN') -> None: - super().__init__() - self.token_env_var = token_env_var - - def get_token(self) -> str: - id_token = os.environ.get(self.token_env_var) - if not id_token: - msg = ( - 'the codebase is not executing in a gitlab environment or the incorrect token environment variable ' - 'was specified' - ) - raise exceptions.NotExecutingInGitlabEnvironment(msg) - return f'OIDC::{id_token}' diff --git a/src/britive/federation_providers/azure_system_assigned_managed_identity.py b/src/britive/federation_providers/azure_system_assigned_managed_identity.py new file mode 100644 index 0000000..13f87d4 --- /dev/null +++ b/src/britive/federation_providers/azure_system_assigned_managed_identity.py @@ -0,0 +1,27 @@ +from britive.exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment + +from .federation_provider import FederationProvider + + +class AzureSystemAssignedManagedIdentityFederationProvider(FederationProvider): + def __init__(self, audience: str = None) -> None: + self.audience = audience if audience else 'https://management.azure.com/' + super().__init__() + + def get_token(self) -> str: + try: + from azure.identity import ManagedIdentityCredential + from azure.identity._exceptions import CredentialUnavailableError + + token = ManagedIdentityCredential().get_token(self.audience).token + return f'OIDC::{token}' + except ImportError as e: + raise MissingAzureDependency( + '`azure-identity` package required to use the azure managed identity federation provider' + ) from e + except CredentialUnavailableError as e: + msg = ( + 'the codebase is not executing in an Azure environment or some other issue is causing the ' + 'managed identity credentials to be unavailable' + ) + raise NotExecutingInAzureEnvironment(msg) from e diff --git a/src/britive/federation_providers/azure_user_assigned_managed_identity.py b/src/britive/federation_providers/azure_user_assigned_managed_identity.py new file mode 100644 index 0000000..b3968a7 --- /dev/null +++ b/src/britive/federation_providers/azure_user_assigned_managed_identity.py @@ -0,0 +1,28 @@ +from britive.exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment + +from .federation_provider import FederationProvider + + +class AzureUserAssignedManagedIdentityFederationProvider(FederationProvider): + def __init__(self, client_id: str, audience: str = None) -> None: + self.audience = audience if audience else 'https://management.azure.com/' + self.client_id = client_id + super().__init__() + + def get_token(self) -> str: + try: + from azure.identity import ManagedIdentityCredential + from azure.identity._exceptions import CredentialUnavailableError + + token = ManagedIdentityCredential(client_id=self.client_id).get_token(self.audience).token + return f'OIDC::{token}' + except ImportError as e: + raise MissingAzureDependency( + '`azure-identity` package required to use the azure managed identity federation provider' + ) from e + except CredentialUnavailableError as e: + msg = ( + 'the codebase is not executing in an Azure environment or some other issue is causing the ' + 'managed identity credentials to be unavailable' + ) + raise NotExecutingInAzureEnvironment(msg) from e diff --git a/src/britive/federation_providers/bitbucket.py b/src/britive/federation_providers/bitbucket.py new file mode 100644 index 0000000..cb2ae2c --- /dev/null +++ b/src/britive/federation_providers/bitbucket.py @@ -0,0 +1,20 @@ +import os + +from britive.exceptions import NotExecutingInBitbucketEnvironment + +from .federation_provider import FederationProvider + + +class BitbucketFederationProvider(FederationProvider): + def __init__(self) -> None: + super().__init__() + + def get_token(self) -> str: + id_token = os.environ.get('BITBUCKET_STEP_OIDC_TOKEN') + if not id_token: + msg = ( + 'the codebase is not executing in a bitbucket environment and/or the `oidc` flag ' + 'is not set on the pipeline step' + ) + raise NotExecutingInBitbucketEnvironment(msg) + return f'OIDC::{id_token}' diff --git a/src/britive/federation_providers/federation_provider.py b/src/britive/federation_providers/federation_provider.py new file mode 100644 index 0000000..758aa28 --- /dev/null +++ b/src/britive/federation_providers/federation_provider.py @@ -0,0 +1,6 @@ +class FederationProvider: + def __init__(self) -> None: + pass + + def get_token(self) -> None: + raise NotImplementedError diff --git a/src/britive/federation_providers/github.py b/src/britive/federation_providers/github.py new file mode 100644 index 0000000..1a4c1f1 --- /dev/null +++ b/src/britive/federation_providers/github.py @@ -0,0 +1,32 @@ +import os + +import requests + +from britive.exceptions import NotExecutingInGithubEnvironment + +from .federation_provider import FederationProvider + + +class GithubFederationProvider(FederationProvider): + def __init__(self, audience: str = None) -> None: + self.audience = audience + super().__init__() + + def get_token(self) -> str: + url = os.environ.get('ACTIONS_ID_TOKEN_REQUEST_URL') + bearer_token = os.environ.get('ACTIONS_ID_TOKEN_REQUEST_TOKEN') + + if not url or not bearer_token: + msg = ( + 'the codebase is not executing in a github environment and/or the action is ' + 'not set to use oidc permissions' + ) + raise NotExecutingInGithubEnvironment(msg) + + headers = {'User-Agent': 'actions/oidc-client', 'Authorization': f'Bearer {bearer_token}'} + + if self.audience: + url += f'&audience={self.audience}' + + response = requests.get(url, headers=headers) + return f'OIDC::{response.json()["value"]}' diff --git a/src/britive/federation_providers/gitlab.py b/src/britive/federation_providers/gitlab.py new file mode 100644 index 0000000..a32184b --- /dev/null +++ b/src/britive/federation_providers/gitlab.py @@ -0,0 +1,21 @@ +import os + +from britive.exceptions import NotExecutingInGitlabEnvironment + +from .federation_provider import FederationProvider + + +class GitlabFederationProvider(FederationProvider): + def __init__(self, token_env_var: str = 'BRITIVE_OIDC_TOKEN') -> None: + super().__init__() + self.token_env_var = token_env_var + + def get_token(self) -> str: + id_token = os.environ.get(self.token_env_var) + if not id_token: + msg = ( + 'the codebase is not executing in a gitlab environment or the incorrect token environment variable ' + 'was specified' + ) + raise NotExecutingInGitlabEnvironment(msg) + return f'OIDC::{id_token}' diff --git a/src/britive/federation_providers/spacelift.py b/src/britive/federation_providers/spacelift.py new file mode 100644 index 0000000..62d3030 --- /dev/null +++ b/src/britive/federation_providers/spacelift.py @@ -0,0 +1,18 @@ +import os + +from britive.exceptions import NotExecutingInSpaceliftEnvironment + +from .federation_provider import FederationProvider + + +class SpaceliftFederationProvider(FederationProvider): + def __init__(self) -> None: + super().__init__() + + # https://docs.spacelift.io/integrations/cloud-providers/oidc/ + def get_token(self) -> str: + id_token = os.environ.get('SPACELIFT_OIDC_TOKEN') + if not id_token: + msg = 'the codebase is not executing in a spacelift.io environment or not using a paid account' + raise NotExecutingInSpaceliftEnvironment(msg) + return f'OIDC::{id_token}' diff --git a/src/britive/global_settings/__init__.py b/src/britive/global_settings/__init__.py new file mode 100644 index 0000000..0ff7b79 --- /dev/null +++ b/src/britive/global_settings/__init__.py @@ -0,0 +1,10 @@ +from .banner import Banner +from .firewall import Firewall +from .notification_mediums import NotificationMediums + + +class GlobalSettings: + def __init__(self, britive) -> None: + self.banner = Banner(britive) + self.firewall = Firewall(britive) + self.notification_mediums = NotificationMediums(britive) diff --git a/src/britive/settings/banner.py b/src/britive/global_settings/banner.py similarity index 97% rename from src/britive/settings/banner.py rename to src/britive/global_settings/banner.py index 59ee6e7..0fdaf93 100644 --- a/src/britive/settings/banner.py +++ b/src/britive/global_settings/banner.py @@ -1,7 +1,7 @@ from datetime import datetime -class SettingsBanner: +class Banner: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/settings/banner' @@ -47,7 +47,7 @@ def set( if not all_schedule_fields_present and not no_schedule_fields_present: raise ValueError( - 'if providing schedule information then start_datetime, ' 'end_datetime, and time_zone are required' + 'if providing schedule information then start_datetime, end_datetime, and time_zone are required' ) if all_schedule_fields_present: diff --git a/src/britive/global_settings/firewall.py b/src/britive/global_settings/firewall.py new file mode 100644 index 0000000..fc63df2 --- /dev/null +++ b/src/britive/global_settings/firewall.py @@ -0,0 +1,87 @@ +class Firewall: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/settings/firewall' + + def list_rules(self) -> list: + """ + Get the firewall rules. + + :returns: List of the firewall rules. + """ + + return self.britive.get(self.base_url) + + def save_rules(self, rules: list, default_action: str = 'DENY', antilockout: bool = False) -> dict: + """ + Save firewall rules. + + :param rules: List of firewall rules. + Rule parameters: + priority: + type: int + desc: Required - The rules are executed as per the priority number + action: + type: str + desc: Required - `ALLOW` or `DENY`. + field: + type: str + desc: Required - `country` or `client_ip` + invert: + type: bool + desc: `True` of `False` - Default: `False` + operator: + type: str + desc: Required - Depends on field - Options: `EQUALS`|`CONTAINS`|`STARTSWITH`|`ENDSWITH` + values: + type: list + desc: Required - Depends on field - List of client IPs(v4 and/or v6) or country codes. + :param default_action: `ALLOW` or `DENY` - Default: `DENY` + :param antilockout: + :returns: Details of the saved firewall rules. + """ + + data = { + 'rules': rules, + 'default_action': default_action, + 'antilockout': antilockout, + } + + return self.britive.post(self.base_url, json=data) + + def save_fields(self, fields: dict) -> None: + """ + Save firewall fields. + + :param fields: Dict of firewall fields. + Key: + type: str + desc: Name of the firewall field, e.g. `country` or `client_ip`. + Value: + type: dict + desc: key: value pairs containing requisite values for [`header`, `field_type`, `info`, `operators`] + header: + type: str + desc: The rules are executed as per the priority number + field_type: + type: str + desc: `ip` or `string`. + info: + type: str + desc: Information about, or description of, the field. + operators: + type: str + desc: Options: `EQUALS`|`CONTAINS`|`STARTSWITH`|`ENDSWITH` + :returns: Details of the saved firewall fields. + """ + + return self.britive.post(f'{self.base_url}/fields', json={'fields': fields}) + + def list_fields(self) -> list: + """ + Get the firewall fields. + + :returns: List of the firewall fields. + """ + + return self.britive.get(f'{self.base_url}/fields') diff --git a/src/britive/notification_mediums.py b/src/britive/global_settings/notification_mediums.py similarity index 100% rename from src/britive/notification_mediums.py rename to src/britive/global_settings/notification_mediums.py diff --git a/src/britive/helpers/__init__.py b/src/britive/helpers/__init__.py index e69de29..0ba44fb 100644 --- a/src/britive/helpers/__init__.py +++ b/src/britive/helpers/__init__.py @@ -0,0 +1,6 @@ +from .methods import HelperMethods + + +class Helpers: + def __init__(self, britive) -> None: + self.helper_methods = HelperMethods(britive) diff --git a/src/britive/helpers/methods.py b/src/britive/helpers/methods.py index 899b475..bdb75e7 100644 --- a/src/britive/helpers/methods.py +++ b/src/britive/helpers/methods.py @@ -1,8 +1,53 @@ -from typing import Union +class HelperMethods: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/access' + def get_profile_and_environment_ids_given_names( + self, profile_name: str, environment_name: str, application_name: str = None + ) -> dict: + ids = None + environment_found = False + profile_found = False + for app in self.britive.get(self.base_url): + if application_name and app['appName'].lower() != application_name.lower(): + continue + if not ( + profile := next((p for p in app['profiles'] if p['profileName'].lower() == profile_name.lower()), None) + ): + continue + profile_found = True + if environment := next( + (e for e in profile['environments'] if e['environmentName'].lower() == environment_name.lower()), None + ): + environment_found = True + if ids: + raise ValueError( + f'multiple combinations of profile `{profile_name}` and environment ' + f'`{environment_name}` exist so no unique combination can be determined. Please ' + f'provide the optional parameter `application_name` to clarify which application ' + f'the environment belongs to.' + ) + ids = {'profile_id': profile['profileId'], 'environment_id': environment['environmentId']} + if not profile_found: + raise ValueError(f'profile `{profile_name}` not found.') + if profile_found and not environment_found: + raise ValueError(f'profile `{profile_name}` found but not in environment `{environment_name}`.') + return ids -def safe_list_get(lst, idx, default) -> Union[str, None]: - try: - return lst[idx] - except IndexError: - return default + def get_profile_and_resource_ids_given_names(self, profile_name: str, resource_name: str) -> dict: + resource_profile_map = { + f'{item["resourceName"].lower()}|{item["profileName"].lower()}': { + 'profile_id': item['profileId'], + 'resource_id': item['resourceId'], + } + for item in self.britive.get(f'{self.britive.base_url}/resource-manager/my-resources') + } + + item = resource_profile_map.get(f'{resource_name.lower()}|{profile_name.lower()}') + + # do some error checking + if not item: + raise ValueError('resource and profile combination not found') + + return item diff --git a/src/britive/helpers/utils.py b/src/britive/helpers/utils.py new file mode 100644 index 0000000..4891d7a --- /dev/null +++ b/src/britive/helpers/utils.py @@ -0,0 +1,174 @@ +import socket +from typing import Optional, Union + +import requests + +from britive.exceptions import BritiveException, InvalidFederationProvider, allowed_exceptions +from britive.exceptions.badrequest import bad_request_code_map +from britive.exceptions.generic import generic_code_map +from britive.exceptions.unauthorized import unauthorized_code_map +from britive.federation_providers import ( + AwsFederationProvider, + AzureSystemAssignedManagedIdentityFederationProvider, + AzureUserAssignedManagedIdentityFederationProvider, + BitbucketFederationProvider, + GithubFederationProvider, + GitlabFederationProvider, + SpaceliftFederationProvider, +) + + +def check_response_for_error(status_code, content) -> None: + if status_code in allowed_exceptions: + if isinstance(content, dict): + error_code = content.get('errorCode', 'E0000') + message = f"{status_code} - {error_code} - {content.get('message', 'no message available')}" + if content.get('details'): + message += f" - {content.get('details')}" + else: + message = f'{status_code} - {content}' + raise unauthorized_code_map.get( + error_code, + bad_request_code_map.get( + error_code, + generic_code_map.get(error_code, allowed_exceptions.get(status_code, BritiveException)), + ), + )(message) + + +def handle_response(response): + try: + return response.json() + # Can likely drop to just the `requests` exception, with `>=2.32.0`, but leaving both for now. + except requests.exceptions.JSONDecodeError: + return response.content.decode('utf-8') + + +def pagination_type(headers, result) -> str: + is_dict = isinstance(result, dict) + has_next_page_header = 'next-page' in headers + + if is_dict and all(x in result for x in ('count', 'page', 'size', 'data')): + return 'inline' + if is_dict and has_next_page_header and all(x in result for x in ('data', 'reportId')): # reports + return 'report' + if has_next_page_header: # this interesting way of paginating is how audit_logs.query() does it + return 'audit' + if is_dict and all(x in result for x in ('result', 'pagination')): + return 'secmgr' + return 'none' + + +def parse_tenant(tenant: str) -> str: + domain = tenant.replace('https://', '').replace('http://', '').split('/')[0] # remove scheme and paths + try: + socket.getaddrinfo(host=domain, port=443) # if success then a full domain was provided + return domain + except socket.gaierror: # assume just the tenant name was provided (originally the only supported method) + resolved_domain = f'{tenant}.britive-app.com' + try: + socket.getaddrinfo(host=resolved_domain, port=443) # validate the hostname is real + return resolved_domain # and if so set the tenant accordingly + except socket.gaierror as e: + raise Exception(f'Invalid tenant provided: {tenant}. DNS resolution failed.') from e + + +def response_has_no_content(response) -> bool: + # handle 204 No Content response + return response.status_code in (204,) or (response.status_code == 200 and len(response.content) == 0) + + +def safe_list_get(lst: list, idx: int, default: str = None) -> Union[str, None]: + try: + return lst[idx] + except IndexError: + return default + + +def source_federation_token(provider: str, tenant: Optional[str] = None, duration_seconds: int = 900) -> str: + """ + Returns a token from the specified federation provider. + + The caller must persist this token if required. New tokens can be generated on each invocation + of this class as well. + + This method only works when running within the context of the specified provider. + It is meant to abstract away the complexities of obtaining a federation token + from common federation providers. Other provider federation tokens can still be + sourced outside of this SDK and provided as input via the standard token presentation + options. + + Six federation providers are currently supported by this method. + + * AWS IAM/STS, with optional profile specified - (aws) + * Azure System Assigned Managed Identities (azuresmi) + * Azure User Assigned Managed Identities (azureumi) + * Bitbucket Pipelines (bitbucket) + * Github Actions (github) + * Gitlab (gitlab) + * spacelift.io (spacelift) + + Any other OIDC federation provider can be used and tokens can be provided to this class for authentication + to a Britive tenant. Details of how to construct these tokens can be found at https://docs.britive.com. + + :param provider: The name of the federation provider. Valid options are `aws`, `github`, `bitbucket`, + `azuresmi`, `azureumi`, `spacelift`, and `gitlab`. + + For the AWS provider it is possible to provide a profile via value `aws-profile`. If no profile is provided + then the boto3 `Session.get_credentials()` method will be used to obtain AWS credentials, which follows + the order provided here: + https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#configuring-credentials + + For Azure User Assigned Managed Identities (azureumi) a client id is required. It must be + provided in the form `azureumi-`. From the Azure documentation...a user-assigned identity's + client ID or, when using Pod Identity, the client ID of an Azure AD app registration. This argument + is supported in all hosting environments. + + For both Azure Managed Identity options it is possible to provide an OIDC audience value via + `azuresmi-` and `azureumi-|`. If no audience is provided the default audience + of `https://management.azure.com/` will be used. + + For the Github provider it is possible to provide an OIDC audience value via `github-`. If no + audience is provided the default Github audience value will be used. + + For the Gitlab provider a token environment variable name can optionally be specified via `gitlab-ENV_VAR`. + Anything after `gitlab-` will be interpreted to represent the name of the environment variable specified + in the YAML file for the ID token. If not provided it will default to `BRITIVE_OIDC_TOKEN`. + + :param tenant: The name of the tenant. This field is optional but if not provided then the tenant will be + sourced from environment variable BRITIVE_TENANT. Knowing the actual tenant is required for the AWS + federation provider. This field can be ignored for non AWS federation providers. + :param duration_seconds: Only applicable for the AWS provider. Specify the number of seconds for which the + generated token is valid. Defaults to 900 seconds (15 minutes). + :return: A federation token that can be used to authenticate to a Britive tenant. + """ + + helper = provider.split('-', maxsplit=1) + provider_name = helper[0] + + federation_providers = { + 'aws': lambda: AwsFederationProvider( + profile=safe_list_get(helper, 1), tenant=tenant, duration=duration_seconds + ).get_token(), + 'bitbucket': lambda: BitbucketFederationProvider().get_token(), + 'github': lambda: GithubFederationProvider(audience=safe_list_get(helper, 1)).get_token(), + 'gitlab': lambda: GitlabFederationProvider(token_env_var=safe_list_get(helper, 1)).get_token(), + 'spacelift': lambda: SpaceliftFederationProvider().get_token(), + } + + if provider_name in federation_providers: + return federation_providers[provider_name]() + + if provider_name == 'azuresmi': + return AzureSystemAssignedManagedIdentityFederationProvider(audience=safe_list_get(helper, 1)).get_token() + + if provider_name == 'azureumi': + return AzureUserAssignedManagedIdentityFederationProvider( + client_id=helper[1].split('|')[0], audience=safe_list_get(helper[1].split('|'), 1) + ).get_token() + + raise InvalidFederationProvider(f'federation provider {provider_name} not supported') + + +def tenant_is_under_maintenance(response) -> bool: + return response.status_code == 503 and response.json().get('errorCode') == 'MAINT0001' diff --git a/src/britive/identity_attributes.py b/src/britive/identity_attributes.py deleted file mode 100644 index 09f1189..0000000 --- a/src/britive/identity_attributes.py +++ /dev/null @@ -1,42 +0,0 @@ -class IdentityAttributes: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/users/attributes' - - def list(self) -> list: - """ - Return a list of identity attributes. - - :return: List of identity attributes. - """ - - return self.britive.get(self.base_url) - - def create(self, name: str, description: str, data_type: str, multi_valued: bool) -> dict: - """ - Create a new identity attribute. - - :param name: The name of the identity attribute. - :param description: The description of the identity attribute. - :param data_type: The data type of the identity attribute. Valid values are - `String`, `Number`, 'Boolean`, 'Date`. - :param multi_valued: Whether the attribute should be considered multi-valued. - :return: Details of the newly created identity attribute. - """ - - if data_type not in ['String', 'Number', 'Boolean', 'Date']: - raise ValueError(f'invalid data_type {data_type}') - - data = {'name': name, 'description': description, 'dataType': data_type, 'multiValued': multi_valued} - - return self.britive.post(self.base_url, json=data) - - def delete(self, attribute_id: str) -> None: - """ - Delete the specified identity attribute. - - :param attribute_id: The ID of the identity attribute to delete. - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{attribute_id}') diff --git a/src/britive/identity_management/__init__.py b/src/britive/identity_management/__init__.py new file mode 100644 index 0000000..4d45abe --- /dev/null +++ b/src/britive/identity_management/__init__.py @@ -0,0 +1,17 @@ +from .identity_attributes import IdentityAttributes +from .identity_providers import IdentityProviders +from .service_identities import ServiceIdentities, ServiceIdentityTokens +from .tags import Tags +from .users import Users +from .workload import Workload + + +class IdentityManagement: + def __init__(self, britive) -> None: + self.identity_attributes = IdentityAttributes(britive) + self.identity_providers = IdentityProviders(britive) + self.service_identities = ServiceIdentities(britive) + self.service_identity_tokens = ServiceIdentityTokens(britive) + self.tags = Tags(britive) + self.users = Users(britive) + self.workload = Workload(britive) diff --git a/src/britive/helpers/custom_attributes.py b/src/britive/identity_management/identity_attributes.py similarity index 68% rename from src/britive/helpers/custom_attributes.py rename to src/britive/identity_management/identity_attributes.py index 7f2f065..b3b76c7 100644 --- a/src/britive/helpers/custom_attributes.py +++ b/src/britive/identity_management/identity_attributes.py @@ -1,10 +1,54 @@ from typing import Any +class IdentityAttributes: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/users/attributes' + + def list(self) -> list: + """ + Return a list of identity attributes. + + :return: List of identity attributes. + """ + + return self.britive.get(self.base_url) + + def create(self, name: str, description: str, data_type: str, multi_valued: bool) -> dict: + """ + Create a new identity attribute. + + :param name: The name of the identity attribute. + :param description: The description of the identity attribute. + :param data_type: The data type of the identity attribute. Valid values are + `String`, `Number`, 'Boolean`, 'Date`. + :param multi_valued: Whether the attribute should be considered multi-valued. + :return: Details of the newly created identity attribute. + """ + + if data_type not in ['String', 'Number', 'Boolean', 'Date']: + raise ValueError(f'invalid data_type {data_type}') + + data = {'name': name, 'description': description, 'dataType': data_type, 'multiValued': multi_valued} + + return self.britive.post(self.base_url, json=data) + + def delete(self, attribute_id: str) -> None: + """ + Delete the specified identity attribute. + + :param attribute_id: The ID of the identity attribute to delete. + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{attribute_id}') + + class CustomAttributes: def __init__(self, principal) -> None: - self.britive = principal.britive - self.base_url: str = principal.britive.base_url + '/users/{id}/custom-attributes' # will .format(id=...) later + self.britive = principal + self.base_url: str = principal.base_url + '/users/{id}/custom-attributes' # will .format(id=...) later def get(self, principal_id: str, as_dict: bool = False) -> Any: """ @@ -27,8 +71,7 @@ def get(self, principal_id: str, as_dict: bool = False) -> Any: else: attrs[current_attr_id] = attribute['attributeValue'] return attrs - else: - return response + return response def add(self, principal_id: str, custom_attributes: dict) -> None: """ @@ -56,7 +99,9 @@ def remove(self, principal_id: str, custom_attributes: dict) -> None: def _build_list(self, operation: str, custom_attributes: dict) -> list: # first get list of existing custom identity attributes and build some helpers - existing_attrs = [attr for attr in self.britive.identity_attributes.list() if not attr['builtIn']] + existing_attrs = [ + attr for attr in self.britive.identity_management.identity_attributes.list() if not attr['builtIn'] + ] existing_attr_ids = [attr['id'] for attr in existing_attrs] attrs_by_name = {attr['name']: attr['id'] for attr in existing_attrs} diff --git a/src/britive/identity_providers.py b/src/britive/identity_management/identity_providers.py similarity index 97% rename from src/britive/identity_providers.py rename to src/britive/identity_management/identity_providers.py index 4f6d51e..e7572cd 100644 --- a/src/britive/identity_providers.py +++ b/src/britive/identity_management/identity_providers.py @@ -258,12 +258,12 @@ def update_mapping(self, identity_provider_id: str, mappings: list) -> None: Each key is explained below in further detail. - scimAttributeName: The name of the SCIM attribute returned by - `britive.identity_providers.scim_attributes.list()`. + `britive.identity_management.identity_providers.scim_attributes.list()`. - builtIn: True if the identity attribute is from the built-in list, False if user generated. - attributeId: The ID of the identity attribute to be mapped with the identity provider. This can be - obtained by calling `britive.identity_attributes.list()`. + obtained by calling `britive.identity_management.identity_attributes.list()`. - attributeName: The name of the identity attribute to be mapped with the identity provider. This can be - obtained by calling `britive.identity_attributes.list()`. + obtained by calling `britive.identity_management.identity_attributes.list()`. - op: The operation to perform. Valid values are `add` and `remove`. :return: None """ diff --git a/src/britive/service_identities.py b/src/britive/identity_management/service_identities.py similarity index 72% rename from src/britive/service_identities.py rename to src/britive/identity_management/service_identities.py index 7a49c64..865ce48 100644 --- a/src/britive/service_identities.py +++ b/src/britive/identity_management/service_identities.py @@ -1,4 +1,4 @@ -from .helpers.custom_attributes import CustomAttributes +from .identity_attributes import CustomAttributes valid_statues = ['active', 'inactive'] @@ -7,7 +7,7 @@ class ServiceIdentities: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/users' - self.custom_attributes = CustomAttributes(self) + self.custom_attributes = CustomAttributes(britive) def list(self, filter_expression: str = None, include_tags: bool = False) -> list: """ @@ -46,8 +46,7 @@ def get_by_name(self, name: str) -> list: :return: Details of the specified service identities. If no service identity is found will return an empty list. """ - service_identities = self.list(filter_expression=f'name co "{name}"') - return service_identities + return self.list(filter_expression=f'name co "{name}"') def get_by_status(self, status: str) -> list: """ @@ -98,8 +97,7 @@ def create(self, **kwargs) -> dict: if not all(x in kwargs for x in required_fields): raise ValueError('Not all required keyword arguments were provided.') - response = self.britive.post(self.base_url, json=kwargs) - return response + return self.britive.post(self.base_url, json=kwargs) def update(self, service_identity_id: str, **kwargs) -> dict: """ @@ -186,3 +184,59 @@ def disable(self, service_identity_id: str = None, service_identity_ids: list = if not service_identity_ids: return response[0] return response + + +class ServiceIdentityTokens: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}' + + def __validate_token_expiration(self, days) -> None: + if not (1 <= days <= 90): + raise ValueError('invalid token expiration value - must ust be between 1 and 90') + + def create(self, service_identity_id: str, token_expiration_days: int = 90) -> dict: + """ + Create a token for a given service identity. + + The token has the same privileges assigned to the service identity. When this token is created, the old token + associated with the identity provider will be removed. A service identity can have only one token at any given + time. The token that is generated will be returned only once. + + :param service_identity_id: The ID of the service identity. + :param token_expiration_days: The number of days in which token would expire since it was last used. + The token expiration days can be any value between 1 day and 90 days. + :return: + """ + + self.__validate_token_expiration(token_expiration_days) + + data = {'tokenExpirationDays': token_expiration_days} + + return self.britive.post(f'{self.base_url}/users/{service_identity_id}/tokens', json=data) + + def update(self, service_identity_id: str, token_expiration_days: int = 90) -> None: + """ + Update the token expiration days for an existing service identity token. + + :param service_identity_id: The ID of the service identity. + :param token_expiration_days: The number of days in which token would expire since it was last used. + The token expiration days can be any value between 1 day and 90 days. + :return: None + """ + + self.__validate_token_expiration(token_expiration_days) + + data = {'tokenExpirationDays': token_expiration_days} + + return self.britive.post(f'{self.base_url}/users/{service_identity_id}/tokens', json=data) + + def get(self, service_identity_id: str) -> dict: + """ + Return details of the token associated with the service identity. Only one token can exist per service identity. + + :param service_identity_id: The ID of the service identity. + :return: Details of the service identity token + """ + + return self.britive.get(f'{self.base_url}/users/{service_identity_id}/tokens') diff --git a/src/britive/tags.py b/src/britive/identity_management/tags.py similarity index 99% rename from src/britive/tags.py rename to src/britive/identity_management/tags.py index 092e95c..3d0bd52 100644 --- a/src/britive/tags.py +++ b/src/britive/identity_management/tags.py @@ -17,7 +17,7 @@ def build(self, attribute_id_or_name: str, operator: str, value: str) -> dict: raise ValueError('invalid operator provided.') # first get list of existing identity attributes and build some helpers - existing_attrs = [attr for attr in self.britive.identity_attributes.list()] + existing_attrs = self.britive.identity_management.identity_attributes.list() existing_attr_ids = [attr['id'] for attr in existing_attrs] attrs_by_name = {attr['name']: attr['id'] for attr in existing_attrs} diff --git a/src/britive/users.py b/src/britive/identity_management/users.py similarity index 95% rename from src/britive/users.py rename to src/britive/identity_management/users.py index 2858b8e..564f582 100644 --- a/src/britive/users.py +++ b/src/britive/identity_management/users.py @@ -1,9 +1,10 @@ -from .exceptions import ( +from britive.exceptions import ( UserDoesNotHaveMFAEnabled, UserNotAllowedToChangePassword, UserNotAssociatedWithDefaultIdentityProvider, ) -from .helpers.custom_attributes import CustomAttributes + +from .identity_attributes import CustomAttributes valid_statues = ['active', 'inactive'] @@ -12,7 +13,7 @@ class Users: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/users' - self.custom_attributes = CustomAttributes(self) + self.custom_attributes = CustomAttributes(britive) self.enable_mfa = EnableMFA(britive) def list(self, filter_expression: str = None, include_tags: bool = False) -> list: @@ -114,8 +115,7 @@ def create(self, idp: str = None, **kwargs) -> dict: if not all(x in kwargs for x in required_fields): raise ValueError('Not all required keyword arguments were provided.') - response = self.britive.post(self.base_url, json=kwargs) - return response + return self.britive.post(self.base_url, json=kwargs) def update(self, user_id: str, **kwargs) -> dict: """ @@ -221,10 +221,10 @@ def reset_password(self, user_id: str, password: str) -> None: user = self.get(user_id) if not user['canChangeOrResetPassword']: - raise UserNotAllowedToChangePassword() + raise UserNotAllowedToChangePassword if user['identityProvider']['type'] != 'DEFAULT': - raise UserNotAssociatedWithDefaultIdentityProvider() + raise UserNotAssociatedWithDefaultIdentityProvider return self.britive.post(f'{self.base_url}/{user_id}/resetpassword', json={'password': password}) @@ -239,8 +239,8 @@ def reset_mfa(self, user_id: str) -> None: """ user = self.get(user_id) - if not user['identityProvider']['mfaEnabled']: - raise UserDoesNotHaveMFAEnabled() + if not user['identityProvider'].get('mfaEnabled'): + raise UserDoesNotHaveMFAEnabled return self.britive.patch(f'{self.base_url}/{user_id}/resetmfa') diff --git a/src/britive/identity_management/workload.py b/src/britive/identity_management/workload.py new file mode 100644 index 0000000..acb11c8 --- /dev/null +++ b/src/britive/identity_management/workload.py @@ -0,0 +1,433 @@ +from typing import Union + + +class Workload: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/workload' + self.identity_providers = WorkloadIdentityProviders(self) + self.service_identities = WorkloadServiceIdentities(self) + self.scim_user = WorkloadScimUser(self) + + +class WorkloadIdentityProviders: + def __init__(self, workload) -> None: + self.britive = workload.britive + self.base_url = f'{workload.base_url}/identity-providers' + + def list(self, idp_type: str = None) -> list: + """ + Return a list of all workload identity providers. + + :param idp_type: Optional filter to apply to reduce the results to a specific workload identity provider + type. Valid values are `AWS` and `OIDC`. + :returns: List of all workload identity providers. + """ + + params = {} + if idp_type: + params['type'] = idp_type + return self.britive.get(self.base_url, params=params) + + def get(self, workload_identity_provider_id: int) -> dict: + """ + Return details of the specified workload identity provider. + + :param workload_identity_provider_id: The ID of the workload identity provider. + :returns: Details of the specified workload identity provider. + """ + + return self.britive.get(f'{self.base_url}/{workload_identity_provider_id}') + + def _build_attributes_map_list(self, attributes_map: dict) -> list: + # first get list of existing custom identity attributes and build some helpers + existing_attrs = [ + attr for attr in self.britive.identity_management.identity_attributes.list() if not attr['builtIn'] + ] + existing_attr_ids = [attr['id'] for attr in existing_attrs] + attrs_by_name = {attr['name']: attr['id'] for attr in existing_attrs} + + # for each attributeMap key/value provided ensure we convert to ID and build the list + attrs_list = [] + for idp_attr, custom_identity_attribute in attributes_map.items(): + if (check_custom_identity_attribute := custom_identity_attribute) not in existing_attr_ids and not ( + check_custom_identity_attribute := attrs_by_name.get(custom_identity_attribute) + ): + raise ValueError(f'custom identity attribute name {custom_identity_attribute} not found.') + attrs_list.append({'idpAttr': idp_attr, 'userAttr': check_custom_identity_attribute}) + return attrs_list + + def create(self, **kwargs) -> dict: + """ + Create a workload identity provider. + + This method accepts a list of kwargs which match the required and optional fields for creating a + workload identity provider. It mostly exists to support the creation of new workload identity provider + types before this SDK can be updated to support the new type natively. + + Generally, the caller should opt to use the `create_aws` or `create_oidc` methods instead of calling + this method directly. + + The field `attributesMap` is in format `{'idp_attr_value': 'custom_identity_attribute_name_or_id', ...}`. + + :param kwargs: A list of keyword arguments which will be provided directly to the API backend without + any further inspection. Valid fields are `name`, `description`, `idpType`, `attributesMap`, and + `validationWindow`. For the AWS provider an additional field `maxDuration` is valid. For the OIDC + provider additional fields `issuerUrl` and `allowedAudiences` are valid. + :returns: Details of the newly created workload identity provider. + """ + + if 'attributesMap' in kwargs: + kwargs['attributesMap'] = self._build_attributes_map_list(attributes_map=kwargs['attributesMap']) + + return self.britive.post(self.base_url, json=kwargs) + + def create_aws( + self, + name: str, + attributes_map: dict, + description: str = None, + validation_window: int = 30, + max_duration: int = 5, + ) -> dict: + """ + Create an AWS workload identity provider. + + :param name: Name of the AWS workload identity provider. + :param attributes_map: The mapping of the available AWS workload token fields to custom identity attributes + which are associated with service identities. Defaults to None.Can provide either the name of the + custom identity attribute or the ID. + :param description: Optional description of the AWS workload identity provider. + :param validation_window: The number of seconds allowed to validate the AWS workload token from its + "issued time". Defaults to 30 seconds. + :param max_duration: The max number of hours (whole numbers only) for which the AWS workload token is valid. + :returns: Details of the newly created AWS workload identity provider. + """ + + params = { + 'name': name, + 'idpType': 'AWS', + 'validationWindow': validation_window, + 'maxDuration': max_duration, + } + if description: + params['description'] = description + if attributes_map: + params['attributesMap'] = attributes_map + + return self.create(**params) + + def create_oidc( + self, + name: str, + issuer_url: str, + attributes_map: dict = None, + description: str = None, + validation_window: int = 30, + allowed_audiences: list = None, + ) -> dict: + """ + Create an OIDC workload identity provider. + + :param name: Name of the OIDC workload identity provider. + :param issuer_url: The issuer url for the OIDC provider. + :param attributes_map: The mapping of the available OIDC workload token fields to custom identity attributes + which are associated with service identities. Defaults to None. Can provide either the name of the + custom identity attribute or the ID. + :param description: Optional description of the AWS workload identity provider. + :param validation_window: The number of seconds allowed to validate the AWS workload token from its + "issued time". Defaults to 30 seconds. + :param allowed_audiences: The list of allowed audience values as strings. Defaults to None. + :returns: Details of the newly created OIDC workload identity provider. + """ + + params = {'name': name, 'idpType': 'OIDC', 'validationWindow': validation_window, 'issuerUrl': issuer_url} + if description: + params['description'] = description + if allowed_audiences: + params['allowedAudiences'] = allowed_audiences + if attributes_map: + params['attributesMap'] = attributes_map + + return self.create(**params) + + def update(self, workload_identity_provider_id: int, **kwargs) -> dict: + """ + Updates a workload identity provider. + + This method accepts a list of kwargs which match the required and optional fields for creating a + workload identity provider. It mostly exists to support the update of new workload identity provider + types before this SDK can be updated to support the new type natively. + + Generally, the caller should opt to use the `update_aws` or `update_oidc` methods instead of calling + this method directly. + + The field `attributesMap` is in format `{'idp_attr_value': 'custom_identity_attribute_name_or_id', ...}`. + + :param workload_identity_provider_id: The ID of the workload identity provider to update. + :param kwargs: A list of keyword arguments which will be provided directly to the API backend without + any further inspection. Valid fields are `name`, `description`, `idpType`, `attributesMap`, and + `validationWindow`. For the AWS provider an additional field `maxDuration` is valid. For the OIDC + provider additional fields `issuerUrl` and `allowedAudiences` are valid. + :returns: Details of the updated workload identity provider. + """ + + kwargs['id'] = workload_identity_provider_id + + if 'attributesMap' in kwargs: + kwargs['attributesMap'] = self._build_attributes_map_list(attributes_map=kwargs['attributesMap']) + + # since this is a PUT call and not a PATCH call we need to get the existing idp configuration + # and merge in the things that have changed + + existing = self.get(workload_identity_provider_id=workload_identity_provider_id) + return self.britive.put(self.base_url, json={**existing, **kwargs}) + + def update_aws( + self, + workload_identity_provider_id: int, + name: str = None, + attributes_map: dict = None, + description: str = None, + validation_window: int = None, + max_duration: int = None, + ) -> dict: + """ + Update an AWS workload identity provider. + + All fields except `workload_identity_provider_id` are optional. + + :param workload_identity_provider_id: The ID of the workload identity provider to update. + :param name: Name of the AWS workload identity provider. + :param attributes_map: The mapping of the available AWS workload token fields to custom identity attributes + which are associated with service identities.Can provide either the name of the custom identity + attribute or the ID. + :param description: Description of the AWS workload identity provider. + :param validation_window: The number of seconds allowed to validate the AWS workload token from its + "issued time". + :param max_duration: The max number of hours (whole numbers only) for which the AWS workload token is valid. + :returns: Details of the updated AWS workload identity provider. + """ + + params = {'idpType': 'AWS'} + + if name: + params['name'] = name + if description: + params['description'] = description + if validation_window: + params['validationWindow'] = validation_window + if max_duration: + params['maxDuration'] = max_duration + if attributes_map: + params['attributesMap'] = attributes_map + + return self.update(workload_identity_provider_id=workload_identity_provider_id, **params) + + def update_oidc( + self, + workload_identity_provider_id: int, + name: str = None, + issuer_url: str = None, + attributes_map: dict = None, + description: str = None, + validation_window: int = None, + allowed_audiences: list = None, + ) -> dict: + """ + Update an OIDC workload identity provider. + + All fields except `workload_identity_provider_id` are optional. + + :param workload_identity_provider_id: The ID of the workload identity provider to update. + :param name: Name of the OIDC workload identity provider. + :param issuer_url: The issuer url for the OIDC provider. + :param attributes_map: The mapping of the available OIDC workload token fields to custom identity attributes + which are associated with service identities. Can provide either the name of the custom identity + attribute or the ID. + :param description: Description of the AWS workload identity provider. + :param validation_window: The number of seconds allowed to validate the AWS workload token from its + "issued time". + :param allowed_audiences: The list of allowed audience values as strings. + :returns: Details of the update OIDC workload identity provider. + """ + + params = {'idpType': 'OIDC'} + + if name: + params['name'] = name + if description: + params['description'] = description + if validation_window: + params['validationWindow'] = validation_window + if issuer_url: + params['issuerUrl'] = issuer_url + if attributes_map: + params['attributesMap'] = attributes_map + if allowed_audiences: + params['allowedAudiences'] = allowed_audiences + + return self.update(workload_identity_provider_id=workload_identity_provider_id, **params) + + def delete(self, workload_identity_provider_id) -> None: + """ + Deletes a workload identity provider. + + :param workload_identity_provider_id: The ID of the workload identity provider. + :returns: None. + """ + return self.britive.delete(f'{self.base_url}/{workload_identity_provider_id}') + + def generate_attribute_map( + self, + idp_attribute_name: str, + custom_identity_attribute_name: str = None, + custom_identity_attribute_id: str = None, + ) -> dict: + """ + Generates a dictionary that can be appended to a list used for the `attributesMap`. + + This method would mostly be used when invoking the `create` or `update` methods directly instead of + using the type specific (`create_aws`, `create_oidc`, `update_aws`, `update_oidc`) methods which provide + a more pythonic way to capture the attribute mappings. + + :param idp_attribute_name: The name of the workload identity provider attribute to map. This will always + be a string as it is controlled by the identity provider. + :param custom_identity_attribute_name: The name of the Britive custom identity attribute. One of + `custom_identity_attribute_name` or `custom_identity_attribute_id` must be provided. The name will be + translated to the ID of the custom identity attribute on behalf of the caller. + :param custom_identity_attribute_id: The id of the Britive custom identity attribute. One of + `custom_identity_attribute_name` or `custom_identity_attribute_id` must be provided. + :returns: Dictionary representing the attribute map. + """ + + if custom_identity_attribute_id and custom_identity_attribute_name: + raise ValueError( + 'only one of custom_identity_attribute_id and custom_identity_attribute_name should be provided' + ) + + if not custom_identity_attribute_id and not custom_identity_attribute_name: + raise ValueError('one of custom_identity_attribute_id or custom_identity_attribute_name should be provided') + + if custom_identity_attribute_name: + found = False + for attr in self.britive.identity_management.identity_attributes.list(): + if attr['name'] == custom_identity_attribute_name: + custom_identity_attribute_id = attr['id'] + found = True + break + if not found: + raise ValueError(f'custom_identity_attribute_name value of {custom_identity_attribute_name} not found.') + + return {'idpAttr': idp_attribute_name, 'userAttr': custom_identity_attribute_id} + + +class WorkloadServiceIdentities: + def __init__(self, workload) -> None: + self.britive = workload.britive + self.base_url: str = workload.base_url + '/users/{id}/identity-provider' # will .format(id=...) later + + def get(self, service_identity_id: str) -> Union[dict, None]: + """ + Returns details about the workload identity provider associated with the specified service identity. + + :param service_identity_id: The ID of the service identity. + :returns: Details about the workload identity provider associated with the specified service identity. + Returns None if there is no workload identity provider associated with the specified service identity. + """ + + return self.britive.get(self.base_url.format(id=service_identity_id)) + + def assign( + self, service_identity_id: str, idp_id: str, federated_attributes: dict, token_duration: int = 300 + ) -> dict: + """ + Associates an OIDC provider with the specified Service Identity. + + :param service_identity_id: The ID of the service identity. + :param idp_id: The ID of the OIDC Identity Provider. + :param token_duration: Duration in seconds (from now) before a token provided by the client expires. + This will be evaluated alongside the OIDC JWT expiration field and the earlier of the two values + will govern when the token expires. This field is optional and defaults to 300 seconds. + :param federated_attributes: An attribute map where keys are the custom attribute ids or names + and values are strings or list of strings (for multivalued attributes) which map back to the mapped + token claims. + :returns: Details of the newly assigned workload identity provider. + """ + + mapping_attributes = [] + converted_federated_attributes = {} + converted_attributes = self.britive.identity_management.service_identities.custom_attributes._build_list( + operation='add', custom_attributes=federated_attributes + ) + + for attr in converted_attributes: + custom_attr = attr['customUserAttribute'] + attr_id = custom_attr['attributeId'] + if attr_id not in converted_federated_attributes: + converted_federated_attributes[attr_id] = [] + converted_federated_attributes[attr_id].append(custom_attr['attributeValue']) + + for custom_attribute_id, values in converted_federated_attributes.items(): + mapping_attributes.append({'attrId': custom_attribute_id, 'values': values}) + + params = {'idpId': idp_id, 'tokenDuration': token_duration, 'mappingAttributes': mapping_attributes} + + return self.britive.post(self.base_url.format(id=service_identity_id), json=params) + + def unassign(self, service_identity_id: str) -> None: + """ + Removes/deletes the service identity's assigned identity provider, along with any custom attribute mappings. + + This will revert the service identity back to use static API tokens for authentication. + + :param service_identity_id: The ID of the Service Identity. + :returns: None. + """ + return self.britive.delete(self.base_url.format(id=service_identity_id)) + + +class WorkloadScimUser: + def __init__(self, workload) -> None: + self.britive = workload.britive + self.base_url = f'{workload.base_url}/scim-user/identity-provider' + + def get(self, idp_name: str) -> dict: + """ + Gets details of the workload federation enabled service identity associated with the identity provider. + + The identity provider name provided must be a SAML identity provider. + + :param idp_name: The name of the SAML Identity Provider. + :returns: Details of the newly assigned service identity to the identity provider. + """ + + return self.britive.get(f'{self.base_url}/{idp_name}') + + def assign(self, service_identity_id: str, idp_name: str) -> dict: + """ + Associates a workload federation enabled service identity with an identity provider SCIM service. + + The service identity must already be configured for workload federation. + The identity provider name provided must be a SAML identity provider. + + :param service_identity_id: The ID of the service identity. + :param idp_name: The name of the SAML Identity Provider. + :returns: Details of the newly assigned service identity to the identity provider. + """ + + params = {'idpName': idp_name, 'userId': service_identity_id} + + return self.britive.post(self.base_url, json=params) + + def unassign(self, idp_name: str) -> dict: + """ + Removes a workload federation enabled service identity from an identity provider SCIM service. + + The identity provider name provided must be a SAML identity provider. + + :param idp_name: The name of the SAML Identity Provider. + :returns: Details of the newly assigned service identity to the identity provider. + """ + + return self.britive.delete(f'{self.base_url}/{idp_name}') diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 2afb63a..28da3a4 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -1,20 +1,33 @@ -import sys import time from typing import Any, Callable -from . import exceptions +from .exceptions import ( + ApprovalRequiredButNoJustificationProvided, + ProfileApprovalRejected, + ProfileApprovalTimedOut, + ProfileApprovalWithdrawn, + StepUpAuthFailed, + StepUpAuthRequiredButNotProvided, + TransactionNotFound, +) +from .exceptions.badrequest import ( + ApprovalJustificationRequiredError, + ProfileApprovalRequiredError, +) +from .exceptions.generic import BritiveGenericError, StepUpAuthenticationRequiredError +from .helpers import HelperMethods +from .my_requests import MyAccessRequests approval_exceptions = { - 'rejected': exceptions.ProfileApprovalRejected(), - 'cancelled': exceptions.ProfileApprovalWithdrawn(), - 'timeout': exceptions.ProfileApprovalTimedOut(), + 'rejected': ProfileApprovalRejected(), + 'cancelled': ProfileApprovalWithdrawn(), + 'timeout': ProfileApprovalTimedOut(), } class MyAccess: """ - This class is meant to be called by end users (as part of custom API integration work or the yet to be built - Python based Britive CLI tooling). It is an API layer on top of the actions that can be performed on the + This class is meant to be called by end users, it is an API layer on top of the actions that can be performed on the "My Access" page of the Britive UI. No "administrative" access is required by the methods in this class. Each method will only return resources/allow @@ -28,6 +41,36 @@ class MyAccess: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/access' + self._get_profile_and_environment_ids_given_names = HelperMethods( + self.britive + ).get_profile_and_environment_ids_given_names + + # MyRequests + __my_requests = MyAccessRequests(self.britive) + self.request_approval = __my_requests.request_approval + self.request_approval_by_name = __my_requests.request_approval_by_name + self.withdraw_approval_request = __my_requests.withdraw_approval_request + self.withdraw_approval_request_by_name = __my_requests.withdraw_approval_request_by_name + + def list(self, filter_text: str = None, search_text: str = None, size: int = None) -> list: + """ + List the access details for the current user. + + :param filter_text: filter details by key, using eq|co|sw operators, e.g. `filter_text='key co text'` + :param search_text: filter details by search text. + :param size: reduce the size of the response to the specified limit. + :return: List of profiles. + """ + + params = {'type': 'sdk'} + if filter_text: + params['filter'] = filter_text + if search_text: + params['search'] = search_text + if size: + params['size'] = size + + return self.britive.get(self.base_url, params=params) def list_profiles(self) -> list: """ @@ -38,14 +81,26 @@ def list_profiles(self) -> list: return self.britive.get(self.base_url) - def list_checked_out_profiles(self) -> list: + def list_checked_out_profiles(self, include_profile_details: bool = False) -> list: """ Return list of details on currently checked out profiles for the user. + :param include_profile_details: Include `details` for each checked out profile. :return: List of checked out profiles. """ - return self.britive.get(f'{self.base_url}/app-access-status') + checked_out_profiles = self.britive.get(f'{self.base_url}/app-access-status') + + if include_profile_details: + for profile in checked_out_profiles: + profile['details'] = [ + a + for a in self.list_profiles() + if profile['appContainerId'] == a['appContainerId'] + and [p for p in a['profiles'] if profile['papId'] == p['profileId']] + ] + + return checked_out_profiles def get_checked_out_profile(self, transaction_id: str) -> dict: """ @@ -58,7 +113,36 @@ def get_checked_out_profile(self, transaction_id: str) -> dict: for t in self.list_checked_out_profiles(): if t['transactionId'] == transaction_id: return t - raise exceptions.TransactionNotFound() + raise TransactionNotFound + + def get_profile_settings(self, profile_id: str, environment_id: str) -> dict: + """ + Retrieve settings of a profile. + + :param profile_id: The ID of the profile. + :param environment_id: The ID of the environment. + :return: Dict of the profile settings. + """ + + return self.britive.get(f'{self.base_url}/{profile_id}/environments/{environment_id}/settings') + + def get_profile_settings_by_name( + self, profile_name: str, environment_name: str, application_name: str = None + ) -> dict: + """ + Retrieve settings of a profile by name. + + :param profile_name: The name of the profile. + :param environment_name: The name of the environment. + :param application_name: Optionally, the name of the application. + :return: Dict of the profile settings. + """ + + ids = self._get_profile_and_environment_ids_given_names( + profile_name=profile_name, environment_name=environment_name, application_name=application_name + ) + + return self.get_profile_settings(profile_id=ids['profile_id'], environment_id=ids['environment_id']) def extend_checkout(self, transaction_id: str) -> dict: """ @@ -98,233 +182,23 @@ def extend_checkout_by_name( transaction_id = transaction['transactionId'] break if not transaction_id: - raise exceptions.TransactionNotFound() + raise TransactionNotFound return self.extend_checkout(transaction_id=transaction_id) - def request_approval_by_name( - self, - profile_name: str, - environment_name: str, - application_name: str = None, - justification: str = None, - wait_time: int = 60, - max_wait_time: int = 600, - block_until_disposition: bool = False, - progress_func: Callable = None, - ) -> Any: - """ - Requests approval to checkout a profile at a later time, using names of entities instead of IDs. - - Console vs. Programmatic access is not applicable here. The request for approval will allow the caller - to checkout either type of access once the request has been approved. - - :param profile_name: The name of the profile. Use `list_profiles()` to obtain the eligible profiles. - :param environment_name: The name of the environment. Use `list_profiles()` to obtain the eligible environments. - :param application_name: Optionally the name of the application, which can help disambiguate between profiles - with the same name across applications. - :param justification: Optional justification if checking out the profile requires approval. - :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout - was approved. Only applicable if `block_until_disposition = True`. - :param max_wait_time: The maximum number of seconds to wait for an approval before throwing - an exception. Only applicable if `block_until_disposition = True`. - :param block_until_disposition: Should this method wait/block until the request has been either approved, - rejected, or withdrawn. If `True` then `wait_time` and `max_wait_time` will govern how long to wait before - exiting. - :param progress_func: An optional callback that will be invoked as the checkout process progresses. - :return: If `block_until_disposition = True` then returns the final status of the request. If - `block_until_disposition = False` then returns details about the approval request. - :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. - """ - - ids = self._get_profile_and_environment_ids_given_names(profile_name, environment_name, application_name) - - return self.request_approval( - profile_id=ids['profile_id'], - environment_id=ids['environment_id'], - justification=justification, - wait_time=wait_time, - max_wait_time=max_wait_time, - block_until_disposition=block_until_disposition, - progress_func=progress_func, - ) - - def request_approval( - self, - profile_id: str, - environment_id: str, - justification: str, - wait_time: int = 60, - max_wait_time: int = 600, - block_until_disposition: bool = False, - progress_func: Callable = None, - ) -> Any: - """ - Requests approval to checkout a profile at a later time. - - Console vs. Programmatic access is not applicable here. The request for approval will allow the caller - to checkout either type of access once the request has been approved. - - :param profile_id: The ID of the profile. Use `list_profiles()` to obtain the eligible profiles. - :param environment_id: The ID of the environment. Use `list_profiles()` to obtain the eligible environments. - :param justification: Optional justification if checking out the profile requires approval. - :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout - was approved. Only applicable if `block_until_disposition = True`. - :param max_wait_time: The maximum number of seconds to wait for an approval before throwing - an exception. Only applicable if `block_until_disposition = True`. - :param block_until_disposition: Should this method wait/block until the request has been either approved, - rejected, or withdrawn. If `True` then `wait_time` and `max_wait_time` will govern how long to wait before - exiting. - :param progress_func: An optional callback that will be invoked as the checkout process progresses. - :return: If `block_until_disposition = True` then returns the final status of the request. If - `block_until_disposition = False` then returns details about the approval request. - :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. - """ - - data = {'justification': justification} - - request = self.britive.post( - f'{self.base_url}/{profile_id}/environments/{environment_id}/approvalRequest', json=data - ) - - if request is None: - raise exceptions.ProfileCheckoutAlreadyApproved() - - request_id = request['requestId'] - - if block_until_disposition: - try: - quit_time = time.time() + max_wait_time - while True: - status = self.approval_request_status(request_id=request_id)['status'].lower() - if status == 'pending': - if time.time() >= quit_time: - raise exceptions.ProfileApprovalMaxBlockTimeExceeded() - if progress_func: - progress_func('awaiting approval') - time.sleep(wait_time) - continue - # status == timeout or approved or rejected or cancelled - return status - except KeyboardInterrupt: # handle Ctrl+C (^C) - # the first ^C we get we will try to withdraw the request - # if we get another ^C while doing this we simply exit immediately - try: - time.sleep(1) # give the caller a small window to ^C again - self.withdraw_approval_request(request_id=request_id) - sys.exit() - except KeyboardInterrupt: - sys.exit() - - else: - return request - - def approval_request_status(self, request_id: str) -> dict: - """ - Provides details on and approval request. - - :param request_id: The ID of the approval request. - :return: Details of the approval request. - """ - - return self.britive.get(f'{self.britive.base_url}/v1/approvals/{request_id}') - - def withdraw_approval_request_by_name( - self, profile_name: str, environment_name: str, application_name: str = None - ) -> None: - """ - Withdraws a pending approval request, using names of entities instead of IDs. - - :param profile_name: The name of the profile. Use `list_profiles()` to obtain the eligible profiles. - :param environment_name: The name of the environment. Use `list_profiles()` to obtain the eligible environments. - :param application_name: Optionally the name of the application, which can help disambiguate between profiles - with the same name across applications. - :return: None - """ - - ids = self._get_profile_and_environment_ids_given_names(profile_name, environment_name, application_name) - - return self.withdraw_approval_request(profile_id=ids['profile_id'], environment_id=ids['environment_id']) - - def withdraw_approval_request( - self, request_id: str = None, profile_id: str = None, environment_id: str = None - ) -> None: - """ - Withdraws a pending approval request. - - Either `request_id` or (`profile_id` AND `environment_id`) are required. - - :param request_id: The ID of the approval request. - :param profile_id: The ID of the profile. - :param environment_id: The ID of the environment. - :return: None - """ - - url = None - if request_id: - url = f'{self.britive.base_url}/v1/approvals/{request_id}' - else: - if not profile_id: - raise ValueError('profile_id is required.') - if not environment_id: - raise ValueError('environment_id is required') - url = ( - f'{self.britive.base_url}/v1/approvals/consumer/papservice/resource?resourceId=' - f'{profile_id}/{environment_id}' - ) - - return self.britive.delete(url) - - def approve_request(self, request_id: str, comments: str = '') -> None: - """ - Approves a request. - - :param request_id: The ID of the request. - :param comments: Approver comments. - :return: None. - """ - - params = {'approveRequest': 'yes'} - data = {'approverComment': comments} - - return self.britive.patch(f'{self.britive.base_url}/v1/approvals/{request_id}', params=params, json=data) - - def reject_request(self, request_id: str, comments: str = '') -> None: - """ - Rejects a request. - - :param request_id: The ID of the request. - :param comments: Approver comments. - :return: None. - """ - - params = {'approveRequest': 'no'} - data = {'approverComment': comments} - - return self.britive.patch(f'{self.britive.base_url}/v1/approvals/{request_id}', params=params, json=data) - - def list_approvals(self) -> dict: - """ - Lists approval requests. - - :return: List of approval requests. - """ - - params = {'requestType': 'myApprovals', 'consumer': 'papservice'} - - return self.britive.get(f'{self.britive.base_url}/v1/approvals/', params=params) - def _checkout( self, profile_id: str, environment_id: str, - programmatic: bool = True, include_credentials: bool = False, + iteration_num: int = 1, justification: str = None, - otp: str = None, - wait_time: int = 60, max_wait_time: int = 600, + otp: str = None, + programmatic: bool = True, progress_func: Callable = None, - iteration_num: int = 1, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, ) -> dict: params = {'accessType': 'PROGRAMMATIC' if programmatic else 'CONSOLE'} @@ -359,46 +233,43 @@ def _checkout( # if not check it out if not transaction: if otp: - response = self.britive.step_up.authenticate(otp=otp) - print(str(response)) + response = self.britive.security.step_up_auth.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise exceptions.StepUpAuthFailed() + raise StepUpAuthFailed try: transaction = self.britive.post( f'{self.base_url}/{profile_id}/environments/{environment_id}', params=params, json=data ) - except exceptions.ForbiddenRequest as e: - if 'PE-0028' in str(e): # Check for stepup totp - raise exceptions.StepUpAuthRequiredButNotProvided() from e - except exceptions.InvalidRequest as e: - if 'MA-0009' in str(e): # old approval process that coupled approval and checkout - raise exceptions.ApprovalRequiredButNoJustificationProvided() from e - if 'MA-0010' in str(e): # new approval process that de-couples approval from checkout - # if the caller has not provided a justification we know for sure the call will fail - # so raise the exception - if not justification: - raise exceptions.ApprovalRequiredButNoJustificationProvided() from e - - # request approval - status = self.request_approval( - profile_id=profile_id, - environment_id=environment_id, - justification=justification, - wait_time=wait_time, - max_wait_time=max_wait_time, - block_until_disposition=True, - progress_func=progress_func, + except StepUpAuthenticationRequiredError as e: + raise StepUpAuthRequiredButNotProvided(e) from e + except (ApprovalJustificationRequiredError, ProfileApprovalRequiredError) as e: + if not justification: + raise ApprovalRequiredButNoJustificationProvided(e) from e + + # request approval + approval_request = { + 'block_until_disposition': True, + 'environment_id': environment_id, + 'justification': justification, + 'max_wait_time': max_wait_time, + 'profile_id': profile_id, + 'progress_func': progress_func, + 'ticket_id': ticket_id, + 'ticket_type': ticket_type, + 'wait_time': wait_time, + } + status = self.request_approval(**approval_request) + + # handle the response based on the value of status + if status == 'approved': + transaction = self.britive.post( + f'{self.base_url}/{profile_id}/environments/{environment_id}', params=params, json=data ) - - # handle the response based on the value of status - if status == 'approved': - transaction = self.britive.post( - f'{self.base_url}/{profile_id}/environments/{environment_id}', params=params, json=data - ) - else: - raise approval_exceptions[status] from e - if 'e1001 - user has already checked out profile for this environment' in str(e).lower(): + else: + raise approval_exceptions[status](e) from e + except BritiveGenericError as e: + if 'user has already checked out profile for this environment' in str(e).lower(): # this is a rare race condition...explained below # if 2 or more calls from the same user to checkout a profile occur at the same time +/- 1/2 seconds # both calls will get the list of checked out profiles and notice that the profile is not currently @@ -412,40 +283,23 @@ def _checkout( if iteration_num > 2: raise e return self._checkout( - profile_id=profile_id, environment_id=environment_id, - programmatic=programmatic, include_credentials=include_credentials, + iteration_num=iteration_num + 1, justification=justification, - wait_time=wait_time, max_wait_time=max_wait_time, - progress_func=progress_func, - iteration_num=iteration_num + 1, otp=otp, + profile_id=profile_id, + programmatic=programmatic, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, ) raise e transaction_id = transaction['transactionId'] - # this approval workflow logic is for the legacy workflow when approval and checkout were coupled together - # this logic can be removed once the new approval logic is deployed to production. - if transaction['status'] == 'checkOutInApproval': # wait for approval or until timeout occurs - quit_time = time.time() + max_wait_time - while True: - try: - transaction = self.get_checked_out_profile(transaction_id=transaction_id) - except exceptions.TransactionNotFound as e: - raise exceptions.ApprovalWorkflowRejected() from e - if transaction['status'] == 'checkOutInApproval': # we have an approval workflow occurring - if time.time() >= quit_time: - raise exceptions.ApprovalWorkflowTimedOut() - if progress_func: - progress_func('awaiting approval') - time.sleep(wait_time) - continue - # status == checkedOut - break - # inject credentials if asked if include_credentials: # if the transaction is not in status of checkedOut here it will be after the @@ -466,13 +320,15 @@ def checkout( self, profile_id: str, environment_id: str, - programmatic: bool = True, include_credentials: bool = False, justification: str = None, - otp: str = None, - wait_time: int = 60, max_wait_time: int = 600, + otp: str = None, + programmatic: bool = True, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, ) -> dict: """ Checkout a profile. @@ -486,38 +342,40 @@ def checkout( :param profile_id: The ID of the profile. Use `list_profiles()` to obtain the eligible profiles. :param environment_id: The ID of the environment. Use `list_profiles()` to obtain the eligible environments. - :param programmatic: True for programmatic credential checkout. False for console checkout. :param include_credentials: True if tokens should be included in the response. False if the caller wishes to call `credentials()` at a later time. If True, the `credentials` key will be included in the response which contains the response from `credentials()`. Setting this parameter to `True` will result in a synchronous call vs. setting to `False` will allow for an async call. :param justification: Optional justification if checking out the profile requires approval. - :param otp: Optional time based one-time passcode use for step up authentication. - :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout - was approved. :param max_wait_time: The maximum number of seconds to wait for an approval before throwing an exception. + :param otp: Optional time based one-time passcode use for step up authentication. + :param programmatic: True for programmatic credential checkout. False for console checkout. :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. :return: Details about the checked out profile, and optionally the credentials generated by the checkout. :raises ApprovalRequiredButNoJustificationProvided: if approval is required but no justification is provided. - :raises ApprovalWorkflowTimedOut: if max_wait_time has been reached while waiting for approval. - :raises ApprovalWorkflowRejected: if the request to check out the profile was rejected. + :raises ProfileApprovalRejected: if the approval request was rejected by the approver. :raises ProfileApprovalTimedOut: if the approval request timed out exceeded the max time as specified by the profile policy. - :raises ProfileApprovalRejected: if the approval request was rejected by the approver. :raises ProfileApprovalWithdrawn: if the approval request was withdrawn by the requester. """ return self._checkout( profile_id=profile_id, environment_id=environment_id, - programmatic=programmatic, include_credentials=include_credentials, justification=justification, - wait_time=wait_time, max_wait_time=max_wait_time, - progress_func=progress_func, otp=otp, + programmatic=programmatic, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, ) def checkout_by_name( @@ -525,13 +383,15 @@ def checkout_by_name( profile_name: str, environment_name: str, application_name: str = None, - programmatic: bool = True, include_credentials: bool = False, justification: str = None, - otp: str = None, - wait_time: int = 60, max_wait_time: int = 600, + otp: str = None, + programmatic: bool = True, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, ) -> dict: """ Checkout a profile by supplying the names of entities vs. the IDs of those entities. @@ -545,22 +405,26 @@ def checkout_by_name( :param environment_name: The name of the environment. Use `list_profiles()` to obtain the eligible environments. :param application_name: Optionally the name of the application, which can help disambiguate between profiles with the same name across applications. - :param programmatic: True for programmatic credential checkout. False for console checkout. :param include_credentials: True if tokens should be included in the response. False if the caller wishes to call `credentials()` at a later time. If True, the `credentials` key will be included in the response which contains the response from `credentials()`. Setting this parameter to `True` will result in a synchronous call vs. setting to `False` will allow for an async call. :param justification: Optional justification if checking out the profile requires approval. - :param otp: Optional time based one-time passcode use for step up authentication. - :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout - was approved. :param max_wait_time: The maximum number of seconds to wait for an approval before throwing an exception. + :param otp: Optional time based one-time passcode use for step up authentication. + :param programmatic: True for programmatic credential checkout. False for console checkout. :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. :return: Details about the checked out profile, and optionally the credentials generated by the checkout. :raises ApprovalRequiredButNoJustificationProvided: if approval is required but no justification is provided. - :raises ApprovalWorkflowTimedOut: if max_wait_time has been reached while waiting for approval. - :raises ApprovalWorkflowRejected: if the request to check out the profile was rejected. + :raises ProfileApprovalRejected: if the approval request was rejected by the approver. + :raises ProfileApprovalTimedOut: if the approval request timed out exceeded the max time as specified by the + profile policy. + :raises ProfileApprovalWithdrawn: if the approval request was withdrawn by the requester. """ ids = self._get_profile_and_environment_ids_given_names(profile_name, environment_name, application_name) @@ -568,13 +432,15 @@ def checkout_by_name( return self._checkout( profile_id=ids['profile_id'], environment_id=ids['environment_id'], - programmatic=programmatic, include_credentials=include_credentials, justification=justification, - wait_time=wait_time, max_wait_time=max_wait_time, - progress_func=progress_func, otp=otp, + programmatic=programmatic, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, ) def credentials( @@ -654,9 +520,18 @@ def checkin_by_name(self, profile_name: str, environment_name: str, application_ return self.checkin(transaction_id=transaction_id) + def whoami(self) -> dict: + """ + Return details about the currently authenticated identity (user or service). + + :return: Details of the currently authenticated identity. + """ + + return self.britive.post(f'{self.britive.base_url}/auth/validate')['authenticationResult'] + def frequents(self) -> list: """ - Return list of frequently used profiles for the user. + Return list of frequently used profiles for the current user. :return: List of profiles. """ @@ -665,65 +540,93 @@ def frequents(self) -> list: def favorites(self) -> list: """ - Return list of favorite profiles for the user. + Return list of favorite profiles for the current user. :return: List of profiles. """ return self.britive.get(f'{self.base_url}/favorites') - def whoami(self) -> dict: + def create_filter(self, filter_name: str, filter_properties: str) -> dict: """ - Return details about the currently authenticated identity (user or service). + Create a filter for the current user. - :return: Details of the currently authenticated identity. + :param filter_name: Name of the filter. + :param filter_properties: Dict of the filter properties. + Filter properties: + applications: + type: list + desc: Application ID(s) + associations: + type: list + desc: Assocation(s) + profiles: + type: list + desc: Profile ID(s) + statuses: + type: list + desc: Status(es) + application_types: + type: list + desc: Application Type(s) + :return: Details of the created filter. """ - return self.britive.post(f'{self.britive.base_url}/auth/validate')['authenticationResult'] + if application_types := filter_properties.pop('application_types', None): + filter_properties['applicationTypes'] = application_types - def _get_profile_and_environment_ids_given_names( - self, profile_name: str, environment_name: str, application_name: str = None - ) -> dict: - ids = None - profile_found = False - environment_found = False - - # collect relevant profile/environment combinations to which the identity is entitled - for app in self.list_profiles(): - app_name = app['appName'].lower() - if application_name and app_name != application_name.lower(): # restrict to one app if provided - continue - for profile in app['profiles']: - prof_name = profile['profileName'].lower() - prof_id = profile['profileId'] - - if prof_name == profile_name.lower(): - profile_found = True - for env in profile['environments']: - env_name = env['environmentName'].lower() - env_id = env['environmentId'] - - if env_name == environment_name.lower(): - environment_found = True - # lets check to see if `ids` has already been set - # if so we should error because we don't know which name combo to use - if ids: - raise ValueError( - f'multiple combinations of profile `{profile_name}` and environment ' - f'`{environment_name}` exist so no unique combination can be determined. Please ' - f'provide the optional parameter `application_name` to clarify which application ' - f'the environment belongs to.' - ) - # set the IDs the first time - ids = {'profile_id': prof_id, 'environment_id': env_id} - - # do some error checking - if not profile_found: - raise ValueError(f'profile `{profile_name}` not found.') - - if profile_found and not environment_found: - raise ValueError(f'profile `{profile_name}` found but not in environment `{environment_name}`.') - - # if we get here we found both the profile and environment and they are unique so - # we can use the `ids` dict with confidence - return ids + data = {'name': filter_name, 'filter': filter_properties} + + return self.britive.post(f"{self.base_url}/{self.whoami()['userId']}/filters", json=data) + + def list_filters(self) -> list: + """ + Return list of filters for the current user. + + :return: List of filters. + """ + + return self.britive.get(f"{self.base_url}/{self.whoami()['userId']}/filters") + + def update_filter(self, filter_id: str, filter_name: str, filter_properties: str) -> dict: + """ + Update a filter for the current user. + + :param filter_id: ID of the filter. + :param filter_name: Name of the filter. + :param filter_properties: Dict of the filter properties. + Filter properties: + applications: + type: list + desc: Application ID(s) + associations: + type: list + desc: Assocation(s) + profiles: + type: list + desc: Profile ID(s) + statuses: + type: list + desc: Status(es) + application_types: + type: list + desc: Application Type(s) + :return: Details of the updated filter. + """ + + if application_types := filter_properties.pop('application_types', None): + filter_properties['applicationTypes'] = application_types + + data = {'name': filter_name, 'filter': filter_properties} + + return self.britive.put(f"{self.base_url}/{self.whoami()['userId']}/filters/{filter_id}", json=data) + + def delete_filter(self, filter_id: str) -> None: + """ + Delete a filter for the current user. + + :param filter_id: ID of the filter. + :return: None. + """ + + return self.britive.delete(f"{self.base_url}/{self.whoami()['userId']}/filters/{filter_id}") diff --git a/src/britive/my_approvals.py b/src/britive/my_approvals.py new file mode 100644 index 0000000..62fae38 --- /dev/null +++ b/src/britive/my_approvals.py @@ -0,0 +1,55 @@ +class MyApprovals: + """ + This class is meant to be called by end users. It is an API layer on top of the actions that can be performed on the + "My Approvals" page of the Britive UI. + + No "administrative" access is required by the methods in this class. Each method will only return approvals/allow + actions which are permitted to be performed by the user/service identity, as identified by an API token or + interactive login bearer token. + + It is entirely possible that an administrator who makes these API calls could get nothing returned, as that + administrator may not have any pending approvals. + """ + + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/approvals' + + def approve_request(self, request_id: str, comments: str = '') -> None: + """ + Approves a request. + + :param request_id: The ID of the request. + :param comments: Approver comments. + :return: None. + """ + + params = {'approveRequest': 'yes'} + data = {'approverComment': comments} + + return self.britive.patch(f'{self.base_url}/{request_id}', params=params, json=data) + + def reject_request(self, request_id: str, comments: str = '') -> None: + """ + Rejects a request. + + :param request_id: The ID of the request. + :param comments: Approver comments. + :return: None. + """ + + params = {'approveRequest': 'no'} + data = {'approverComment': comments} + + return self.britive.patch(f'{self.base_url}/{request_id}', params=params, json=data) + + def list(self) -> dict: + """ + Lists approval requests. + + :return: List of approval requests. + """ + + params = {'requestType': 'myApprovals'} + + return self.britive.get(f'{self.base_url}/', params=params) diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py new file mode 100644 index 0000000..5197b6c --- /dev/null +++ b/src/britive/my_requests.py @@ -0,0 +1,451 @@ +import sys +import time +from typing import Any, Callable + +from .exceptions import ( + ProfileApprovalMaxBlockTimeExceeded, + ProfileCheckoutAlreadyApproved, +) +from .helpers import HelperMethods + + +class MyRequests: + """ + This class is meant to be called by end users. It is an API layer on top of the actions that can be performed on the + "My Requests" page of the Britive UI or when requesting approval for a profile checkout. + + No "administrative" access is required by the methods in this class. Each method will only return request/allow + actions which are permitted to be performed by the user/service identity, as identified by an API token or + interactive login bearer token. + + It is entirely possible that an administrator who makes these API calls could get nothing returned, as that + administrator may not have any requests or profiles which require one. + """ + + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/approvals' + self._helper = HelperMethods(self.britive) + + def list(self) -> list: + """ + List My Requests + + :return: List of My Requests. + """ + + return self.britive.get(f'{self.base_url}/', params={'requestType': 'myRequests'}) + + def approval_request_status(self, request_id: str) -> dict: + """ + Get the details of an approval request. + + :param request_id: The ID of the approval request. + :return: Details of the approval request. + """ + + return self.britive.get(f'{self.base_url}/{request_id}') + + def withdraw_approval_request(self, request_id: str) -> None: + """ + Withdraws a pending approval request. + + :param request_id: The ID of the approval request. + :return: None + """ + + return self._withdraw_approval_request(request_id=request_id) + + def _request_approval( + self, + profile_id: str, + justification: str, + entity_id: str, + entity_type: str, + block_until_disposition: bool = False, + max_wait_time: int = 600, + progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, + ) -> Any: + data = {'justification': justification} + + if ticket_id and ticket_type: + data.update(ticketId=ticket_id, ticketType=ticket_type) + + url = ( + f'{self.britive.base_url}/access/{profile_id}/{entity_type}/{entity_id}/approvalRequest' + if entity_type == 'environments' + else ( + f'{self.britive.base_url}/resource-manager/my-resources/profiles/' + f'{profile_id}/resources/{entity_id}/approvalRequest' + ) + ) + request = self.britive.post(url, json=data) + + if request is None: + raise ProfileCheckoutAlreadyApproved + + request_id = request['requestId'] + + if block_until_disposition: + try: + quit_time = time.time() + max_wait_time + while time.time() <= quit_time: + status = self.approval_request_status(request_id=request_id)['status'].lower() + if status == 'pending': + if progress_func: + progress_func('awaiting approval') + time.sleep(wait_time) + continue + # status == timeout or approved or rejected or cancelled + return status + raise ProfileApprovalMaxBlockTimeExceeded + except KeyboardInterrupt: # handle Ctrl+C (^C) + # the first ^C we get we will try to withdraw the request + try: + time.sleep(1) # give the caller a small window to ^C again + self.withdraw_approval_request(request_id=request_id) + sys.exit() + except KeyboardInterrupt: + sys.exit() + + else: + return request + + def _request_approval_by_name( + self, + justification: str, + profile_name: str, + entity_name: str, + entity_type: str, + application_name: str = None, + block_until_disposition: bool = False, + max_wait_time: int = 600, + progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, + ) -> Any: + if entity_type == 'environments': + ids = self._helper.get_profile_and_environment_ids_given_names(profile_name, entity_name, application_name) + return self._request_approval( + profile_id=ids['profile_id'], + justification=justification, + entity_id=ids['environment_id'], + entity_type=entity_type, + block_until_disposition=block_until_disposition, + max_wait_time=max_wait_time, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + ids = self._helper.get_profile_and_resource_ids_given_names(profile_name, entity_name) + return self._request_approval( + profile_id=ids['profile_id'], + justification=justification, + entity_id=ids['resource_id'], + entity_type=entity_type, + block_until_disposition=block_until_disposition, + max_wait_time=max_wait_time, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + + def _withdraw_approval_request( + self, request_id: str = None, profile_id: str = None, entity_id: str = None, entity_type: str = None + ) -> None: + url = request_id if request_id else f'consumer/{entity_type}/resource?resourceId={profile_id}/{entity_id}' + + return self.britive.delete(f'{self.base_url}/{url}') + + +class MyAccessRequests(MyRequests): + def request_approval_by_name( + self, + environment_name: str, + justification: str, + profile_name: str, + application_name: str = None, + block_until_disposition: bool = False, + max_wait_time: int = 600, + progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, + ) -> Any: + """ + Requests approval to checkout a profile at a later time, using names of entities instead of IDs. + + Console vs. Programmatic access is not applicable here. The request for approval will allow the caller + to checkout either type of access once the request has been approved. + + :param profile_name: The name of the profile. + Use `my_access.list_profiles()` to obtain the eligible profiles. + :param environment_name: The name of the environment. + Use `my_access.list_profiles()` to obtain the eligible environments. + :param application_name: Optionally the name of the application, which can help disambiguate between profiles + with the same name across applications. + :param justification: Justification for checking out a profile that requires approval. + :param block_until_disposition: Should this method wait/block until the request has been either approved, + rejected, or withdrawn. If `True` then `wait_time` and `max_wait_time` will govern how long to wait before + exiting. + :param max_wait_time: The maximum number of seconds to wait for an approval before throwing + an exception. Only applicable if `block_until_disposition = True`. + :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. Only applicable if `block_until_disposition = True`. + :return: If `block_until_disposition = True` then returns the final status of the request. If + `block_until_disposition = False` then returns details about the approval request. + :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. + """ + + return self._request_approval_by_name( + entity_name=environment_name, + justification=justification, + profile_name=profile_name, + application_name=application_name, + block_until_disposition=block_until_disposition, + entity_type='environments', + max_wait_time=max_wait_time, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + + def request_approval( + self, + profile_id: str, + justification: str, + block_until_disposition: bool = False, + environment_id: str = None, + max_wait_time: int = 600, + progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, + ) -> Any: + """ + Requests approval to checkout a profile at a later time. + + Console vs. Programmatic access is not applicable here. The request for approval will allow the caller + to checkout either type of access once the request has been approved. + + :param profile_id: The ID of the profile. + Use `my_access.list_profiles()` to obtain the eligible profiles. + :param environment_id: The ID of the environment. + Use `my_access.list_profiles()` to obtain the eligible environments. + :param justification: Justification for checking out a profile that requires approval. + :param block_until_disposition: Should this method wait/block until the request has been either approved, + rejected, or withdrawn. If `True` then `wait_time` and `max_wait_time` will govern how long to wait before + exiting. + :param max_wait_time: The maximum number of seconds to wait for an approval before throwing + an exception. Only applicable if `block_until_disposition = True`. + :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. Only applicable if `block_until_disposition = True`. + :return: If `block_until_disposition = True` then returns the final status of the request. If + `block_until_disposition = False` then returns details about the approval request. + :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. + """ + + return self._request_approval( + justification=justification, + profile_id=profile_id, + entity_id=environment_id, + entity_type='environments', + block_until_disposition=block_until_disposition, + max_wait_time=max_wait_time, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + + def withdraw_approval_request_by_name( + self, profile_name: str, environment_name: str = None, application_name: str = None + ) -> None: + """ + Withdraws a pending approval request, using names of entities instead of IDs. + + :param profile_name: The name of the profile. Use `list_profiles()` to obtain the eligible profiles. + :param environment_name: The name of the environment. Use `list_profiles()` to obtain the eligible environments. + :param application_name: Optionally the name of the application, which can help disambiguate between profiles + with the same name across applications. + :return: None + """ + + ids = self._helper.get_profile_and_environment_ids_given_names(profile_name, environment_name, application_name) + + return self._withdraw_approval_request( + profile_id=ids['profile_id'], entity_id=ids['environment_id'], entity_type='papservice' + ) + + def withdraw_approval_request( + self, request_id: str = None, profile_id: str = None, environment_id: str = None + ) -> None: + """ + Withdraws a pending approval request. + + Either `request_id` or (`profile_id` AND `environment_id`) are required. + + :param request_id: The ID of the approval request. + :param profile_id: The ID of the profile. + :param environment_id: The ID of the environment. + :return: None + """ + if not request_id and not all([profile_id, environment_id]): + raise ValueError('profile_id and environment_id are required') + + return self._withdraw_approval_request( + profile_id=profile_id, entity_id=environment_id, entity_type='papservice' + ) + + +class MyResourcesRequests(MyRequests): + def request_approval( + self, + justification: str, + profile_id: str, + resource_id: str, + block_until_disposition: bool = False, + max_wait_time: int = 600, + progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, + ) -> Any: + """ + Requests approval to checkout a profile at a later time. + + Console vs. Programmatic access is not applicable here. The request for approval will allow the caller + to checkout either type of access once the request has been approved. + + :param justification: Justification for checking out a profile that requires approval. + :param profile_id: The ID of the profile. + Use `my_resources.list_profiles()` to obtain the eligible profiles. + :param resource_id: The ID of the resource. + Use `my_resources.list_profiles()` to obtain the eligible resources. + :param block_until_disposition: Should this method wait/block until the request has been either approved, + rejected, or withdrawn. If `True` then `wait_time` and `max_wait_time` will govern how long to wait before + exiting. + :param max_wait_time: The maximum number of seconds to wait for an approval before throwing + an exception. Only applicable if `block_until_disposition = True`. + :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. Only applicable if `block_until_disposition = True`. + :return: If `block_until_disposition = True` then returns the final status of the request. If + `block_until_disposition = False` then returns details about the approval request. + :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. + """ + + return self._request_approval( + justification=justification, + profile_id=profile_id, + entity_id=resource_id, + entity_type='resource', + block_until_disposition=block_until_disposition, + max_wait_time=max_wait_time, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + + def request_approval_by_name( + self, + justification: str, + profile_name: str, + resource_name: str, + block_until_disposition: bool = False, + max_wait_time: int = 600, + progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, + ) -> Any: + """ + Requests approval to checkout a profile at a later time, using names of entities instead of IDs. + + Console vs. Programmatic access is not applicable here. The request for approval will allow the caller + to checkout either type of access once the request has been approved. + + :param justification: Justification for checking out a profile that requires approval. + :param profile_name: The name of the profile. + Use `my_resources.list_profiles()` to obtain the eligible profiles. + :param resource_name: The name of the resource. + Use `my_resources.list_profiles()` to obtain the eligible resources. + :param block_until_disposition: Should this method wait/block until the request has been either approved, + rejected, or withdrawn. If `True` then `wait_time` and `max_wait_time` will govern how long to wait before + exiting. + :param max_wait_time: The maximum number of seconds to wait for an approval before throwing + an exception. Only applicable if `block_until_disposition = True`. + :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. Only applicable if `block_until_disposition = True`. + :return: If `block_until_disposition = True` then returns the final status of the request. If + `block_until_disposition = False` then returns details about the approval request. + :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. + """ + + return self._request_approval_by_name( + justification=justification, + profile_name=profile_name, + entity_name=resource_name, + entity_type='resource', + block_until_disposition=block_until_disposition, + max_wait_time=max_wait_time, + progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + + def withdraw_approval_request_by_name(self, profile_name: str, resource_name: str = None) -> None: + """ + Withdraws a pending approval request, using names of entities instead of IDs. + + :param profile_name: The name of the profile. Use `list_profiles()` to obtain the eligible profiles. + :param resource_name: The name of the resource. Use `list_profiles()` to obtain the eligible resources. + :return: None + """ + + ids = self._helper.get_profile_and_resource_ids_given_names(profile_name, resource_name) + + return self._withdraw_approval_request( + profile_id=ids['profile_id'], entity_id=ids['resource_id'], entity_type='resourceprofile' + ) + + def withdraw_approval_request( + self, request_id: str = None, profile_id: str = None, resource_id: str = None + ) -> None: + """ + Withdraws a pending approval request. + + Either `request_id` or (`profile_id` AND `resource_id`) are required. + + :param request_id: The ID of the approval request. + :param profile_id: The ID of the profile. + :param resource_id: The ID of the resource. + :return: None + """ + + if not request_id and not all([profile_id, resource_id]): + raise ValueError('profile_id and resource_id are required') + + return self._withdraw_approval_request( + profile_id=profile_id, entity_id=resource_id, entity_type='resourceprofile' + ) diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 8efd500..28eec94 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -1,12 +1,24 @@ import time from typing import Any, Callable -from . import exceptions +from .exceptions import ( + ApprovalRequiredButNoJustificationProvided, + ProfileApprovalRejected, + ProfileApprovalTimedOut, + ProfileApprovalWithdrawn, + StepUpAuthFailed, + StepUpAuthRequiredButNotProvided, + TransactionNotFound, +) +from .exceptions.badrequest import ApprovalJustificationRequiredError, ProfileApprovalRequiredError +from .exceptions.generic import StepUpAuthenticationRequiredError +from .helpers import HelperMethods +from .my_requests import MyResourcesRequests approval_exceptions = { - 'rejected': exceptions.ProfileApprovalRejected(), - 'cancelled': exceptions.ProfileApprovalWithdrawn(), - 'timeout': exceptions.ProfileApprovalTimedOut(), + 'rejected': ProfileApprovalRejected(), + 'cancelled': ProfileApprovalWithdrawn(), + 'timeout': ProfileApprovalTimedOut(), } @@ -26,15 +38,30 @@ class MyResources: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/resource-manager/my-resources' - - def list_profiles(self, list_type: str = None, search_text: str = None) -> list: + self._get_profile_and_resource_ids_given_names = HelperMethods( + self.britive + ).get_profile_and_resource_ids_given_names + + # MyRequests + __my_requests = MyResourcesRequests(self.britive) + self.request_approval = __my_requests.request_approval + self.request_approval_by_name = __my_requests.request_approval_by_name + self.withdraw_approval_request = __my_requests.withdraw_approval_request + self.withdraw_approval_request_by_name = __my_requests.withdraw_approval_request_by_name + + def list_profiles(self, filter_text: str = None, list_type: str = None, search_text: str = None) -> list: """ List the profiles for which the user has access. + :param filter_text: filter resource by key, e.g. `filter_text='key eq env'` + :param list_type: filter resources by type, e.g. `list_type='frequentlyUsed'` + :param search_text: filter resources by search text. :return: List of profiles. """ params = {} + if filter_text: + params['filter'] = filter_text if list_type: params['type'] = list_type if search_text: @@ -61,6 +88,16 @@ def list_checked_out_profiles(self) -> list: return [i for i in self.list_profiles() if i['transactionId']] + def list_response_templates(self, transaction_id: str) -> list: + """ + List the Response Templates for a checked out profile. + + :param transaction_id: Transaction ID of the checked out profile. + :return: List of response templates. + """ + + return self.britive.get(f'{self.base_url}/{transaction_id}/templates') + def get_checked_out_profile(self, transaction_id: str) -> dict: """ Retrieve details of a given checked out profile. @@ -72,7 +109,7 @@ def get_checked_out_profile(self, transaction_id: str) -> dict: for t in self.list_checked_out_profiles(): if t['transactionId'] == transaction_id: return t - raise exceptions.TransactionNotFound() + raise TransactionNotFound def _checkout( self, @@ -80,10 +117,13 @@ def _checkout( resource_id: str, include_credentials: bool = False, justification: str = None, - otp: str = None, - wait_time: int = 60, max_wait_time: int = 600, + otp: str = None, progress_func: Callable = None, + response_template: str = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, ) -> dict: data = {'justification': justification} @@ -115,42 +155,40 @@ def _checkout( # if not check it out if not transaction: if otp: - response = self.britive.step_up.authenticate(otp=otp) + response = self.britive.security.step_up_auth.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise exceptions.StepUpAuthFailed() + raise StepUpAuthFailed try: transaction = self.britive.post( f'{self.base_url}/profiles/{profile_id}/resources/{resource_id}/checkout', json=data ) - except exceptions.ForbiddenRequest as e: - if 'PE-0028' in str(e): # Check for stepup totp - raise exceptions.StepUpAuthRequiredButNotProvided() from e - # except exceptions.InvalidRequest as e: - # if 'MA-0010' in str(e): # new approval process that de-couples approval from checkout - # # if the caller has not provided a justification we know for sure the call will fail - # # so raise the exception - # if not justification: - # raise exceptions.ApprovalRequiredButNoJustificationProvided() - # - # # request approval - # status = self.request_approval( - # profile_id=profile_id, - # environment_id=environment_id, - # justification=justification, - # wait_time=wait_time, - # max_wait_time=max_wait_time, - # block_until_disposition=True, - # progress_func=progress_func, - # ) - # - # # handle the response based on the value of status - # if status == 'approved': - # transaction = self.britive.post( - # f'{self.base_url}/{profile_id}/environments/{environment_id}', params=params, json=data - # ) - # else: - # raise approval_exceptions[status] + except StepUpAuthenticationRequiredError as e: + raise StepUpAuthRequiredButNotProvided(e) from e + except (ApprovalJustificationRequiredError, ProfileApprovalRequiredError) as e: + if not justification: + raise ApprovalRequiredButNoJustificationProvided from e + + # request approval + status = self.request_approval( + block_until_disposition=True, + justification=justification, + max_wait_time=max_wait_time, + profile_id=profile_id, + progress_func=progress_func, + resource_id=resource_id, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, + ) + + # handle the response based on the value of status + if status == 'approved': + transaction = self.britive.post( + f'{self.base_url}/{profile_id}/resources/{resource_id}/checkout', json=data + ) + else: + raise approval_exceptions[status](e) from e raise e transaction_id = transaction['transactionId'] @@ -160,9 +198,10 @@ def _checkout( # if the transaction is not in status of checkedOut here it will be after the # return of this call and we update the transaction object accordingly credentials, transaction = self.credentials( + response_template=response_template, + return_transaction_details=True, transaction_id=transaction_id, transaction=transaction, - return_transaction_details=True, progress_func=progress_func, ) transaction['credentials'] = credentials @@ -177,10 +216,13 @@ def checkout( resource_id: str, include_credentials: bool = False, justification: str = None, - otp: str = None, - wait_time: int = 60, max_wait_time: int = 600, + otp: str = None, progress_func: Callable = None, + response_template: str = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, ) -> dict: """ Checkout a profile. @@ -199,19 +241,20 @@ def checkout( contains the response from `credentials()`. Setting this parameter to `True` will result in a synchronous call vs. setting to `False` will allow for an async call. :param justification: Optional justification if checking out the profile requires approval. - :param otp: Optional time based one-time passcode use for step up authentication. - :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout - was approved. :param max_wait_time: The maximum number of seconds to wait for an approval before throwing an exception. + :param otp: Optional time based one-time passcode use for step up authentication. :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param response_template: Optional response template to use in conjunction with `include_credentials`. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. :return: Details about the checked out profile, and optionally the credentials generated by the checkout. :raises ApprovalRequiredButNoJustificationProvided: if approval is required but no justification is provided. - :raises ApprovalWorkflowTimedOut: if max_wait_time has been reached while waiting for approval. - :raises ApprovalWorkflowRejected: if the request to check out the profile was rejected. + :raises ProfileApprovalRejected: if the approval request was rejected by the approver. :raises ProfileApprovalTimedOut: if the approval request timed out exceeded the max time as specified by the profile policy. - :raises ProfileApprovalRejected: if the approval request was rejected by the approver. :raises ProfileApprovalWithdrawn: if the approval request was withdrawn by the requester. """ @@ -220,10 +263,13 @@ def checkout( resource_id=resource_id, include_credentials=include_credentials, justification=justification, - wait_time=wait_time, max_wait_time=max_wait_time, - progress_func=progress_func, otp=otp, + progress_func=progress_func, + response_template=response_template, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, ) def checkout_by_name( @@ -232,10 +278,13 @@ def checkout_by_name( resource_name: str, include_credentials: bool = False, justification: str = None, - otp: str = None, - wait_time: int = 60, max_wait_time: int = 600, + otp: str = None, progress_func: Callable = None, + response_template: str = None, + ticket_id: str = None, + ticket_type: str = None, + wait_time: int = 60, ) -> dict: """ Checkout a profile by supplying the names of entities vs. the IDs of those entities. @@ -252,16 +301,21 @@ def checkout_by_name( contains the response from `credentials()`. Setting this parameter to `True` will result in a synchronous call vs. setting to `False` will allow for an async call. :param justification: Optional justification if checking out the profile requires approval. - :param otp: Optional time based one-time passcode use for step up authentication. - :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout - was approved. :param max_wait_time: The maximum number of seconds to wait for an approval before throwing an exception. + :param otp: Optional time based one-time passcode use for step up authentication. :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param response_template: Optional response template to use in conjunction with `include_credentials`. + :param ticket_id: Optional ITSM ticket ID + :param ticket_type: Optional ITSM ticket type or category + :param wait_time: The number of seconds to sleep/wait between polling to check if the profile checkout + was approved. :return: Details about the checked out profile, and optionally the credentials generated by the checkout. :raises ApprovalRequiredButNoJustificationProvided: if approval is required but no justification is provided. - :raises ApprovalWorkflowTimedOut: if max_wait_time has been reached while waiting for approval. - :raises ApprovalWorkflowRejected: if the request to check out the profile was rejected. + :raises ProfileApprovalRejected: if the approval request was rejected by the approver. + :raises ProfileApprovalTimedOut: if the approval request timed out exceeded the max time as specified by the + profile policy. + :raises ProfileApprovalWithdrawn: if the approval request was withdrawn by the requester. """ ids = self._get_profile_and_resource_ids_given_names(profile_name, resource_name) @@ -271,27 +325,32 @@ def checkout_by_name( resource_id=ids['resource_id'], include_credentials=include_credentials, justification=justification, - otp=otp, - wait_time=wait_time, max_wait_time=max_wait_time, + otp=otp, progress_func=progress_func, + response_template=response_template, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, ) def credentials( self, transaction_id: str, - transaction: dict = None, - return_transaction_details: bool = False, progress_func: Callable = None, + response_template: str = None, + return_transaction_details: bool = False, + transaction: dict = None, ) -> Any: """ Return credentials of a checked out profile given the transaction ID. :param transaction_id: The ID of the transaction. - :param transaction: Optional - the details of the transaction. Primary use is for internal purposes. + :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param response_template: Optional - return the string value of a given response template. :param return_transaction_details: Optional - whether to return the details of the transaction. Primary use is for internal purposes. - :param progress_func: An optional callback that will be invoked as the checkout process progresses. + :param transaction: Optional - the details of the transaction. Primary use is for internal purposes. :return: Credentials associated with the checked out profile represented by the specified transaction. """ @@ -310,7 +369,10 @@ def credentials( break # step 2: make the proper API call - creds = self.britive.post(f'{self.base_url}/{transaction_id}/credentials') + creds = self.britive.post( + f'{self.base_url}/{transaction_id}/credentials', + params={'templateName': response_template} if response_template else {}, + ) if return_transaction_details: return creds, transaction @@ -365,19 +427,49 @@ def favorites(self) -> list: return self.list_profiles(list_type='favorites') - def _get_profile_and_resource_ids_given_names(self, profile_name: str, resource_name: str) -> dict: - resource_profile_map = { - f'{item["resourceName"].lower()}|{item["profileName"].lower()}': { - 'profile_id': item['profileId'], - 'resource_id': item['resourceId'], - } - for item in self.list_profiles() - } + def add_favorite(self, resource_id: str, profile_id: str) -> dict: + """ + Add a resource favorite. + + :param resource_id: The resource ID of the resource favorite to add. + :param profile_id: The profile ID of the resource favorite to add. + :return: Details of the favorite resource. + """ + + data = {'resource-id': resource_id, 'profile-id': profile_id} + + return self.post(f'{self.base_url}/favorites', json=data) + + def delete_favorite(self, favorite_id: str) -> None: + """ + Delete a resource favorite. + + :param favorite_id: The ID of the resource favorite to delete. + :return: None + """ - item = resource_profile_map.get(f'{resource_name.lower()}|{profile_name.lower()}') + return self.delete(f'{self.base_url}/favorites/{favorite_id}') + + def get_profile_settings(self, profile_id: str, resource_id: str) -> dict: + """ + Retrieve settings of a profile. + + :param profile_id: The ID of the profile. + :param resource_id: The ID of the resource. + :return: Dict of the profile settings. + """ + + return self.britive.get(f'{self.base_url}/{profile_id}/resources/{resource_id}/settings') + + def get_profile_settings_by_name(self, profile_name: str, resource_name: str) -> dict: + """ + Retrieve settings of a profile by name. + + :param profile_name: The name of the profile. + :param resource_name: The name of the resource. + :return: Dict of the profile settings. + """ - # do some error checking - if not item: - raise ValueError('resource and profile combination not found') + ids = self._get_profile_and_resource_ids_given_names(profile_name=profile_name, resource_name=resource_name) - return item + return self.get_profile_settings(profile_id=ids['profile_id'], resource_id=ids['resource_id']) diff --git a/src/britive/my_secrets.py b/src/britive/my_secrets.py index a7ac383..062891a 100644 --- a/src/britive/my_secrets.py +++ b/src/britive/my_secrets.py @@ -1,13 +1,28 @@ import time from datetime import datetime, timedelta, timezone -from . import exceptions +from .exceptions import ( + AccessDenied, + ApprovalRequiredButNoJustificationProvided, + ApprovalWorkflowRejected, + ApprovalWorkflowTimedOut, + ForbiddenRequest, + NoSecretsVaultFound, + StepUpAuthFailed, + StepUpAuthRequiredButNotProvided, +) +from .exceptions.generic import ( + ApprovalPendingError, + ApprovalRequiredError, + EvaluationError, + StepUpAuthenticationRequiredError, +) class MySecrets: """ - This class is meant to be called by end users (as part of custom API integration work or pybritive CLI). - It is an API layer on top of the actions that can be performed on the "My Secrets" page of the Britive UI. + This class is meant to be called by end users. It is an API layer on top of the actions that can be performed on + the "My Secrets" page of the Britive UI. No "administrative" access is required by the methods in this class. Each method will only return resources/allow actions which are permitted to be performed by the user/service identity, as identified by an API token or @@ -27,7 +42,7 @@ def __get_vault_id(self) -> str: return self.britive.get(f'{self.base_url}/vault')['id'] except KeyError as e: if 'id' in str(e): - raise exceptions.NoSecretsVaultFound() from e + raise NoSecretsVaultFound from e def list(self, path: str = '/', search: str = None) -> list: """ @@ -80,35 +95,32 @@ def view( try: # handle when the time has expired waiting for approval if datetime.now(timezone.utc) >= quit_time: - raise exceptions.ApprovalWorkflowTimedOut() + raise ApprovalWorkflowTimedOut # handle stepup totp if otp: - response = self.britive.step_up.authenticate(otp=otp) + response = self.britive.security.step_up_auth.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise exceptions.StepUpAuthFailed() + raise StepUpAuthFailed # attempt to get the secret value and return it return self.britive.post( f'{self.base_url}/vault/{vault_id}/accesssecrets', params=params, json=data if first else None )['value'] - - # 403 will be returned when approval or stepup auth are required or pending, or access is denied - except exceptions.ForbiddenRequest as e: - if 'PE-0002' in str(e): - raise exceptions.AccessDenied() from e - if 'PE-0010' in str(e): # approval to view the secret is pending... - first = False - time.sleep(wait_time) - if 'PE-0011' in str(e) and not justification: + except EvaluationError as e: + raise AccessDenied(e) from e + except ApprovalPendingError: # approval to view the secret is pending... + first = False + time.sleep(wait_time) + except ApprovalRequiredError as e: + if not justification: if first: - raise exceptions.ApprovalRequiredButNoJustificationProvided() from e - else: - raise exceptions.ApprovalWorkflowRejected() from e - if 'PE-0028' in str(e): # Check for stepup totp - raise exceptions.StepUpAuthRequiredButNotProvided() from e - else: - raise e + raise ApprovalRequiredButNoJustificationProvided(e) from e + raise ApprovalWorkflowRejected(e) from e + except StepUpAuthenticationRequiredError as e: + raise StepUpAuthRequiredButNotProvided(e) from e + except ForbiddenRequest as e: + raise e def download( self, path: str, justification: str = None, otp: str = None, wait_time: int = 60, max_wait_time: int = 600 @@ -142,26 +154,22 @@ def download( try: # handle stepup totp if otp: - response = self.britive.step_up.authenticate(otp=otp) + response = self.britive.security.step_up_auth.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise exceptions.StepUpAuthFailed() + raise StepUpAuthFailed # attempt to get the secret file and return it return self.britive.get(f'{self.base_url}/vault/{vault_id}/downloadfile', params=params) # 403 will be returned when approval is required or access is denied - except exceptions.ForbiddenRequest as e: - if 'PE-0002' in str(e): - raise exceptions.AccessDenied() from e - if 'PE-0011' in str(e): # justification is required which means we have an approval workflow to deal with - # lets call view so we can go through the full approval process - self.view( - path=path, justification=justification, otp=otp, wait_time=wait_time, max_wait_time=max_wait_time - ) - - # and then we can get the file again - return self.britive.get(f'{self.base_url}/vault/{vault_id}/downloadfile', params=params) - if 'PE-0028' in str(e): # Check for stepup totp - raise exceptions.StepUpAuthRequiredButNotProvided() from e - - else: - raise e + except EvaluationError as e: + raise AccessDenied(e) from e + except ApprovalRequiredError: + # justification is required which means we have an approval workflow to deal with + # lets call view so we can go through the full approval process + self.view(path=path, justification=justification, otp=otp, wait_time=wait_time, max_wait_time=max_wait_time) + # and then we can get the file again + return self.britive.get(f'{self.base_url}/vault/{vault_id}/downloadfile', params=params) + except StepUpAuthenticationRequiredError as e: + raise StepUpAuthRequiredButNotProvided(e) from e + except ForbiddenRequest as e: + raise e diff --git a/src/britive/reports.py b/src/britive/reports/__init__.py similarity index 86% rename from src/britive/reports.py rename to src/britive/reports/__init__.py index 95c7a37..7fd2194 100644 --- a/src/britive/reports.py +++ b/src/britive/reports/__init__.py @@ -47,8 +47,7 @@ def run(self, report_id: str, csv: bool = False, filter_expression: str = None) # as the base and convert to dict if the client asked for dict if csv: return csv_results - else: - dict_results = [] - for row in csv_lib.DictReader(StringIO(csv_results), quoting=csv_lib.QUOTE_MINIMAL): - dict_results.append({k: _json_loads(v) for k, v in row.items()}) - return dict_results + dict_results = [] + for row in csv_lib.DictReader(StringIO(csv_results), quoting=csv_lib.QUOTE_MINIMAL): + dict_results.append({k: _json_loads(v) for k, v in row.items()}) + return dict_results diff --git a/src/britive/secrets_manager.py b/src/britive/secrets_manager.py deleted file mode 100644 index 17861b2..0000000 --- a/src/britive/secrets_manager.py +++ /dev/null @@ -1,701 +0,0 @@ -import json -from typing import Union - - -class SecretsManager: - def __init__(self, britive) -> None: - self.vaults = Vaults(britive) - self.password_policies = PasswordPolicies(britive) - self.secrets = Secrets(britive) - self.policies = Policies(britive) - self.static_secret_templates = StaticSecretTemplates(britive) - self.resources = Resources(britive) - self.folders = Folders(britive) - - -class Vaults: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/secretmanager/vault' - - def list(self) -> list: - """ - Provide a list of all vaults. - - :return: List of all vaults - """ - - params = {'getmetadata': 'true'} - return self.britive.get(self.base_url, params=params) - - def get_vault_by_id(self, vault_id: str) -> dict: - """ - Provide details of the given vault, from a vault id. - - :param vault_id: The ID of the vault. - :return: Details of the specified vault. - """ - - return self.britive.get(f'{self.base_url}/{vault_id}') - - def create( - self, - name: str, - description: str = 'Default vault description', - rotation_time: int = 30, - encryption_algorithm: str = 'AES_256', - default_notification_medium_id: str = '', - users: list = None, - tags: list = None, - channels: list = None, - ) -> dict: - """ - Create a new vault. - - :param name: the name of the vault - :param description: the description of the vault - :param rotation_time: in hours, how often the vault should rotate keys - :param encryption_algorithm : the encryption algorithm to use for the vault - :param default_notification_medium_id : the default notification medium to use for the vault - :param users: a list of user IDs to recieve notifications for the vault - :param tags: a list of tags to recieve notifications for the vault - :param channels : a list of channels to recieve notifications for the vault (only for slack) - :return: Details of the newly created vault. - """ - - if users is None: - users = [] - if tags is None: - tags = [] - if channels is None: - channels = [] - - if default_notification_medium_id == '': - for medium in self.britive.notification_mediums.list(): - if medium['name'] == 'Email': - default_notification_medium_id = medium['id'] - params = { - 'name': name, - 'description': description, - 'rotationTime': rotation_time, - 'encryptionAlgorithm': encryption_algorithm, - 'defaultNotificationMediumId': default_notification_medium_id, - 'recipients': {'userIds': users, 'tags': tags, 'channelIds': channels}, - } - return self.britive.post(self.base_url, json=params) - - def delete(self, vault_id: str) -> None: - """ - Deletes a vault. - - :param vault_id: the ID of the vault - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{vault_id}') - - def update(self, vault_id: str, **kwargs) -> None: - """ - Updates a vault. - - If not all kwargs a provided, the vault will update with the default values of the unprovided kwargs. - - :param vault_id: The ID of the vault. - :param kwargs: Valid fields are... - name - required - description - rotationTime - time in days between key rotations - encryptionAlgorithm - the encryption algorithm to use for the vault - defaultNotificationMediumId - the default notification medium to use for the vault - recipients - a list of user IDs or tags to recieve notifications for the vault - :return: None - """ - - return self.britive.patch(f'{self.base_url}/{vault_id}', json=kwargs) - - def rotate_keys(self) -> None: - """ - Rotate vault keys. - - :return: None - """ - - return self.britive.post(f'{self.britive.base_url}/v1/secretmanager/keys/rotate') - - -class PasswordPolicies: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/secretmanager/pwdpolicies' - - def get(self, password_policy_id: str) -> dict: - """ - Provide details of the given password policy, from a password policy id. - - :param password_policy_id: The ID of the password policy. - :return: Details of the specified password policy. - """ - - return self.britive.get(f'{self.base_url}/{password_policy_id}') - - def list(self, filter_str: str = None) -> list: - """ - Provide a list of all password policies - - :param filter_str: filter to apply to the listing - :return: List of all password policies - """ - - params = {} - if filter_str: - params['filter'] = filter_str - - return self.britive.get(self.base_url, params=params) - - def create( - self, - name: str, - description: str = 'Default description', - password_type: str = 'alphanumeric', - min_password_length: int = 8, - has_upper_case_chars: bool = True, - has_lower_case_chars: bool = True, - has_numbers: bool = True, - has_special_chars: bool = True, - allowed_special_chars: str = '~`!@#$%^&*()-_+=[]{}|/;:"?/\\.><,\'', - ) -> dict: - """ - Creates a new password policy. - - :param name: required, name of the password policy - :param description: description of the password policy - :param password_type: type of password to use for the policy - :param min_password_length: minimum length of the password - :param has_upper_case_chars: whether to require uppercase characters - :param has_lower_case_chars: whether to require lowercase characters - :param has_numbers: whether to require numbers - :param has_special_chars: whether to require special characters - :param allowed_special_chars: a string of special characters to allow in the password - :return: Details of the newly created password policy. - """ - - params = { - 'name': name, - 'description': description, - 'passwordType': password_type, - 'minPasswordLength': min_password_length, - 'hasUpperCaseChars': has_upper_case_chars, - 'hasLowerCaseChars': has_lower_case_chars, - 'hasNumbers': has_numbers, - 'hasSpecialChars': has_special_chars, - 'allowedSpecialChars': allowed_special_chars, - } - return self.britive.post(self.base_url, json=params) - - def create_pin(self, name: str, description: str = 'Default description', pin_length: int = 4) -> dict: - """ - Creates a new pin password policy. - - :param name: required, name of the pin password policy - :param description: description of the pin password policy - :param pin_length: length of the pin to use for the policy - :return: Details of the newly created pin password policy. - """ - - params = { - 'name': name, - 'description': description, - 'pinLength': pin_length, - 'passwordType': 'pin', - } - return self.britive.post(self.base_url, json=params) - - def update(self, password_policy_id: str, **kwargs) -> None: - """ - Updates a password policy. - - :param password_policy_id: the ID of the password policy - :param kwargs: Valid fields are... - name: name of the password policy - description: description of the password policy - passwordType: type of password to use for the policy - minPasswordLength: minimum length of the password - hasUpperCaseChars: whether to require uppercase characters - hasLowercaseChars: whether to require lowercase characters - hasNumbers: whether to require numbers - hasSpecialChars: whether to require special characters - allowedSpecialChars: a string of special characters to allow in the password - pinLength: the length of the pin to use for the policy (only for pins) - :return: None - """ - - current = self.get(password_policy_id=password_policy_id) - - return self.britive.patch(f'{self.base_url}/{password_policy_id}', json={**current, **kwargs}) - - def delete(self, password_policy_id: str) -> None: - """ - Deletes a password policy. - - :param password_policy_id: the ID of the password policy - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{password_policy_id}') - - def generate_password(self, password_policy_id: str) -> dict: - """ - Generates a password for the given password policy. - - :param password_policy_id: the ID of the password policy - :return: the generated password - """ - - params = {'action': 'generatePasswordOrPin'} - return self.britive.get(f'{self.base_url}/{password_policy_id}', params=params)['passwordOrPin'] - - def validate(self, password_policy_id: str, password: str) -> dict: - """ - Validates a password for the given password policy. - - :param password_policy_id: the ID of the password policy - :param password: the password to validate - :return: whether the password is valid - """ - - data = { - 'id': password_policy_id, - 'passwordOrPin': password, - } - - params = {'action': 'validatePasswordOrPin'} - return self.britive.post(self.base_url, json=data, params=params) - - -class Folders: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/secretmanager/vault' - - def create(self, name: str, vault_id: str, path: str = '/') -> dict: - """ - Creates a new folder in the vault. - - :param name: The name of the folder. - :param vault_id: The ID of the vault. - :param path: The path of the folder (include the leading /). - :return: Details of the newly created folder. - """ - - data = {'entityType': 'node', 'name': name} - return self.britive.post(f'{self.base_url}/{vault_id}/secrets?path={path}', json=data) - - def delete(self, vault_id: str, path: str) -> None: - """ - Deletes a folder from the vault. - - :param vault_id: ID of the vault to delete the folder from - :param path: path of the folder, include the / at the beginning - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{vault_id}/secrets?path={path}') - - -class Secrets: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/secretmanager/vault' - - def create( - self, - name: str, - vault_id: str, - path: str = '/', - static_secret_template_id: str = '7a5f41d8-f7af-46a0-88f7-edf0403607ae', - secret_mode: str = 'shared', - secret_nature: str = 'static', - value: dict = None, - file: bytes = None, - ) -> dict: - """ - Creates a new secret in the vault. - - For creating a secret with a file, you must read the file in with: - with open(file_path, 'rb') as f: - britive.secrets_manager.secrets.create(...., file=f) - - - :param name: name of the secret - :param vault_id: ID of the vault - :param path: path of the secret, include the / the beginning - :param static_secret_template_id: ID of the static secret template - :param secret_mode: mode of the secret - :param secret_nature: nature of the secret - :param value: value of the secret (must be in the format of the static secret template) - :param file: file to upload as the secret - :return: Details of the newly created secret. - """ - if value is None: - value = {'Note': 'This is the default note'} - - if not file: - return self.britive.post( - f'{self.base_url}/{vault_id}/secrets?path={path}', - json={ - 'name': name, - 'entityType': 'secret', - 'staticSecretTemplateId': static_secret_template_id, - 'secretMode': secret_mode, - 'secretNature': secret_nature, - 'value': value, - }, - ) - else: - secret_data = { - 'entityType': 'secret', - 'name': name, - 'staticSecretTemplateId': static_secret_template_id, - 'secretMode': secret_mode, - 'secretNature': secret_nature, - 'value': value, - } - return self.britive.post_upload( - f'{self.base_url}/{vault_id}/secrets/file?path={path}', - files={'file': file, 'secretData': (None, json.dumps(secret_data))}, - ) - - def update(self, vault_id: str, path: str = '/', value: dict = None) -> None: - """ - Updates a secret's value - - :param vault_id: ID of the vault to update the secret in - :param path: path of the secret, include the / at the beginning - :param value: value of the secret - :return: None - """ - if value is None: - value = {} - - return self.britive.patch(f'{self.base_url}/{vault_id}/secrets?path={path}', json={'value': value}) - - def rename(self, vault_id: str, path: str = '/', new_name: str = '') -> None: - """ - Update the name of a secret. - - :param vault_id: ID of the vault to update the secret in - :param path: path of the secret, include the / at the beginning and the secret name - :param new_name: new name of the secret - :return: None - """ - - return self.britive.patch(f'{self.base_url}/{vault_id}/secrets?path={path}', json={'name': new_name}) - - def get( - self, - vault_id: str, - path: str, - secret_type: str = 'node', - filter_type: str = None, - recursive_secrets: bool = False, - get_metadata: bool = True, - ) -> dict: - """ - Gets a secret from the vault. - - :param vault_id: ID of the vault to get the secret from - :param path: path of the secret, include the / at the beginning - :param secret_type: type of the secret (node or secret) - :param filter_type: filter to apply to the secret (NONE, ALL, SHARED, PRIVATE) - :param recursive_secrets: whether or not to recursively get all secrets in the folder - :param get_metadata: whether or not to get the metadata of the secret - :return: Details of the secret. - """ - - params = { - 'type': secret_type, - 'filter': filter_type, - 'recursiveSecrets': (str(recursive_secrets)).lower(), - 'getMetadata': get_metadata, - } - return self.britive.get(f'{self.base_url}/{vault_id}/secrets?path={path}', params=params) - - def delete(self, vault_id: str, path: str) -> None: - """ - Deletes a secret from the vault. - - :param vault_id: ID of the vault to delete the secret from - :param path: path of the secret, include the / at the beginning - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{vault_id}/secrets?path={path}') - - def access(self, vault_id: str, path: str, get_metadata: bool = False) -> dict: - """ - Accesses a secret from the vault. - - :param vault_id: ID of the vault to get the secret from - :param path: path of the secret, include the / at the beginning - :param get_metadata: whether or not to get the metadata of the secret - :return: Details of the secret. - """ - - params = {'getmetadata': get_metadata} - return self.britive.get(f'{self.base_url}/{vault_id}/secrets?path={path}', params=params) - - -class Policies: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/policy-admin/policies' - - def list(self, path: str = '/', filter_str: str = None) -> dict: - """ - Gets all policies in the vault. - - :param path: path of the policy, include the / at the beginning - :param filter_str: filter to apply to the listing - :return: Details of the policies. - """ - - params = {'resource': path, 'consumer': 'secretmanager'} - if filter_str: - params['filter'] = filter_str - return self.britive.get(f'{self.base_url}', params=params) - - def delete(self, policy_id: str, path: str = '/') -> None: - """ - Deletes a policy from the vault. - - :param policy_id: ID of the policy to delete - :param path: path of the policy, include the / at the beginning - :return: None - """ - - params = {'consumer': 'secretmanager', 'resource': path} - return self.britive.delete(f'{self.base_url}/{policy_id}', params=params) - - def build( # noqa: PLR0913 - self, - name: str, - access_level: str = None, - description: str = '', - draft: bool = False, - active: bool = True, - read_only: bool = False, - users: list = None, - tags: list = None, - tokens: list = None, - service_identities: list = None, - ips: list = None, - date_schedule: dict = None, - days_schedule: dict = None, - approval_notification_medium: Union[str, list] = None, - time_to_approve: int = 5, - access_validity_time: int = 120, - approver_users: list = None, - approver_tags: list = None, - access_type: str = 'Allow', - identifier_type: str = 'name', - condition_as_dict: bool = False, - ) -> dict: - """ - Build a policy document given the provided inputs. - - :param name: The name of the policy. - :param access_level: The level of access. Valid values are SM_View, SM_Manage, SM_CRUD. Defaults to SM_View - if not provided. - :param description: An optional description of the policy. - :param draft: Indicates if the policy is a draft. Defaults to `False`. - :param active: Indicates if the policy is active. Defaults to `True`. - :param read_only: Indicates if the policy is a read only. Defaults to `False`. - :param users: Optional list of user names or ids to which this policy applies. - :param tags: Optional list of tag names or ids to which this policy applies. - :param tokens: Optional list of token names or ids to which this policy applies. - :param service_identities: Optional list of service identity names or ids to which this policy applies. - :param ips: Optional list of IP addresses for which this policy applies. Provide in CIDR notation - or dotted decimal format for individual (/32) IP addresses. - :param date_schedule: A dict in the format - { - 'fromDate': '2022-10-29 10:30:00', - 'toDate': '2022-11-05 18:30:00', - 'timezone': 'UTC' - } - Timezone formats can be found in the TZ Identifier column of the following page. - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - :param days_schedule: A dict in the format - { - 'fromTime': '10:30:00', - 'toTime': '18:30:00', - 'timezone': 'UTC', - 'days': ['MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY','SUNDAY'] - } - Timezone formats can be found in the TZ Identifier column of the following page. - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - :param approval_notification_medium: Optional notification medium name to which approval requests will be - delivered. Can also specify a list of notification medium names. Specifying this parameter indicates the - desire to enable approvals for this policy. - :param time_to_approve: Optional number of minutes to wait for an approval before denying the action. Defaults - to 5 minutes. - :param access_validity_time: Optional number of minutes the access is valid after approval. Defaults to 120 - minutes. - :param approver_users: Optional list of user names or ids who are to be considered approvers. - If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required. - :param approver_tags: Optional list of tag names who are considered approvers. - If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required. - :param access_type: The type of access this policy provides. Valid values are `Allow` and `Deny`. Defaults - to `Allow`. - :param identifier_type: Valid values are `id` or `name`. Defaults to `name`. Represents which type of - identifiers are being provided to the other parameters. Either all identifiers must be names or all - identifiers must be IDs. - :param condition_as_dict: Prior to version 2.22.0 the only acceptable format for the condition block of - a policy was as a stringifed json object. As of 2.22.0 the condition block can also be built as a raw - python dictionary. This parameter will default to `False` to support backwards compatibility. Setting to - `True` will result in the policy condition being returned/built as a python dictionary. - :return: A dict which can be provided as a secret manager policy to `create` and `update`. - """ - - policy = self.britive.system.policies.build( - name=name, - description=description, - draft=draft, - active=active, - read_only=read_only, - users=users, - tags=tags, - tokens=tokens, - service_identities=service_identities, - ips=ips, - date_schedule=date_schedule, - days_schedule=days_schedule, - approval_notification_medium=approval_notification_medium, - time_to_approve=time_to_approve, - access_validity_time=access_validity_time, - approver_users=approver_users, - approver_tags=approver_tags, - access_type=access_type, - identifier_type=identifier_type, - condition_as_dict=condition_as_dict, - ) - - policy.pop('permissions', None) - policy.pop('roles', None) - policy['accessLevel'] = access_level or 'SM_View' - policy['consumer'] = 'secretmanager' - return policy - - def create(self, policy: dict, path: str) -> dict: - """ - Creates a policy in the vault. - - :param policy: policy to create - :param path: path of the policy, include the / at the beginning - :return: Details of the policy. - """ - - policy['resource'] = path - - return self.britive.post(f'{self.base_url}?resource={path}&consumer=secretmanager', json=policy) - - -class StaticSecretTemplates: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/secretmanager/secret-templates/static' - - def get(self, secret_template_id: str) -> dict: - """ - Gets a secret template from the vault. - - :param secret_template_id: ID of the secret template to get - :return: Details of the secret template. - """ - - return self.britive.get(f'{self.base_url}/{secret_template_id}') - - def list(self, filter_str: str = None) -> dict: - """ - Lists all secret templates in the vault. - - :param filter_str: filter to apply to the listing - :return: Details of the secret templates. - """ - - params = {'filter': filter_str} - return self.britive.get(f'{self.base_url}', params=params) - - def delete(self, secret_template_id: str) -> None: - """ - Deletes a secret template from the vault. - - :param secret_template_id: ID of the secret template to delete - :return: None - """ - - return self.britive.delete(f'{self.base_url}/{secret_template_id}') - - def create( - self, - name: str, - password_policy_id: str, - description: str = '', - rotation_interval: int = 30, - parameters: list = None, - ) -> dict: - """ - Creates a secret template - - :param name: name of the secret template - :param password_policy_id: ID of the password policy to use - :param description: description of the secret template - :param rotation_interval: rotation interval of the secret template - :param parameters: list of parameters to use in the secret template - :return: Details of the secret template. - """ - - params = { - 'secretType': name, - 'passwordPolicyId': password_policy_id, - 'description': description, - 'rotationInterval': rotation_interval, - 'parameters': [parameters], - } - - return self.britive.post(f'{self.base_url}', json=params) - - def update(self, static_secret_template_id: str, **kwargs) -> None: - """ - Updates a secret template - - :param static_secret_template_id: ID of the secret template to update - :param kwargs: key-value pairs to update the secret template with - valid keys are: - name: name of the secret template - passwordPolicyId: ID of the password policy to use - description: description of the secret template - rotationInterval: rotation interval of the secret template - parameters: list of parameters to use in the secret template - :return: None - """ - - current = self.get(secret_template_id=static_secret_template_id) - - return self.britive.patch(f'{self.base_url}/{static_secret_template_id}', json={**current, **kwargs}) - - -class Resources: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/v1/secretmanager/resourceContainers' - - def get(self, path: str = '/') -> dict: - """ - Gets a resource from the vault - - :param path: path of the resource, include the / at the beginning - :return: Details of the resource. - """ - - params = {'path': path} - return self.britive.get(f'{self.base_url}', params=params) diff --git a/src/britive/secrets_manager/__init__.py b/src/britive/secrets_manager/__init__.py new file mode 100644 index 0000000..733a0d2 --- /dev/null +++ b/src/britive/secrets_manager/__init__.py @@ -0,0 +1,17 @@ +from .folders import Folders +from .policies import PasswordPolicies, Policies +from .resources import Resources +from .secrets import Secrets +from .templates import StaticSecretTemplates +from .vaults import Vaults + + +class SecretsManager: + def __init__(self, britive) -> None: + self.vaults = Vaults(britive) + self.password_policies = PasswordPolicies(britive) + self.secrets = Secrets(britive) + self.policies = Policies(britive) + self.static_secret_templates = StaticSecretTemplates(britive) + self.resources = Resources(britive) + self.folders = Folders(britive) diff --git a/src/britive/secrets_manager/folders.py b/src/britive/secrets_manager/folders.py new file mode 100644 index 0000000..873835b --- /dev/null +++ b/src/britive/secrets_manager/folders.py @@ -0,0 +1,28 @@ +class Folders: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/secretmanager/vault' + + def create(self, name: str, vault_id: str, path: str = '/') -> dict: + """ + Creates a new folder in the vault. + + :param name: The name of the folder. + :param vault_id: The ID of the vault. + :param path: The path of the folder (include the leading /). + :return: Details of the newly created folder. + """ + + data = {'entityType': 'node', 'name': name} + return self.britive.post(f'{self.base_url}/{vault_id}/secrets?path={path}', json=data) + + def delete(self, vault_id: str, path: str) -> None: + """ + Deletes a folder from the vault. + + :param vault_id: ID of the vault to delete the folder from + :param path: path of the folder, include the / at the beginning + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{vault_id}/secrets?path={path}') diff --git a/src/britive/secrets_manager/policies.py b/src/britive/secrets_manager/policies.py new file mode 100644 index 0000000..c3e4abb --- /dev/null +++ b/src/britive/secrets_manager/policies.py @@ -0,0 +1,304 @@ +from typing import Union + + +class PasswordPolicies: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/secretmanager/pwdpolicies' + + def get(self, password_policy_id: str) -> dict: + """ + Provide details of the given password policy, from a password policy id. + + :param password_policy_id: The ID of the password policy. + :return: Details of the specified password policy. + """ + + return self.britive.get(f'{self.base_url}/{password_policy_id}') + + def list(self, filter_str: str = None) -> list: + """ + Provide a list of all password policies + + :param filter_str: filter to apply to the listing + :return: List of all password policies + """ + + params = {} + if filter_str: + params['filter'] = filter_str + + return self.britive.get(self.base_url, params=params) + + def create( + self, + name: str, + description: str = 'Default description', + password_type: str = 'alphanumeric', + min_password_length: int = 8, + has_upper_case_chars: bool = True, + has_lower_case_chars: bool = True, + has_numbers: bool = True, + has_special_chars: bool = True, + allowed_special_chars: str = '~`!@#$%^&*()-_+=[]{}|/;:"?/\\.><,\'', + ) -> dict: + """ + Creates a new password policy. + + :param name: required, name of the password policy + :param description: description of the password policy + :param password_type: type of password to use for the policy + :param min_password_length: minimum length of the password + :param has_upper_case_chars: whether to require uppercase characters + :param has_lower_case_chars: whether to require lowercase characters + :param has_numbers: whether to require numbers + :param has_special_chars: whether to require special characters + :param allowed_special_chars: a string of special characters to allow in the password + :return: Details of the newly created password policy. + """ + + params = { + 'name': name, + 'description': description, + 'passwordType': password_type, + 'minPasswordLength': min_password_length, + 'hasUpperCaseChars': has_upper_case_chars, + 'hasLowerCaseChars': has_lower_case_chars, + 'hasNumbers': has_numbers, + 'hasSpecialChars': has_special_chars, + 'allowedSpecialChars': allowed_special_chars, + } + return self.britive.post(self.base_url, json=params) + + def create_pin(self, name: str, description: str = 'Default description', pin_length: int = 4) -> dict: + """ + Creates a new pin password policy. + + :param name: required, name of the pin password policy + :param description: description of the pin password policy + :param pin_length: length of the pin to use for the policy + :return: Details of the newly created pin password policy. + """ + + params = { + 'name': name, + 'description': description, + 'pinLength': pin_length, + 'passwordType': 'pin', + } + return self.britive.post(self.base_url, json=params) + + def update(self, password_policy_id: str, **kwargs) -> None: + """ + Updates a password policy. + + :param password_policy_id: the ID of the password policy + :param kwargs: Valid fields are... + name: name of the password policy + description: description of the password policy + passwordType: type of password to use for the policy + minPasswordLength: minimum length of the password + hasUpperCaseChars: whether to require uppercase characters + hasLowercaseChars: whether to require lowercase characters + hasNumbers: whether to require numbers + hasSpecialChars: whether to require special characters + allowedSpecialChars: a string of special characters to allow in the password + pinLength: the length of the pin to use for the policy (only for pins) + :return: None + """ + + current = self.get(password_policy_id=password_policy_id) + + return self.britive.patch(f'{self.base_url}/{password_policy_id}', json={**current, **kwargs}) + + def delete(self, password_policy_id: str) -> None: + """ + Deletes a password policy. + + :param password_policy_id: the ID of the password policy + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{password_policy_id}') + + def generate_password(self, password_policy_id: str) -> dict: + """ + Generates a password for the given password policy. + + :param password_policy_id: the ID of the password policy + :return: the generated password + """ + + params = {'action': 'generatePasswordOrPin'} + return self.britive.get(f'{self.base_url}/{password_policy_id}', params=params)['passwordOrPin'] + + def validate(self, password_policy_id: str, password: str) -> dict: + """ + Validates a password for the given password policy. + + :param password_policy_id: the ID of the password policy + :param password: the password to validate + :return: whether the password is valid + """ + + data = { + 'id': password_policy_id, + 'passwordOrPin': password, + } + + params = {'action': 'validatePasswordOrPin'} + return self.britive.post(self.base_url, json=data, params=params) + + +class Policies: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/policy-admin/policies' + + def list(self, path: str = '/', filter_str: str = None) -> dict: + """ + Gets all policies in the vault. + + :param path: path of the policy, include the / at the beginning + :param filter_str: filter to apply to the listing + :return: Details of the policies. + """ + + params = {'resource': path, 'consumer': 'secretmanager'} + if filter_str: + params['filter'] = filter_str + return self.britive.get(f'{self.base_url}', params=params) + + def delete(self, policy_id: str, path: str = '/') -> None: + """ + Deletes a policy from the vault. + + :param policy_id: ID of the policy to delete + :param path: path of the policy, include the / at the beginning + :return: None + """ + + params = {'consumer': 'secretmanager', 'resource': path} + return self.britive.delete(f'{self.base_url}/{policy_id}', params=params) + + def build( # noqa: PLR0913 + self, + name: str, + access_level: str = None, + description: str = '', + draft: bool = False, + active: bool = True, + read_only: bool = False, + users: list = None, + tags: list = None, + tokens: list = None, + service_identities: list = None, + ips: list = None, + date_schedule: dict = None, + days_schedule: dict = None, + approval_notification_medium: Union[str, list] = None, + time_to_approve: int = 5, + access_validity_time: int = 120, + approver_users: list = None, + approver_tags: list = None, + access_type: str = 'Allow', + identifier_type: str = 'name', + condition_as_dict: bool = False, + ) -> dict: + """ + Build a policy document given the provided inputs. + + :param name: The name of the policy. + :param access_level: The level of access. Valid values are SM_View, SM_Manage, SM_CRUD. Defaults to SM_View + if not provided. + :param description: An optional description of the policy. + :param draft: Indicates if the policy is a draft. Defaults to `False`. + :param active: Indicates if the policy is active. Defaults to `True`. + :param read_only: Indicates if the policy is a read only. Defaults to `False`. + :param users: Optional list of user names or ids to which this policy applies. + :param tags: Optional list of tag names or ids to which this policy applies. + :param tokens: Optional list of token names or ids to which this policy applies. + :param service_identities: Optional list of service identity names or ids to which this policy applies. + :param ips: Optional list of IP addresses for which this policy applies. Provide in CIDR notation + or dotted decimal format for individual (/32) IP addresses. + :param date_schedule: A dict in the format + { + 'fromDate': '2022-10-29 10:30:00', + 'toDate': '2022-11-05 18:30:00', + 'timezone': 'UTC' + } + Timezone formats can be found in the TZ Identifier column of the following page. + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + :param days_schedule: A dict in the format + { + 'fromTime': '10:30:00', + 'toTime': '18:30:00', + 'timezone': 'UTC', + 'days': ['MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY','SUNDAY'] + } + Timezone formats can be found in the TZ Identifier column of the following page. + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + :param approval_notification_medium: Optional notification medium name to which approval requests will be + delivered. Can also specify a list of notification medium names. Specifying this parameter indicates the + desire to enable approvals for this policy. + :param time_to_approve: Optional number of minutes to wait for an approval before denying the action. Defaults + to 5 minutes. + :param access_validity_time: Optional number of minutes the access is valid after approval. Defaults to 120 + minutes. + :param approver_users: Optional list of user names or ids who are to be considered approvers. + If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required. + :param approver_tags: Optional list of tag names who are considered approvers. + If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required. + :param access_type: The type of access this policy provides. Valid values are `Allow` and `Deny`. Defaults + to `Allow`. + :param identifier_type: Valid values are `id` or `name`. Defaults to `name`. Represents which type of + identifiers are being provided to the other parameters. Either all identifiers must be names or all + identifiers must be IDs. + :param condition_as_dict: Prior to version 2.22.0 the only acceptable format for the condition block of + a policy was as a stringifed json object. As of 2.22.0 the condition block can also be built as a raw + python dictionary. This parameter will default to `False` to support backwards compatibility. Setting to + `True` will result in the policy condition being returned/built as a python dictionary. + :return: A dict which can be provided as a secret manager policy to `create` and `update`. + """ + + policy = self.britive.system.policies.build( + name=name, + description=description, + draft=draft, + active=active, + read_only=read_only, + users=users, + tags=tags, + tokens=tokens, + service_identities=service_identities, + ips=ips, + date_schedule=date_schedule, + days_schedule=days_schedule, + approval_notification_medium=approval_notification_medium, + time_to_approve=time_to_approve, + access_validity_time=access_validity_time, + approver_users=approver_users, + approver_tags=approver_tags, + access_type=access_type, + identifier_type=identifier_type, + condition_as_dict=condition_as_dict, + ) + + policy.pop('permissions', None) + policy.pop('roles', None) + policy['accessLevel'] = access_level or 'SM_View' + policy['consumer'] = 'secretmanager' + return policy + + def create(self, policy: dict, path: str) -> dict: + """ + Creates a policy in the vault. + + :param policy: policy to create + :param path: path of the policy, include the / at the beginning + :return: Details of the policy. + """ + + policy['resource'] = path + + return self.britive.post(f'{self.base_url}?resource={path}&consumer=secretmanager', json=policy) diff --git a/src/britive/secrets_manager/resources.py b/src/britive/secrets_manager/resources.py new file mode 100644 index 0000000..30df6c6 --- /dev/null +++ b/src/britive/secrets_manager/resources.py @@ -0,0 +1,15 @@ +class Resources: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/secretmanager/resourceContainers' + + def get(self, path: str = '/') -> dict: + """ + Gets a resource from the vault + + :param path: path of the resource, include the / at the beginning + :return: Details of the resource. + """ + + params = {'path': path} + return self.britive.get(f'{self.base_url}', params=params) diff --git a/src/britive/secrets_manager/secrets.py b/src/britive/secrets_manager/secrets.py new file mode 100644 index 0000000..252355b --- /dev/null +++ b/src/britive/secrets_manager/secrets.py @@ -0,0 +1,143 @@ +import json + + +class Secrets: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/secretmanager/vault' + + def create( + self, + name: str, + vault_id: str, + path: str = '/', + static_secret_template_id: str = '7a5f41d8-f7af-46a0-88f7-edf0403607ae', + secret_mode: str = 'shared', + secret_nature: str = 'static', + value: dict = None, + file: bytes = None, + ) -> dict: + """ + Creates a new secret in the vault. + + For creating a secret with a file, you must read the file in with: + with open(file_path, 'rb') as f: + britive.secrets_manager.secrets.create(...., file=f) + + + :param name: name of the secret + :param vault_id: ID of the vault + :param path: path of the secret, include the / the beginning + :param static_secret_template_id: ID of the static secret template + :param secret_mode: mode of the secret + :param secret_nature: nature of the secret + :param value: value of the secret (must be in the format of the static secret template) + :param file: file to upload as the secret + :return: Details of the newly created secret. + """ + if value is None: + value = {'Note': 'This is the default note'} + + if not file: + return self.britive.post( + f'{self.base_url}/{vault_id}/secrets?path={path}', + json={ + 'name': name, + 'entityType': 'secret', + 'staticSecretTemplateId': static_secret_template_id, + 'secretMode': secret_mode, + 'secretNature': secret_nature, + 'value': value, + }, + ) + secret_data = { + 'entityType': 'secret', + 'name': name, + 'staticSecretTemplateId': static_secret_template_id, + 'secretMode': secret_mode, + 'secretNature': secret_nature, + 'value': value, + } + return self.britive.post_upload( + f'{self.base_url}/{vault_id}/secrets/file?path={path}', + files={'file': file, 'secretData': (None, json.dumps(secret_data))}, + ) + + def update(self, vault_id: str, path: str = '/', value: dict = None) -> None: + """ + Updates a secret's value + + :param vault_id: ID of the vault to update the secret in + :param path: path of the secret, include the / at the beginning + :param value: value of the secret + :return: None + """ + if value is None: + value = {} + + return self.britive.patch(f'{self.base_url}/{vault_id}/secrets?path={path}', json={'value': value}) + + def rename(self, vault_id: str, path: str = '/', new_name: str = '') -> None: + """ + Update the name of a secret. + + :param vault_id: ID of the vault to update the secret in + :param path: path of the secret, include the / at the beginning and the secret name + :param new_name: new name of the secret + :return: None + """ + + return self.britive.patch(f'{self.base_url}/{vault_id}/secrets?path={path}', json={'name': new_name}) + + def get( + self, + vault_id: str, + path: str, + secret_type: str = 'node', + filter_type: str = None, + recursive_secrets: bool = False, + get_metadata: bool = True, + ) -> dict: + """ + Gets a secret from the vault. + + :param vault_id: ID of the vault to get the secret from + :param path: path of the secret, include the / at the beginning + :param secret_type: type of the secret (node or secret) + :param filter_type: filter to apply to the secret (NONE, ALL, SHARED, PRIVATE) + :param recursive_secrets: whether or not to recursively get all secrets in the folder + :param get_metadata: whether or not to get the metadata of the secret + :return: Details of the secret. + """ + + params = { + 'type': secret_type, + 'filter': filter_type, + 'recursiveSecrets': (str(recursive_secrets)).lower(), + 'getMetadata': get_metadata, + } + return self.britive.get(f'{self.base_url}/{vault_id}/secrets?path={path}', params=params) + + def delete(self, vault_id: str, path: str) -> None: + """ + Deletes a secret from the vault. + + :param vault_id: ID of the vault to delete the secret from + :param path: path of the secret, include the / at the beginning + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{vault_id}/secrets?path={path}') + + def access(self, vault_id: str, path: str, get_metadata: bool = False) -> dict: + """ + Accesses a secret from the vault. + + :param vault_id: ID of the vault to get the secret from + :param path: path of the secret, include the / at the beginning + :param get_metadata: whether or not to get the metadata of the secret + :return: Details of the secret. + """ + + params = {'getmetadata': get_metadata} + return self.britive.get(f'{self.base_url}/{vault_id}/secrets?path={path}', params=params) diff --git a/src/britive/secrets_manager/templates.py b/src/britive/secrets_manager/templates.py new file mode 100644 index 0000000..977088b --- /dev/null +++ b/src/britive/secrets_manager/templates.py @@ -0,0 +1,83 @@ +class StaticSecretTemplates: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/secretmanager/secret-templates/static' + + def get(self, secret_template_id: str) -> dict: + """ + Gets a secret template from the vault. + + :param secret_template_id: ID of the secret template to get + :return: Details of the secret template. + """ + + return self.britive.get(f'{self.base_url}/{secret_template_id}') + + def list(self, filter_str: str = None) -> dict: + """ + Lists all secret templates in the vault. + + :param filter_str: filter to apply to the listing + :return: Details of the secret templates. + """ + + params = {'filter': filter_str} + return self.britive.get(f'{self.base_url}', params=params) + + def delete(self, secret_template_id: str) -> None: + """ + Deletes a secret template from the vault. + + :param secret_template_id: ID of the secret template to delete + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{secret_template_id}') + + def create( + self, + name: str, + password_policy_id: str, + description: str = '', + rotation_interval: int = 30, + parameters: list = None, + ) -> dict: + """ + Creates a secret template + + :param name: name of the secret template + :param password_policy_id: ID of the password policy to use + :param description: description of the secret template + :param rotation_interval: rotation interval of the secret template + :param parameters: list of parameters to use in the secret template + :return: Details of the secret template. + """ + + params = { + 'secretType': name, + 'passwordPolicyId': password_policy_id, + 'description': description, + 'rotationInterval': rotation_interval, + 'parameters': [parameters], + } + + return self.britive.post(f'{self.base_url}', json=params) + + def update(self, static_secret_template_id: str, **kwargs) -> None: + """ + Updates a secret template + + :param static_secret_template_id: ID of the secret template to update + :param kwargs: key-value pairs to update the secret template with + valid keys are: + name: name of the secret template + passwordPolicyId: ID of the password policy to use + description: description of the secret template + rotationInterval: rotation interval of the secret template + parameters: list of parameters to use in the secret template + :return: None + """ + + current = self.get(secret_template_id=static_secret_template_id) + + return self.britive.patch(f'{self.base_url}/{static_secret_template_id}', json={**current, **kwargs}) diff --git a/src/britive/secrets_manager/vaults.py b/src/britive/secrets_manager/vaults.py new file mode 100644 index 0000000..7a14582 --- /dev/null +++ b/src/britive/secrets_manager/vaults.py @@ -0,0 +1,108 @@ +class Vaults: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/secretmanager/vault' + + def list(self) -> list: + """ + Provide a list of all vaults. + + :return: List of all vaults + """ + + params = {'getmetadata': 'true'} + return self.britive.get(self.base_url, params=params) + + def get_vault_by_id(self, vault_id: str) -> dict: + """ + Provide details of the given vault, from a vault id. + + :param vault_id: The ID of the vault. + :return: Details of the specified vault. + """ + + return self.britive.get(f'{self.base_url}/{vault_id}') + + def create( + self, + name: str, + description: str = 'Default vault description', + rotation_time: int = 30, + encryption_algorithm: str = 'AES_256', + default_notification_medium_id: str = '', + users: list = None, + tags: list = None, + channels: list = None, + ) -> dict: + """ + Create a new vault. + + :param name: the name of the vault + :param description: the description of the vault + :param rotation_time: in hours, how often the vault should rotate keys + :param encryption_algorithm : the encryption algorithm to use for the vault + :param default_notification_medium_id : the default notification medium to use for the vault + :param users: a list of user IDs to recieve notifications for the vault + :param tags: a list of tags to recieve notifications for the vault + :param channels : a list of channels to recieve notifications for the vault (only for slack) + :return: Details of the newly created vault. + """ + + if users is None: + users = [] + if tags is None: + tags = [] + if channels is None: + channels = [] + + if default_notification_medium_id == '': + for medium in self.britive.global_settings.notification_mediums.list(): + if medium['name'] == 'Email': + default_notification_medium_id = medium['id'] + params = { + 'name': name, + 'description': description, + 'rotationTime': rotation_time, + 'encryptionAlgorithm': encryption_algorithm, + 'defaultNotificationMediumId': default_notification_medium_id, + 'recipients': {'userIds': users, 'tags': tags, 'channelIds': channels}, + } + return self.britive.post(self.base_url, json=params) + + def delete(self, vault_id: str) -> None: + """ + Deletes a vault. + + :param vault_id: the ID of the vault + :return: None + """ + + return self.britive.delete(f'{self.base_url}/{vault_id}') + + def update(self, vault_id: str, **kwargs) -> None: + """ + Updates a vault. + + If not all kwargs a provided, the vault will update with the default values of the unprovided kwargs. + + :param vault_id: The ID of the vault. + :param kwargs: Valid fields are... + name - required + description + rotationTime - time in days between key rotations + encryptionAlgorithm - the encryption algorithm to use for the vault + defaultNotificationMediumId - the default notification medium to use for the vault + recipients - a list of user IDs or tags to recieve notifications for the vault + :return: None + """ + + return self.britive.patch(f'{self.base_url}/{vault_id}', json=kwargs) + + def rotate_keys(self) -> None: + """ + Rotate vault keys. + + :return: None + """ + + return self.britive.post(f'{self.britive.base_url}/v1/secretmanager/keys/rotate') diff --git a/src/britive/security/__init__.py b/src/britive/security/__init__.py new file mode 100644 index 0000000..6348ae4 --- /dev/null +++ b/src/britive/security/__init__.py @@ -0,0 +1,12 @@ +from .api_tokens import ApiTokens +from .policies import SecurityPolicies +from .saml import Saml +from .step_up import StepUpAuth + + +class Security: + def __init__(self, britive) -> None: + self.api_tokens = ApiTokens(britive) + self.saml = Saml(britive) + self.security_policies = SecurityPolicies(britive) + self.step_up_auth = StepUpAuth(britive) diff --git a/src/britive/api_tokens.py b/src/britive/security/api_tokens.py similarity index 95% rename from src/britive/api_tokens.py rename to src/britive/security/api_tokens.py index 9fef44b..2132820 100644 --- a/src/britive/api_tokens.py +++ b/src/britive/security/api_tokens.py @@ -1,4 +1,4 @@ -from . import exceptions +from britive.exceptions import ApiTokenNotFound class ApiTokens: @@ -19,7 +19,7 @@ def get(self, token_id: str) -> dict: for token in self.list(): if token['id'] == token_id: return token - raise exceptions.ApiTokenNotFound() + raise ApiTokenNotFound def create(self, name: str = None, expiration_days: int = 90) -> dict: """ @@ -52,10 +52,11 @@ def revoke(self, token_id: str) -> None: """ response = self.britive.delete(f'{self.base_url}/{token_id}') + if response == 'Successfully revoked token': - return None - else: - raise Exception(str(response)) + return + + raise Exception(str(response)) def delete(self, token_id: str) -> None: """ diff --git a/src/britive/security_policies.py b/src/britive/security/policies.py similarity index 100% rename from src/britive/security_policies.py rename to src/britive/security/policies.py diff --git a/src/britive/saml.py b/src/britive/security/saml.py similarity index 100% rename from src/britive/saml.py rename to src/britive/security/saml.py diff --git a/src/britive/step_up.py b/src/britive/security/step_up.py similarity index 100% rename from src/britive/step_up.py rename to src/britive/security/step_up.py diff --git a/src/britive/service_identity_tokens.py b/src/britive/service_identity_tokens.py deleted file mode 100644 index dc8a2ef..0000000 --- a/src/britive/service_identity_tokens.py +++ /dev/null @@ -1,55 +0,0 @@ -def validate_token_expiration(days) -> None: - if not (1 <= days <= 90): - raise ValueError('invalid token expiration value - must ust be between 1 and 90') - - -class ServiceIdentityTokens: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}' - - def create(self, service_identity_id: str, token_expiration_days: int = 90) -> dict: - """ - Create a token for a given service identity. - - The token has the same privileges assigned to the service identity. When this token is created, the old token - associated with the identity provider will be removed. A service identity can have only one token at any given - time. The token that is generated will be returned only once. - - :param service_identity_id: The ID of the service identity. - :param token_expiration_days: The number of days in which token would expire since it was last used. - The token expiration days can be any value between 1 day and 90 days. - :return: - """ - - validate_token_expiration(token_expiration_days) - - data = {'tokenExpirationDays': token_expiration_days} - - return self.britive.post(f'{self.base_url}/users/{service_identity_id}/tokens', json=data) - - def update(self, service_identity_id: str, token_expiration_days: int = 90) -> None: - """ - Update the token expiration days for an existing service identity token. - - :param service_identity_id: The ID of the service identity. - :param token_expiration_days: The number of days in which token would expire since it was last used. - The token expiration days can be any value between 1 day and 90 days. - :return: None - """ - - validate_token_expiration(token_expiration_days) - - data = {'tokenExpirationDays': token_expiration_days} - - return self.britive.post(f'{self.base_url}/users/{service_identity_id}/tokens', json=data) - - def get(self, service_identity_id: str) -> dict: - """ - Return details of the token associated with the service identity. Only one token can exist per service identity. - - :param service_identity_id: The ID of the service identity. - :return: Details of the service identity token - """ - - return self.britive.get(f'{self.base_url}/users/{service_identity_id}/tokens') diff --git a/src/britive/settings/__init__.py b/src/britive/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/britive/settings/settings.py b/src/britive/settings/settings.py deleted file mode 100644 index 7568a53..0000000 --- a/src/britive/settings/settings.py +++ /dev/null @@ -1,10 +0,0 @@ -from .banner import SettingsBanner - -# this class is just a logical grouping construct - - -class Settings: - def __init__(self, britive) -> None: - self.britive = britive - self.banner = SettingsBanner(britive) - diff --git a/src/britive/system/__init__.py b/src/britive/system/__init__.py index e69de29..fb5a08f 100644 --- a/src/britive/system/__init__.py +++ b/src/britive/system/__init__.py @@ -0,0 +1,14 @@ +from .actions import SystemActions +from .consumers import SystemConsumers +from .permissions import SystemPermissions +from .policies import SystemPolicies +from .roles import SystemRoles + + +class System: + def __init__(self, britive) -> None: + self.roles = SystemRoles(britive) + self.policies = SystemPolicies(britive) + self.permissions = SystemPermissions(britive) + self.consumers = SystemConsumers(britive) + self.actions = SystemActions(britive) diff --git a/src/britive/system/permissions.py b/src/britive/system/permissions.py index 25aa816..a2911e3 100644 --- a/src/britive/system/permissions.py +++ b/src/britive/system/permissions.py @@ -61,11 +61,11 @@ def update(self, permission_identifier: str, permission: dict, identifier_type: self._validate_identifier_type(identifier_type) - if permission.get('isInline'): + if permission.pop('isInline', False): # InvalidRequest: 400 - PA-0059 - isInline is not allowed to update raise ValueError('attribute isInline is set to True - cannot update an inline permission.') - permission.pop('isInline', None) # InvalidRequest: 400 - PA-0059 - isInline is not allowed to update permission.pop('isReadOnly', None) + return self.britive.patch(f'{self.base_url}/{permission_identifier}', json=permission) def delete(self, permission_identifier: str, identifier_type: str = 'name') -> None: @@ -109,7 +109,7 @@ def build( resources = [] # put it all together - permission = { + return { 'name': name, 'description': description, 'isReadOnly': read_only, @@ -118,5 +118,3 @@ def build( 'actions': actions, 'resources': resources if len(resources) > 0 else ['*'], } - - return permission diff --git a/src/britive/system/policies.py b/src/britive/system/policies.py index e62a3eb..4233b74 100644 --- a/src/britive/system/policies.py +++ b/src/britive/system/policies.py @@ -268,8 +268,6 @@ def build( # noqa: PLR0913 prompt = 'true' if always_prompt_stepup_auth else 'false' step_up_condition = {'factor': 'TOTP', 'alwaysPrompt': prompt} condition['stepUpCondition'] = step_up_condition - # else: - # condition = {} # put it all together policy = { diff --git a/src/britive/system/roles.py b/src/britive/system/roles.py index 0e8e3ea..c0d6aed 100644 --- a/src/britive/system/roles.py +++ b/src/britive/system/roles.py @@ -92,11 +92,9 @@ def build( """ # put it all together - role = { + return { 'name': name, 'description': description, 'isReadOnly': read_only, 'permissions': [{identifier_type: p} for p in permissions], } - - return role diff --git a/src/britive/system/system.py b/src/britive/system/system.py deleted file mode 100644 index b23a3ea..0000000 --- a/src/britive/system/system.py +++ /dev/null @@ -1,17 +0,0 @@ -from .actions import SystemActions -from .consumers import SystemConsumers -from .permissions import SystemPermissions -from .policies import SystemPolicies -from .roles import SystemRoles - -# this class is just a logical grouping construct - - -class System: - def __init__(self, britive) -> None: - self.britive = britive - self.roles = SystemRoles(britive) - self.policies = SystemPolicies(britive) - self.permissions = SystemPermissions(britive) - self.consumers = SystemConsumers(britive) - self.actions = SystemActions(britive) diff --git a/src/britive/workflows/__init__.py b/src/britive/workflows/__init__.py new file mode 100644 index 0000000..8a13e68 --- /dev/null +++ b/src/britive/workflows/__init__.py @@ -0,0 +1,10 @@ +from .notifications import Notifications +from .task_services import TaskServices +from .tasks import Tasks + + +class Workflows: + def __init__(self, britive) -> None: + self.notifications = Notifications(britive) + self.task_services = TaskServices(britive) + self.tasks = Tasks(britive) diff --git a/src/britive/notifications.py b/src/britive/workflows/notifications.py similarity index 90% rename from src/britive/notifications.py rename to src/britive/workflows/notifications.py index a934336..984bd19 100644 --- a/src/britive/notifications.py +++ b/src/britive/workflows/notifications.py @@ -107,15 +107,16 @@ def configure( For all optional parameters omitting the parameter will leave the value unchanged. :param notification_id: The ID of the notification. - :param rules: List of rules to apply. Obtain rule options from `britive.notifications.available_rules()` and - use results from that API call to populate this list. Maximum of 3 rules are allowed. + :param rules: List of rules to apply. + Obtain rule options from `britive.workflows.notifications.available_rules()` and use results from that API + call to populate this list. Maximum of 3 rules are allowed. :param users: List of user ids to apply. This is the list of users who will be notified if any of the rules are triggered. An empty list means that no users will be notified. :param user_tags: List of user tag ids to apply. This is the list of user tags who will be notified if any of the rules are triggered. An empty list means that no user tags will be notified. - :param applications: List of applications to which this notification applies. Obtain applications options from - `britive.notifications.available_applications()` and use results from that API call to populate this list. - An empty list indicates the event applies to all applications. + :param applications: List of applications to which this notification applies. + Obtain applications options from `britive.workflows.notifications.available_applications()` and use results + from that API call to populate this list. An empty list indicates the event applies to all applications. :param send_no_changes: Boolean indicating whether to send notification regardless of whether any changes have occurred or not. :param notification_medium_id: The ID of the notification medium to use for this notification. @@ -130,10 +131,10 @@ def configure( data = self.get(notification_id=notification_id) members = [] - for user in self.britive.users.minimized_user_details(user_ids=users): + for user in self.britive.identity_management.users.minimized_user_details(user_ids=users): members.append({'id': user['id'], 'memberType': 'User', 'name': user['username'], 'condition': None}) - for tag in self.britive.tags.minimized_tag_details(tag_ids=user_tags): + for tag in self.britive.identity_management.tags.minimized_tag_details(tag_ids=user_tags): members.append({'id': tag['userTagId'], 'memberType': 'Tag', 'name': tag['name'], 'condition': None}) # set the possible parameters diff --git a/src/britive/task_services.py b/src/britive/workflows/task_services.py similarity index 100% rename from src/britive/task_services.py rename to src/britive/workflows/task_services.py diff --git a/src/britive/tasks.py b/src/britive/workflows/tasks.py similarity index 93% rename from src/britive/tasks.py rename to src/britive/workflows/tasks.py index 0b7a75e..4ea595d 100644 --- a/src/britive/tasks.py +++ b/src/britive/workflows/tasks.py @@ -7,7 +7,7 @@ def list(self, task_service_id: str) -> list: """ Return a list of tasks for the given `task_service_id`. - Make a call to `britive.task_services.get()` to obtain the appropriate `task_service_id`. + Make a call to `britive.workflows.task_services.get()` to obtain the appropriate `task_service_id`. :param task_service_id: The ID of the task service. :return: List of tasks. @@ -19,7 +19,7 @@ def get(self, task_service_id: str, task_id: str) -> dict: """ Return details of a task. - Make a call to `britive.task_services.get()` to obtain the appropriate `task_service_id`. + Make a call to `britive.workflows.task_services.get()` to obtain the appropriate `task_service_id`. :param task_service_id: The ID of the task service. :param task_id: The ID of the task. @@ -40,7 +40,7 @@ def create( """ Create a new task. - Make a call to `britive.task_services.get()` to obtain the appropriate `task_service_id`. + Make a call to `britive.workflows.task_services.get()` to obtain the appropriate `task_service_id`. :param task_service_id: The ID of the task service. :param name: The name of the task. @@ -105,7 +105,7 @@ def update( Only provide parameters that should be updated. - Make a call to `britive.task_services.get()` to obtain the appropriate `task_service_id`. + Make a call to `britive.workflows.task_services.get()` to obtain the appropriate `task_service_id`. :param task_service_id: The ID of the task service. :param task_id: The ID of the task. diff --git a/src/britive/workload.py b/src/britive/workload.py deleted file mode 100644 index 79d7b64..0000000 --- a/src/britive/workload.py +++ /dev/null @@ -1,432 +0,0 @@ -from typing import Union - - -class Workload: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/workload' - self.identity_providers = self.IdentityProviders(self) - self.service_identities = self.ServiceIdentities(self) - self.scim_user = self.ScimUser(self) - - class IdentityProviders: - def __init__(self, workload) -> None: - self.britive = workload.britive - self.base_url = f'{workload.base_url}/identity-providers' - - def list(self, idp_type: str = None) -> list: - """ - Return a list of all workload identity providers. - - :param idp_type: Optional filter to apply to reduce the results to a specific workload identity provider - type. Valid values are `AWS` and `OIDC`. - :returns: List of all workload identity providers. - """ - - params = {} - if idp_type: - params['type'] = idp_type - return self.britive.get(self.base_url, params=params) - - def get(self, workload_identity_provider_id: int) -> dict: - """ - Return details of the specified workload identity provider. - - :param workload_identity_provider_id: The ID of the workload identity provider. - :returns: Details of the specified workload identity provider. - """ - - return self.britive.get(f'{self.base_url}/{workload_identity_provider_id}') - - def _build_attributes_map_list(self, attributes_map: dict) -> list: - # first get list of existing custom identity attributes and build some helpers - existing_attrs = [attr for attr in self.britive.identity_attributes.list() if not attr['builtIn']] - existing_attr_ids = [attr['id'] for attr in existing_attrs] - attrs_by_name = {attr['name']: attr['id'] for attr in existing_attrs} - - # for each attributeMap key/value provided ensure we convert to ID and build the list - attrs_list = [] - for idp_attr, custom_identity_attribute in attributes_map.items(): - if (check_custom_identity_attribute := custom_identity_attribute) not in existing_attr_ids and not ( - check_custom_identity_attribute := attrs_by_name.get(custom_identity_attribute) - ): - raise ValueError(f'custom identity attribute name {custom_identity_attribute} not found.') - attrs_list.append({'idpAttr': idp_attr, 'userAttr': check_custom_identity_attribute}) - return attrs_list - - def create(self, **kwargs) -> dict: - """ - Create a workload identity provider. - - This method accepts a list of kwargs which match the required and optional fields for creating a - workload identity provider. It mostly exists to support the creation of new workload identity provider - types before this SDK can be updated to support the new type natively. - - Generally, the caller should opt to use the `create_aws` or `create_oidc` methods instead of calling - this method directly. - - The field `attributesMap` is in format `{'idp_attr_value': 'custom_identity_attribute_name_or_id', ...}`. - - :param kwargs: A list of keyword arguments which will be provided directly to the API backend without - any further inspection. Valid fields are `name`, `description`, `idpType`, `attributesMap`, and - `validationWindow`. For the AWS provider an additional field `maxDuration` is valid. For the OIDC - provider additional fields `issuerUrl` and `allowedAudiences` are valid. - :returns: Details of the newly created workload identity provider. - """ - - if 'attributesMap' in kwargs: - kwargs['attributesMap'] = self._build_attributes_map_list(attributes_map=kwargs['attributesMap']) - - return self.britive.post(self.base_url, json=kwargs) - - def create_aws( - self, - name: str, - attributes_map: dict, - description: str = None, - validation_window: int = 30, - max_duration: int = 5, - ) -> dict: - """ - Create an AWS workload identity provider. - - :param name: Name of the AWS workload identity provider. - :param attributes_map: The mapping of the available AWS workload token fields to custom identity attributes - which are associated with service identities. Defaults to None.Can provide either the name of the - custom identity attribute or the ID. - :param description: Optional description of the AWS workload identity provider. - :param validation_window: The number of seconds allowed to validate the AWS workload token from its - "issued time". Defaults to 30 seconds. - :param max_duration: The max number of hours (whole numbers only) for which the AWS workload token is valid. - :returns: Details of the newly created AWS workload identity provider. - """ - - params = { - 'name': name, - 'idpType': 'AWS', - 'validationWindow': validation_window, - 'maxDuration': max_duration, - } - if description: - params['description'] = description - if attributes_map: - params['attributesMap'] = attributes_map - - return self.create(**params) - - def create_oidc( - self, - name: str, - issuer_url: str, - attributes_map: dict = None, - description: str = None, - validation_window: int = 30, - allowed_audiences: list = None, - ) -> dict: - """ - Create an OIDC workload identity provider. - - :param name: Name of the OIDC workload identity provider. - :param issuer_url: The issuer url for the OIDC provider. - :param attributes_map: The mapping of the available OIDC workload token fields to custom identity attributes - which are associated with service identities. Defaults to None. Can provide either the name of the - custom identity attribute or the ID. - :param description: Optional description of the AWS workload identity provider. - :param validation_window: The number of seconds allowed to validate the AWS workload token from its - "issued time". Defaults to 30 seconds. - :param allowed_audiences: The list of allowed audience values as strings. Defaults to None. - :returns: Details of the newly created OIDC workload identity provider. - """ - - params = {'name': name, 'idpType': 'OIDC', 'validationWindow': validation_window, 'issuerUrl': issuer_url} - if description: - params['description'] = description - if allowed_audiences: - params['allowedAudiences'] = allowed_audiences - if attributes_map: - params['attributesMap'] = attributes_map - - return self.create(**params) - - def update(self, workload_identity_provider_id: int, **kwargs) -> dict: - """ - Updates a workload identity provider. - - This method accepts a list of kwargs which match the required and optional fields for creating a - workload identity provider. It mostly exists to support the update of new workload identity provider - types before this SDK can be updated to support the new type natively. - - Generally, the caller should opt to use the `update_aws` or `update_oidc` methods instead of calling - this method directly. - - The field `attributesMap` is in format `{'idp_attr_value': 'custom_identity_attribute_name_or_id', ...}`. - - :param workload_identity_provider_id: The ID of the workload identity provider to update. - :param kwargs: A list of keyword arguments which will be provided directly to the API backend without - any further inspection. Valid fields are `name`, `description`, `idpType`, `attributesMap`, and - `validationWindow`. For the AWS provider an additional field `maxDuration` is valid. For the OIDC - provider additional fields `issuerUrl` and `allowedAudiences` are valid. - :returns: Details of the updated workload identity provider. - """ - - kwargs['id'] = workload_identity_provider_id - - if 'attributesMap' in kwargs: - kwargs['attributesMap'] = self._build_attributes_map_list(attributes_map=kwargs['attributesMap']) - - # since this is a PUT call and not a PATCH call we need to get the existing idp configuration - # and merge in the things that have changed - - existing = self.get(workload_identity_provider_id=workload_identity_provider_id) - return self.britive.put(self.base_url, json={**existing, **kwargs}) - - def update_aws( - self, - workload_identity_provider_id: int, - name: str = None, - attributes_map: dict = None, - description: str = None, - validation_window: int = None, - max_duration: int = None, - ) -> dict: - """ - Update an AWS workload identity provider. - - All fields except `workload_identity_provider_id` are optional. - - :param workload_identity_provider_id: The ID of the workload identity provider to update. - :param name: Name of the AWS workload identity provider. - :param attributes_map: The mapping of the available AWS workload token fields to custom identity attributes - which are associated with service identities.Can provide either the name of the custom identity - attribute or the ID. - :param description: Description of the AWS workload identity provider. - :param validation_window: The number of seconds allowed to validate the AWS workload token from its - "issued time". - :param max_duration: The max number of hours (whole numbers only) for which the AWS workload token is valid. - :returns: Details of the updated AWS workload identity provider. - """ - - params = {'idpType': 'AWS'} - - if name: - params['name'] = name - if description: - params['description'] = description - if validation_window: - params['validationWindow'] = validation_window - if max_duration: - params['maxDuration'] = max_duration - if attributes_map: - params['attributesMap'] = attributes_map - - return self.update(workload_identity_provider_id=workload_identity_provider_id, **params) - - def update_oidc( - self, - workload_identity_provider_id: int, - name: str = None, - issuer_url: str = None, - attributes_map: dict = None, - description: str = None, - validation_window: int = None, - allowed_audiences: list = None, - ) -> dict: - """ - Update an OIDC workload identity provider. - - All fields except `workload_identity_provider_id` are optional. - - :param workload_identity_provider_id: The ID of the workload identity provider to update. - :param name: Name of the OIDC workload identity provider. - :param issuer_url: The issuer url for the OIDC provider. - :param attributes_map: The mapping of the available OIDC workload token fields to custom identity attributes - which are associated with service identities. Can provide either the name of the custom identity - attribute or the ID. - :param description: Description of the AWS workload identity provider. - :param validation_window: The number of seconds allowed to validate the AWS workload token from its - "issued time". - :param allowed_audiences: The list of allowed audience values as strings. - :returns: Details of the update OIDC workload identity provider. - """ - - params = {'idpType': 'OIDC'} - - if name: - params['name'] = name - if description: - params['description'] = description - if validation_window: - params['validationWindow'] = validation_window - if issuer_url: - params['issuerUrl'] = issuer_url - if attributes_map: - params['attributesMap'] = attributes_map - if allowed_audiences: - params['allowedAudiences'] = allowed_audiences - - return self.update(workload_identity_provider_id=workload_identity_provider_id, **params) - - def delete(self, workload_identity_provider_id) -> None: - """ - Deletes a workload identity provider. - - :param workload_identity_provider_id: The ID of the workload identity provider. - :returns: None. - """ - return self.britive.delete(f'{self.base_url}/{workload_identity_provider_id}') - - def generate_attribute_map( - self, - idp_attribute_name: str, - custom_identity_attribute_name: str = None, - custom_identity_attribute_id: str = None, - ) -> dict: - """ - Generates a dictionary that can be appended to a list used for the `attributesMap`. - - This method would mostly be used when invoking the `create` or `update` methods directly instead of - using the type specific (`create_aws`, `create_oidc`, `update_aws`, `update_oidc`) methods which provide - a more pythonic way to capture the attribute mappings. - - :param idp_attribute_name: The name of the workload identity provider attribute to map. This will always - be a string as it is controlled by the identity provider. - :param custom_identity_attribute_name: The name of the Britive custom identity attribute. One of - `custom_identity_attribute_name` or `custom_identity_attribute_id` must be provided. The name will be - translated to the ID of the custom identity attribute on behalf of the caller. - :param custom_identity_attribute_id: The id of the Britive custom identity attribute. One of - `custom_identity_attribute_name` or `custom_identity_attribute_id` must be provided. - :returns: Dictionary representing the attribute map. - """ - - if custom_identity_attribute_id and custom_identity_attribute_name: - raise ValueError( - 'only one of custom_identity_attribute_id and custom_identity_attribute_name should be provided' - ) - - if not custom_identity_attribute_id and not custom_identity_attribute_name: - raise ValueError( - 'one of custom_identity_attribute_id or custom_identity_attribute_name should be provided' - ) - - if custom_identity_attribute_name: - found = False - for attr in self.britive.identity_attributes.list(): - if attr['name'] == custom_identity_attribute_name: - custom_identity_attribute_id = attr['id'] - found = True - break - if not found: - raise ValueError( - f'custom_identity_attribute_name value of {custom_identity_attribute_name} ' f'not found.' - ) - - return {'idpAttr': idp_attribute_name, 'userAttr': custom_identity_attribute_id} - - class ServiceIdentities: - def __init__(self, workload) -> None: - self.britive = workload.britive - self.base_url: str = workload.base_url + '/users/{id}/identity-provider' # will .format(id=...) later - - def get(self, service_identity_id: str) -> Union[dict, None]: - """ - Returns details about the workload identity provider associated with the specified service identity. - - :param service_identity_id: The ID of the service identity. - :returns: Details about the workload identity provider associated with the specified service identity. - Returns None if there is no workload identity provider associated with the specified service identity. - """ - - return self.britive.get(self.base_url.format(id=service_identity_id)) - - def assign( - self, service_identity_id: str, idp_id: str, federated_attributes: dict, token_duration: int = 300 - ) -> dict: - """ - Associates an OIDC provider with the specified Service Identity. - - :param service_identity_id: The ID of the service identity. - :param idp_id: The ID of the OIDC Identity Provider. - :param token_duration: Duration in seconds (from now) before a token provided by the client expires. - This will be evaluated alongside the OIDC JWT expiration field and the earlier of the two values - will govern when the token expires. This field is optional and defaults to 300 seconds. - :param federated_attributes: An attribute map where keys are the custom attribute ids or names - and values are strings or list of strings (for multivalued attributes) which map back to the mapped - token claims. - :returns: Details of the newly assigned workload identity provider. - """ - - mapping_attributes = [] - converted_federated_attributes = {} - converted_attributes = self.britive.service_identities.custom_attributes._build_list( - operation='add', custom_attributes=federated_attributes - ) - - for attr in converted_attributes: - custom_attr = attr['customUserAttribute'] - attr_id = custom_attr['attributeId'] - if attr_id not in converted_federated_attributes: - converted_federated_attributes[attr_id] = [] - converted_federated_attributes[attr_id].append(custom_attr['attributeValue']) - - for custom_attribute_id, values in converted_federated_attributes.items(): - mapping_attributes.append({'attrId': custom_attribute_id, 'values': values}) - - params = {'idpId': idp_id, 'tokenDuration': token_duration, 'mappingAttributes': mapping_attributes} - - return self.britive.post(self.base_url.format(id=service_identity_id), json=params) - - def unassign(self, service_identity_id: str) -> None: - """ - Removes/deletes the service identity's assigned identity provider, along with any custom attribute mappings. - - This will revert the service identity back to use static API tokens for authentication. - - :param service_identity_id: The ID of the Service Identity. - :returns: None. - """ - return self.britive.delete(self.base_url.format(id=service_identity_id)) - - class ScimUser: - def __init__(self, workload) -> None: - self.britive = workload.britive - self.base_url: str = workload.base_url + '/scim-user/identity-provider' - - def get(self, idp_name: str) -> dict: - """ - Gets details of the workload federation enabled service identity associated with the identity provider. - - The identity provider name provided must be a SAML identity provider. - - :param idp_name: The name of the SAML Identity Provider. - :returns: Details of the newly assigned service identity to the identity provider. - """ - - return self.britive.get(f'{self.base_url}/{idp_name}') - - def assign(self, service_identity_id: str, idp_name: str) -> dict: - """ - Associates a workload federation enabled service identity with an identity provider SCIM service. - - The service identity must already be configured for workload federation. - The identity provider name provided must be a SAML identity provider. - - :param service_identity_id: The ID of the service identity. - :param idp_name: The name of the SAML Identity Provider. - :returns: Details of the newly assigned service identity to the identity provider. - """ - - params = {'idpName': idp_name, 'userId': service_identity_id} - - return self.britive.post(self.base_url, json=params) - - def unassign(self, idp_name: str) -> dict: - """ - Removes a workload federation enabled service identity from an identity provider SCIM service. - - The identity provider name provided must be a SAML identity provider. - - :param idp_name: The name of the SAML Identity Provider. - :returns: Details of the newly assigned service identity to the identity provider. - """ - - return self.britive.delete(f'{self.base_url}/{idp_name}') diff --git a/tests/test_005-identity_attributes.py b/tests/000-global_settings-01-identity_attributes.py similarity index 81% rename from tests/test_005-identity_attributes.py rename to tests/000-global_settings-01-identity_attributes.py index 60a3488..b8f58e1 100644 --- a/tests/test_005-identity_attributes.py +++ b/tests/000-global_settings-01-identity_attributes.py @@ -2,7 +2,7 @@ def test_list(): - attributes = britive.identity_attributes.list() + attributes = britive.identity_management.identity_attributes.list() assert isinstance(attributes, list) assert len(attributes) >= 7 # there are 7 default attributes that are managed by the system diff --git a/tests/test_260-notification_mediums.py b/tests/000-global_settings-02-notification_mediums.py similarity index 54% rename from tests/test_260-notification_mediums.py rename to tests/000-global_settings-02-notification_mediums.py index ec53965..3deab6c 100644 --- a/tests/test_260-notification_mediums.py +++ b/tests/000-global_settings-02-notification_mediums.py @@ -7,20 +7,20 @@ def test_create(cached_notification_medium): def test_list(): - response = britive.notification_mediums.list() + response = britive.global_settings.notification_mediums.list() assert isinstance(response, list) assert isinstance(response[0], dict) def test_get(cached_notification_medium): - response = britive.notification_mediums.get(cached_notification_medium['id']) + response = britive.global_settings.notification_mediums.get(cached_notification_medium['id']) assert isinstance(response, dict) assert response['name'] == cached_notification_medium['name'] def test_update(cached_notification_medium): - r = str(random.randint(0, 1000000)) - new_name = f'pytest-nm-{r}' - britive.notification_mediums.update(cached_notification_medium['id'], parameters={'name': new_name}) - response = britive.notification_mediums.get(cached_notification_medium['id']) + r = str(random.randint(0, 999)) + new_name = f'{cached_notification_medium["name"]}-{r}' + britive.global_settings.notification_mediums.update(cached_notification_medium['id'], parameters={'name': new_name}) + response = britive.global_settings.notification_mediums.get(cached_notification_medium['id']) assert response['name'] == new_name diff --git a/tests/test_320-settings_banner.py b/tests/000-global_settings-03-banner.py similarity index 79% rename from tests/test_320-settings_banner.py rename to tests/000-global_settings-03-banner.py index 7433817..b5a409b 100644 --- a/tests/test_320-settings_banner.py +++ b/tests/000-global_settings-03-banner.py @@ -7,12 +7,12 @@ def test_get(): - banner = britive.settings.banner.get() + banner = britive.global_settings.banner.get() assert isinstance(banner, dict) def test_set_no_schedule(): - banner = britive.settings.banner.set(message='test', display_banner=True, message_type='INFO') + banner = britive.global_settings.banner.set(message='test', display_banner=True, message_type='INFO') assert isinstance(banner, dict) for key in ['status', 'messageType', 'message']: assert key in banner @@ -21,7 +21,7 @@ def test_set_no_schedule(): def test_set_with_schedule(): start_datetime = datetime.datetime.today() - banner = britive.settings.banner.set( + banner = britive.global_settings.banner.set( message='test', display_banner=True, message_type='INFO', @@ -38,7 +38,7 @@ def test_set_with_schedule(): def test_set_with_incorrect_schedule(): with pytest.raises(ValueError): - britive.settings.banner.set( + britive.global_settings.banner.set( message='test', display_banner=True, message_type='INFO', @@ -47,7 +47,7 @@ def test_set_with_incorrect_schedule(): ) with pytest.raises(ValueError): - britive.settings.banner.set( + britive.global_settings.banner.set( message='test', display_banner=True, message_type='INFO', @@ -56,7 +56,7 @@ def test_set_with_incorrect_schedule(): ) with pytest.raises(ValueError): - britive.settings.banner.set( + britive.global_settings.banner.set( message='test', display_banner=True, message_type='INFO', @@ -65,7 +65,7 @@ def test_set_with_incorrect_schedule(): ) with pytest.raises(ValueError): - britive.settings.banner.set( + britive.global_settings.banner.set( message='test', display_banner=True, message_type='INFO', @@ -73,7 +73,7 @@ def test_set_with_incorrect_schedule(): ) with pytest.raises(ValueError): - britive.settings.banner.set( + britive.global_settings.banner.set( message='test', display_banner=True, message_type='INFO', @@ -81,7 +81,7 @@ def test_set_with_incorrect_schedule(): ) with pytest.raises(ValueError): - britive.settings.banner.set(message='test', display_banner=True, message_type='INFO', time_zone='UTC') + britive.global_settings.banner.set(message='test', display_banner=True, message_type='INFO', time_zone='UTC') def test_banner_end_user(): @@ -92,7 +92,7 @@ def test_banner_end_user(): def test_banner_off(): - banner = britive.settings.banner.set(display_banner=False, message='dont care', message_type='INFO') + banner = britive.global_settings.banner.set(display_banner=False, message='dont care', message_type='INFO') assert 'status' in banner assert banner['status'] == 'OFF' sleep(5) diff --git a/tests/test_010-users.py b/tests/100-identity_management-01-users.py similarity index 56% rename from tests/test_010-users.py rename to tests/100-identity_management-01-users.py index 8df437e..7864c42 100644 --- a/tests/test_010-users.py +++ b/tests/100-identity_management-01-users.py @@ -1,5 +1,7 @@ import pyotp +from britive.exceptions import UserDoesNotHaveMFAEnabled + from .cache import * # will also import some globals like `britive` user_keys = { @@ -31,7 +33,7 @@ def test_create(cached_user): def test_list(cached_user): - response = britive.users.list() + response = britive.identity_management.users.list() assert isinstance(response, list) assert len(response) > 0 assert isinstance(response[0], dict) @@ -40,21 +42,21 @@ def test_list(cached_user): def test_get(cached_user): - user = britive.users.get(cached_user['userId']) + user = britive.identity_management.users.get(cached_user['userId']) assert isinstance(user, dict) assert user_keys.issubset(user) assert user['userId'] == cached_user['userId'] def test_get_by_name_co(cached_user): - users = britive.users.get_by_name(cached_user['lastName']) + users = britive.identity_management.users.get_by_name(cached_user['lastName']) assert isinstance(users, list) assert isinstance(users[0], dict) assert user_keys.issubset(users[0]) def test_search(cached_user): - users = britive.users.search(cached_user['email'].split('@')[0]) + users = britive.identity_management.users.search(cached_user['email'].split('@')[0]) assert isinstance(users, list) assert len(users) > 0 assert isinstance(users[0], dict) @@ -62,7 +64,7 @@ def test_search(cached_user): def test_get_by_status(): - users = britive.users.get_by_status('active') + users = britive.identity_management.users.get_by_status('active') assert isinstance(users, list) assert len(users) > 0 assert isinstance(users[0], dict) @@ -70,47 +72,47 @@ def test_get_by_status(): def test_update(cached_user): - user = britive.users.update(cached_user['userId'], mobile='1234567890') + user = britive.identity_management.users.update(cached_user['userId'], mobile='1234567890') assert isinstance(user, dict) assert user['mobile'] == '1234567890' def test_disable_single(cached_user): - user = britive.users.disable(user_id=cached_user['userId']) + user = britive.identity_management.users.disable(user_id=cached_user['userId']) assert isinstance(user, dict) assert user['status'] == 'inactive' def test_enable_single(cached_user): - user = britive.users.enable(user_id=cached_user['userId']) + user = britive.identity_management.users.enable(user_id=cached_user['userId']) assert isinstance(user, dict) assert user['status'] == 'active' def test_disable_list(cached_user): - user = britive.users.disable(user_id=cached_user['userId'], user_ids=[cached_user['userId']]) + user = britive.identity_management.users.disable(user_id=cached_user['userId'], user_ids=[cached_user['userId']]) assert isinstance(user, list) assert user[0]['status'] == 'inactive' def test_enable_list(cached_user): - user = britive.users.enable(user_id=cached_user['userId'], user_ids=[cached_user['userId']]) + user = britive.identity_management.users.enable(user_id=cached_user['userId'], user_ids=[cached_user['userId']]) assert isinstance(user, list) assert user[0]['status'] == 'active' def test_reset_password(cached_user): - response = britive.users.reset_password(cached_user['userId'], generate_random_password()) + response = britive.identity_management.users.reset_password(cached_user['userId'], generate_random_password()) assert response is None def test_reset_mfa(cached_user): - with pytest.raises(exceptions.UserDoesNotHaveMFAEnabled): - britive.users.reset_mfa(cached_user['userId']) + with pytest.raises(UserDoesNotHaveMFAEnabled): + britive.identity_management.users.reset_mfa(cached_user['userId']) def test_set_custom_identity_attributes(cached_user, cached_identity_attribute): - response = britive.service_identities.custom_attributes.add( + response = britive.identity_management.service_identities.custom_attributes.add( principal_id=cached_user['userId'], custom_attributes={cached_identity_attribute['id']: [f'test-attr-value-{random.randint(0, 1000000)}']}, ) @@ -118,7 +120,9 @@ def test_set_custom_identity_attributes(cached_user, cached_identity_attribute): def test_get_custom_identity_attributes_list(cached_user, cached_identity_attribute): - response = britive.service_identities.custom_attributes.get(principal_id=cached_user['userId'], as_dict=False) + response = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_user['userId'], as_dict=False + ) assert isinstance(response, list) assert len(response) == 1 assert isinstance(response[0], dict) @@ -126,37 +130,44 @@ def test_get_custom_identity_attributes_list(cached_user, cached_identity_attrib def test_get_custom_identity_attributes_dict(cached_user, cached_identity_attribute): - response = britive.service_identities.custom_attributes.get(principal_id=cached_user['userId'], as_dict=True) + response = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_user['userId'], as_dict=True + ) assert isinstance(response, dict) assert cached_identity_attribute['id'] in response assert response[cached_identity_attribute['id']].startswith('test-attr-value') def test_remove_custom_identity_attributes(cached_user, cached_identity_attribute): - value = britive.service_identities.custom_attributes.get(principal_id=cached_user['userId'], as_dict=True)[ - cached_identity_attribute['id'] - ] - response = britive.service_identities.custom_attributes.remove( + value = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_user['userId'], as_dict=True + )[cached_identity_attribute['id']] + response = britive.identity_management.service_identities.custom_attributes.remove( principal_id=cached_user['userId'], custom_attributes={cached_identity_attribute['name']: [value]} ) assert response is None - attributes = britive.service_identities.custom_attributes.get(principal_id=cached_user['userId'], as_dict=False) + attributes = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_user['userId'], as_dict=False + ) assert len(attributes) == 0 def test_minimized_user_details(cached_user): - details = britive.users.minimized_user_details(user_id=cached_user['userId']) + details = britive.identity_management.users.minimized_user_details(user_id=cached_user['userId']) assert isinstance(details, list) assert len(details) == 1 - details = britive.users.minimized_user_details(user_ids=[cached_user['userId']]) + details = britive.identity_management.users.minimized_user_details(user_ids=[cached_user['userId']]) assert isinstance(details, list) assert len(details) == 1 def test_stepup_mfa(): - challenge = britive.users.enable_mfa.enable() - totp = pyotp.TOTP(challenge.get('additionalDetails').get('key')) - totp = totp.now() - assert len(str(totp)) == 6 + challenge = britive.identity_management.users.enable_mfa.enable() + if challenge_key := challenge.get('additionalDetails', {}).get('key'): + totp = pyotp.TOTP(challenge_key) + totp = totp.now() + assert len(str(totp)) == 6 + if 'is already registered for MFA factor: TOTP' in challenge.get('message', ''): + assert challenge.get('errorCode') == 'MFA-0001' diff --git a/tests/test_020-tags.py b/tests/100-identity_management-02-tags.py similarity index 52% rename from tests/test_020-tags.py rename to tests/100-identity_management-02-tags.py index fd1df13..b8d829e 100644 --- a/tests/test_020-tags.py +++ b/tests/100-identity_management-02-tags.py @@ -9,64 +9,68 @@ def test_create(cached_tag): def test_get(cached_tag): - tag = britive.tags.get(cached_tag['userTagId']) + tag = britive.identity_management.tags.get(cached_tag['userTagId']) assert isinstance(tag, dict) assert set(tag_keys).issubset(tag.keys()) def test_list(cached_tag): - tags = britive.tags.list() + tags = britive.identity_management.tags.list() assert isinstance(tags, list) assert isinstance(tags[0], dict) assert cached_tag['name'] in [t['name'] for t in tags] def test_search(cached_tag): - tags = britive.tags.search(cached_tag['name']) + tags = britive.identity_management.tags.search(cached_tag['name']) assert isinstance(tags, list) assert isinstance(tags[0], dict) assert cached_tag['name'] == tags[0]['name'] def test_users_for_tag_zero(cached_tag): - users = britive.tags.users_for_tag(tag_id=cached_tag['userTagId']) + users = britive.identity_management.tags.users_for_tag(tag_id=cached_tag['userTagId']) assert isinstance(users, list) assert len(users) == 0 def test_available_users_for_tag(cached_tag): - users = britive.tags.available_users_for_tag(tag_id=cached_tag['userTagId']) + users = britive.identity_management.tags.available_users_for_tag(tag_id=cached_tag['userTagId']) assert isinstance(users, list) assert len(users) > 0 assert isinstance(users[0], dict) def test_add_user(cached_tag, cached_user): - user_added = britive.tags.add_user(tag_id=cached_tag['userTagId'], user_id=cached_user['userId']) + user_added = britive.identity_management.tags.add_user( + tag_id=cached_tag['userTagId'], user_id=cached_user['userId'] + ) assert isinstance(user_added, dict) def test_users_for_tag_one(cached_tag): - users = britive.tags.users_for_tag(tag_id=cached_tag['userTagId']) + users = britive.identity_management.tags.users_for_tag(tag_id=cached_tag['userTagId']) assert isinstance(users, list) assert len(users) == 1 assert isinstance(users[0], dict) def test_remove_user(cached_tag, cached_user): - response = britive.tags.remove_user(tag_id=cached_tag['userTagId'], user_id=cached_user['userId']) + response = britive.identity_management.tags.remove_user( + tag_id=cached_tag['userTagId'], user_id=cached_user['userId'] + ) assert response is None def test_enable(cached_tag): - response = britive.tags.enable(cached_tag['userTagId']) + response = britive.identity_management.tags.enable(cached_tag['userTagId']) assert isinstance(response, dict) assert response['userTagId'] == cached_tag['userTagId'] assert response['status'] == 'Active' def test_disable(cached_tag): - response = britive.tags.disable(cached_tag['userTagId']) + response = britive.identity_management.tags.disable(cached_tag['userTagId']) assert isinstance(response, dict) assert response['userTagId'] == cached_tag['userTagId'] assert response['status'] == 'Inactive' @@ -74,64 +78,68 @@ def test_disable(cached_tag): def test_update(cached_tag): r = str(random.randint(0, 1000000)) - tag = britive.tags.update(cached_tag['userTagId'], name=f'testpythonapteiwrappertag-{r}') + tag = britive.identity_management.tags.update(cached_tag['userTagId'], name=f'testpythonapteiwrappertag-{r}') assert isinstance(tag, dict) assert set(tag_keys).issubset(tag.keys()) assert tag['name'] == f'testpythonapteiwrappertag-{r}' # set it back for downstream processes - britive.tags.update(cached_tag['userTagId'], name=cached_tag['name']) + britive.identity_management.tags.update(cached_tag['userTagId'], name=cached_tag['name']) def test_membership_rules_list(cached_tag): - response = britive.tags.membership_rules.list(tag_id=cached_tag['userTagId']) + response = britive.identity_management.tags.membership_rules.list(tag_id=cached_tag['userTagId']) assert len(response) == 0 def test_membership_rules_create(cached_tag, cached_user): rules = [ - britive.tags.membership_rules.build(attribute_id_or_name='Email', operator='is', value=cached_user['email']) + britive.identity_management.tags.membership_rules.build( + attribute_id_or_name='Email', operator='is', value=cached_user['email'] + ) ] - response = britive.tags.membership_rules.create(tag_id=cached_tag['userTagId'], rules=rules) + response = britive.identity_management.tags.membership_rules.create(tag_id=cached_tag['userTagId'], rules=rules) assert len(response) == 1 - response = britive.tags.membership_rules.list(tag_id=cached_tag['userTagId']) + response = britive.identity_management.tags.membership_rules.list(tag_id=cached_tag['userTagId']) assert len(response) == 1 def test_membership_rules_update(cached_tag, cached_user): rules = [ - britive.tags.membership_rules.build(attribute_id_or_name='Email', operator='is', value=cached_user['email']), - britive.tags.membership_rules.build( + britive.identity_management.tags.membership_rules.build( + attribute_id_or_name='Email', operator='is', value=cached_user['email'] + ), + britive.identity_management.tags.membership_rules.build( attribute_id_or_name='Username', operator='is', value=cached_user['username'] ), ] - response = britive.tags.membership_rules.update(tag_id=cached_tag['userTagId'], rules=rules) + response = britive.identity_management.tags.membership_rules.update(tag_id=cached_tag['userTagId'], rules=rules) assert response is None - response = britive.tags.membership_rules.list(tag_id=cached_tag['userTagId']) + response = britive.identity_management.tags.membership_rules.list(tag_id=cached_tag['userTagId']) assert len(response) == 2 def test_membership_rules_matched_users(cached_tag, cached_user): - response = britive.tags.membership_rules.matched_users(tag_id=cached_tag['userTagId']) + response = britive.identity_management.tags.membership_rules.matched_users(tag_id=cached_tag['userTagId']) assert len(response) == 1 assert response[0]['email'] == cached_user['email'] def test_membership_rules_delete(cached_tag): - response = britive.tags.membership_rules.delete(tag_id=cached_tag['userTagId']) + response = britive.identity_management.tags.membership_rules.delete(tag_id=cached_tag['userTagId']) assert response is None - response = britive.tags.membership_rules.list(tag_id=cached_tag['userTagId']) + response = britive.identity_management.tags.membership_rules.list(tag_id=cached_tag['userTagId']) assert len(response) == 0 def test_minimized_user_details(cached_tag): - details = britive.tags.minimized_tag_details(tag_id=cached_tag['userTagId']) + details = britive.identity_management.tags.minimized_tag_details(tag_id=cached_tag['userTagId']) assert isinstance(details, list) assert len(details) == 1 - details = britive.tags.minimized_tag_details(tag_ids=[cached_tag['userTagId']]) + details = britive.identity_management.tags.minimized_tag_details(tag_ids=[cached_tag['userTagId']]) assert isinstance(details, list) assert len(details) == 1 diff --git a/tests/test_030-service_identities.py b/tests/100-identity_management-03-service_identities.py similarity index 71% rename from tests/test_030-service_identities.py rename to tests/100-identity_management-03-service_identities.py index 43bbdb1..522f0ca 100644 --- a/tests/test_030-service_identities.py +++ b/tests/100-identity_management-03-service_identities.py @@ -9,7 +9,7 @@ def test_create(cached_service_identity): def test_list(cached_service_identity): - response = britive.service_identities.list() + response = britive.identity_management.service_identities.list() assert isinstance(response, list) assert len(response) > 0 assert isinstance(response[0], dict) @@ -18,20 +18,20 @@ def test_list(cached_service_identity): def test_get(cached_service_identity): - user = britive.service_identities.get(cached_service_identity['userId']) + user = britive.identity_management.service_identities.get(cached_service_identity['userId']) assert isinstance(user, dict) assert set(service_identity_keys).issubset(user.keys()) def test_get_by_name_co(cached_service_identity): - users = britive.service_identities.get_by_name(cached_service_identity['name']) + users = britive.identity_management.service_identities.get_by_name(cached_service_identity['name']) assert isinstance(users, list) assert isinstance(users[0], dict) assert set(service_identity_keys).issubset(users[0].keys()) def test_search(cached_service_identity): - users = britive.service_identities.search(cached_service_identity['name'].split('@')[0]) + users = britive.identity_management.service_identities.search(cached_service_identity['name'].split('@')[0]) assert isinstance(users, list) assert len(users) > 0 assert isinstance(users[0], dict) @@ -39,7 +39,7 @@ def test_search(cached_service_identity): def test_get_by_status(): - users = britive.service_identities.get_by_status('active') + users = britive.identity_management.service_identities.get_by_status('active') assert isinstance(users, list) assert len(users) > 0 assert isinstance(users[0], dict) @@ -47,25 +47,25 @@ def test_get_by_status(): def test_update(cached_service_identity): - user = britive.service_identities.update(cached_service_identity['userId'], description='test2') + user = britive.identity_management.service_identities.update(cached_service_identity['userId'], description='test2') assert isinstance(user, dict) assert user['description'] == 'test2' def test_disable_single(cached_service_identity): - user = britive.service_identities.disable(service_identity_id=cached_service_identity['userId']) + user = britive.identity_management.service_identities.disable(service_identity_id=cached_service_identity['userId']) assert isinstance(user, dict) assert user['status'] == 'inactive' def test_enable_single(cached_service_identity): - user = britive.service_identities.enable(service_identity_id=cached_service_identity['userId']) + user = britive.identity_management.service_identities.enable(service_identity_id=cached_service_identity['userId']) assert isinstance(user, dict) assert user['status'] == 'active' def test_disable_list(cached_service_identity): - user = britive.service_identities.disable( + user = britive.identity_management.service_identities.disable( service_identity_id=cached_service_identity['userId'], service_identity_ids=[cached_service_identity['userId']] ) assert isinstance(user, list) @@ -73,7 +73,7 @@ def test_disable_list(cached_service_identity): def test_enable_list(cached_service_identity): - user = britive.service_identities.enable( + user = britive.identity_management.service_identities.enable( service_identity_id=cached_service_identity['userId'], service_identity_ids=[cached_service_identity['userId']] ) assert isinstance(user, list) @@ -81,7 +81,7 @@ def test_enable_list(cached_service_identity): def test_set_custom_identity_attributes(cached_service_identity, cached_identity_attribute): - response = britive.service_identities.custom_attributes.add( + response = britive.identity_management.service_identities.custom_attributes.add( principal_id=cached_service_identity['userId'], custom_attributes={cached_identity_attribute['id']: [f'test-attr-value-{random.randint(0, 1000000)}']}, ) @@ -89,7 +89,7 @@ def test_set_custom_identity_attributes(cached_service_identity, cached_identity def test_get_custom_identity_attributes_list(cached_service_identity, cached_identity_attribute): - response = britive.service_identities.custom_attributes.get( + response = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity['userId'], as_dict=False ) assert isinstance(response, list) @@ -99,7 +99,7 @@ def test_get_custom_identity_attributes_list(cached_service_identity, cached_ide def test_get_custom_identity_attributes_dict(cached_service_identity, cached_identity_attribute): - response = britive.service_identities.custom_attributes.get( + response = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity['userId'], as_dict=True ) assert isinstance(response, dict) @@ -108,15 +108,15 @@ def test_get_custom_identity_attributes_dict(cached_service_identity, cached_ide def test_remove_custom_identity_attributes(cached_service_identity, cached_identity_attribute): - value = britive.service_identities.custom_attributes.get( + value = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity['userId'], as_dict=True )[cached_identity_attribute['id']] - response = britive.service_identities.custom_attributes.remove( + response = britive.identity_management.service_identities.custom_attributes.remove( principal_id=cached_service_identity['userId'], custom_attributes={cached_identity_attribute['name']: [value]} ) assert response is None - attributes = britive.service_identities.custom_attributes.get( + attributes = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity['userId'], as_dict=False ) diff --git a/tests/test_040-service_identity_tokens.py b/tests/100-identity_management-04-service_identity_tokens.py similarity index 88% rename from tests/test_040-service_identity_tokens.py rename to tests/100-identity_management-04-service_identity_tokens.py index a728eda..1867d21 100644 --- a/tests/test_040-service_identity_tokens.py +++ b/tests/100-identity_management-04-service_identity_tokens.py @@ -14,7 +14,7 @@ def test_service_identity_tokens_update(cached_service_identity_token_updated): def test_service_identity_tokens_get(cached_service_identity): - token = britive.service_identity_tokens.get(cached_service_identity['userId']) + token = britive.identity_management.service_identity_tokens.get(cached_service_identity['userId']) assert isinstance(token, dict) assert 'tokenExpirationDays' in token assert token['tokenExpirationDays'] == 45 diff --git a/tests/test_210-identity_providers.py b/tests/100-identity_management-05-identity_providers.py similarity index 64% rename from tests/test_210-identity_providers.py rename to tests/100-identity_management-05-identity_providers.py index 9c0beea..c64150a 100644 --- a/tests/test_210-identity_providers.py +++ b/tests/100-identity_management-05-identity_providers.py @@ -1,4 +1,4 @@ -from britive import exceptions +from britive.exceptions.generic import BritiveGenericError from .cache import * # will also import some globals like `britive` @@ -8,7 +8,7 @@ def test_create(cached_identity_provider): def test_list(cached_identity_provider): - idps = britive.identity_providers.list() + idps = britive.identity_management.identity_providers.list() assert isinstance(idps, list) assert len(idps) > 0 assert isinstance(idps[0], dict) @@ -16,23 +16,27 @@ def test_list(cached_identity_provider): def test_get_by_id(cached_identity_provider): - idp = britive.identity_providers.get(identity_provider_id=cached_identity_provider['id']) + idp = britive.identity_management.identity_providers.get(identity_provider_id=cached_identity_provider['id']) assert isinstance(idp, dict) assert idp['id'] == cached_identity_provider['id'] def test_get_by_name(cached_identity_provider): - idp = britive.identity_providers.get_by_name(identity_provider_name=cached_identity_provider['name']) + idp = britive.identity_management.identity_providers.get_by_name( + identity_provider_name=cached_identity_provider['name'] + ) assert isinstance(idp, dict) assert idp['name'] == cached_identity_provider['name'] def test_update(cached_identity_provider): - response = britive.identity_providers.update( + response = britive.identity_management.identity_providers.update( identity_provider_id=cached_identity_provider['id'], sso_provider='Azure' ) assert response is None - idp = britive.identity_providers.get_by_name(identity_provider_name=cached_identity_provider['name']) + idp = britive.identity_management.identity_providers.get_by_name( + identity_provider_name=cached_identity_provider['name'] + ) assert isinstance(idp, dict) assert idp['ssoProvider'] == 'Azure' @@ -43,29 +47,33 @@ def test_scim_token_create(cached_scim_token): def test_scim_token_get(cached_scim_token, cached_identity_provider): - token = britive.identity_providers.scim_tokens.get(identity_provider_id=cached_identity_provider['id']) + token = britive.identity_management.identity_providers.scim_tokens.get( + identity_provider_id=cached_identity_provider['id'] + ) assert isinstance(token, dict) assert token['name'] == cached_scim_token['name'] def test_scim_token_update(cached_identity_provider): - response = britive.identity_providers.scim_tokens.update( + response = britive.identity_management.identity_providers.scim_tokens.update( identity_provider_id=cached_identity_provider['id'], token_expiration_days=30 ) assert response is None - token = britive.identity_providers.scim_tokens.get(identity_provider_id=cached_identity_provider['id']) + token = britive.identity_management.identity_providers.scim_tokens.get( + identity_provider_id=cached_identity_provider['id'] + ) assert token['tokenExpirationDays'] == 30 def test_scim_attributes_list(): - attributes = britive.identity_providers.scim_attributes.list() + attributes = britive.identity_management.identity_providers.scim_attributes.list() assert isinstance(attributes, list) assert len(attributes) > 0 assert isinstance(attributes[0], str) def test_scim_tokens_update_attribute_mapping(cached_identity_provider): - attributes = britive.identity_attributes.list() + attributes = britive.identity_management.identity_attributes.list() phone_id = None for attribute in attributes: if attribute['builtIn'] and attribute['name'] == 'Phone': @@ -81,11 +89,11 @@ def test_scim_tokens_update_attribute_mapping(cached_identity_provider): 'op': 'remove', } ] - response = britive.identity_providers.scim_attributes.update_mapping( + response = britive.identity_management.identity_providers.scim_attributes.update_mapping( identity_provider_id=cached_identity_provider['id'], mappings=mappings ) assert response is None - idp = britive.identity_providers.get(identity_provider_id=cached_identity_provider['id']) + idp = britive.identity_management.identity_providers.get(identity_provider_id=cached_identity_provider['id']) assert 'userAttributeScimMappings' in idp assert isinstance(idp['userAttributeScimMappings'], list) mappings = idp['userAttributeScimMappings'] @@ -93,22 +101,22 @@ def test_scim_tokens_update_attribute_mapping(cached_identity_provider): def test_configure_mfa(cached_identity_provider): - with pytest.raises(exceptions.InvalidRequest) as e: - britive.identity_providers.configure_mfa( + with pytest.raises(BritiveGenericError) as e: + britive.identity_management.identity_providers.configure_mfa( identity_provider_id=cached_identity_provider['id'], root_user=False, non_root_user=True ) assert 'E1001 - MFA can only be enabled on default identity provider' in str(e) def test_signing_certificate(): - cert = britive.identity_providers.signing_certificate() + cert = britive.identity_management.identity_providers.signing_certificate() assert isinstance(cert, str) assert '-----BEGIN CERTIFICATE-----' in cert def test_set_metadata(cached_identity_provider): - response = britive.identity_providers.set_metadata( - identity_provider_id=cached_identity_provider['id'], metadata_xml=britive.saml.metadata() + response = britive.identity_management.identity_providers.set_metadata( + identity_provider_id=cached_identity_provider['id'], metadata_xml=britive.security.saml.metadata() ) assert isinstance(response, dict) assert 'certificateDn' in response @@ -116,7 +124,9 @@ def test_set_metadata(cached_identity_provider): def test_delete(cached_identity_provider): - response = britive.identity_providers.delete(identity_provider_id=cached_identity_provider['id']) + response = britive.identity_management.identity_providers.delete( + identity_provider_id=cached_identity_provider['id'] + ) assert response is None cleanup('identity-provider') cleanup('scim-token') diff --git a/tests/test_215-workload.py b/tests/100-identity_management-06-workload.py similarity index 65% rename from tests/test_215-workload.py rename to tests/100-identity_management-06-workload.py index d4c1231..19815c0 100644 --- a/tests/test_215-workload.py +++ b/tests/100-identity_management-06-workload.py @@ -1,10 +1,10 @@ -from britive import exceptions +from britive.exceptions.generic import BritiveGenericException from .cache import * # will also import some globals like `britive` def test_identity_provider_list(): - response = britive.workload.identity_providers.list() + response = britive.identity_management.workload.identity_providers.list() assert isinstance(response, list) @@ -24,7 +24,7 @@ def test_identity_provider_create_oidc(cached_workload_identity_provider_oidc): def test_identity_provider_get(cached_workload_identity_provider_oidc): - response = britive.workload.identity_providers.get( + response = britive.identity_management.workload.identity_providers.get( workload_identity_provider_id=cached_workload_identity_provider_oidc['id'] ) assert isinstance(response, dict) @@ -35,25 +35,25 @@ def test_identity_provider_update_aws(cached_workload_identity_provider_aws): # we may very well need to set the update back to what it was originally as # we can only have 1 aws provider, so we want to restore that provider to its # original state - existing_max_duration = britive.workload.identity_providers.get( + existing_max_duration = britive.identity_management.workload.identity_providers.get( workload_identity_provider_id=cached_workload_identity_provider_aws['id'] )['maxDuration'] # do the update - response = britive.workload.identity_providers.update_aws( + response = britive.identity_management.workload.identity_providers.update_aws( workload_identity_provider_id=cached_workload_identity_provider_aws['id'], max_duration=1 ) assert isinstance(response, dict) assert response['maxDuration'] == 1 # restore it back - britive.workload.identity_providers.update_aws( + britive.identity_management.workload.identity_providers.update_aws( workload_identity_provider_id=cached_workload_identity_provider_aws['id'], max_duration=existing_max_duration ) def test_identity_provider_update_oidc(cached_workload_identity_provider_oidc): - response = britive.workload.identity_providers.update_oidc( + response = britive.identity_management.workload.identity_providers.update_oidc( workload_identity_provider_id=cached_workload_identity_provider_oidc['id'], issuer_url='https://id2.fakedomain.com', ) @@ -62,7 +62,7 @@ def test_identity_provider_update_oidc(cached_workload_identity_provider_oidc): def test_generate_attribute_map(cached_identity_attribute): - response = britive.workload.identity_providers.generate_attribute_map( + response = britive.identity_management.workload.identity_providers.generate_attribute_map( idp_attribute_name='sub', custom_identity_attribute_id=cached_identity_attribute['id'] ) assert isinstance(response, dict) @@ -71,7 +71,7 @@ def test_generate_attribute_map(cached_identity_attribute): assert response['idpAttr'] == 'sub' assert response['userAttr'] == cached_identity_attribute['id'] - response = britive.workload.identity_providers.generate_attribute_map( + response = britive.identity_management.workload.identity_providers.generate_attribute_map( idp_attribute_name='sub', custom_identity_attribute_name=cached_identity_attribute['name'] ) assert isinstance(response, dict) @@ -81,62 +81,68 @@ def test_generate_attribute_map(cached_identity_attribute): assert response['userAttr'] == cached_identity_attribute['id'] -def test_service_identity_get_when_nothing_associated(cached_service_identity): - with pytest.raises(exceptions.NotFound): - britive.workload.service_identities.get(service_identity_id=cached_service_identity['userId']) +def test_service_identity_get_when_nothing_associated(cached_service_identity_federated): + with pytest.raises(BritiveGenericException): + britive.identity_management.workload.service_identities.get( + service_identity_id=cached_service_identity_federated['userId'] + ) def test_service_identity_assign_and_unassign( - cached_service_identity, cached_identity_attribute, cached_workload_identity_provider_oidc + cached_service_identity_federated, cached_identity_attribute, cached_workload_identity_provider_oidc ): - response = britive.workload.service_identities.assign( - service_identity_id=cached_service_identity['userId'], + response = britive.identity_management.workload.service_identities.assign( + service_identity_id=cached_service_identity_federated['userId'], idp_id=cached_workload_identity_provider_oidc['id'], federated_attributes={cached_identity_attribute['id']: 'test'}, ) assert isinstance(response, dict) - attrs = britive.service_identities.custom_attributes.get( - principal_id=cached_service_identity['userId'], as_dict=True + attrs = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_service_identity_federated['userId'], as_dict=True ) assert isinstance(attrs, dict) assert len(attrs) == 1 assert attrs[cached_identity_attribute['id']] == 'test' - response = britive.workload.service_identities.unassign(service_identity_id=cached_service_identity['userId']) + response = britive.identity_management.workload.service_identities.unassign( + service_identity_id=cached_service_identity_federated['userId'] + ) assert response is None - attrs = britive.service_identities.custom_attributes.get( - principal_id=cached_service_identity['userId'], as_dict=False + attrs = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_service_identity_federated['userId'], as_dict=False ) assert isinstance(attrs, list) assert len(attrs) == 1 assert attrs[0]['attributeName'] is None - response = britive.workload.service_identities.assign( - service_identity_id=cached_service_identity['userId'], + response = britive.identity_management.workload.service_identities.assign( + service_identity_id=cached_service_identity_federated['userId'], idp_id=cached_workload_identity_provider_oidc['id'], federated_attributes={cached_identity_attribute['name']: 'test'}, ) assert isinstance(response, dict) - attrs = britive.service_identities.custom_attributes.get( - principal_id=cached_service_identity['userId'], as_dict=True + attrs = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_service_identity_federated['userId'], as_dict=True ) assert isinstance(attrs, dict) assert len(attrs) == 1 assert attrs[cached_identity_attribute['id']] == 'test' - response = britive.workload.service_identities.unassign(service_identity_id=cached_service_identity['userId']) + response = britive.identity_management.workload.service_identities.unassign( + service_identity_id=cached_service_identity_federated['userId'] + ) assert response is None - attrs = britive.service_identities.custom_attributes.get( - principal_id=cached_service_identity['userId'], as_dict=False + attrs = britive.identity_management.service_identities.custom_attributes.get( + principal_id=cached_service_identity_federated['userId'], as_dict=False ) assert isinstance(attrs, list) @@ -148,11 +154,11 @@ def test_identity_provider_delete(cached_workload_identity_provider_oidc, cached try: # we do not want to delete the pre-existing aws provider if cached_workload_identity_provider_aws['name'].startswith('python-sdk-aws'): - aws = britive.workload.identity_providers.delete( + aws = britive.identity_management.workload.identity_providers.delete( workload_identity_provider_id=cached_workload_identity_provider_aws['id'] ) assert aws is None - oidc = britive.workload.identity_providers.delete( + oidc = britive.identity_management.workload.identity_providers.delete( workload_identity_provider_id=cached_workload_identity_provider_oidc['id'] ) assert oidc is None diff --git a/tests/test_240-secrets_manager.py b/tests/150-secrets_manager-01-secrets_manager.py similarity index 98% rename from tests/test_240-secrets_manager.py rename to tests/150-secrets_manager-01-secrets_manager.py index e3c5f52..88c5510 100644 --- a/tests/test_240-secrets_manager.py +++ b/tests/150-secrets_manager-01-secrets_manager.py @@ -53,8 +53,8 @@ def test_password_policies_list(): def test_password_policies_update(cached_password_policies): - r = str(random.randint(0, 1000000)) - new_name = f'pytestpolicy-{r}' + r = str(random.randint(0, 999)) + new_name = f'{cached_password_policies["name"]}-{r}' britive.secrets_manager.password_policies.update(cached_password_policies['id'], name=new_name) assert britive.secrets_manager.password_policies.get(cached_password_policies['id'])['name'] == new_name diff --git a/tests/test_050-applications.py b/tests/200-application_management-01-applications.py similarity index 77% rename from tests/test_050-applications.py rename to tests/200-application_management-01-applications.py index fd4bf23..fcbca16 100644 --- a/tests/test_050-applications.py +++ b/tests/200-application_management-01-applications.py @@ -20,19 +20,19 @@ def test_create(cached_application): def test_disable(cached_application): - response = britive.applications.disable(application_id=cached_application['appContainerId']) + response = britive.application_management.applications.disable(application_id=cached_application['appContainerId']) assert isinstance(response, dict) assert response['status'] == 'inactive' def test_enable(cached_application): - response = britive.applications.enable(application_id=cached_application['appContainerId']) + response = britive.application_management.applications.enable(application_id=cached_application['appContainerId']) assert isinstance(response, dict) assert response['status'] == 'active' def test_list(cached_application): - apps = britive.applications.list() + apps = britive.application_management.applications.list() assert isinstance(apps, list) assert len(apps) > 0 assert isinstance(apps[0], dict) @@ -40,13 +40,13 @@ def test_list(cached_application): def test_get(cached_application): - app = britive.applications.get(application_id=cached_application['appContainerId']) + app = britive.application_management.applications.get(application_id=cached_application['appContainerId']) assert app['appContainerId'] == cached_application['appContainerId'] assert app['catalogAppDisplayName'] == cached_application['catalogAppDisplayName'] def test_test_failure(cached_application): - response = britive.applications.test(application_id=cached_application['appContainerId']) + response = britive.application_management.applications.test(application_id=cached_application['appContainerId']) assert isinstance(response, dict) assert 'success' in response assert 'message' in response @@ -58,7 +58,7 @@ def test_update(cached_application): idp = os.environ.get('BRITIVE_IDP_NAME_OVERRIDE') or f'BritivePythonApiWrapperTesting-{tenant}' role = os.environ.get('BRITIVE_INTEGRATION_ROLE_NAME_OVERRIDE') or f'britive-integration-role-{tenant}' - app = britive.applications.update( + app = britive.application_management.applications.update( application_id=cached_application['appContainerId'], showAwsAccountNumber=True, identityProvider=idp, @@ -74,7 +74,9 @@ def test_update(cached_application): def test_set_user_account_mapping(cached_application): - app = britive.applications.set_user_account_mapping(cached_application['appContainerId'], 'email') + app = britive.application_management.applications.set_user_account_mapping( + cached_application['appContainerId'], 'email' + ) assert isinstance(app['userAccountMappings'], list) assert len(app['userAccountMappings']) == 1 assert app['userAccountMappings'][0]['name'] == 'email' diff --git a/tests/test_060-environment_groups.py b/tests/200-application_management-02-environment_groups.py similarity index 77% rename from tests/test_060-environment_groups.py rename to tests/200-application_management-02-environment_groups.py index 71e5130..7a33a18 100644 --- a/tests/test_060-environment_groups.py +++ b/tests/200-application_management-02-environment_groups.py @@ -6,14 +6,14 @@ def test_environment_group_create(cached_environment_group): def test_environment_group_list(cached_application): - groups = britive.environment_groups.list(application_id=cached_application['appContainerId']) + groups = britive.application_management.environment_groups.list(application_id=cached_application['appContainerId']) assert isinstance(groups, list) assert len(groups) > 0 assert isinstance(groups[0], dict) def test_environment_group_get(cached_application, cached_environment_group): - group_get = britive.environment_groups.get( + group_get = britive.application_management.environment_groups.get( application_id=cached_application['appContainerId'], environment_group_id=cached_environment_group['id'] ) assert isinstance(group_get, dict) diff --git a/tests/test_070-environments.py b/tests/200-application_management-03-environments.py similarity index 83% rename from tests/test_070-environments.py rename to tests/200-application_management-03-environments.py index df9a91f..35f1770 100644 --- a/tests/test_070-environments.py +++ b/tests/200-application_management-03-environments.py @@ -7,7 +7,7 @@ def test_environment_create(cached_environment): def test_environment_update(cached_application, cached_environment): account_id = os.environ['BRITIVE_TEST_ENV_ACCOUNT_ID'] - response = britive.environments.update( + response = britive.application_management.environments.update( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], accountId=account_id, @@ -24,23 +24,22 @@ def test_environment_update(cached_application, cached_environment): def test_environment_list_one(cached_application): - envs = britive.environments.list(application_id=cached_application['appContainerId']) + envs = britive.application_management.environments.list(application_id=cached_application['appContainerId']) assert isinstance(envs, list) assert len(envs) == 1 assert isinstance(envs[0], dict) def test_environment_test(cached_application, cached_environment): - response = britive.environments.test( + response = britive.application_management.environments.test( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) - print(response) assert isinstance(response, dict) assert 'success' in response def test_environment_get(cached_application, cached_environment): - env = britive.environments.get( + env = britive.application_management.environments.get( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) assert isinstance(env, dict) diff --git a/tests/test_080-scans.py b/tests/200-application_management-04-scans.py similarity index 82% rename from tests/test_080-scans.py rename to tests/200-application_management-04-scans.py index 8e2dff2..83c5f60 100644 --- a/tests/test_080-scans.py +++ b/tests/200-application_management-04-scans.py @@ -13,7 +13,7 @@ def test_scan(cached_scan): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_status(cached_scan): while True: - status = britive.scans.status(task_id=cached_scan['taskId']) + status = britive.application_management.scans.status(task_id=cached_scan['taskId']) if status['status'] == 'Success': break if status['status'] == 'Error': @@ -24,14 +24,14 @@ def test_status(cached_scan): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_history(cached_application): - response = britive.scans.history(application_id=cached_application['appContainerId']) + response = britive.application_management.scans.history(application_id=cached_application['appContainerId']) assert isinstance(response, list) assert len(response) > 0 @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_diff_accounts(cached_application, cached_environment): - response = britive.scans.diff( + response = britive.application_management.scans.diff( resource='accounts', application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], @@ -42,7 +42,7 @@ def test_diff_accounts(cached_application, cached_environment): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_diff_groups(cached_application, cached_environment): - response = britive.scans.diff( + response = britive.application_management.scans.diff( resource='groups', application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) assert isinstance(response, list) @@ -51,7 +51,7 @@ def test_diff_groups(cached_application, cached_environment): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_diff_permissions(cached_application, cached_environment): - response = britive.scans.diff( + response = britive.application_management.scans.diff( resource='permissions', application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], diff --git a/tests/test_090-accounts.py b/tests/200-application_management-05-accounts.py similarity index 86% rename from tests/test_090-accounts.py rename to tests/200-application_management-05-accounts.py index ee0191a..26ab177 100644 --- a/tests/test_090-accounts.py +++ b/tests/200-application_management-05-accounts.py @@ -4,7 +4,7 @@ # starting with map so we can get a cached account to use for testing @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_map(cached_application, cached_environment, cached_user, cached_account): - response = britive.accounts.map( + response = britive.application_management.accounts.map( user_id=cached_user['userId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], @@ -17,7 +17,7 @@ def test_map(cached_application, cached_environment, cached_user, cached_account @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_mapped_users(cached_application, cached_environment, cached_user, cached_account): - response = britive.accounts.mapped_users( + response = britive.application_management.accounts.mapped_users( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], account_id=cached_account['accountId'], @@ -29,7 +29,7 @@ def test_mapped_users(cached_application, cached_environment, cached_user, cache @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_users_available_to_map(cached_application, cached_environment, cached_user, cached_account): - response = britive.accounts.users_available_to_map( + response = britive.application_management.accounts.users_available_to_map( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], account_id=cached_account['accountId'], @@ -41,7 +41,7 @@ def test_users_available_to_map(cached_application, cached_environment, cached_u @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_unmap(cached_application, cached_environment, cached_user, cached_account): - response = britive.accounts.unmap( + response = britive.application_management.accounts.unmap( user_id=cached_user['userId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], @@ -53,7 +53,7 @@ def test_unmap(cached_application, cached_environment, cached_user, cached_accou @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_list(cached_application, cached_environment): - accounts = britive.accounts.list( + accounts = britive.application_management.accounts.list( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) assert isinstance(accounts, list) @@ -63,7 +63,7 @@ def test_list(cached_application, cached_environment): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_permissions(cached_application, cached_environment, cached_account): - permissions = britive.accounts.permissions( + permissions = britive.application_management.accounts.permissions( account_id=cached_account['accountId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], @@ -74,7 +74,7 @@ def test_permissions(cached_application, cached_environment, cached_account): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_groups(cached_application, cached_environment, cached_account): - groups = britive.accounts.groups( + groups = britive.application_management.accounts.groups( account_id=cached_account['accountId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], diff --git a/tests/test_100-permissions.py b/tests/200-application_management-06-permissions.py similarity index 84% rename from tests/test_100-permissions.py rename to tests/200-application_management-06-permissions.py index 2b0f085..b638063 100644 --- a/tests/test_100-permissions.py +++ b/tests/200-application_management-06-permissions.py @@ -3,7 +3,7 @@ @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_list(cached_application, cached_environment): - permissions = britive.permissions.list( + permissions = britive.application_management.permissions.list( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) assert isinstance(permissions, list) @@ -13,7 +13,7 @@ def test_list(cached_application, cached_environment): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_accounts(cached_application, cached_environment, cached_permission): - accounts = britive.permissions.accounts( + accounts = britive.application_management.permissions.accounts( permission_id=cached_permission['appPermissionId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], @@ -24,7 +24,7 @@ def test_accounts(cached_application, cached_environment, cached_permission): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_groups(cached_application, cached_environment, cached_permission): - groups = britive.permissions.groups( + groups = britive.application_management.permissions.groups( permission_id=cached_permission['appPermissionId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], diff --git a/tests/test_110-groups.py b/tests/200-application_management-07-groups.py similarity index 84% rename from tests/test_110-groups.py rename to tests/200-application_management-07-groups.py index ff9457f..5f6ae12 100644 --- a/tests/test_110-groups.py +++ b/tests/200-application_management-07-groups.py @@ -3,7 +3,7 @@ @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_list(cached_application, cached_environment): - groups = britive.accounts.list( + groups = britive.application_management.accounts.list( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) assert isinstance(groups, list) @@ -13,7 +13,7 @@ def test_list(cached_application, cached_environment): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_permissions(cached_application, cached_environment, cached_group): - permissions = britive.groups.permissions( + permissions = britive.application_management.groups.permissions( group_id=cached_group['appPermissionId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], @@ -24,7 +24,7 @@ def test_permissions(cached_application, cached_environment, cached_group): @pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_accounts(cached_application, cached_environment, cached_group): - groups = britive.groups.accounts( + groups = britive.application_management.groups.accounts( group_id=cached_group['appPermissionId'], application_id=cached_application['appContainerId'], environment_id=cached_environment['id'], diff --git a/tests/test_130-profiles.py b/tests/200-application_management-08-profiles.py similarity index 75% rename from tests/test_130-profiles.py rename to tests/200-application_management-08-profiles.py index 36ba06f..6425ac7 100644 --- a/tests/test_130-profiles.py +++ b/tests/200-application_management-08-profiles.py @@ -11,7 +11,7 @@ def test_create(cached_profile): def test_list(cached_profile): - profiles = britive.profiles.list(application_id=cached_profile['appContainerId']) + profiles = britive.application_management.profiles.list(application_id=cached_profile['appContainerId']) assert isinstance(profiles, list) assert len(profiles) > 0 assert isinstance(profiles[0], dict) @@ -19,13 +19,15 @@ def test_list(cached_profile): def test_get(cached_profile): - profile = britive.profiles.get(application_id=cached_profile['appContainerId'], profile_id=cached_profile['papId']) + profile = britive.application_management.profiles.get( + application_id=cached_profile['appContainerId'], profile_id=cached_profile['papId'] + ) assert isinstance(profile, dict) assert profile['name'].startswith('test') def test_update(cached_profile): - profile = britive.profiles.update( + profile = britive.application_management.profiles.update( application_id=cached_profile['appContainerId'], profile_id=cached_profile['papId'], description='test desc' ) assert isinstance(profile, dict) @@ -33,7 +35,7 @@ def test_update(cached_profile): def test_set_scopes(cached_profile, cached_environment): - scopes = britive.profiles.set_scopes( + scopes = britive.application_management.profiles.set_scopes( profile_id=cached_profile['papId'], scopes=[{'type': 'Environment', 'value': cached_environment['id']}] ) assert isinstance(scopes, list) @@ -43,7 +45,7 @@ def test_set_scopes(cached_profile, cached_environment): def test_get_scopes(cached_profile, cached_environment): - scopes = britive.profiles.get_scopes(profile_id=cached_profile['papId']) + scopes = britive.application_management.profiles.get_scopes(profile_id=cached_profile['papId']) assert isinstance(scopes, list) assert len(scopes) == 1 assert isinstance(scopes[0], dict) @@ -51,21 +53,21 @@ def test_get_scopes(cached_profile, cached_environment): def test_remove_single_environment_scope(cached_profile, cached_environment): - response = britive.profiles.remove_single_environment_scope( + response = britive.application_management.profiles.remove_single_environment_scope( profile_id=cached_profile['papId'], environment_id=cached_environment['id'] ) assert response is None def test_add_single_environment_scope(cached_profile, cached_environment): - response = britive.profiles.add_single_environment_scope( + response = britive.application_management.profiles.add_single_environment_scope( profile_id=cached_profile['papId'], environment_id=cached_environment['id'] ) assert response is None def test_disable(cached_profile): - profile = britive.profiles.disable( + profile = britive.application_management.profiles.disable( application_id=cached_profile['appContainerId'], profile_id=cached_profile['papId'] ) assert isinstance(profile, dict) @@ -73,7 +75,7 @@ def test_disable(cached_profile): def test_enable(cached_profile): - profile = britive.profiles.enable( + profile = britive.application_management.profiles.enable( application_id=cached_profile['appContainerId'], profile_id=cached_profile['papId'] ) assert isinstance(profile, dict) @@ -88,14 +90,14 @@ def test_session_attributes_add_dynamic(cached_dynamic_session_attribute): assert isinstance(cached_dynamic_session_attribute, dict) -def test_session_attributes_list(cached_profile): - attributes = britive.profiles.session_attributes.list(profile_id=cached_profile['papId']) +def test_session_attributes_list(cached_profile, cached_static_session_attribute, cached_dynamic_session_attribute): + attributes = britive.application_management.profiles.session_attributes.list(profile_id=cached_profile['papId']) assert isinstance(attributes, list) assert len(attributes) == 2 def test_session_attributes_update_static(cached_profile, cached_static_session_attribute): - attribute = britive.profiles.session_attributes.update_static( + attribute = britive.application_management.profiles.session_attributes.update_static( profile_id=cached_profile['papId'], attribute_id=cached_static_session_attribute['id'], tag_name='test-static-2', @@ -106,7 +108,7 @@ def test_session_attributes_update_static(cached_profile, cached_static_session_ def test_session_attributes_update_dynamic(cached_profile, cached_dynamic_session_attribute): - attribute = britive.profiles.session_attributes.update_dynamic( + attribute = britive.application_management.profiles.session_attributes.update_dynamic( profile_id=cached_profile['papId'], attribute_id=cached_dynamic_session_attribute['id'], identity_attribute_id='w2zQ4R9xoyrkWY4phEV9', @@ -118,12 +120,12 @@ def test_session_attributes_update_dynamic(cached_profile, cached_dynamic_sessio def test_session_attributes_remove(cached_profile, cached_static_session_attribute, cached_dynamic_session_attribute): try: - static = britive.profiles.session_attributes.remove( + static = britive.application_management.profiles.session_attributes.remove( profile_id=cached_profile['papId'], attribute_id=cached_static_session_attribute['id'] ) assert static is None - dynamic = britive.profiles.session_attributes.remove( + dynamic = britive.application_management.profiles.session_attributes.remove( profile_id=cached_profile['papId'], attribute_id=cached_dynamic_session_attribute['id'] ) assert dynamic is None @@ -135,13 +137,14 @@ def test_session_attributes_remove(cached_profile, cached_static_session_attribu def test_policies_create(cached_profile_policy): - print(cached_profile_policy) assert isinstance(cached_profile_policy, dict) assert cached_profile_policy['members']['tags'] def test_list_include_policies(cached_profile): - profiles = britive.profiles.list(application_id=cached_profile['appContainerId'], include_policies=True) + profiles = britive.application_management.profiles.list( + application_id=cached_profile['appContainerId'], include_policies=True + ) assert isinstance(profiles, list) assert len(profiles) > 0 assert isinstance(profiles[0], dict) @@ -150,33 +153,37 @@ def test_list_include_policies(cached_profile): def test_disable_mfa(cached_profile, cached_tag, cached_profile_policy): - profile_policy = britive.profiles.policies.build( + profile_policy = britive.application_management.profiles.policies.build( name=cached_profile['papId'], description=cached_tag['name'], active=False, stepup_auth=False, always_prompt_stepup_auth=False, ) - response = britive.profiles.policies.update( + response = britive.application_management.profiles.policies.update( profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'], policy=profile_policy ) - response = britive.profiles.policies.get(profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id']) + response = britive.application_management.profiles.policies.get( + profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'] + ) assert isinstance(json.loads(response.get('condition')), dict) assert json.loads(response.get('condition')).get('stepUpCondition', '') == '' def test_enable_mfa(cached_profile, cached_tag, cached_profile_policy): - profile_policy = britive.profiles.policies.build( + profile_policy = britive.application_management.profiles.policies.build( name=cached_profile['papId'], description=cached_tag['name'], active=False, stepup_auth=True, always_prompt_stepup_auth=False, ) - response = britive.profiles.policies.update( + response = britive.application_management.profiles.policies.update( profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'], policy=profile_policy ) - response = britive.profiles.policies.get(profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id']) + response = britive.application_management.profiles.policies.get( + profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'] + ) assert isinstance(json.loads(response.get('condition')), dict) assert json.loads(response.get('condition')).get('stepUpCondition').get('factor') == 'TOTP' @@ -189,8 +196,8 @@ def test_policies_create_with_approval_single_notification_medium(cached_profile def test_policies_create_with_approval_multiple_notification_medium( cached_profile, cached_service_identity, cached_user ): - policy = britive.profiles.policies.build( - name=f"{cached_profile['papId']}-2", + policy = britive.application_management.profiles.policies.build( + name=f'{cached_profile["papId"]}-2', description='', service_identities=[cached_service_identity['username']], approval_notification_medium=['Email'], @@ -200,19 +207,21 @@ def test_policies_create_with_approval_multiple_notification_medium( def test_policies_list(cached_profile): - policies = britive.profiles.policies.list(profile_id=cached_profile['papId']) + policies = britive.application_management.profiles.policies.list(profile_id=cached_profile['papId']) assert isinstance(policies, list) def test_policies_get(cached_profile, cached_profile_policy): - policy = britive.profiles.policies.get(profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id']) + policy = britive.application_management.profiles.policies.get( + profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'] + ) assert isinstance(policy, dict) def test_policies_condition_created_as_json_get_formatted_json( cached_profile, cached_profile_policy_condition_as_json_str ): - policy = britive.profiles.policies.get( + policy = britive.application_management.profiles.policies.get( profile_id=cached_profile['papId'], policy_id=cached_profile_policy_condition_as_json_str['id'], condition_as_dict=False, @@ -223,7 +232,7 @@ def test_policies_condition_created_as_json_get_formatted_json( def test_policies_condition_created_as_json_get_formatted_dict( cached_profile, cached_profile_policy_condition_as_json_str ): - policy = britive.profiles.policies.get( + policy = britive.application_management.profiles.policies.get( profile_id=cached_profile['papId'], policy_id=cached_profile_policy_condition_as_json_str['id'], condition_as_dict=True, @@ -232,7 +241,7 @@ def test_policies_condition_created_as_json_get_formatted_dict( def test_policies_condition_created_as_dict_get_formatted_json(cached_profile, cached_profile_policy_condition_as_dict): - policy = britive.profiles.policies.get( + policy = britive.application_management.profiles.policies.get( profile_id=cached_profile['papId'], policy_id=cached_profile_policy_condition_as_dict['id'], condition_as_dict=False, @@ -241,7 +250,7 @@ def test_policies_condition_created_as_dict_get_formatted_json(cached_profile, c def test_policies_condition_created_as_dict_get_formatted_dict(cached_profile, cached_profile_policy_condition_as_dict): - policy = britive.profiles.policies.get( + policy = britive.application_management.profiles.policies.get( profile_id=cached_profile['papId'], policy_id=cached_profile_policy_condition_as_dict['id'], condition_as_dict=True, @@ -252,7 +261,7 @@ def test_policies_condition_created_as_dict_get_formatted_dict(cached_profile, c def test_policies_update(cached_profile, cached_profile_policy): policy = {'members': {'tags': [{'id': tag['id']} for tag in cached_profile_policy['members']['tags']]}} assert ( - britive.profiles.policies.update( + britive.application_management.profiles.policies.update( profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'], policy=policy ) is None @@ -262,7 +271,9 @@ def test_policies_update(cached_profile, cached_profile_policy): def test_policies_delete(cached_profile, cached_profile_policy): try: assert ( - britive.profiles.policies.delete(profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id']) + britive.application_management.profiles.policies.delete( + profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'] + ) is None ) finally: @@ -271,13 +282,13 @@ def test_policies_delete(cached_profile, cached_profile_policy): @pytest.mark.skipif(constraints, reason=constraints_skip) def test_constraints_list_supported_types(cached_gcp_profile_big_query, cached_gcp_profile_storage): - response = britive.profiles.permissions.constraints.list_supported_types( + response = britive.application_management.profiles.permissions.constraints.list_supported_types( profile_id=cached_gcp_profile_big_query['papId'], permission_name='BigQuery Admin', permission_type='role' ) assert 'bigquery.datasets' in response assert 'bigquery.tables' in response - response = britive.profiles.permissions.constraints.list_supported_types( + response = britive.application_management.profiles.permissions.constraints.list_supported_types( profile_id=cached_gcp_profile_storage['papId'], permission_name='Storage Admin', permission_type='role' ) @@ -287,7 +298,7 @@ def test_constraints_list_supported_types(cached_gcp_profile_big_query, cached_g @pytest.mark.skipif(constraints, reason=constraints_skip) def test_constraints_get_before_add(cached_gcp_profile_big_query, cached_gcp_profile_storage): - response = britive.profiles.permissions.constraints.get( + response = britive.application_management.profiles.permissions.constraints.get( profile_id=cached_gcp_profile_big_query['papId'], permission_name='BigQuery Admin', permission_type='role', @@ -296,7 +307,7 @@ def test_constraints_get_before_add(cached_gcp_profile_big_query, cached_gcp_pro assert response is None - response = britive.profiles.permissions.constraints.get( + response = britive.application_management.profiles.permissions.constraints.get( profile_id=cached_gcp_profile_big_query['papId'], permission_name='BigQuery Admin', permission_type='role', @@ -305,7 +316,7 @@ def test_constraints_get_before_add(cached_gcp_profile_big_query, cached_gcp_pro assert response is None - response = britive.profiles.permissions.constraints.get( + response = britive.application_management.profiles.permissions.constraints.get( profile_id=cached_gcp_profile_storage['papId'], permission_name='Storage Admin', permission_type='role', @@ -322,7 +333,7 @@ def test_constraints_lint_condition(cached_gcp_profile_storage): "resource.type != 'storage.googleapis.com/Object') || " "resource.name.startsWith('projects/_/buckets/my-first-project-demo-bucket-1')" ) - response = britive.profiles.permissions.constraints.lint_condition( + response = britive.application_management.profiles.permissions.constraints.lint_condition( profile_id=cached_gcp_profile_storage['papId'], permission_name='Storage Admin', permission_type='role', @@ -335,7 +346,7 @@ def test_constraints_lint_condition(cached_gcp_profile_storage): @pytest.mark.skipif(constraints, reason=constraints_skip) def test_constraints_add_big_query(cached_gcp_profile_big_query): - response = britive.profiles.permissions.constraints.add( + response = britive.application_management.profiles.permissions.constraints.add( profile_id=cached_gcp_profile_big_query['papId'], permission_name='BigQuery Admin', permission_type='role', @@ -346,7 +357,7 @@ def test_constraints_add_big_query(cached_gcp_profile_big_query): assert response is None try: - britive.profiles.permissions.constraints.add( + britive.application_management.profiles.permissions.constraints.add( profile_id=cached_gcp_profile_big_query['papId'], permission_name='BigQuery Admin', permission_type='role', @@ -367,7 +378,7 @@ def test_constraints_add_storage(cached_gcp_profile_storage): constraint = {'title': 'test', 'description': 'test', 'expression': expression} - response = britive.profiles.permissions.constraints.add( + response = britive.application_management.profiles.permissions.constraints.add( profile_id=cached_gcp_profile_storage['papId'], permission_name='Storage Admin', permission_type='role', @@ -378,7 +389,7 @@ def test_constraints_add_storage(cached_gcp_profile_storage): assert response is None try: - britive.profiles.permissions.constraints.add( + britive.application_management.profiles.permissions.constraints.add( profile_id=cached_gcp_profile_storage['papId'], permission_name='Storage Admin', permission_type='role', @@ -392,7 +403,7 @@ def test_constraints_add_storage(cached_gcp_profile_storage): @pytest.mark.skipif(constraints, reason=constraints_skip) def test_constraints_remove_big_query(cached_gcp_profile_big_query): try: - response = britive.profiles.permissions.constraints.remove( + response = britive.application_management.profiles.permissions.constraints.remove( profile_id=cached_gcp_profile_big_query['papId'], permission_name='BigQuery Admin', permission_type='role', @@ -402,7 +413,7 @@ def test_constraints_remove_big_query(cached_gcp_profile_big_query): assert response is None finally: - britive.profiles.delete( + britive.application_management.profiles.delete( application_id=os.getenv('BRITIVE_GCP_TEST_APP_ID'), profile_id=cached_gcp_profile_big_query['papId'] ) cleanup('gcp-profile-bq') @@ -411,7 +422,7 @@ def test_constraints_remove_big_query(cached_gcp_profile_big_query): @pytest.mark.skipif(constraints, reason=constraints_skip) def test_constraints_remove_storage(cached_gcp_profile_storage): try: - response = britive.profiles.permissions.constraints.remove( + response = britive.application_management.profiles.permissions.constraints.remove( profile_id=cached_gcp_profile_storage['papId'], permission_name='Storage Admin', permission_type='role', @@ -421,7 +432,7 @@ def test_constraints_remove_storage(cached_gcp_profile_storage): assert response is None finally: - britive.profiles.delete( + britive.application_management.profiles.delete( application_id=os.getenv('BRITIVE_GCP_TEST_APP_ID'), profile_id=cached_gcp_profile_storage['papId'] ) cleanup('gcp-profile-storage') diff --git a/tests/test_265-access_builder.py b/tests/200-application_management-09-access_builder.py similarity index 100% rename from tests/test_265-access_builder.py rename to tests/200-application_management-09-access_builder.py diff --git a/tests/test_270-system_policies.py b/tests/250-system-01-policies.py similarity index 98% rename from tests/test_270-system_policies.py rename to tests/250-system-01-policies.py index b27c9ff..f273f57 100644 --- a/tests/test_270-system_policies.py +++ b/tests/250-system-01-policies.py @@ -1,3 +1,5 @@ +from britive.exceptions import NotFound + from .cache import * # will also import some globals like `britive` @@ -98,7 +100,6 @@ def test_policies_condition_created_as_dict_get_formatted_json(cached_system_lev identifier_type='id', condition_as_dict=False, ) - print(response) assert 'condition' in response assert 'name' in response assert isinstance(response['condition'], str) @@ -206,7 +207,7 @@ def test_delete(cached_system_level_policy): britive.system.policies.delete(policy_identifier=cached_system_level_policy['id'], identifier_type='id') is None ) - with pytest.raises(exceptions.NotFound): + with pytest.raises(NotFound): britive.system.policies.get(cached_system_level_policy['id']) finally: cleanup('policy-system-level') @@ -218,4 +219,4 @@ def test_evaluate(): ) assert isinstance(response, dict) assert len(response) == 1 - assert 'PolicyEvalRequest' in list(response)[0] + assert 'PolicyEvalRequest' in next(iter(response)) diff --git a/tests/test_280_system_actions.py b/tests/250-system-02-actions.py similarity index 86% rename from tests/test_280_system_actions.py rename to tests/250-system-02-actions.py index 42be523..2f299be 100644 --- a/tests/test_280_system_actions.py +++ b/tests/250-system-02-actions.py @@ -18,7 +18,7 @@ def test_list(): assert len(response2) < len(response1) - consumers = list(set([c['consumer'] for c in response2])) + consumers = {c['consumer'] for c in response2} assert len(consumers) == 1 - assert consumers[0] == 'apps' + assert next(iter(consumers)) == 'apps' diff --git a/tests/test_290_system_consumers.py b/tests/250-system-03-consumers.py similarity index 100% rename from tests/test_290_system_consumers.py rename to tests/250-system-03-consumers.py diff --git a/tests/test_300-system_roles.py b/tests/250-system-04-roles.py similarity index 97% rename from tests/test_300-system_roles.py rename to tests/250-system-04-roles.py index 5f9783e..81ea514 100644 --- a/tests/test_300-system_roles.py +++ b/tests/250-system-04-roles.py @@ -1,3 +1,5 @@ +from britive.exceptions import NotFound + from .cache import * # will also import some globals like `britive` @@ -78,7 +80,7 @@ def test_delete(cached_system_level_role): try: assert britive.system.roles.delete(role_identifier=cached_system_level_role['id'], identifier_type='id') is None - with pytest.raises(exceptions.NotFound): + with pytest.raises(NotFound): britive.system.roles.get(cached_system_level_role['id']) finally: cleanup('role-system-level') diff --git a/tests/test_310-system_permissions.py b/tests/250-system-05-permissions.py similarity index 97% rename from tests/test_310-system_permissions.py rename to tests/250-system-05-permissions.py index 3135f58..df3452e 100644 --- a/tests/test_310-system_permissions.py +++ b/tests/250-system-05-permissions.py @@ -1,3 +1,5 @@ +from britive.exceptions import NotFound + from .cache import * # will also import some globals like `britive` @@ -82,7 +84,7 @@ def test_delete(cached_system_level_permission): is None ) - with pytest.raises(exceptions.NotFound): + with pytest.raises(NotFound): britive.system.permissions.get(cached_system_level_permission['id']) finally: cleanup('permission-system-level') diff --git a/tests/test_140-task_services.py b/tests/300-workflows-01-task_services.py similarity index 70% rename from tests/test_140-task_services.py rename to tests/300-workflows-01-task_services.py index 4698733..83b7cb5 100644 --- a/tests/test_140-task_services.py +++ b/tests/300-workflows-01-task_services.py @@ -8,12 +8,12 @@ def test_get(cached_task_service): def test_enable(cached_task_service): - response = britive.task_services.enable(task_service_id=cached_task_service['taskServiceId']) + response = britive.workflows.task_services.enable(task_service_id=cached_task_service['taskServiceId']) assert isinstance(response, dict) assert response['enabled'] def test_disable(cached_task_service): - response = britive.task_services.disable(task_service_id=cached_task_service['taskServiceId']) + response = britive.workflows.task_services.disable(task_service_id=cached_task_service['taskServiceId']) assert isinstance(response, dict) assert not response['enabled'] diff --git a/tests/test_150-tasks.py b/tests/300-workflows-02-tasks.py similarity index 60% rename from tests/test_150-tasks.py rename to tests/300-workflows-02-tasks.py index f2e6438..0f0424d 100644 --- a/tests/test_150-tasks.py +++ b/tests/300-workflows-02-tasks.py @@ -7,20 +7,22 @@ def test_create(cached_task): def test_list(cached_task_service): - tasks = britive.tasks.list(task_service_id=cached_task_service['taskServiceId']) + tasks = britive.workflows.tasks.list(task_service_id=cached_task_service['taskServiceId']) assert isinstance(tasks, list) assert len(tasks) == 1 assert tasks[0]['name'] == 'test' def test_get(cached_task_service, cached_task): - task = britive.tasks.get(task_service_id=cached_task_service['taskServiceId'], task_id=cached_task['taskId']) + task = britive.workflows.tasks.get( + task_service_id=cached_task_service['taskServiceId'], task_id=cached_task['taskId'] + ) assert isinstance(task, dict) assert task['name'] == 'test' def test_update(cached_task_service, cached_task): - task = britive.tasks.update( + task = britive.workflows.tasks.update( task_service_id=cached_task_service['taskServiceId'], task_id=cached_task['taskId'], name='test2' ) assert isinstance(task, dict) @@ -28,8 +30,10 @@ def test_update(cached_task_service, cached_task): def test_delete(cached_task_service, cached_task): - task = britive.tasks.delete(task_service_id=cached_task_service['taskServiceId'], task_id=cached_task['taskId']) + task = britive.workflows.tasks.delete( + task_service_id=cached_task_service['taskServiceId'], task_id=cached_task['taskId'] + ) assert task is None - tasks = britive.tasks.list(task_service_id=cached_task_service['taskServiceId']) + tasks = britive.workflows.tasks.list(task_service_id=cached_task_service['taskServiceId']) assert len(tasks) == 0 cleanup('task') diff --git a/tests/test_230-notifications.py b/tests/300-workflows-03-notifications.py similarity index 78% rename from tests/test_230-notifications.py rename to tests/300-workflows-03-notifications.py index 115f749..388a904 100644 --- a/tests/test_230-notifications.py +++ b/tests/300-workflows-03-notifications.py @@ -7,19 +7,19 @@ def test_create(cached_notification): def test_list(): - notifications = britive.notifications.list() + notifications = britive.workflows.notifications.list() assert isinstance(notifications, list) assert len(notifications) > 0 def test_get(cached_notification): - notification = britive.notifications.get(notification_id=cached_notification['notificationId']) + notification = britive.workflows.notifications.get(notification_id=cached_notification['notificationId']) assert isinstance(notification, dict) assert notification['notificationId'] == cached_notification['notificationId'] def test_update(cached_notification): - notification = britive.notifications.update( + notification = britive.workflows.notifications.update( notification_id=cached_notification['notificationId'], description='test2' ) assert isinstance(notification, dict) @@ -27,13 +27,13 @@ def test_update(cached_notification): def test_disable(cached_notification): - notification = britive.notifications.disable(notification_id=cached_notification['notificationId']) + notification = britive.workflows.notifications.disable(notification_id=cached_notification['notificationId']) assert isinstance(notification, dict) assert notification['status'] == 'Inactive' def test_enable(cached_notification): - notification = britive.notifications.enable(notification_id=cached_notification['notificationId']) + notification = britive.workflows.notifications.enable(notification_id=cached_notification['notificationId']) assert isinstance(notification, dict) assert notification['status'] == 'Active' @@ -60,7 +60,7 @@ def test_configure(cached_notification, cached_notification_rules, cached_notifi if rule['predicate'] in ['AccountsCreated', 'AccountsDeleted']: rules.append(rule) - response = britive.notifications.configure( + response = britive.workflows.notifications.configure( notification_id=cached_notification['notificationId'], users=[cached_user['userId']], rules=rules, @@ -74,7 +74,7 @@ def test_configure(cached_notification, cached_notification_rules, cached_notifi def test_delete(cached_notification): - response = britive.notifications.delete(notification_id=cached_notification['notificationId']) + response = britive.workflows.notifications.delete(notification_id=cached_notification['notificationId']) assert response is None cleanup('notification') cleanup('notification-available-rules') diff --git a/tests/test_330-response_templates.py b/tests/350-access_broker-01-response_templates.py similarity index 100% rename from tests/test_330-response_templates.py rename to tests/350-access_broker-01-response_templates.py diff --git a/tests/test_340-resource_types.py b/tests/350-access_broker-02-resource_types.py similarity index 100% rename from tests/test_340-resource_types.py rename to tests/350-access_broker-02-resource_types.py diff --git a/tests/test_350-resource_labels.py b/tests/350-access_broker-03-resource_labels.py similarity index 100% rename from tests/test_350-resource_labels.py rename to tests/350-access_broker-03-resource_labels.py diff --git a/tests/test_360-resource.py b/tests/350-access_broker-04-resource.py similarity index 100% rename from tests/test_360-resource.py rename to tests/350-access_broker-04-resource.py diff --git a/tests/test_370-resource_permissions.py b/tests/350-access_broker-05-resource_permissions.py similarity index 100% rename from tests/test_370-resource_permissions.py rename to tests/350-access_broker-05-resource_permissions.py diff --git a/tests/test_380-access_broker_profiles.py b/tests/350-access_broker-06-profiles.py similarity index 100% rename from tests/test_380-access_broker_profiles.py rename to tests/350-access_broker-06-profiles.py diff --git a/tests/test_390-access_broker_profiles_policies.py b/tests/350-access_broker-07-profiles_policies.py similarity index 100% rename from tests/test_390-access_broker_profiles_policies.py rename to tests/350-access_broker-07-profiles_policies.py diff --git a/tests/test_400-access_broker_permissions.py b/tests/350-access_broker-08-permissions.py similarity index 100% rename from tests/test_400-access_broker_permissions.py rename to tests/350-access_broker-08-permissions.py diff --git a/tests/400-security-01-policies.py b/tests/400-security-01-policies.py new file mode 100644 index 0000000..0dfad0a --- /dev/null +++ b/tests/400-security-01-policies.py @@ -0,0 +1,46 @@ +from .cache import * # will also import some globals like `britive` + + +def test_create(cached_security_policy): + assert isinstance(cached_security_policy, dict) + + +def test_list(cached_security_policy): + policies = britive.security.security_policies.list() + assert isinstance(policies, list) + assert cached_security_policy['id'] in [p['id'] for p in policies] + + +def test_get(cached_security_policy): + policy = britive.security.security_policies.get(security_policy_id=cached_security_policy['id']) + assert isinstance(policy, dict) + + +def test_disable(cached_security_policy): + response = britive.security.security_policies.disable(security_policy_id=cached_security_policy['id']) + assert response is None + policy = britive.security.security_policies.get(security_policy_id=cached_security_policy['id']) + assert policy['status'] == 'Inactive' + + +def test_enable(cached_security_policy): + response = britive.security.security_policies.enable(security_policy_id=cached_security_policy['id']) + assert response is None + policy = britive.security.security_policies.get(security_policy_id=cached_security_policy['id']) + assert policy['status'] == 'Active' + + +def test_update(cached_security_policy): + response = britive.security.security_policies.update( + security_policy_id=cached_security_policy['id'], ips=['2.2.2.2'] + ) + assert response is None + + policy = britive.security.security_policies.get(security_policy_id=cached_security_policy['id']) + assert policy['conditions'][0]['values'] == ['2.2.2.2'] + + +def test_delete(cached_security_policy): + response = britive.security.security_policies.delete(security_policy_id=cached_security_policy['id']) + assert response is None + cleanup('security-policy') diff --git a/tests/test_170-saml.py b/tests/400-security-02-saml.py similarity index 74% rename from tests/test_170-saml.py rename to tests/400-security-02-saml.py index d968112..d6143f0 100644 --- a/tests/test_170-saml.py +++ b/tests/400-security-02-saml.py @@ -2,25 +2,25 @@ def test_settings(): - settings = britive.saml.settings() + settings = britive.security.saml.settings() assert isinstance(settings, dict) assert 'issuer' in settings assert 'id' in settings assert 'signInUrl' in settings assert 'signOutUrl' in settings assert 'x509CertExpirationDate' in settings - settings = britive.saml.settings(as_list=True) + settings = britive.security.saml.settings(as_list=True) assert isinstance(settings, list) assert len(settings) == 1 def test_metadata(): - metadata = britive.saml.metadata() + metadata = britive.security.saml.metadata() assert isinstance(metadata, str) assert 'ds:X509Certificate' in metadata def test_download(): - certificate = britive.saml.certificate() + certificate = britive.security.saml.certificate() assert isinstance(certificate, str) assert '-----BEGIN CERTIFICATE-----' in certificate diff --git a/tests/test_180-api_tokens.py b/tests/400-security-03-api_tokens.py similarity index 100% rename from tests/test_180-api_tokens.py rename to tests/400-security-03-api_tokens.py diff --git a/tests/500-audit_logs-01-logs.py b/tests/500-audit_logs-01-logs.py new file mode 100644 index 0000000..64c821d --- /dev/null +++ b/tests/500-audit_logs-01-logs.py @@ -0,0 +1,31 @@ +from datetime import datetime, timedelta, timezone + +from .cache import britive + + +def test_fields(): + fields = britive.audit_logs.logs.fields() + assert isinstance(fields, dict) + assert len(fields) == 18 + + +def test_operators(): + operators = britive.audit_logs.logs.operators() + assert isinstance(operators, dict) + assert len(operators) == 4 + + +def test_query_json(): + events = britive.audit_logs.logs.query( + from_time=(datetime.now(timezone.utc) - timedelta(1)), to_time=datetime.now(timezone.utc) + ) + assert isinstance(events, list) + assert isinstance(events[0], dict) + assert len(events) % 100 != 0 # v2.8.1 - adding check due to pagination bug not including the last page + + +def test_query_csv(): + csv = britive.audit_logs.logs.query( + from_time=datetime.now(timezone.utc) - timedelta(1), to_time=datetime.now(timezone.utc), csv=True + ) + assert '"timestamp","actor.display_name"' in csv diff --git a/tests/test_275-audit_logs_webhooks.py b/tests/500-audit_logs-02-webhooks.py similarity index 100% rename from tests/test_275-audit_logs_webhooks.py rename to tests/500-audit_logs-02-webhooks.py diff --git a/tests/test_200-reports.py b/tests/550-reports-01-reports.py similarity index 100% rename from tests/test_200-reports.py rename to tests/550-reports-01-reports.py diff --git a/tests/test_220-my_access.py b/tests/600-britive-01-my_access.py similarity index 64% rename from tests/test_220-my_access.py rename to tests/600-britive-01-my_access.py index 0afb930..432771a 100644 --- a/tests/test_220-my_access.py +++ b/tests/600-britive-01-my_access.py @@ -9,6 +9,7 @@ def test_whoami(): print(json.dumps(me, indent=2, default=str)) +@pytest.mark.skipif(scan_skip, reason=scan_skip_message) def test_list_profiles(): profiles = britive.my_access.list_profiles() assert isinstance(profiles, list) @@ -83,59 +84,3 @@ def test_frequents(): def test_favorites(): profiles = britive.my_access.favorites() assert isinstance(profiles, list) - - -@pytest.mark.skipif(scan_skip, reason=scan_skip_message) -def test_request_and_approve( - cached_profile, cached_service_identity_token, cached_environment, cached_service_identity -): - token = britive.service_identity_tokens.create(cached_service_identity['userId'], 90)['token'] - other_britive = Britive(token=token, query_features=False) - - request = other_britive.my_access.request_approval( - profile_id=cached_profile['papId'], environment_id=cached_environment['id'], justification='let me in' - ) - - assert 'requestId' in request - - request_id = request['requestId'] - - approvals = britive.my_access.list_approvals() - for approval in approvals: - if approval['requestId'] == request_id: - assert approval['status'] == 'PENDING' - break - - response = britive.my_access.reject_request(request_id=request_id) - - assert response is None - - approvals = britive.my_access.list_approvals() - for approval in approvals: - if approval['requestId'] == request_id: - assert approval['status'] == 'REJECTED' - break - - request = other_britive.my_access.request_approval( - profile_id=cached_profile['papId'], environment_id=cached_environment['id'], justification='let me in' - ) - - assert 'requestId' in request - - request_id = request['requestId'] - - approvals = britive.my_access.list_approvals() - for approval in approvals: - if approval['requestId'] == request_id: - assert approval['status'] == 'PENDING' - break - - response = britive.my_access.approve_request(request_id=request_id) - - assert response is None - - approvals = britive.my_access.list_approvals() - for approval in approvals: - if approval['requestId'] == request_id: - assert approval['status'] == 'APPROVED' - break diff --git a/tests/test_250-my_secrets.py b/tests/600-britive-02-my_secrets.py similarity index 100% rename from tests/test_250-my_secrets.py rename to tests/600-britive-02-my_secrets.py diff --git a/tests/600-britive-03-my_requests.py b/tests/600-britive-03-my_requests.py new file mode 100644 index 0000000..a65818a --- /dev/null +++ b/tests/600-britive-03-my_requests.py @@ -0,0 +1,14 @@ +from .cache import * # will also import some globals like `britive` + + +@pytest.mark.skipif(scan_skip, reason=scan_skip_message) +def test_request(cached_profile_checkout_request): + assert 'requestId' in cached_profile_checkout_request + + request_id = request['requestId'] + + approvals = britive.my_requests.list() + for approval in approvals: + if approval['requestId'] == request_id: + assert approval['status'] == 'PENDING' + break diff --git a/tests/600-britive-04-my_approvals.py b/tests/600-britive-04-my_approvals.py new file mode 100644 index 0000000..23de0a8 --- /dev/null +++ b/tests/600-britive-04-my_approvals.py @@ -0,0 +1,16 @@ +from .cache import * # will also import some globals like `britive` + + +@pytest.mark.skipif(scan_skip, reason=scan_skip_message) +def test_approval(cached_profile_checkout_request): + request_id = request['requestId'] + + response = britive.my_approvals.reject_request(request_id=request_id) + + assert response is None + + approvals = britive.my_approvals.list() + for approval in approvals: + if approval['requestId'] == request_id: + assert approval['status'] == 'REJECTED' + break diff --git a/tests/test_990-delete_all_resources.py b/tests/999-cleanup-01-delete_all_resources.py similarity index 79% rename from tests/test_990-delete_all_resources.py rename to tests/999-cleanup-01-delete_all_resources.py index 9af2545..25b6ef5 100644 --- a/tests/test_990-delete_all_resources.py +++ b/tests/999-cleanup-01-delete_all_resources.py @@ -7,7 +7,18 @@ from .cache import * # will also import some globals like `britive` -# 400-access_broker_permissions +# 500-audit_logs +def test_audit_logs_webhook_delete(cached_notification_medium_webhook): + try: + response = britive.audit_logs.webhooks.delete(notification_medium_id=cached_notification_medium_webhook['id']) + assert response is None + except exceptions.InvalidRequest as e: + assert 'Auditlog webhook not found' in str(e) + finally: + cleanup('audit-logs-webhook') + + +# 350-access_broker def test_delete_access_broker_profile_permission(cached_access_broker_profile_permission, cached_access_broker_profile): try: response = britive.access_broker.profiles.permissions.delete_permission( @@ -19,7 +30,6 @@ def test_delete_access_broker_profile_permission(cached_access_broker_profile_pe cleanup('access-broker-profile-permission') -# 390-access_broker_profiles_policies def test_delete_access_broker_profile_policy(cached_access_broker_profile_policy, cached_access_broker_profile): try: response = britive.access_broker.profiles.policies.delete( @@ -30,7 +40,6 @@ def test_delete_access_broker_profile_policy(cached_access_broker_profile_policy cleanup('access-broker-profile-policy') -# 380-access_broker_profiles def test_delete_access_broker_profile(cached_access_broker_profile): try: response = britive.access_broker.profiles.delete(cached_access_broker_profile['profileId']) @@ -39,7 +48,6 @@ def test_delete_access_broker_profile(cached_access_broker_profile): cleanup('access-broker-profile') -# 360-resource_permissions def test_delete_access_broker_resource(cached_access_broker_resource): try: response = britive.access_broker.resources.delete(cached_access_broker_resource['resourceId']) @@ -48,7 +56,6 @@ def test_delete_access_broker_resource(cached_access_broker_resource): cleanup('access-broker-resource') -# 350-resource_labels def test_delete_resource_label(cached_access_broker_resource_label): try: response = britive.access_broker.resources.labels.delete(cached_access_broker_resource_label['keyId']) @@ -57,7 +64,6 @@ def test_delete_resource_label(cached_access_broker_resource_label): cleanup('access-broker-resource-label') -# 340-resource_types def test_delete_resource_type(cached_access_broker_resource_type): remaining_permissions = britive.access_broker.resources.permissions.list( resource_type_id=cached_access_broker_resource_type['resourceTypeId'] @@ -77,7 +83,6 @@ def test_delete_resource_type(cached_access_broker_resource_type): cleanup('access-broker-resource-type') -# 330-response_templates def test_response_template_delete(cached_access_broker_response_template): try: response = britive.access_broker.response_templates.delete(cached_access_broker_response_template['templateId']) @@ -86,7 +91,7 @@ def test_response_template_delete(cached_access_broker_response_template): cleanup('access-broker-response-template') -# 310-system_permissions +# 250-system def test_system_level_permission_delete(cached_system_level_permission): try: response = britive.system.permissions.delete(cached_system_level_permission['id']) @@ -95,7 +100,6 @@ def test_system_level_permission_delete(cached_system_level_permission): cleanup('permission-system-level') -# 300-system_roles def test_system_level_role_delete(cached_system_level_role): try: response = britive.system.roles.delete(cached_system_level_role['id']) @@ -104,19 +108,7 @@ def test_system_level_role_delete(cached_system_level_role): cleanup('role-system-level') -# 275-audit_logs_webhooks -def test_audit_logs_webhook_delete(cached_notification_medium_webhook): - try: - response = britive.audit_logs.webhooks.delete(notification_medium_id=cached_notification_medium_webhook['id']) - assert response is None - except exceptions.InvalidRequest as e: - assert 'Auditlog webhook not found' in str(e) - finally: - cleanup('audit-logs-webhook') - - -# 270-system_policies -def test_system_level_policy_delete_delete(cached_system_level_policy): +def test_system_level_policy_delete(cached_system_level_policy): try: response = britive.system.policies.delete(cached_system_level_policy['id']) assert response is None @@ -142,10 +134,10 @@ def test_system_level_policy_condition_as_dictionary_delete(cached_system_level_ cleanup('policy-system-level-condition-as-dict') -# 265-access-buildersettings +# 200-application_management def test_access_builder_associations_delete(cached_application, cached_access_builder_associations): try: - response = britive.access_builder.associations.delete( + response = britive.application_management.access_builder.associations.delete( application_id=cached_application['appContainerId'], association_id=cached_access_builder_associations['associationApproversSummary'][0]['id'], ) @@ -156,7 +148,7 @@ def test_access_builder_associations_delete(cached_application, cached_access_bu def test_access_builder_approvers_groups_delete(cached_application, cached_access_builder_approvers_groups): try: - response = britive.access_builder.approvers_groups.delete( + response = britive.application_management.access_builder.approvers_groups.delete( application_id=cached_application['appContainerId'], group_id=cached_access_builder_approvers_groups.get('id'), ) @@ -167,7 +159,7 @@ def test_access_builder_approvers_groups_delete(cached_application, cached_acces def test_add_notification_to_access_builder_delete(cached_application, cached_add_notification_to_access_builder): try: - response = britive.access_builder.notifications.update( + response = britive.application_management.access_builder.notifications.update( cached_application['appContainerId'], notification_mediums=[] ) assert response is None @@ -177,104 +169,9 @@ def test_add_notification_to_access_builder_delete(cached_application, cached_ad cleanup('access-builder-approvers-groups') -# 260-notification-mediums -def test_notification_medium_delete(cached_notification_medium): - try: - response = britive.notification_mediums.delete(cached_notification_medium['id']) - assert response is None - finally: - cleanup('notification-medium') - - -def test_notification_medium_webhook_delete(cached_notification_medium_webhook): - try: - response = britive.notification_mediums.delete(cached_notification_medium_webhook['id']) - assert response is None - finally: - cleanup('notification-medium-webhook') - - -# 240-secrets_manager -def test_folder_delete(cached_folder, cached_vault): - try: - response = britive.secrets_manager.folders.delete(path=cached_folder['path'], vault_id=cached_vault['id']) - assert response is None - finally: - cleanup('folder') - - -def test_secret_delete(cached_secret, cached_vault): - try: - response = britive.secrets_manager.secrets.delete(path=cached_secret['path'], vault_id=cached_vault['id']) - assert response is None - finally: - cleanup('secret') - - -def test_static_secret_templates_delete(cached_static_secret_template): - try: - response = britive.secrets_manager.static_secret_templates.delete(cached_static_secret_template['id']) - assert response is None - finally: - cleanup('static-secret-templates') - - -def test_password_policies_delete(cached_password_policies): - try: - response = britive.secrets_manager.password_policies.delete(cached_password_policies['id']) - assert response is None - finally: - cleanup('password-policies') - - -def test_pin_policy_delete(cached_pin_policies): - try: - response = britive.secrets_manager.password_policies.delete(cached_pin_policies['id']) - assert response is None - finally: - cleanup('pin-policies') - - -def test_policy_delete(cached_policy): - try: - response = britive.secrets_manager.policies.delete(cached_policy['id']) - assert response is None - finally: - cleanup('policy') - - -def test_vault_delete(cached_vault): - try: - if cached_vault.get('DONOTDELETE'): - assert True - else: - response = britive.secrets_manager.vaults.delete(cached_vault['id']) - assert response is None - finally: - cleanup('vault') - - -# 215-workload -def test_workload_identity_provider_aws_delete(cached_workload_identity_provider_aws): - try: - response = britive.workload.identity_providers.delete(cached_workload_identity_provider_aws['id']) - assert response is None - finally: - cleanup('workload-identity-provider-aws') - - -def test_workload_identity_provider_oidc_delete(cached_workload_identity_provider_oidc): - try: - response = britive.workload.identity_providers.delete(cached_workload_identity_provider_oidc['id']) - assert response is None - finally: - cleanup('workload-identity-provider-oidc') - - -# 130-profiles def test_profile_approval_policy_delete(cached_profile, cached_profile_approval_policy): try: - response = britive.profiles.policies.delete( + response = britive.application_management.profiles.policies.delete( profile_id=cached_profile['papId'], policy_id=cached_profile_approval_policy['id'] ) assert response is None @@ -284,7 +181,7 @@ def test_profile_approval_policy_delete(cached_profile, cached_profile_approval_ def test_profile_policy_condition_as_dict_delete(cached_profile, cached_profile_policy_condition_as_dict): try: - response = britive.profiles.policies.delete( + response = britive.application_management.profiles.policies.delete( profile_id=cached_profile['papId'], policy_id=cached_profile_policy_condition_as_dict['id'] ) assert response is None @@ -294,7 +191,7 @@ def test_profile_policy_condition_as_dict_delete(cached_profile, cached_profile_ def test_profile_policy_condition_as_json_str_delete(cached_profile, cached_profile_policy_condition_as_json_str): try: - response = britive.profiles.policies.delete( + response = britive.application_management.profiles.policies.delete( profile_id=cached_profile['papId'], policy_id=cached_profile_policy_condition_as_json_str['id'] ) assert response is None @@ -304,7 +201,7 @@ def test_profile_policy_condition_as_json_str_delete(cached_profile, cached_prof def test_profile_policy_delete(cached_profile, cached_profile_policy): try: - response = britive.profiles.policies.delete( + response = britive.application_management.profiles.policies.delete( profile_id=cached_profile['papId'], policy_id=cached_profile_policy['id'] ) assert response is None @@ -314,11 +211,11 @@ def test_profile_policy_delete(cached_profile, cached_profile_policy): def test_profile_delete(cached_profile): try: - response = britive.profiles.delete( + response = britive.application_management.profiles.delete( application_id=cached_profile['appContainerId'], profile_id=cached_profile['papId'] ) - profiles = britive.profiles.list(application_id=cached_profile['appContainerId']) + profiles = britive.application_management.profiles.list(application_id=cached_profile['appContainerId']) assert response is None assert cached_profile['papId'] not in [p['papId'] for p in profiles] @@ -328,10 +225,9 @@ def test_profile_delete(cached_profile): cleanup('static-session-attribute') -# 070-environments def test_environment_delete(cached_application, cached_environment): try: - response = britive.environments.delete( + response = britive.application_management.environments.delete( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) assert response is None @@ -340,21 +236,24 @@ def test_environment_delete(cached_application, cached_environment): cleanup('scan') -# 060-environment_groups def test_environment_group_delete(cached_application, cached_environment_group): try: - groups = britive.environment_groups.list(application_id=cached_application['appContainerId']) + groups = britive.application_management.environment_groups.list( + application_id=cached_application['appContainerId'] + ) num_root_groups = 0 for group in groups: if group['parentId'] != '': # cannot delete root groups - error A-0003 is thrown when attempting - response = britive.environment_groups.delete( + response = britive.application_management.environment_groups.delete( application_id=cached_application['appContainerId'], environment_group_id=cached_environment_group['id'], ) assert response is None else: num_root_groups += 1 - groups = britive.environment_groups.list(application_id=cached_application['appContainerId']) + groups = britive.application_management.environment_groups.list( + application_id=cached_application['appContainerId'] + ) assert isinstance(groups, list) assert len(groups) == num_root_groups # the root group will remain assert isinstance(groups[0], dict) @@ -362,14 +261,15 @@ def test_environment_group_delete(cached_application, cached_environment_group): cleanup('environment-group') -# 050-applications def test_application_delete(cached_application): try: while True: try: - response = britive.applications.delete(application_id=cached_application['appContainerId']) + response = britive.application_management.applications.delete( + application_id=cached_application['appContainerId'] + ) break - except exceptions.InvalidRequest: + except (exceptions.InvalidRequest, exceptions.badrequest.ApplicationDeletionError): sleep(5) assert response is None finally: @@ -381,56 +281,141 @@ def test_application_delete(cached_application): cleanup('task-service') -# 040-service_identity_tokens -def test_service_identity_tokens_delete(cached_service_identity): +# 150-secrets_manager +def test_folder_delete(cached_folder, cached_vault): try: - britive.service_identities.delete(cached_service_identity['userId']) - with pytest.raises(exceptions.InvalidRequest): - britive.service_identity_tokens.get(cached_service_identity['userId']) + response = britive.secrets_manager.folders.delete(path=cached_folder['path'], vault_id=cached_vault['id']) + assert response is None finally: - cleanup('service-identity-token') - cleanup('service-identity-token-updated') + cleanup('folder') + + +def test_secret_delete(cached_secret, cached_vault): + try: + response = britive.secrets_manager.secrets.delete(path=cached_secret['path'], vault_id=cached_vault['id']) + assert response is None + finally: + cleanup('secret') -# 030-service_identities -def test_service_identities_delete(cached_service_identity): +def test_static_secret_templates_delete(cached_static_secret_template): try: - response = britive.service_identities.delete(service_identity_id=cached_service_identity['userId']) + response = britive.secrets_manager.static_secret_templates.delete(cached_static_secret_template['id']) assert response is None + finally: + cleanup('static-secret-templates') + + +def test_password_policies_delete(cached_password_policies): + try: + response = britive.secrets_manager.password_policies.delete(cached_password_policies['id']) + assert response is None + finally: + cleanup('password-policies') + - with pytest.raises(exceptions.NotFound): - britive.service_identities.get_by_name(name=cached_service_identity['name']) +def test_pin_policy_delete(cached_pin_policies): + try: + response = britive.secrets_manager.password_policies.delete(cached_pin_policies['id']) + assert response is None + finally: + cleanup('pin-policies') + + +def test_policy_delete(cached_policy): + try: + response = britive.secrets_manager.policies.delete(cached_policy['id']) + assert response is None + finally: + cleanup('policy') + + +def test_vault_delete(cached_vault): + try: + if cached_vault.get('DONOTDELETE'): + assert True + else: + response = britive.secrets_manager.vaults.delete(cached_vault['id']) + assert response is None + finally: + cleanup('vault') + + +# 100-identity_management +def test_workload_identity_provider_aws_delete(cached_workload_identity_provider_aws): + try: + response = britive.identity_management.workload.identity_providers.delete( + cached_workload_identity_provider_aws['id'] + ) + assert response is None + finally: + cleanup('workload-identity-provider-aws') + + +def test_workload_identity_provider_oidc_delete(cached_workload_identity_provider_oidc): + try: + response = britive.identity_management.workload.identity_providers.delete( + cached_workload_identity_provider_oidc['id'] + ) + assert response is None + finally: + cleanup('workload-identity-provider-oidc') + + +def test_service_identities_delete(cached_service_identity, cached_service_identity_federated): + try: + for si in [cached_service_identity, cached_service_identity_federated]: + response = britive.identity_management.service_identities.delete(service_identity_id=si['userId']) + assert response is None + assert not britive.identity_management.service_identities.get_by_name(name=si['name']) except exceptions.NotFound: pass finally: cleanup('service-identity') + cleanup('service-identity-federated') + cleanup('service-identity-token') + cleanup('service-identity-token-updated') -# 020-tags def test_tags_delete(cached_tag): try: - response = britive.tags.delete(cached_tag['userTagId']) + response = britive.identity_management.tags.delete(cached_tag['userTagId']) assert response is None finally: cleanup('tag') -# 010-users def test_user_delete(cached_user): try: - response = britive.users.delete(user_id=cached_user['userId']) + response = britive.identity_management.users.delete(user_id=cached_user['userId']) assert response is None - users = britive.users.get_by_name(name=cached_user['lastName']) + users = britive.identity_management.users.get_by_name(name=cached_user['lastName']) assert isinstance(users, list) assert len(users) == 0 finally: cleanup('user') -# 005-identity_attributes +# 000-global_settings +def test_notification_medium_delete(cached_notification_medium): + try: + response = britive.global_settings.notification_mediums.delete(cached_notification_medium['id']) + assert response is None + finally: + cleanup('notification-medium') + + +def test_notification_medium_webhook_delete(cached_notification_medium_webhook): + try: + response = britive.global_settings.notification_mediums.delete(cached_notification_medium_webhook['id']) + assert response is None + finally: + cleanup('notification-medium-webhook') + + def test_identity_attribute_delete(cached_identity_attribute): try: - response = britive.identity_attributes.delete(attribute_id=cached_identity_attribute['id']) + response = britive.identity_management.identity_attributes.delete(attribute_id=cached_identity_attribute['id']) assert response is None finally: cleanup('identity-attribute') diff --git a/tests/cache.py b/tests/cache.py index 3140f22..b79c1cb 100644 --- a/tests/cache.py +++ b/tests/cache.py @@ -6,11 +6,11 @@ import pytest -from britive import exceptions # exceptions used in test files so including here for ease - # don't worry about these invalid references - it will be fixed up if we are running local tests # vs running it through tox from britive.britive import Britive +from britive.exceptions import InternalServerError +from britive.exceptions.badrequest import UserCreationError britive = Britive() # source details from environment variables scan_skip = bool(os.getenv('BRITIVE_TEST_IGNORE_SCAN')) @@ -75,14 +75,14 @@ def cached_user(pytestconfig, timestamp): 'password': generate_random_password(), 'status': 'active', } - return britive.users.create(**user_to_create) + return britive.identity_management.users.create(**user_to_create) @pytest.fixture(scope='session') @cached_resource(name='tag') def cached_tag(pytestconfig, timestamp): tag_to_create = {'name': f'testpythonapiwrappertag-{timestamp}'} - return britive.tags.create(**tag_to_create) + return britive.identity_management.tags.create(**tag_to_create) @pytest.fixture(scope='session') @@ -92,25 +92,41 @@ def cached_service_identity(pytestconfig, timestamp): 'name': f'testpythonapiwrapperserviceidentity{timestamp}', 'status': 'active', } - return britive.service_identities.create(**service_identity_to_create) + try: + return britive.identity_management.service_identities.create(**service_identity_to_create) + except UserCreationError: + return britive.identity_management.service_identities.get_by_name(service_identity_to_create['name'])[0] + + +@pytest.fixture(scope='session') +@cached_resource(name='service-identity-federated') +def cached_service_identity_federated(pytestconfig, timestamp): + service_identity_to_create = { + 'name': f'testpythonapiwrapperfederated{timestamp}', + 'status': 'active', + } + try: + return britive.identity_management.service_identities.create(**service_identity_to_create) + except UserCreationError: + return britive.identity_management.service_identities.get_by_name(service_identity_to_create['name'])[0] @pytest.fixture(scope='session') @cached_resource(name='service-identity-token') def cached_service_identity_token(pytestconfig, cached_service_identity): - return britive.service_identity_tokens.create(cached_service_identity['userId'], 90) + return britive.identity_management.service_identity_tokens.create(cached_service_identity['userId'], 90) @pytest.fixture(scope='session') @cached_resource(name='service-identity-token-updated') def cached_service_identity_token_updated(pytestconfig, cached_service_identity): - return britive.service_identity_tokens.update(cached_service_identity['userId'], 45) + return britive.identity_management.service_identity_tokens.update(cached_service_identity['userId'], 45) @pytest.fixture(scope='session') @cached_resource(name='catalog') def cached_catalog(pytestconfig): - apps = britive.applications.catalog() + apps = britive.application_management.applications.catalog() catalog = {} for app in apps: catalog[app['key']] = app @@ -121,7 +137,7 @@ def cached_catalog(pytestconfig): @cached_resource(name='application') def cached_application(pytestconfig, timestamp, cached_catalog): aws_standalone_catalog_id = cached_catalog['AWS Standalone-1.0']['catalogAppId'] - return britive.applications.create( + return britive.application_management.applications.create( catalog_id=aws_standalone_catalog_id, application_name=f'aws-pythonapiwrapper-test-{timestamp}' ) @@ -129,14 +145,16 @@ def cached_application(pytestconfig, timestamp, cached_catalog): @pytest.fixture(scope='session') @cached_resource(name='application-updated') def cached_application_updated(pytestconfig, cached_catalog): - return britive.applications.update(application_id=cached_application['appContainerId'], region='us-east-1') + return britive.application_management.applications.update( + application_id=cached_application['appContainerId'], region='us-east-1' + ) @pytest.fixture(scope='session') @cached_resource(name='environment-group') def cached_environment_group(pytestconfig, timestamp, cached_application): environment_group_to_create = {'name': f'Test-{timestamp}'} - return britive.environment_groups.create( + return britive.application_management.environment_groups.create( application_id=cached_application['appContainerId'], name=environment_group_to_create['name'] ) @@ -145,7 +163,7 @@ def cached_environment_group(pytestconfig, timestamp, cached_application): @cached_resource(name='environment') def cached_environment(pytestconfig, timestamp, cached_application): environment_to_create = {'name': f'Sigma Labs Test-{timestamp}'} - return britive.environments.create( + return britive.application_management.environments.create( application_id=cached_application['appContainerId'], name=environment_to_create['name'] ) @@ -153,7 +171,7 @@ def cached_environment(pytestconfig, timestamp, cached_application): @pytest.fixture(scope='session') @cached_resource(name='scan') def cached_scan(pytestconfig, cached_application, cached_environment): - return britive.scans.scan( + return britive.application_management.scans.scan( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) @@ -161,7 +179,7 @@ def cached_scan(pytestconfig, cached_application, cached_environment): @pytest.fixture(scope='session') @cached_resource(name='account') def cached_account(pytestconfig, cached_application, cached_environment): - accounts = britive.accounts.list( + accounts = britive.application_management.accounts.list( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) @@ -176,7 +194,7 @@ def cached_account(pytestconfig, cached_application, cached_environment): @pytest.fixture(scope='session') @cached_resource(name='permission') def cached_permission(pytestconfig, cached_application, cached_environment): - return britive.permissions.list( + return britive.application_management.permissions.list( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] )[0] @@ -184,7 +202,7 @@ def cached_permission(pytestconfig, cached_application, cached_environment): @pytest.fixture(scope='session') @cached_resource(name='group') def cached_group(pytestconfig, cached_application, cached_environment): - return britive.groups.list( + return britive.application_management.groups.list( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] )[0] @@ -192,7 +210,7 @@ def cached_group(pytestconfig, cached_application, cached_environment): @pytest.fixture(scope='session') @cached_resource(name='identity-attribute') def cached_identity_attribute(pytestconfig, timestamp): - return britive.identity_attributes.create( + return britive.identity_management.identity_attributes.create( name=f'python-sdk-test-{timestamp}', description='test', data_type='String', multi_valued=False ) @@ -200,65 +218,76 @@ def cached_identity_attribute(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='profile') def cached_profile(pytestconfig, timestamp, cached_application): - return britive.profiles.create(application_id=cached_application['appContainerId'], name=f'test-{timestamp}') + return britive.application_management.profiles.create( + application_id=cached_application['appContainerId'], name=f'test-{timestamp}' + ) @pytest.fixture(scope='session') @cached_resource(name='profile-policy') def cached_profile_policy(pytestconfig, cached_profile, cached_tag): - policy = britive.profiles.policies.build( + policy = britive.application_management.profiles.policies.build( name=cached_profile['papId'], description=cached_tag['name'], tags=[cached_tag['name']], stepup_auth=True, always_prompt_stepup_auth=False, ) - return britive.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) + return britive.application_management.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) @pytest.fixture(scope='session') @cached_resource(name='profile-policy-str') def cached_profile_policy_condition_as_json_str(pytestconfig, cached_profile, cached_tag): - policy = britive.profiles.policies.build( + policy = britive.application_management.profiles.policies.build( name=f"{cached_profile['papId']}_json", description=cached_tag['name'], tags=[cached_tag['name']], ips=['12.12.12.12', '13.13.13.13'], condition_as_dict=False, ) - return britive.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) + return britive.application_management.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) @pytest.fixture(scope='session') @cached_resource(name='profile-policy-dict') def cached_profile_policy_condition_as_dict(pytestconfig, cached_profile, cached_tag): - policy = britive.profiles.policies.build( + policy = britive.application_management.profiles.policies.build( name=f"{cached_profile['papId']}_dict", description=cached_tag['name'], tags=[cached_tag['name']], ips=['12.12.12.12', '13.13.13.13'], condition_as_dict=True, ) - return britive.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) + return britive.application_management.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) @pytest.fixture(scope='session') @cached_resource(name='profile-approval-policy') def cached_profile_approval_policy(pytestconfig, cached_profile, cached_service_identity, cached_user): - policy = britive.profiles.policies.build( + policy = britive.application_management.profiles.policies.build( name=f"{cached_profile['papId']}-2", description='', service_identities=[cached_service_identity['username']], approval_notification_medium='Email', approver_users=[cached_user['username']], ) - return britive.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) + return britive.application_management.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) + + +@pytest.fixture(scope='session') +@cached_resource(name='profile-checkout-request') +def cached_profile_checkout_request(pytestconfig, cached_profile, cached_service_identity_token): + other_britive = Britive(token=cached_service_identity_token['token'], query_features=False) + return other_britive.my_access.request_approval( + profile_id=cached_profile['papId'], environment_id=cached_environment['id'], justification='reject me' + ) @pytest.fixture(scope='session') @cached_resource(name='static-session-attribute') def cached_static_session_attribute(pytestconfig, cached_profile): - return britive.profiles.session_attributes.add_static( + return britive.application_management.profiles.session_attributes.add_static( profile_id=cached_profile['papId'], tag_name='test-static', tag_value='test' ) @@ -266,14 +295,14 @@ def cached_static_session_attribute(pytestconfig, cached_profile): @pytest.fixture(scope='session') @cached_resource(name='dynamic-session-attribute') def cached_dynamic_session_attribute(pytestconfig, cached_profile): - attributes = britive.identity_attributes.list() + attributes = britive.identity_management.identity_attributes.list() email_id = None for attribute in attributes: if attribute['builtIn'] and attribute['name'] == 'Email': email_id = attribute['id'] break - return britive.profiles.session_attributes.add_dynamic( + return britive.application_management.profiles.session_attributes.add_dynamic( profile_id=cached_profile['papId'], identity_attribute_id=email_id, tag_name='test-dynamic' ) @@ -281,13 +310,13 @@ def cached_dynamic_session_attribute(pytestconfig, cached_profile): @pytest.fixture(scope='session') @cached_resource(name='task-service') def cached_task_service(pytestconfig, cached_application): - return britive.task_services.get(application_id=cached_application['appContainerId']) + return britive.workflows.task_services.get(application_id=cached_application['appContainerId']) @pytest.fixture(scope='session') @cached_resource(name='task') def cached_task(pytestconfig, cached_task_service, cached_application, cached_environment): - return britive.tasks.create( + return britive.workflows.tasks.create( task_service_id=cached_task_service['taskServiceId'], name='test', frequency_type='Monthly', @@ -304,7 +333,7 @@ def cached_task(pytestconfig, cached_task_service, cached_application, cached_en @pytest.fixture(scope='session') @cached_resource(name='security-policy') def cached_security_policy(pytestconfig, timestamp, cached_service_identity_token_updated): - return britive.security_policies.create( + return britive.security.security_policies.create( name=f'test-{timestamp}', description='test', ips=['1.1.1.1', '10.0.0.0/16'], @@ -322,13 +351,13 @@ def cached_api_token(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='identity-provider') def cached_identity_provider(pytestconfig, timestamp): - return britive.identity_providers.create(name=f'pythonapiwrappertest-{timestamp}') + return britive.identity_management.identity_providers.create(name=f'pythonapiwrappertest-{timestamp}') @pytest.fixture(scope='session') @cached_resource(name='scim-token') def cached_scim_token(pytestconfig, cached_identity_provider): - return britive.identity_providers.scim_tokens.create( + return britive.identity_management.identity_providers.scim_tokens.create( identity_provider_id=cached_identity_provider['id'], token_expiration_days=60 ) @@ -340,20 +369,20 @@ def cached_checked_out_profile(pytestconfig, cached_profile, cached_environment, calling_user_details = britive.my_access.whoami() - policy = britive.profiles.policies.build( + policy = britive.application_management.profiles.policies.build( name=cached_profile['papId'], users=[calling_user_details['username']], description=cached_tag['name'], ) - britive.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) + britive.application_management.profiles.policies.create(profile_id=cached_profile['papId'], policy=policy) # add a permission (just take the first in the list) - permissions = britive.profiles.permissions.list_available(profile_id=cached_profile['papId']) + permissions = britive.application_management.profiles.permissions.list_available(profile_id=cached_profile['papId']) # for AWS only 1 IAM role can be assigned in permissions so list_available returns an empty list if there is # already a permission assigned to the profile if len(permissions) > 0: - britive.profiles.permissions.add( + britive.application_management.profiles.permissions.add( profile_id=cached_profile['papId'], permission_type=permissions[0]['type'], permission_name=permissions[0]['name'], @@ -384,31 +413,31 @@ def cached_checked_out_profile_by_name(pytestconfig, cached_profile, cached_envi @pytest.fixture(scope='session') @cached_resource(name='notification') def cached_notification(pytestconfig, timestamp): - return britive.notifications.create(name=f'pythonapiwrappertest-{timestamp}', description='test') + return britive.workflows.notifications.create(name=f'pythonapiwrappertest-{timestamp}', description='test') @pytest.fixture(scope='session') @cached_resource(name='notification-available-rules') def cached_notification_rules(pytestconfig): - return britive.notifications.available_rules() + return britive.workflows.notifications.available_rules() @pytest.fixture(scope='session') @cached_resource(name='notification-available-users') def cached_notification_users(pytestconfig, cached_notification): - return britive.notifications.available_users(notification_id=cached_notification['notificationId']) + return britive.workflows.notifications.available_users(notification_id=cached_notification['notificationId']) @pytest.fixture(scope='session') @cached_resource(name='notification-available-user-tags') def cached_notification_user_tags(pytestconfig, cached_notification): - return britive.notifications.available_user_tags(notification_id=cached_notification['notificationId']) + return britive.workflows.notifications.available_user_tags(notification_id=cached_notification['notificationId']) @pytest.fixture(scope='session') @cached_resource(name='notification-available-applications') def cached_notification_applications(pytestconfig, cached_notification): - return britive.notifications.available_applications(notification_id=cached_notification['notificationId']) + return britive.workflows.notifications.available_applications(notification_id=cached_notification['notificationId']) @pytest.fixture(scope='session') @@ -475,7 +504,7 @@ def cached_policy(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='notification-medium') def cached_notification_medium(pytestconfig, timestamp): - return britive.notification_mediums.create( + return britive.global_settings.notification_mediums.create( notification_medium_type='teams', name=f'pytest-nm-teams-{timestamp}', url='https://teams.microsoft.com', @@ -485,7 +514,7 @@ def cached_notification_medium(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='notification-medium-webhook') def cached_notification_medium_webhook(pytestconfig, timestamp): - return britive.notification_mediums.create( + return britive.global_settings.notification_mediums.create( notification_medium_type='webhook', name=f'pytest-nm-webhook-{timestamp}', url='https://www.britive.com', @@ -495,7 +524,7 @@ def cached_notification_medium_webhook(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='access-builder-approvers-groups') def cached_access_builder_approvers_groups(pytestconfig, timestamp, cached_application, cached_user): - return britive.access_builder.approvers_groups.create( + return britive.application_management.access_builder.approvers_groups.create( application_id=cached_application['appContainerId'], name=f'python-sdk-access-builder-{timestamp}', condition='Any', @@ -508,7 +537,7 @@ def cached_access_builder_approvers_groups(pytestconfig, timestamp, cached_appli def cached_access_builder_approvers_groups_update( pytestconfig, cached_application, cached_user, cached_tag, cached_access_builder_approvers_groups ): - britive.access_builder.approvers_groups.update( + britive.application_management.access_builder.approvers_groups.update( application_id=cached_application['appContainerId'], group_id=cached_access_builder_approvers_groups['id'], name=cached_access_builder_approvers_groups['name'], @@ -519,7 +548,7 @@ def cached_access_builder_approvers_groups_update( ], ) - return britive.access_builder.approvers_groups.list_approvers_group_members( + return britive.application_management.access_builder.approvers_groups.list_approvers_group_members( application_id=cached_application['appContainerId'], group_id=cached_access_builder_approvers_groups['id'], ) @@ -532,14 +561,16 @@ def cached_access_builder_associations( ): associations = [{'type': 0, 'id': cached_environment['id']}] approvers_groups = [{'id': cached_access_builder_approvers_groups['id']}] - britive.access_builder.associations.create( + britive.application_management.access_builder.associations.create( application_id=cached_application['appContainerId'], name='AccessBuilderAssociation', associations=associations, approvers_groups=approvers_groups, ) - return britive.access_builder.associations.list(application_id=cached_application['appContainerId']) + return britive.application_management.access_builder.associations.list( + application_id=cached_application['appContainerId'] + ) @pytest.fixture(scope='session') @@ -558,13 +589,13 @@ def cached_access_builder_associations_update( ] approvers_groups = [{'id': cached_access_builder_approvers_groups['id']}] association_id = cached_access_builder_associations['associationApproversSummary'][0]['id'] - britive.access_builder.associations.update( + britive.application_management.access_builder.associations.update( application_id=cached_application['appContainerId'], association_id=association_id, associations=associations, approvers_groups=approvers_groups, ) - return britive.access_builder.associations.get( + return britive.application_management.access_builder.associations.get( application_id=cached_application['appContainerId'], association_id=association_id ) @@ -572,7 +603,9 @@ def cached_access_builder_associations_update( @pytest.fixture(scope='session') @cached_resource(name='access-builder-associations-list') def cached_access_builder_associations_list(pytestconfig, cached_application): - return britive.access_builder.associations.list(application_id=cached_application['appContainerId']) + return britive.application_management.access_builder.associations.list( + application_id=cached_application['appContainerId'] + ) @pytest.fixture(scope='session') @@ -580,11 +613,13 @@ def cached_access_builder_associations_list(pytestconfig, cached_application): def cached_add_requesters_to_access_builder(pytestconfig, cached_application, cached_user): user_tag_members = [{'id': cached_user['userId'], 'memberType': 'User', 'condition': 'Include'}] - britive.access_builder.requesters.update( + britive.application_management.access_builder.requesters.update( application_id=cached_application['appContainerId'], user_tag_members=user_tag_members ) - return britive.access_builder.requesters.list(application_id=cached_application['appContainerId']) + return britive.application_management.access_builder.requesters.list( + application_id=cached_application['appContainerId'] + ) @pytest.fixture(scope='session') @@ -598,27 +633,29 @@ def cached_add_notification_to_access_builder(pytestconfig, cached_application, 'channels': cached_notification_medium.get('channels', []), } - britive.access_builder.notifications.update( + britive.application_management.access_builder.notifications.update( application_id=cached_application['appContainerId'], notification_mediums=[notification_medium] ) - return britive.access_builder.notifications.list(application_id=cached_application['appContainerId']) + return britive.application_management.access_builder.notifications.list( + application_id=cached_application['appContainerId'] + ) @pytest.fixture(scope='session') @cached_resource(name='access-builder-enable') def cached_enable_access_requests(pytestconfig, cached_application): - britive.access_builder.enable(application_id=cached_application['appContainerId']) + britive.application_management.access_builder.enable(application_id=cached_application['appContainerId']) - return britive.access_builder.get(application_id=cached_application['appContainerId']) + return britive.application_management.access_builder.get(application_id=cached_application['appContainerId']) @pytest.fixture(scope='session') @cached_resource(name='access-builder-disable') def cached_disable_access_requests(pytestconfig, cached_application): - britive.access_builder.disable(application_id=cached_application['appContainerId']) + britive.application_management.access_builder.disable(application_id=cached_application['appContainerId']) - return britive.access_builder.get(application_id=cached_application['appContainerId']) + return britive.application_management.access_builder.get(application_id=cached_application['appContainerId']) @pytest.fixture(scope='session') @@ -626,28 +663,26 @@ def cached_disable_access_requests(pytestconfig, cached_application): def cached_workload_identity_provider_aws(pytestconfig, timestamp, cached_identity_attribute): # do this up front to avoid the exponential backoff and retry logic # if the aws identity provider already exists - for idp in britive.workload.identity_providers.list(): + for idp in britive.identity_management.workload.identity_providers.list(): if idp['idpType'] == 'AWS': return idp try: - response = britive.workload.identity_providers.create_aws( + return britive.identity_management.workload.identity_providers.create_aws( name=f'python-sdk-aws-{timestamp}', attributes_map={'UserId': cached_identity_attribute['id']} ) - return response - except exceptions.InternalServerError as e: + except InternalServerError as e: raise Exception('AWS provider could not be created and none found') from e @pytest.fixture(scope='session') @cached_resource(name='workload-identity-provider-oidc') def cached_workload_identity_provider_oidc(pytestconfig, timestamp, cached_identity_attribute): - response = britive.workload.identity_providers.create_oidc( + return britive.identity_management.workload.identity_providers.create_oidc( name=f'python-sdk-oidc-{timestamp}', attributes_map={'sub': cached_identity_attribute['name']}, issuer_url='https://id.fakedomain.com', ) - return response @pytest.fixture(scope='session') @@ -656,8 +691,7 @@ def cached_system_level_policy(pytestconfig, timestamp, cached_tag): policy = britive.system.policies.build( name=f'python-sdk-{timestamp}', tags=[cached_tag['name']], roles=['UserViewRole'] ) - response = britive.system.policies.create(policy=policy) - return response + return britive.system.policies.create(policy=policy) @pytest.fixture(scope='session') @@ -670,8 +704,7 @@ def cached_system_level_policy_condition_as_default_json_str(pytestconfig, times ips=['11.11.11.11', '12.12.12.12'], condition_as_dict=False, ) - response = britive.system.policies.create(policy=policy) - return response + return britive.system.policies.create(policy=policy) @pytest.fixture(scope='session') @@ -684,16 +717,14 @@ def cached_system_level_policy_condition_as_dictionary(pytestconfig, timestamp, ips=['11.11.11.11', '12.12.12.12'], condition_as_dict=True, ) - response = britive.system.policies.create(policy=policy) - return response + return britive.system.policies.create(policy=policy) @pytest.fixture(scope='session') @cached_resource(name='role-system-level') def cached_system_level_role(pytestconfig, timestamp): role = britive.system.roles.build(name=f'python-sdk-{timestamp}', permissions=['NMAdminPermission']) - response = britive.system.roles.create(role=role) - return response + return britive.system.roles.create(role=role) @pytest.fixture(scope='session') @@ -702,20 +733,19 @@ def cached_system_level_permission(pytestconfig, timestamp): permission = britive.system.permissions.build( name=f'python-sdk-{timestamp}', consumer='apps', actions=['apps.app.view'] ) - response = britive.system.permissions.create(permission=permission) - return response + return britive.system.permissions.create(permission=permission) @pytest.fixture(scope='session') @cached_resource(name='gcp-profile-bq') def cached_gcp_profile_big_query(pytestconfig, timestamp): - response = britive.profiles.create( + response = britive.application_management.profiles.create( application_id=os.getenv('BRITIVE_GCP_TEST_APP_ID'), name=f'test-bq-constraints-{timestamp}', scope=[{'type': 'EnvironmentGroup', 'value': '881409387174'}], ) - britive.profiles.permissions.add( + britive.application_management.profiles.permissions.add( profile_id=response['papId'], permission_name='BigQuery Admin', permission_type='role' ) @@ -725,13 +755,13 @@ def cached_gcp_profile_big_query(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='gcp-profile-storage') def cached_gcp_profile_storage(pytestconfig, timestamp): - response = britive.profiles.create( + response = britive.application_management.profiles.create( application_id=os.getenv('BRITIVE_GCP_TEST_APP_ID'), name=f'test-storage-constraints-{timestamp}', scope=[{'type': 'EnvironmentGroup', 'value': '881409387174'}], ) - britive.profiles.permissions.add( + britive.application_management.profiles.permissions.add( profile_id=response['papId'], permission_name='Storage Admin', permission_type='role' ) @@ -741,14 +771,12 @@ def cached_gcp_profile_storage(pytestconfig, timestamp): @pytest.fixture(scope='session') @cached_resource(name='audit-logs-webhook') def cached_audit_logs_webhook_create(pytestconfig, timestamp, cached_notification_medium_webhook): - response = britive.audit_logs.webhooks.create_or_update( + return britive.audit_logs.webhooks.create_or_update( notification_medium_id=cached_notification_medium_webhook['id'], jmespath_filter="contains('event.eventType', 'checkout')", description=f'python-sdk-aws-audit-log-webhook-{timestamp}', ) - return response - @pytest.fixture(scope='session') @cached_resource(name='access-broker-response-template') @@ -792,7 +820,7 @@ def cached_access_broker_resource_permission_id( list_perms = britive.access_broker.resources.permissions.list( resource_type_id=cached_access_broker_resource_type['resourceTypeId'] ) - return [p['permissionId'] for p in list_perms if p['name'] == cached_access_broker_resource_permission['name']][0] + return next(p['permissionId'] for p in list_perms if p['name'] == cached_access_broker_resource_permission['name']) @pytest.fixture(scope='session') @@ -824,7 +852,6 @@ def cached_access_broker_resource_label(pytestconfig, timestamp): ) if britive.access_broker.resources.labels.get(label_id=label['keyId']): return label - print(label) @pytest.fixture(scope='session') @@ -844,11 +871,11 @@ def cached_access_broker_profile_permission(pytestconfig, cached_access_broker_p available_permissions = britive.access_broker.profiles.permissions.list_available_permissions( profile_id=cached_access_broker_profile['profileId'] ) - resource_type_id = [ + resource_type_id = next( r['resourceTypeId'] for r in britive.access_broker.resources.types.list() if r['name'] == available_permissions[0]['resourceTypeName'] - ][0] + ) return britive.access_broker.profiles.permissions.add_permissions( profile_id=cached_access_broker_profile['profileId'], permission_id=available_permissions[0]['permissionId'], diff --git a/tests/test_160-security_policies.py b/tests/test_160-security_policies.py deleted file mode 100644 index b2c2db9..0000000 --- a/tests/test_160-security_policies.py +++ /dev/null @@ -1,44 +0,0 @@ -from .cache import * # will also import some globals like `britive` - - -def test_create(cached_security_policy): - assert isinstance(cached_security_policy, dict) - - -def test_list(cached_security_policy): - policies = britive.security_policies.list() - assert isinstance(policies, list) - assert cached_security_policy['id'] in [p['id'] for p in policies] - - -def test_get(cached_security_policy): - policy = britive.security_policies.get(security_policy_id=cached_security_policy['id']) - assert isinstance(policy, dict) - - -def test_disable(cached_security_policy): - response = britive.security_policies.disable(security_policy_id=cached_security_policy['id']) - assert response is None - policy = britive.security_policies.get(security_policy_id=cached_security_policy['id']) - assert policy['status'] == 'Inactive' - - -def test_enable(cached_security_policy): - response = britive.security_policies.enable(security_policy_id=cached_security_policy['id']) - assert response is None - policy = britive.security_policies.get(security_policy_id=cached_security_policy['id']) - assert policy['status'] == 'Active' - - -def test_update(cached_security_policy): - response = britive.security_policies.update(security_policy_id=cached_security_policy['id'], ips=['2.2.2.2']) - assert response is None - - policy = britive.security_policies.get(security_policy_id=cached_security_policy['id']) - assert policy['conditions'][0]['values'] == ['2.2.2.2'] - - -def test_delete(cached_security_policy): - response = britive.security_policies.delete(security_policy_id=cached_security_policy['id']) - assert response is None - cleanup('security-policy') diff --git a/tests/test_190-audit_logs.py b/tests/test_190-audit_logs.py deleted file mode 100644 index 8087b2d..0000000 --- a/tests/test_190-audit_logs.py +++ /dev/null @@ -1,27 +0,0 @@ -from datetime import datetime, timedelta - -from .cache import britive - - -def test_fields(): - fields = britive.audit_logs.fields() - assert isinstance(fields, dict) - assert len(fields.keys()) == 18 - - -def test_operators(): - operators = britive.audit_logs.operators() - assert isinstance(operators, dict) - assert len(operators.keys()) == 3 - - -def test_query_json(): - events = britive.audit_logs.query(from_time=datetime.now() - timedelta(7), to_time=datetime.now()) - assert isinstance(events, list) - assert isinstance(events[0], dict) - assert len(events) % 100 != 0 # v2.8.1 - adding check due to pagination bug not including the last page - - -def test_query_csv(): - csv = britive.audit_logs.query(from_time=datetime.now() - timedelta(7), to_time=datetime.now(), csv=True) - assert '"timestamp","actor.display_name"' in csv