From 5000c80593ad15af620a3d66128d8f39202c60c0 Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 11 Nov 2024 11:13:23 -0600 Subject: [PATCH 01/40] refactor:organize code in alignment with UI --- CONTRIBUTING.md | 84 ++-- pyproject.toml | 2 +- .../application_management/__init__.py | 22 + .../access_builder.py | 0 .../{ => application_management}/accounts.py | 0 .../applications.py | 0 .../environment_groups.py | 0 .../environments.py | 0 .../{ => application_management}/groups.py | 2 +- .../permissions.py | 0 .../{ => application_management}/profiles.py | 2 +- .../{ => application_management}/scans.py | 0 src/britive/audit_logs/__init__.py | 8 + .../{audit_logs.py => audit_logs/logs.py} | 66 +-- src/britive/audit_logs/webhooks.py | 63 +++ src/britive/britive.py | 410 ++++++++---------- src/britive/{ => exceptions}/exceptions.py | 0 src/britive/federation_providers/__init__.py | 21 + .../aws.py} | 154 +------ .../azure_system_assigned_managed_identity.py | 28 ++ .../azure_user_assigned_managed_identity.py | 29 ++ src/britive/federation_providers/bitbucket.py | 19 + .../federation_provider.py | 6 + src/britive/federation_providers/github.py | 31 ++ src/britive/federation_providers/gitlab.py | 20 + src/britive/federation_providers/spacelift.py | 17 + src/britive/global_settings/__init__.py | 8 + .../{settings => global_settings}/banner.py | 2 +- src/britive/global_settings/firewall.py | 49 +++ .../notification_mediums.py | 0 src/britive/helpers/__init__.py | 6 + src/britive/identity_management/__init__.py | 17 + .../identity_attributes.py | 0 .../identity_providers.py | 0 .../service_identities.py | 59 ++- src/britive/{ => identity_management}/tags.py | 0 .../{ => identity_management}/users.py | 4 +- .../{ => identity_management}/workload.py | 0 src/britive/my_access.py | 324 +++----------- src/britive/my_approvals.py | 43 ++ src/britive/my_requests.py | 206 +++++++++ src/britive/my_resources.py | 50 ++- src/britive/my_secrets.py | 35 +- src/britive/{settings => reports}/__init__.py | 0 src/britive/{ => reports}/reports.py | 0 src/britive/secrets_manager/__init__.py | 0 .../{ => secrets_manager}/secrets_manager.py | 0 src/britive/security/__init__.py | 12 + src/britive/{ => security}/api_tokens.py | 4 +- .../policies.py} | 0 src/britive/{ => security}/saml.py | 0 src/britive/{ => security}/step_up.py | 0 src/britive/service_identity_tokens.py | 55 --- src/britive/settings/settings.py | 10 - src/britive/system/__init__.py | 14 + src/britive/system/system.py | 17 - src/britive/workflows/__init__.py | 10 + src/britive/{ => workflows}/notifications.py | 0 src/britive/{ => workflows}/task_services.py | 0 src/britive/{ => workflows}/tasks.py | 0 ...global_settings-01-identity_attributes.py} | 0 ...lobal_settings-02-notification_mediums.py} | 0 ...er.py => 000-global_settings-03-banner.py} | 0 ...py => 100-identity_management-01-users.py} | 0 ....py => 100-identity_management-02-tags.py} | 0 ...ntity_management-03-service_identities.py} | 0 ..._management-04-service_identity_tokens.py} | 0 ...ntity_management-05-identity_providers.py} | 0 ...=> 100-identity_management-06-workload.py} | 26 +- ...150-secrets_manager-01-secrets_manager.py} | 0 ...application_management-01-applications.py} | 0 ...ation_management-02-environment_groups.py} | 0 ...application_management-03-environments.py} | 1 - ...=> 200-application_management-04-scans.py} | 0 ...200-application_management-05-accounts.py} | 0 ...-application_management-06-permissions.py} | 0 ...> 200-application_management-07-groups.py} | 0 ...200-application_management-08-profiles.py} | 1 - ...plication_management-09-access_builder.py} | 0 ..._policies.py => 250-system-01-policies.py} | 1 - ...em_actions.py => 250-system-02-actions.py} | 0 ...onsumers.py => 250-system-03-consumers.py} | 0 ...system_roles.py => 250-system-04-roles.py} | 0 ...ssions.py => 250-system-05-permissions.py} | 0 ...s.py => 300-workflows-01-task_services.py} | 0 ...150-tasks.py => 300-workflows-02-tasks.py} | 0 ...s.py => 300-workflows-03-notifications.py} | 0 ...50-access_broker-01-response_templates.py} | 0 ...=> 350-access_broker-02-resource_types.py} | 0 ...> 350-access_broker-03-resource_labels.py} | 0 ...ce.py => 350-access_broker-04-resource.py} | 0 ...-access_broker-05-resource_permissions.py} | 0 ...es.py => 350-access_broker-06-profiles.py} | 0 ...350-access_broker-07-profiles_policies.py} | 0 ...py => 350-access_broker-08-permissions.py} | 0 ...olicies.py => 400-security-01-policies.py} | 0 ...st_170-saml.py => 400-security-02-saml.py} | 0 ...okens.py => 400-security-03-api_tokens.py} | 0 ...udit_logs.py => 500-audit_logs-01-logs.py} | 8 +- ...hooks.py => 500-audit_logs-02-webhooks.py} | 0 ...0-reports.py => 550-reports-01-reports.py} | 0 ..._access.py => 600-britive-01-my_access.py} | 56 --- ...ecrets.py => 600-britive-02-my_secrets.py} | 0 tests/600-britive-03-my_requests.py | 14 + tests/600-britive-04-my_approvals.py | 16 + ...=> 999-cleanup-01-delete_all_resources.py} | 249 +++++------ tests/cache.py | 20 +- 107 files changed, 1219 insertions(+), 1082 deletions(-) create mode 100644 src/britive/application_management/__init__.py rename src/britive/{ => application_management}/access_builder.py (100%) rename src/britive/{ => application_management}/accounts.py (100%) rename src/britive/{ => application_management}/applications.py (100%) rename src/britive/{ => application_management}/environment_groups.py (100%) rename src/britive/{ => application_management}/environments.py (100%) rename src/britive/{ => application_management}/groups.py (97%) rename src/britive/{ => application_management}/permissions.py (100%) rename src/britive/{ => application_management}/profiles.py (99%) rename src/britive/{ => application_management}/scans.py (100%) create mode 100644 src/britive/audit_logs/__init__.py rename src/britive/{audit_logs.py => audit_logs/logs.py} (56%) create mode 100644 src/britive/audit_logs/webhooks.py rename src/britive/{ => exceptions}/exceptions.py (100%) create mode 100644 src/britive/federation_providers/__init__.py rename src/britive/{helpers/federation_providers.py => federation_providers/aws.py} (54%) create mode 100644 src/britive/federation_providers/azure_system_assigned_managed_identity.py create mode 100644 src/britive/federation_providers/azure_user_assigned_managed_identity.py create mode 100644 src/britive/federation_providers/bitbucket.py create mode 100644 src/britive/federation_providers/federation_provider.py create mode 100644 src/britive/federation_providers/github.py create mode 100644 src/britive/federation_providers/gitlab.py create mode 100644 src/britive/federation_providers/spacelift.py create mode 100644 src/britive/global_settings/__init__.py rename src/britive/{settings => global_settings}/banner.py (99%) create mode 100644 src/britive/global_settings/firewall.py rename src/britive/{ => global_settings}/notification_mediums.py (100%) create mode 100644 src/britive/identity_management/__init__.py rename src/britive/{ => identity_management}/identity_attributes.py (100%) rename src/britive/{ => identity_management}/identity_providers.py (100%) rename src/britive/{ => identity_management}/service_identities.py (74%) rename src/britive/{ => identity_management}/tags.py (100%) rename src/britive/{ => identity_management}/users.py (99%) rename src/britive/{ => identity_management}/workload.py (100%) create mode 100644 src/britive/my_approvals.py create mode 100644 src/britive/my_requests.py rename src/britive/{settings => reports}/__init__.py (100%) rename src/britive/{ => reports}/reports.py (100%) create mode 100644 src/britive/secrets_manager/__init__.py rename src/britive/{ => secrets_manager}/secrets_manager.py (100%) create mode 100644 src/britive/security/__init__.py rename src/britive/{ => security}/api_tokens.py (97%) rename src/britive/{security_policies.py => security/policies.py} (100%) rename src/britive/{ => security}/saml.py (100%) rename src/britive/{ => security}/step_up.py (100%) delete mode 100644 src/britive/service_identity_tokens.py delete mode 100644 src/britive/settings/settings.py delete mode 100644 src/britive/system/system.py create mode 100644 src/britive/workflows/__init__.py rename src/britive/{ => workflows}/notifications.py (100%) rename src/britive/{ => workflows}/task_services.py (100%) rename src/britive/{ => workflows}/tasks.py (100%) rename tests/{test_005-identity_attributes.py => 000-global_settings-01-identity_attributes.py} (100%) rename tests/{test_260-notification_mediums.py => 000-global_settings-02-notification_mediums.py} (100%) rename tests/{test_320-settings_banner.py => 000-global_settings-03-banner.py} (100%) rename tests/{test_010-users.py => 100-identity_management-01-users.py} (100%) rename tests/{test_020-tags.py => 100-identity_management-02-tags.py} (100%) rename tests/{test_030-service_identities.py => 100-identity_management-03-service_identities.py} (100%) rename tests/{test_040-service_identity_tokens.py => 100-identity_management-04-service_identity_tokens.py} (100%) rename tests/{test_210-identity_providers.py => 100-identity_management-05-identity_providers.py} (100%) rename tests/{test_215-workload.py => 100-identity_management-06-workload.py} (86%) rename tests/{test_240-secrets_manager.py => 150-secrets_manager-01-secrets_manager.py} (100%) rename tests/{test_050-applications.py => 200-application_management-01-applications.py} (100%) rename tests/{test_060-environment_groups.py => 200-application_management-02-environment_groups.py} (100%) rename tests/{test_070-environments.py => 200-application_management-03-environments.py} (98%) rename tests/{test_080-scans.py => 200-application_management-04-scans.py} (100%) rename tests/{test_090-accounts.py => 200-application_management-05-accounts.py} (100%) rename tests/{test_100-permissions.py => 200-application_management-06-permissions.py} (100%) rename tests/{test_110-groups.py => 200-application_management-07-groups.py} (100%) rename tests/{test_130-profiles.py => 200-application_management-08-profiles.py} (99%) rename tests/{test_265-access_builder.py => 200-application_management-09-access_builder.py} (100%) rename tests/{test_270-system_policies.py => 250-system-01-policies.py} (99%) rename tests/{test_280_system_actions.py => 250-system-02-actions.py} (100%) rename tests/{test_290_system_consumers.py => 250-system-03-consumers.py} (100%) rename tests/{test_300-system_roles.py => 250-system-04-roles.py} (100%) rename tests/{test_310-system_permissions.py => 250-system-05-permissions.py} (100%) rename tests/{test_140-task_services.py => 300-workflows-01-task_services.py} (100%) rename tests/{test_150-tasks.py => 300-workflows-02-tasks.py} (100%) rename tests/{test_230-notifications.py => 300-workflows-03-notifications.py} (100%) rename tests/{test_330-response_templates.py => 350-access_broker-01-response_templates.py} (100%) rename tests/{test_340-resource_types.py => 350-access_broker-02-resource_types.py} (100%) rename tests/{test_350-resource_labels.py => 350-access_broker-03-resource_labels.py} (100%) rename tests/{test_360-resource.py => 350-access_broker-04-resource.py} (100%) rename tests/{test_370-resource_permissions.py => 350-access_broker-05-resource_permissions.py} (100%) rename tests/{test_380-access_broker_profiles.py => 350-access_broker-06-profiles.py} (100%) rename tests/{test_390-access_broker_profiles_policies.py => 350-access_broker-07-profiles_policies.py} (100%) rename tests/{test_400-access_broker_permissions.py => 350-access_broker-08-permissions.py} (100%) rename tests/{test_160-security_policies.py => 400-security-01-policies.py} (100%) rename tests/{test_170-saml.py => 400-security-02-saml.py} (100%) rename tests/{test_180-api_tokens.py => 400-security-03-api_tokens.py} (100%) rename tests/{test_190-audit_logs.py => 500-audit_logs-01-logs.py} (73%) rename tests/{test_275-audit_logs_webhooks.py => 500-audit_logs-02-webhooks.py} (100%) rename tests/{test_200-reports.py => 550-reports-01-reports.py} (100%) rename tests/{test_220-my_access.py => 600-britive-01-my_access.py} (62%) rename tests/{test_250-my_secrets.py => 600-britive-02-my_secrets.py} (100%) create mode 100644 tests/600-britive-03-my_requests.py create mode 100644 tests/600-britive-04-my_approvals.py rename tests/{test_990-delete_all_resources.py => 999-cleanup-01-delete_all_resources.py} (91%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f21252c..60a15ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,48 +117,48 @@ 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/005-identity_attributes.py -v +pytest tests/010-users.py -v +pytest tests/020-tags.py -v +pytest tests/030-service_identities.py -v +pytest tests/040-service_identity_tokens.py -v +pytest tests/050-applications.py -v +pytest tests/060-environment_groups.py -v +pytest tests/070-environments.py -v +pytest tests/080-scans.py -v # WARNING - this one will take a while since it initiates a real scan +pytest tests/090-accounts.py -v # NOTE - a scan must first be completed +pytest tests/100-permissions.py -v # NOTE - a scan must first be completed +pytest tests/110-groups.py -v # NOTE - a scan must first be completed +pytest tests/130-profiles.py -v +pytest tests/140-task_services.py -v +pytest tests/150-tasks.py -v +pytest tests/160-security_policies.py -v +pytest tests/170-saml.py -v +pytest tests/180-api_tokens.py -v +pytest tests/190-audit_logs.py -v +pytest tests/200-reports.py -v +pytest tests/210-identity_providers.py -v +pytest tests/215-workload.py -v +pytest tests/220-my_access.py -v +pytest tests/230-notifications.py -v +pytest tests/240-secrets_manager.py -v +pytest tests/250-my_secrets.py -v +pytest tests/260-notification_mediums.py -v +pytest tests/270-system_policies.py -v +pytest tests/280_system_actions.py -v +pytest tests/290_system_consumers.py -v +pytest tests/300-system_roles.py -v +pytest tests/310-system_permissions.py -v +pytest tests/320-settings_banner.py -v +pytest tests/330-response_templates.py -v +pytest tests/340-resource_types.py -v +pytest tests/350-resource_labels.py -v +pytest tests/360-resource.py -v +pytest tests/370-resource_permissions.py -v +pytest tests/380-access_broker_profiles.py -v +pytest tests/390-access_broker_profiles_policies.py -v +pytest tests/400-access_broker_permissions.py -v +pytest tests/990-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/pyproject.toml b/pyproject.toml index 45f51f0..8edd764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,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/src/britive/application_management/__init__.py b/src/britive/application_management/__init__.py new file mode 100644 index 0000000..41fcf45 --- /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): + 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 100% rename from src/britive/access_builder.py rename to src/britive/application_management/access_builder.py diff --git a/src/britive/accounts.py b/src/britive/application_management/accounts.py similarity index 100% rename from src/britive/accounts.py rename to src/britive/application_management/accounts.py diff --git a/src/britive/applications.py b/src/britive/application_management/applications.py similarity index 100% rename from src/britive/applications.py rename to src/britive/application_management/applications.py 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 100% rename from src/britive/environments.py rename to src/britive/application_management/environments.py 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/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 99% rename from src/britive/profiles.py rename to src/britive/application_management/profiles.py index 057a38b..f0244fb 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 .. import exceptions creation_defaults = { 'expirationDuration': 3600000, diff --git a/src/britive/scans.py b/src/britive/application_management/scans.py similarity index 100% rename from src/britive/scans.py rename to src/britive/application_management/scans.py diff --git a/src/britive/audit_logs/__init__.py b/src/britive/audit_logs/__init__.py new file mode 100644 index 0000000..b60a198 --- /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.audit_logs = Logs(britive) + self.audit_logs.webhooks = Webhooks(britive) diff --git a/src/britive/audit_logs.py b/src/britive/audit_logs/logs.py similarity index 56% rename from src/britive/audit_logs.py rename to src/britive/audit_logs/logs.py index b98000e..44442dc 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: """ @@ -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..ca768a2 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -5,14 +5,9 @@ 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 .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, @@ -21,33 +16,28 @@ TokenMissingError, allowed_exceptions, ) -from .groups import Groups -from .helpers import federation_providers as fp +from .federation_providers import ( + AwsFederationProvider, + AzureSystemAssignedManagedIdentityFederationProvider, + AzureUserAssignedManagedIdentityFederationProvider, + BitbucketFederationProvider, + GithubFederationProvider, + GitlabFederationProvider, + SpaceliftFederationProvider, +) +from .global_settings import GlobalSettings from .helpers import methods as helper_methods -from .identity_attributes import IdentityAttributes -from .identity_providers import IdentityProviders +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 .reports.reports import Reports +from .secrets_manager.secrets_manager import SecretsManager +from .security import ApiTokens, Security +from .system import System +from .workflows import Workflows class Britive: @@ -87,11 +77,11 @@ class Britive: def __init__( self, - tenant: str = None, - token: str = None, - query_features: bool = True, - token_federation_provider: str = None, - token_federation_provider_duration_seconds: int = 900, + tenant=None, + token=None, + query_features=True, + token_federation_provider=None, + token_duration=900, ): """ Instantiate an authenticated interface that can be used to communicate with the Britive API. @@ -112,109 +102,117 @@ def __init__( """ 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' - ) + raise TenantMissingError('Tenant not provided and cannot be sourced from environment.') - 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') + self.__token = self._initialize_token(token, token_federation_provider, token_duration) + self.base_url = f'https://{self.parse_tenant(self.tenant)}/api' + self.session = self._setup_session() - if not self.__token: - raise TokenMissingError( - 'Token not explicitly provided and could not be sourced from environment variable BRITIVE_API_TOKEN' - ) + self.retry_backoff_factor = 1 + self.retry_max_times = 5 + self.retry_response_status = {429, 500, 502, 503, 504} - # 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._initialize_components(query_features) - self.base_url = f'https://{self.tenant}/api' - self.session = requests.Session() - self.retry_max_times = 5 - self.retry_backoff_factor = 1 - self.retry_response_status = [429, 500, 502, 503, 504] + def _initialize_token(self, token, provider, duration): + if provider: + return self.source_federation_token_from(provider, self.tenant, duration) + return token or os.getenv('BRITIVE_API_TOKEN') or TokenMissingError('Token not provided.') + + def _setup_session(self): + session = requests.Session() # 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 + if britive_ca_bundle := os.getenv('PYBRITIVE_CA_BUNDLE'): + session.verify = britive_ca_bundle # 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 + session.verify = False + self._disable_ssl_verification_warnings() - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + token_type = self._determine_token_type() + version = self._get_version() - token_type = 'TOKEN' if len(self.__token) < 50 else 'Bearer' - if len(self.__token.split('::')) > 1: - token_type = 'WorkloadToken' - - try: - import britive - - version = britive.__version__ - except Exception: - version = 'unknown' - - 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): + # 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 + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - self.access_builder = AccessBuilderSettings(self) - self.accounts = Accounts(self) + def _determine_token_type(self): + if len(self.__token) < 50: + return 'TOKEN' + if len(self.__token.split('::')) > 1: + return 'WorkloadToken' + return 'Bearer' + + def _get_version(self): + try: + import britive + + return britive.__version__ + except ImportError: + return 'unknown' + + def _initialize_components(self, query_features): + application_management = ApplicationManagement(self) + identity_management = IdentityManagement(self) + global_settings = GlobalSettings(self) + security = Security(self) + workflows = Workflows(self) + + self.access_builder = application_management.access_builder + self.accounts = application_management.accounts self.api_tokens = ApiTokens(self) - self.applications = Applications(self) - self.audit_logs = AuditLogs(self) - self.environment_groups = EnvironmentGroups(self) - self.environments = Environments(self) + self.applications = application_management.applications + self.audit_logs = AuditLogs(self).audit_logs + self.environment_groups = application_management.environment_groups + self.environments = application_management.environments self.feature_flags = self.features() if query_features else {} - self.groups = Groups(self) - self.identity_attributes = IdentityAttributes(self) - self.identity_providers = IdentityProviders(self) + self.groups = application_management.groups + self.identity_attributes = identity_management.identity_attributes + self.identity_providers = identity_management.identity_providers 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.notification_mediums = global_settings.notification_mediums + self.notifications = workflows.notifications + self.permissions = application_management.permissions + self.profiles = application_management.profiles self.reports = Reports(self) - self.saml = Saml(self) - self.scans = Scans(self) + self.saml = security.saml + self.scans = application_management.scans 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.security_policies = security.security_policies + self.service_identities = identity_management.service_identities + self.service_identity_tokens = identity_management.service_identity_tokens + self.settings = GlobalSettings(self) + self.step_up = security.step_up_auth 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) + self.tags = identity_management.tags + self.task_services = workflows.task_services + self.tasks = workflows.tasks + self.users = identity_management.users + self.workload = identity_management.workload # 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: """ @@ -275,68 +273,45 @@ def source_federation_token_from(provider: str, tenant: str = None, duration_sec """ 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') + provider_name = helper[0] + + federation_providers = { + 'aws': lambda: AwsFederationProvider(profile=helper_methods.safe_list_get(helper, 1)).get_token(), + 'azuresmi': lambda: AzureSystemAssignedManagedIdentityFederationProvider( + audience=helper_methods.safe_list_get(helper, 1) + ).get_token(), + 'azureumi': lambda: AzureUserAssignedManagedIdentityFederationProvider( + client_id=helper[1].split('|')[0], audience=helper_methods.safe_list_get(helper[1].split('|'), 1) + ).get_token(), + 'bitbucket': lambda: BitbucketFederationProvider().get_token(), + 'github': lambda: GithubFederationProvider(audience=helper_methods.safe_list_get(helper, 1)).get_token(), + 'gitlab': lambda: GitlabFederationProvider( + token_env_var=helper_methods.safe_list_get(helper, 1) + ).get_token(), + 'spacelift': lambda: SpaceliftFederationProvider().get_token(), + } + + if provider_name in federation_providers: + return federation_providers[provider_name]() + + raise InvalidFederationProvider(f'federation provider {provider_name} 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 + domain = tenant.replace('https://', '').replace('http://', '').split('/')[0] # remove scheme and paths 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 + 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) - domain = f'{tenant}.britive-app.com' + resolved_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 + 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 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,46 +349,39 @@ 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 self._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}) + return self._handle_response(response) + + @staticmethod + def _handle_response(response): try: return response.json() - except native_json.decoder.JSONDecodeError: # if we cannot decode json then the response isn't json + except native_json.decoder.JSONDecodeError: 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 + content = native_json.loads(response.content.decode('utf-8')) + message = ( + f"{response.status_code} - " + f"{content.get('errorCode', 'E0000')} -" + f" {content.get('message', 'no message available')}" + ) + if content.get('details'): + message += f" - {content.get('details')}" + raise allowed_exceptions[response.status_code](message) @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 + return response.status_code in (204,) or (response.status_code == 200 and len(response.content) == 0) @staticmethod def __pagination_type(headers, result) -> str: @@ -432,105 +400,79 @@ def __pagination_type(headers, result) -> str: @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 response.status_code == 503 and response.json().get('errorCode') == 'MAINT0001' 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 self.__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 - break - self.__check_response_for_error(response) # handle an error response - return response + else: + self.__check_response_for_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 + + while True: + response = self.__request_with_exponential_backoff_and_retry(method, url, params, data, json) + if self.__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 = self._handle_response(response) + pagination_type = pagination_type or self.__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' # 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': 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 - break - params['page'] = page + 1 - elif pagination_type == 'audit': - return_data += result # result is already a list - if 'next-page' not in response.headers: + if result['size'] * (result['page'] + 1) >= result['count']: 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': + params['page'] = result['page'] + 1 + elif pagination_type in ('audit', '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 + 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'] == '': + root_env_group = app.get('rootEnvironmentGroup', {}).get('environmentGroups', []) + for group in root_env_group: + if not group['parentId']: return group['id'] raise RootEnvironmentGroupNotFound() diff --git a/src/britive/exceptions.py b/src/britive/exceptions/exceptions.py similarity index 100% rename from src/britive/exceptions.py rename to src/britive/exceptions/exceptions.py diff --git a/src/britive/federation_providers/__init__.py b/src/britive/federation_providers/__init__.py new file mode 100644 index 0000000..1da8396 --- /dev/null +++ b/src/britive/federation_providers/__init__.py @@ -0,0 +1,21 @@ +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): + 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 54% rename from src/britive/helpers/federation_providers.py rename to src/britive/federation_providers/aws.py index ab5a053..b73cf91 100644 --- a/src/britive/helpers/federation_providers.py +++ b/src/britive/federation_providers/aws.py @@ -5,111 +5,8 @@ import json import os -from .. import exceptions - - -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 ..exceptions import TenantMissingError +from .federation_provider import FederationProvider class AwsFederationProvider(FederationProvider): @@ -121,7 +18,7 @@ def __init__(self, profile: str, tenant: str, duration: int = 900) -> None: 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() self.tenant = Britive.parse_tenant(temp_tenant).split(':')[0] # remove the port if it exists super().__init__() @@ -249,48 +146,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..50bea7f --- /dev/null +++ b/src/britive/federation_providers/azure_system_assigned_managed_identity.py @@ -0,0 +1,28 @@ +from ..exceptions import NotExecutingInAzureEnvironment +from .federation_provider import FederationProvider + +try: + from azure.identity import ManagedIdentityCredential + 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 + + +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: + token = ManagedIdentityCredential().get_token(self.audience).token + return f'OIDC::{token}' + 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..19a080b --- /dev/null +++ b/src/britive/federation_providers/azure_user_assigned_managed_identity.py @@ -0,0 +1,29 @@ +from ..exceptions import NotExecutingInAzureEnvironment +from .federation_provider import FederationProvider + +try: + from azure.identity import ManagedIdentityCredential + 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 + + +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: + token = ManagedIdentityCredential(client_id=self.client_id).get_token(self.audience).token + return f'OIDC::{token}' + 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..96f4ad8 --- /dev/null +++ b/src/britive/federation_providers/bitbucket.py @@ -0,0 +1,19 @@ +import os + +from ..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..2c11702 --- /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..94adeec --- /dev/null +++ b/src/britive/federation_providers/github.py @@ -0,0 +1,31 @@ +import os + +import requests + +from ..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..282e7d6 --- /dev/null +++ b/src/britive/federation_providers/gitlab.py @@ -0,0 +1,20 @@ +import os + +from ..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..a2a4871 --- /dev/null +++ b/src/britive/federation_providers/spacelift.py @@ -0,0 +1,17 @@ +import os + +from ..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..de72bde --- /dev/null +++ b/src/britive/global_settings/__init__.py @@ -0,0 +1,8 @@ +from .banner import Banner +from .notification_mediums import NotificationMediums + + +class GlobalSettings: + def __init__(self, britive) -> None: + self.banner = Banner(britive) + self.notification_mediums = NotificationMediums(britive) diff --git a/src/britive/settings/banner.py b/src/britive/global_settings/banner.py similarity index 99% rename from src/britive/settings/banner.py rename to src/britive/global_settings/banner.py index 59ee6e7..e80de49 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' diff --git a/src/britive/global_settings/firewall.py b/src/britive/global_settings/firewall.py new file mode 100644 index 0000000..834fa70 --- /dev/null +++ b/src/britive/global_settings/firewall.py @@ -0,0 +1,49 @@ +class Firewall: + def __init__(self, britive): + self.britive = britive + self.base_url = f'{self.britive.base_url}/settings/firewall' + + def save(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 list(self): ... + + def get(self): ... + + def updated(self): ... + + def delete(self): ... 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..354a571 100644 --- a/src/britive/helpers/__init__.py +++ b/src/britive/helpers/__init__.py @@ -0,0 +1,6 @@ +from .custom_attributes import CustomAttributes + + +class Helpers: + def __init__(self, britive): + self.custom_attributes = CustomAttributes(britive) diff --git a/src/britive/identity_management/__init__.py b/src/britive/identity_management/__init__.py new file mode 100644 index 0000000..b2ab0c4 --- /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): + 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/identity_attributes.py b/src/britive/identity_management/identity_attributes.py similarity index 100% rename from src/britive/identity_attributes.py rename to src/britive/identity_management/identity_attributes.py diff --git a/src/britive/identity_providers.py b/src/britive/identity_management/identity_providers.py similarity index 100% rename from src/britive/identity_providers.py rename to src/britive/identity_management/identity_providers.py diff --git a/src/britive/service_identities.py b/src/britive/identity_management/service_identities.py similarity index 74% rename from src/britive/service_identities.py rename to src/britive/identity_management/service_identities.py index 7a49c64..310315e 100644 --- a/src/britive/service_identities.py +++ b/src/britive/identity_management/service_identities.py @@ -1,8 +1,13 @@ -from .helpers.custom_attributes import CustomAttributes +from ..helpers import CustomAttributes valid_statues = ['active', 'inactive'] +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 ServiceIdentities: def __init__(self, britive) -> None: self.britive = britive @@ -186,3 +191,55 @@ 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 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/tags.py b/src/britive/identity_management/tags.py similarity index 100% rename from src/britive/tags.py rename to src/britive/identity_management/tags.py diff --git a/src/britive/users.py b/src/britive/identity_management/users.py similarity index 99% rename from src/britive/users.py rename to src/britive/identity_management/users.py index 2858b8e..8ab5d52 100644 --- a/src/britive/users.py +++ b/src/britive/identity_management/users.py @@ -1,9 +1,9 @@ -from .exceptions import ( +from ..exceptions import ( UserDoesNotHaveMFAEnabled, UserNotAllowedToChangePassword, UserNotAssociatedWithDefaultIdentityProvider, ) -from .helpers.custom_attributes import CustomAttributes +from ..helpers.custom_attributes import CustomAttributes valid_statues = ['active', 'inactive'] diff --git a/src/britive/workload.py b/src/britive/identity_management/workload.py similarity index 100% rename from src/britive/workload.py rename to src/britive/identity_management/workload.py diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 2afb63a..3b56809 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -1,13 +1,26 @@ -import sys import time from typing import Any, Callable -from . import exceptions +from .exceptions import ( + ApprovalRequiredButNoJustificationProvided, + ApprovalWorkflowRejected, + ApprovalWorkflowTimedOut, + ForbiddenRequest, + InvalidRequest, + ProfileApprovalRejected, + ProfileApprovalTimedOut, + ProfileApprovalWithdrawn, + StepUpAuthFailed, + StepUpAuthRequiredButNotProvided, + TransactionNotFound, +) +from .my_approvals import MyApprovals +from .my_requests import MyRequests approval_exceptions = { - 'rejected': exceptions.ProfileApprovalRejected(), - 'cancelled': exceptions.ProfileApprovalWithdrawn(), - 'timeout': exceptions.ProfileApprovalTimedOut(), + 'rejected': ProfileApprovalRejected(), + 'cancelled': ProfileApprovalWithdrawn(), + 'timeout': ProfileApprovalTimedOut(), } @@ -30,6 +43,20 @@ def __init__(self, britive) -> None: self.base_url = f'{self.britive.base_url}/access' def list_profiles(self) -> list: + # MyApprovals backwards compatibility + self.__my_approvals = MyApprovals(self.britive) + self.approve_request = self.__my_approvals.approve_request + self.list_approvals = self.__my_approvals.list_approvals + self.reject_request = self.__my_approvals.reject_request + + # MyRequests backwards compatibility + self.__my_requests = MyRequests(self.britive) + self.approval_request_status = self.__my_requests.approval_request_status + self.request_approval = self.__my_requests.request_approval + self.request_approval_by_name = self.__my_requests.request_approval_by_name + self.withdraw_approval_request = self.__my_requests.withdraw_approval_request + self.withdraw_approval_request_by_name = self.__my_requests.withdraw_approval_request_by_name + """ List the profiles for which the user has access. @@ -58,7 +85,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 extend_checkout(self, transaction_id: str) -> dict: """ @@ -98,233 +125,21 @@ 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, + wait_time: int = 60, ) -> dict: params = {'accessType': 'PROGRAMMATIC' if programmatic else 'CONSOLE'} @@ -360,35 +175,34 @@ def _checkout( if not transaction: if otp: response = self.britive.step_up.authenticate(otp=otp) - print(str(response)) 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: + except ForbiddenRequest as e: if 'PE-0028' in str(e): # Check for stepup totp - raise exceptions.StepUpAuthRequiredButNotProvided() from e - except exceptions.InvalidRequest as e: + raise StepUpAuthRequiredButNotProvided() from e + except InvalidRequest as e: if 'MA-0009' in str(e): # old approval process that coupled approval and checkout - raise exceptions.ApprovalRequiredButNoJustificationProvided() from e + raise 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 + raise ApprovalRequiredButNoJustificationProvided() from e # request approval status = self.request_approval( - profile_id=profile_id, + block_until_disposition=True, environment_id=environment_id, justification=justification, - wait_time=wait_time, max_wait_time=max_wait_time, - block_until_disposition=True, + profile_id=profile_id, progress_func=progress_func, + wait_time=wait_time, ) # handle the response based on the value of status @@ -412,16 +226,16 @@ 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, + wait_time=wait_time, ) raise e @@ -434,11 +248,11 @@ def _checkout( while True: try: transaction = self.get_checked_out_profile(transaction_id=transaction_id) - except exceptions.TransactionNotFound as e: - raise exceptions.ApprovalWorkflowRejected() from e + except TransactionNotFound as e: + raise ApprovalWorkflowRejected() from e if transaction['status'] == 'checkOutInApproval': # we have an approval workflow occurring if time.time() >= quit_time: - raise exceptions.ApprovalWorkflowTimedOut() + raise ApprovalWorkflowTimedOut() if progress_func: progress_func('awaiting approval') time.sleep(wait_time) @@ -466,13 +280,13 @@ 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, + wait_time: int = 60, ) -> dict: """ Checkout a profile. @@ -486,18 +300,18 @@ 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 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. @@ -525,13 +339,13 @@ 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, + wait_time: int = 60, ) -> dict: """ Checkout a profile by supplying the names of entities vs. the IDs of those entities. @@ -545,18 +359,18 @@ 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 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. diff --git a/src/britive/my_approvals.py b/src/britive/my_approvals.py new file mode 100644 index 0000000..6adbcfb --- /dev/null +++ b/src/britive/my_approvals.py @@ -0,0 +1,43 @@ +class MyApprovals: + 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.britive.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.britive.base_url}/{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}', params=params) diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py new file mode 100644 index 0000000..39ef802 --- /dev/null +++ b/src/britive/my_requests.py @@ -0,0 +1,206 @@ +import sys +import time +from typing import Any, Callable + +from .exceptions import ( + ProfileApprovalMaxBlockTimeExceeded, + ProfileCheckoutAlreadyApproved, +) + + +class MyRequests: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/v1/approvals' + + def list(self) -> list: + """ + List My Requests + + :return: List of My Requests. + """ + + return self.britive.get(self.britive.base_url, params={'requestType': 'myRequests'}) + + def request_approval_by_name( + self, + profile_name: str, + environment_name: str, + justification: 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 `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: 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. + """ + + 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, + 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( + self, + profile_id: str, + environment_id: str, + justification: 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 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: 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. + """ + + data = {'justification': justification} + + if ticket_id and ticket_type: + data.update(ticketId=ticket_id, ticketType=ticket_type) + + request = self.britive.post( + f'{self.britive.base_url}/access/{profile_id}/environments/{environment_id}/approvalRequest', 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 True: + status = self.approval_request_status(request_id=request_id)['status'].lower() + if status == 'pending': + if time.time() >= quit_time: + raise 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}/{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}/{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}/consumer/papservice/resource?resourceId=' f'{profile_id}/{environment_id}' + + return self.britive.delete(url) diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 8efd500..0db7952 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -1,12 +1,23 @@ import time from typing import Any, Callable -from . import exceptions +from .exceptions import ( + ApprovalRequiredButNoJustificationProvided, + ForbiddenRequest, + InvalidRequest, + ProfileApprovalRejected, + ProfileApprovalTimedOut, + ProfileApprovalWithdrawn, + StepUpAuthFailed, + StepUpAuthRequiredButNotProvided, + TransactionNotFound, +) +from .my_requests import MyRequests approval_exceptions = { - 'rejected': exceptions.ProfileApprovalRejected(), - 'cancelled': exceptions.ProfileApprovalWithdrawn(), - 'timeout': exceptions.ProfileApprovalTimedOut(), + 'rejected': ProfileApprovalRejected(), + 'cancelled': ProfileApprovalWithdrawn(), + 'timeout': ProfileApprovalTimedOut(), } @@ -72,7 +83,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 +91,10 @@ 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, + wait_time: int = 60, ) -> dict: data = {'justification': justification} @@ -117,13 +128,13 @@ def _checkout( if otp: response = self.britive.step_up.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: + except ForbiddenRequest as e: if 'PE-0028' in str(e): # Check for stepup totp raise exceptions.StepUpAuthRequiredButNotProvided() from e # except exceptions.InvalidRequest as e: @@ -177,10 +188,10 @@ 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, + wait_time: int = 60, ) -> dict: """ Checkout a profile. @@ -199,12 +210,12 @@ 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 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. @@ -219,7 +230,6 @@ def checkout( profile_id=profile_id, resource_id=resource_id, include_credentials=include_credentials, - justification=justification, wait_time=wait_time, max_wait_time=max_wait_time, progress_func=progress_func, @@ -232,10 +242,10 @@ 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, + wait_time: int = 60, ) -> dict: """ Checkout a profile by supplying the names of entities vs. the IDs of those entities. @@ -252,12 +262,12 @@ 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 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. diff --git a/src/britive/my_secrets.py b/src/britive/my_secrets.py index a7ac383..8b6b797 100644 --- a/src/britive/my_secrets.py +++ b/src/britive/my_secrets.py @@ -1,7 +1,16 @@ import time from datetime import datetime, timedelta, timezone -from . import exceptions +from .exceptions import ( + AccessDenied, + ApprovalRequiredButNoJustificationProvided, + ApprovalWorkflowRejected, + ApprovalWorkflowTimedOut, + ForbiddenRequest, + NoSecretsVaultFound, + StepUpAuthFailed, + StepUpAuthRequiredButNotProvided, +) class MySecrets: @@ -27,7 +36,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,13 +89,13 @@ 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) if response.get('result') == 'FAILED': - raise exceptions.StepUpAuthFailed() + raise StepUpAuthFailed() # attempt to get the secret value and return it return self.britive.post( @@ -94,19 +103,19 @@ def view( )['value'] # 403 will be returned when approval or stepup auth are required or pending, or access is denied - except exceptions.ForbiddenRequest as e: + except ForbiddenRequest as e: if 'PE-0002' in str(e): - raise exceptions.AccessDenied() from e + raise 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: if first: - raise exceptions.ApprovalRequiredButNoJustificationProvided() from e + raise ApprovalRequiredButNoJustificationProvided() from e else: - raise exceptions.ApprovalWorkflowRejected() from e + raise ApprovalWorkflowRejected() from e if 'PE-0028' in str(e): # Check for stepup totp - raise exceptions.StepUpAuthRequiredButNotProvided() from e + raise StepUpAuthRequiredButNotProvided() from e else: raise e @@ -144,14 +153,14 @@ def download( if otp: response = self.britive.step_up.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: + except ForbiddenRequest as e: if 'PE-0002' in str(e): - raise exceptions.AccessDenied() from e + raise 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( @@ -161,7 +170,7 @@ def download( # 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 + raise StepUpAuthRequiredButNotProvided() from e else: raise e diff --git a/src/britive/settings/__init__.py b/src/britive/reports/__init__.py similarity index 100% rename from src/britive/settings/__init__.py rename to src/britive/reports/__init__.py diff --git a/src/britive/reports.py b/src/britive/reports/reports.py similarity index 100% rename from src/britive/reports.py rename to src/britive/reports/reports.py diff --git a/src/britive/secrets_manager/__init__.py b/src/britive/secrets_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/britive/secrets_manager.py b/src/britive/secrets_manager/secrets_manager.py similarity index 100% rename from src/britive/secrets_manager.py rename to src/britive/secrets_manager/secrets_manager.py diff --git a/src/britive/security/__init__.py b/src/britive/security/__init__.py new file mode 100644 index 0000000..2884b79 --- /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): + 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 97% rename from src/britive/api_tokens.py rename to src/britive/security/api_tokens.py index 9fef44b..2915a4d 100644 --- a/src/britive/api_tokens.py +++ b/src/britive/security/api_tokens.py @@ -1,4 +1,4 @@ -from . import exceptions +from ..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: """ 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/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/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..2286c82 --- /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): + 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 100% rename from src/britive/notifications.py rename to src/britive/workflows/notifications.py 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 100% rename from src/britive/tasks.py rename to src/britive/workflows/tasks.py diff --git a/tests/test_005-identity_attributes.py b/tests/000-global_settings-01-identity_attributes.py similarity index 100% rename from tests/test_005-identity_attributes.py rename to tests/000-global_settings-01-identity_attributes.py diff --git a/tests/test_260-notification_mediums.py b/tests/000-global_settings-02-notification_mediums.py similarity index 100% rename from tests/test_260-notification_mediums.py rename to tests/000-global_settings-02-notification_mediums.py diff --git a/tests/test_320-settings_banner.py b/tests/000-global_settings-03-banner.py similarity index 100% rename from tests/test_320-settings_banner.py rename to tests/000-global_settings-03-banner.py diff --git a/tests/test_010-users.py b/tests/100-identity_management-01-users.py similarity index 100% rename from tests/test_010-users.py rename to tests/100-identity_management-01-users.py diff --git a/tests/test_020-tags.py b/tests/100-identity_management-02-tags.py similarity index 100% rename from tests/test_020-tags.py rename to tests/100-identity_management-02-tags.py diff --git a/tests/test_030-service_identities.py b/tests/100-identity_management-03-service_identities.py similarity index 100% rename from tests/test_030-service_identities.py rename to tests/100-identity_management-03-service_identities.py diff --git a/tests/test_040-service_identity_tokens.py b/tests/100-identity_management-04-service_identity_tokens.py similarity index 100% rename from tests/test_040-service_identity_tokens.py rename to tests/100-identity_management-04-service_identity_tokens.py diff --git a/tests/test_210-identity_providers.py b/tests/100-identity_management-05-identity_providers.py similarity index 100% rename from tests/test_210-identity_providers.py rename to tests/100-identity_management-05-identity_providers.py diff --git a/tests/test_215-workload.py b/tests/100-identity_management-06-workload.py similarity index 86% rename from tests/test_215-workload.py rename to tests/100-identity_management-06-workload.py index d4c1231..e3c7b9a 100644 --- a/tests/test_215-workload.py +++ b/tests/100-identity_management-06-workload.py @@ -81,35 +81,37 @@ 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): +def test_service_identity_get_when_nothing_associated(cached_service_identity_federated): with pytest.raises(exceptions.NotFound): - britive.workload.service_identities.get(service_identity_id=cached_service_identity['userId']) + britive.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'], + 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 + 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.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 + principal_id=cached_service_identity_federated['userId'], as_dict=False ) assert isinstance(attrs, list) @@ -117,26 +119,28 @@ def test_service_identity_assign_and_unassign( assert attrs[0]['attributeName'] is None response = britive.workload.service_identities.assign( - service_identity_id=cached_service_identity['userId'], + 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 + 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.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 + principal_id=cached_service_identity_federated['userId'], as_dict=False ) assert isinstance(attrs, list) diff --git a/tests/test_240-secrets_manager.py b/tests/150-secrets_manager-01-secrets_manager.py similarity index 100% rename from tests/test_240-secrets_manager.py rename to tests/150-secrets_manager-01-secrets_manager.py diff --git a/tests/test_050-applications.py b/tests/200-application_management-01-applications.py similarity index 100% rename from tests/test_050-applications.py rename to tests/200-application_management-01-applications.py diff --git a/tests/test_060-environment_groups.py b/tests/200-application_management-02-environment_groups.py similarity index 100% rename from tests/test_060-environment_groups.py rename to tests/200-application_management-02-environment_groups.py diff --git a/tests/test_070-environments.py b/tests/200-application_management-03-environments.py similarity index 98% rename from tests/test_070-environments.py rename to tests/200-application_management-03-environments.py index df9a91f..a1d1abf 100644 --- a/tests/test_070-environments.py +++ b/tests/200-application_management-03-environments.py @@ -34,7 +34,6 @@ def test_environment_test(cached_application, cached_environment): response = britive.environments.test( application_id=cached_application['appContainerId'], environment_id=cached_environment['id'] ) - print(response) assert isinstance(response, dict) assert 'success' in response diff --git a/tests/test_080-scans.py b/tests/200-application_management-04-scans.py similarity index 100% rename from tests/test_080-scans.py rename to tests/200-application_management-04-scans.py diff --git a/tests/test_090-accounts.py b/tests/200-application_management-05-accounts.py similarity index 100% rename from tests/test_090-accounts.py rename to tests/200-application_management-05-accounts.py diff --git a/tests/test_100-permissions.py b/tests/200-application_management-06-permissions.py similarity index 100% rename from tests/test_100-permissions.py rename to tests/200-application_management-06-permissions.py diff --git a/tests/test_110-groups.py b/tests/200-application_management-07-groups.py similarity index 100% rename from tests/test_110-groups.py rename to tests/200-application_management-07-groups.py diff --git a/tests/test_130-profiles.py b/tests/200-application_management-08-profiles.py similarity index 99% rename from tests/test_130-profiles.py rename to tests/200-application_management-08-profiles.py index 36ba06f..33f7e56 100644 --- a/tests/test_130-profiles.py +++ b/tests/200-application_management-08-profiles.py @@ -135,7 +135,6 @@ 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'] 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 99% rename from tests/test_270-system_policies.py rename to tests/250-system-01-policies.py index b27c9ff..c1fefe7 100644 --- a/tests/test_270-system_policies.py +++ b/tests/250-system-01-policies.py @@ -98,7 +98,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) diff --git a/tests/test_280_system_actions.py b/tests/250-system-02-actions.py similarity index 100% rename from tests/test_280_system_actions.py rename to tests/250-system-02-actions.py 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 100% rename from tests/test_300-system_roles.py rename to tests/250-system-04-roles.py diff --git a/tests/test_310-system_permissions.py b/tests/250-system-05-permissions.py similarity index 100% rename from tests/test_310-system_permissions.py rename to tests/250-system-05-permissions.py diff --git a/tests/test_140-task_services.py b/tests/300-workflows-01-task_services.py similarity index 100% rename from tests/test_140-task_services.py rename to tests/300-workflows-01-task_services.py diff --git a/tests/test_150-tasks.py b/tests/300-workflows-02-tasks.py similarity index 100% rename from tests/test_150-tasks.py rename to tests/300-workflows-02-tasks.py diff --git a/tests/test_230-notifications.py b/tests/300-workflows-03-notifications.py similarity index 100% rename from tests/test_230-notifications.py rename to tests/300-workflows-03-notifications.py 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/test_160-security_policies.py b/tests/400-security-01-policies.py similarity index 100% rename from tests/test_160-security_policies.py rename to tests/400-security-01-policies.py diff --git a/tests/test_170-saml.py b/tests/400-security-02-saml.py similarity index 100% rename from tests/test_170-saml.py rename to tests/400-security-02-saml.py 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/test_190-audit_logs.py b/tests/500-audit_logs-01-logs.py similarity index 73% rename from tests/test_190-audit_logs.py rename to tests/500-audit_logs-01-logs.py index 8087b2d..243ffff 100644 --- a/tests/test_190-audit_logs.py +++ b/tests/500-audit_logs-01-logs.py @@ -6,22 +6,22 @@ def test_fields(): fields = britive.audit_logs.fields() assert isinstance(fields, dict) - assert len(fields.keys()) == 18 + assert len(fields) == 18 def test_operators(): operators = britive.audit_logs.operators() assert isinstance(operators, dict) - assert len(operators.keys()) == 3 + assert len(operators) == 4 def test_query_json(): - events = britive.audit_logs.query(from_time=datetime.now() - timedelta(7), to_time=datetime.now()) + events = britive.audit_logs.query(from_time=(datetime.now() - timedelta(1)), 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) + csv = britive.audit_logs.query(from_time=datetime.now() - timedelta(1), to_time=datetime.now(), 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 62% rename from tests/test_220-my_access.py rename to tests/600-britive-01-my_access.py index 0afb930..b3349b1 100644 --- a/tests/test_220-my_access.py +++ b/tests/600-britive-01-my_access.py @@ -83,59 +83,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..1d4e98f --- /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_access.list_approvals() + 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..d80a824 --- /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_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 diff --git a/tests/test_990-delete_all_resources.py b/tests/999-cleanup-01-delete_all_resources.py similarity index 91% rename from tests/test_990-delete_all_resources.py rename to tests/999-cleanup-01-delete_all_resources.py index 9af2545..9a56d5a 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,7 +134,7 @@ 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( @@ -177,101 +169,6 @@ 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( @@ -328,7 +225,6 @@ 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( @@ -340,7 +236,6 @@ 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']) @@ -362,7 +257,6 @@ def test_environment_group_delete(cached_application, cached_environment_group): cleanup('environment-group') -# 050-applications def test_application_delete(cached_application): try: while True: @@ -381,32 +275,100 @@ 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') + + +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') + + +# 100-identity_management +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') -# 030-service_identities -def test_service_identities_delete(cached_service_identity): +def test_workload_identity_provider_oidc_delete(cached_workload_identity_provider_oidc): try: - response = britive.service_identities.delete(service_identity_id=cached_service_identity['userId']) + response = britive.workload.identity_providers.delete(cached_workload_identity_provider_oidc['id']) assert response is None + finally: + cleanup('workload-identity-provider-oidc') - with pytest.raises(exceptions.NotFound): - britive.service_identities.get_by_name(name=cached_service_identity['name']) + +def test_service_identities_delete(cached_service_identity, cached_service_identity_federated): + try: + for si in [cached_service_identity, cached_service_identity_federated]: + print(si) + response = britive.service_identities.delete(service_identity_id=si['userId']) + assert response is None + with pytest.raises(exceptions.NotFound): + britive.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']) @@ -415,7 +377,6 @@ def test_tags_delete(cached_tag): cleanup('tag') -# 010-users def test_user_delete(cached_user): try: response = britive.users.delete(user_id=cached_user['userId']) @@ -427,7 +388,23 @@ def test_user_delete(cached_user): cleanup('user') -# 005-identity_attributes +# 000-global_settings +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') + + def test_identity_attribute_delete(cached_identity_attribute): try: response = britive.identity_attributes.delete(attribute_id=cached_identity_attribute['id']) diff --git a/tests/cache.py b/tests/cache.py index 3140f22..02d221a 100644 --- a/tests/cache.py +++ b/tests/cache.py @@ -95,6 +95,16 @@ def cached_service_identity(pytestconfig, timestamp): return britive.service_identities.create(**service_identity_to_create) +@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', + } + return britive.service_identities.create(**service_identity_to_create) + + @pytest.fixture(scope='session') @cached_resource(name='service-identity-token') def cached_service_identity_token(pytestconfig, cached_service_identity): @@ -255,6 +265,15 @@ def cached_profile_approval_policy(pytestconfig, cached_profile, cached_service_ return britive.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): @@ -824,7 +843,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') From 90495b3462c9accdcdc605d05a9da18a4b7cb2a3 Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 11 Nov 2024 11:25:22 -0600 Subject: [PATCH 02/40] feat:broker pools and resource param values --- src/britive/access_broker/__init__.py | 14 ++ src/britive/access_broker/access_broker.py | 11 - src/britive/access_broker/brokers.py | 21 ++ src/britive/access_broker/pools.py | 212 ++++++++++++++++++ .../access_broker/resources/resources.py | 7 +- src/britive/my_resources.py | 23 ++ 6 files changed, 276 insertions(+), 12 deletions(-) delete mode 100644 src/britive/access_broker/access_broker.py create mode 100644 src/britive/access_broker/brokers.py create mode 100644 src/britive/access_broker/pools.py diff --git a/src/britive/access_broker/__init__.py b/src/britive/access_broker/__init__.py index e69de29..364f7b4 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.profiles import Profile +from .resources.resources import Resources +from .response_templates import ResponseTemplates + + +class AccessBroker: + def __init__(self, britive): + self.profiles = Profile(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/resources/resources.py b/src/britive/access_broker/resources/resources.py index da2af3c..7ab5f47 100644 --- a/src/britive/access_broker/resources/resources.py +++ b/src/britive/access_broker/resources/resources.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/my_resources.py b/src/britive/my_resources.py index 0db7952..3d938fc 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -375,6 +375,29 @@ def favorites(self) -> list: return self.list_profiles(list_type='favorites') + 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 + """ + + return self.delete(f'{self.base_url}/favorites/{favorite_id}') + 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()}': { From edee68c2685d99aa60f25b7f168ee635d7907e0c Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 11 Nov 2024 11:26:14 -0600 Subject: [PATCH 03/40] feat:my resource checkout approvals and itsm --- src/britive/my_access.py | 14 +++++++ src/britive/my_resources.py | 75 ++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 3b56809..79b9bc2 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -139,6 +139,8 @@ def _checkout( otp: str = None, programmatic: bool = True, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, wait_time: int = 60, ) -> dict: params = {'accessType': 'PROGRAMMATIC' if programmatic else 'CONSOLE'} @@ -202,6 +204,8 @@ def _checkout( 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, ) @@ -235,6 +239,8 @@ def _checkout( profile_id=profile_id, programmatic=programmatic, progress_func=progress_func, + ticket_id=ticket_id, + ticket_type=ticket_type, wait_time=wait_time, ) raise e @@ -286,6 +292,8 @@ def checkout( otp: str = None, programmatic: bool = True, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, wait_time: int = 60, ) -> dict: """ @@ -310,6 +318,8 @@ def checkout( :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. @@ -345,6 +355,8 @@ def checkout_by_name( otp: str = None, programmatic: bool = True, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, wait_time: int = 60, ) -> dict: """ @@ -369,6 +381,8 @@ def checkout_by_name( :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. diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 3d938fc..1af2ba3 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -94,6 +94,8 @@ def _checkout( max_wait_time: int = 600, otp: str = None, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, wait_time: int = 60, ) -> dict: data = {'justification': justification} @@ -136,32 +138,34 @@ def _checkout( ) except 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] + raise StepUpAuthRequiredButNotProvided() from e + except 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 ApprovalRequiredButNoJustificationProvided() from e + + # request approval + status = MyRequests(self.britive).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] from e raise e transaction_id = transaction['transactionId'] @@ -191,6 +195,8 @@ def checkout( max_wait_time: int = 600, otp: str = None, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, wait_time: int = 60, ) -> dict: """ @@ -214,6 +220,8 @@ def checkout( 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 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. @@ -227,13 +235,16 @@ def checkout( """ return self._checkout( - profile_id=profile_id, - resource_id=resource_id, include_credentials=include_credentials, - wait_time=wait_time, + justification=justification, max_wait_time=max_wait_time, - progress_func=progress_func, otp=otp, + profile_id=profile_id, + progress_func=progress_func, + resource_id=resource_id, + ticket_id=ticket_id, + ticket_type=ticket_type, + wait_time=wait_time, ) def checkout_by_name( @@ -245,6 +256,8 @@ def checkout_by_name( max_wait_time: int = 600, otp: str = None, progress_func: Callable = None, + ticket_id: str = None, + ticket_type: str = None, wait_time: int = 60, ) -> dict: """ @@ -266,6 +279,8 @@ def checkout_by_name( 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 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. From 1e7da65aa42fb68dc1270acf9aa1ce2c73c2ffd8 Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 11 Nov 2024 11:26:44 -0600 Subject: [PATCH 04/40] feat:include approval_status when listing profiles --- src/britive/my_access.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 79b9bc2..a4a24b0 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -42,7 +42,6 @@ def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/access' - def list_profiles(self) -> list: # MyApprovals backwards compatibility self.__my_approvals = MyApprovals(self.britive) self.approve_request = self.__my_approvals.approve_request @@ -57,13 +56,25 @@ def list_profiles(self) -> list: self.withdraw_approval_request = self.__my_requests.withdraw_approval_request self.withdraw_approval_request_by_name = self.__my_requests.withdraw_approval_request_by_name + def list_profiles(self, include_approval_status: bool = False) -> list: """ List the profiles for which the user has access. + :param include_approval_status: Include `approval_status` of each profile :return: List of profiles. """ - return self.britive.get(self.base_url) + profiles = self.britive.get(self.base_url) + + if include_approval_status: + approval_status_list = { + a['papId']: a['status'] for a in self.britive.get(self.base_url, params={'type': 'ui'}) + } + for app in profiles: + for profile in app['profiles']: + profile['approval_status'] = approval_status_list[profile['profileId']] + + return profiles def list_checked_out_profiles(self) -> list: """ From bb828e6c35f3561c8ea71b0d40b249083345ed0d Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:05:40 -0600 Subject: [PATCH 05/40] feat:better exception clarity and handling --- src/britive/britive.py | 17 +- src/britive/exceptions/__init__.py | 170 ++++++++ src/britive/exceptions/badrequest.py | 528 +++++++++++++++++++++++++ src/britive/exceptions/exceptions.py | 162 -------- src/britive/exceptions/generic.py | 90 +++++ src/britive/exceptions/unauthorized.py | 120 ++++++ src/britive/my_access.py | 83 ++-- src/britive/my_resources.py | 68 ++-- src/britive/my_secrets.py | 66 ++-- 9 files changed, 1029 insertions(+), 275 deletions(-) create mode 100644 src/britive/exceptions/__init__.py create mode 100644 src/britive/exceptions/badrequest.py delete mode 100644 src/britive/exceptions/exceptions.py create mode 100644 src/britive/exceptions/generic.py create mode 100644 src/britive/exceptions/unauthorized.py diff --git a/src/britive/britive.py b/src/britive/britive.py index ca768a2..a7def8e 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -5,10 +5,14 @@ import requests +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 .access_broker import AccessBroker from .application_management import ApplicationManagement from .audit_logs import AuditLogs from .exceptions import ( + BritiveException, InvalidFederationProvider, RootEnvironmentGroupNotFound, TenantMissingError, @@ -369,14 +373,19 @@ def _handle_response(response): def __check_response_for_error(response) -> None: if response.status_code in allowed_exceptions: content = native_json.loads(response.content.decode('utf-8')) + error_code = content.get('errorCode', 'E0000') message = ( - f"{response.status_code} - " - f"{content.get('errorCode', 'E0000')} -" - f" {content.get('message', 'no message available')}" + f"{response.status_code} - " f"{error_code} -" f" {content.get('message', 'no message available')}" ) if content.get('details'): message += f" - {content.get('details')}" - raise allowed_exceptions[response.status_code](message) + raise unauthorized_code_map.get( + error_code, + bad_request_code_map.get( + error_code, + generic_code_map.get(error_code, allowed_exceptions.get(response.status_code, BritiveException)), + ), + )(message) @staticmethod def __response_has_no_content(response) -> bool: 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/exceptions.py b/src/britive/exceptions/exceptions.py deleted file mode 100644 index abf0edc..0000000 --- a/src/britive/exceptions/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/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/my_access.py b/src/britive/my_access.py index a4a24b0..9e83467 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -1,12 +1,9 @@ +import os import time from typing import Any, Callable from .exceptions import ( ApprovalRequiredButNoJustificationProvided, - ApprovalWorkflowRejected, - ApprovalWorkflowTimedOut, - ForbiddenRequest, - InvalidRequest, ProfileApprovalRejected, ProfileApprovalTimedOut, ProfileApprovalWithdrawn, @@ -14,6 +11,11 @@ StepUpAuthRequiredButNotProvided, TransactionNotFound, ) +from .exceptions.badrequest import ( + ApprovalJustificationRequiredError, + ProfileApprovalRequiredError, +) +from .exceptions.generic import BritiveGenericError, StepUpAuthenticationRequiredError from .my_approvals import MyApprovals from .my_requests import MyRequests @@ -195,39 +197,38 @@ def _checkout( transaction = self.britive.post( f'{self.base_url}/{profile_id}/environments/{environment_id}', params=params, json=data ) - except ForbiddenRequest as e: - if 'PE-0028' in str(e): # Check for stepup totp - raise StepUpAuthRequiredButNotProvided() from e - except InvalidRequest as e: - if 'MA-0009' in str(e): # old approval process that coupled approval and checkout - raise 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 ApprovalRequiredButNoJustificationProvided() from e - - # request approval - status = self.request_approval( - 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, + 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 = dict( + 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, + ) + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() == 'true': + status = MyAccessRequests(self.britive).request_approval(**approval_request) + else: + 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 @@ -335,11 +336,9 @@ def 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. """ @@ -398,8 +397,10 @@ def checkout_by_name( 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) diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 1af2ba3..be12672 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -3,8 +3,6 @@ from .exceptions import ( ApprovalRequiredButNoJustificationProvided, - ForbiddenRequest, - InvalidRequest, ProfileApprovalRejected, ProfileApprovalTimedOut, ProfileApprovalWithdrawn, @@ -13,6 +11,8 @@ TransactionNotFound, ) from .my_requests import MyRequests +from .exceptions.badrequest import ApprovalJustificationRequiredError, ProfileApprovalRequiredError +from .exceptions.generic import StepUpAuthenticationRequiredError approval_exceptions = { 'rejected': ProfileApprovalRejected(), @@ -136,36 +136,32 @@ def _checkout( transaction = self.britive.post( f'{self.base_url}/profiles/{profile_id}/resources/{resource_id}/checkout', json=data ) - except ForbiddenRequest as e: - if 'PE-0028' in str(e): # Check for stepup totp - raise StepUpAuthRequiredButNotProvided() from e - except 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 ApprovalRequiredButNoJustificationProvided() from e - - # request approval - status = MyRequests(self.britive).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, - ) + except StepUpAuthenticationRequiredError as e: + raise StepUpAuthRequiredButNotProvided(e) from e + except (ApprovalJustificationRequiredError, ProfileApprovalRequiredError) as e: + if not justification: + raise ApprovalRequiredButNoJustificationProvided() from e + + # request approval + status = MyResourcesRequests(self.britive).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] from e + # 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'] @@ -226,11 +222,9 @@ def 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. """ @@ -285,8 +279,10 @@ def checkout_by_name( 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) diff --git a/src/britive/my_secrets.py b/src/britive/my_secrets.py index 8b6b797..d4ec9a4 100644 --- a/src/britive/my_secrets.py +++ b/src/britive/my_secrets.py @@ -11,12 +11,18 @@ 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 @@ -101,23 +107,21 @@ def view( 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 ForbiddenRequest as e: - if 'PE-0002' in str(e): - raise 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 ApprovalRequiredButNoJustificationProvided() from e + raise ApprovalRequiredButNoJustificationProvided(e) from e else: - raise ApprovalWorkflowRejected() from e - if 'PE-0028' in str(e): # Check for stepup totp - raise StepUpAuthRequiredButNotProvided() from e - else: - raise 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 @@ -158,19 +162,17 @@ def download( # 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 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: - if 'PE-0002' in str(e): - raise 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 StepUpAuthRequiredButNotProvided() from e - - else: - raise e + raise e From c75d512193b3f8d9f026dd790970a261c3d580df Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:11:18 -0600 Subject: [PATCH 06/40] refactor:additional UI alignment cleanup --- src/britive/access_broker/__init__.py | 2 +- src/britive/access_broker/resources/types.py | 2 +- .../application_management/__init__.py | 2 +- .../application_management/access_builder.py | 10 +- src/britive/audit_logs/__init__.py | 4 +- src/britive/audit_logs/logs.py | 4 +- src/britive/britive.py | 179 +++++---- src/britive/federation_providers/__init__.py | 3 +- .../azure_system_assigned_managed_identity.py | 18 +- .../azure_user_assigned_managed_identity.py | 18 +- src/britive/helpers/__init__.py | 4 +- src/britive/helpers/custom_attributes.py | 4 +- src/britive/helpers/methods.py | 65 ++- src/britive/identity_management/__init__.py | 2 +- .../identity_management/service_identities.py | 2 +- src/britive/identity_management/users.py | 4 +- src/britive/my_access.py | 175 ++++---- src/britive/my_approvals.py | 18 +- src/britive/my_requests.py | 376 ++++++++++++++---- src/britive/my_resources.py | 39 +- src/britive/security/__init__.py | 2 +- src/britive/system/permissions.py | 4 +- src/britive/workflows/__init__.py | 2 +- ...entity_management-05-identity_providers.py | 4 +- tests/100-identity_management-06-workload.py | 4 +- tests/999-cleanup-01-delete_all_resources.py | 4 +- tests/cache.py | 17 +- 27 files changed, 646 insertions(+), 322 deletions(-) diff --git a/src/britive/access_broker/__init__.py b/src/britive/access_broker/__init__.py index 364f7b4..f6d4d27 100644 --- a/src/britive/access_broker/__init__.py +++ b/src/britive/access_broker/__init__.py @@ -6,7 +6,7 @@ class AccessBroker: - def __init__(self, britive): + def __init__(self, britive) -> None: self.profiles = Profile(britive) self.resources = Resources(britive) self.response_templates = ResponseTemplates(britive) 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/application_management/__init__.py b/src/britive/application_management/__init__.py index 41fcf45..0317e79 100644 --- a/src/britive/application_management/__init__.py +++ b/src/britive/application_management/__init__.py @@ -10,7 +10,7 @@ class ApplicationManagement: - def __init__(self, britive): + def __init__(self, britive) -> None: self.access_builder = AccessBuilderSettings(britive) self.accounts = Accounts(britive) self.applications = Applications(britive) diff --git a/src/britive/application_management/access_builder.py b/src/britive/application_management/access_builder.py index 18eb4d6..08dad39 100644 --- a/src/britive/application_management/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' diff --git a/src/britive/audit_logs/__init__.py b/src/britive/audit_logs/__init__.py index b60a198..c1cd5b2 100644 --- a/src/britive/audit_logs/__init__.py +++ b/src/britive/audit_logs/__init__.py @@ -4,5 +4,5 @@ class AuditLogs: def __init__(self, britive) -> None: - self.audit_logs = Logs(britive) - self.audit_logs.webhooks = Webhooks(britive) + self.logs = Logs(britive) + self.webhooks = Webhooks(britive) diff --git a/src/britive/audit_logs/logs.py b/src/britive/audit_logs/logs.py index 44442dc..fd7645f 100644 --- a/src/britive/audit_logs/logs.py +++ b/src/britive/audit_logs/logs.py @@ -58,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 diff --git a/src/britive/britive.py b/src/britive/britive.py index a7def8e..783d1fe 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -8,6 +8,8 @@ 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 . import __version__ from .access_broker import AccessBroker from .application_management import ApplicationManagement from .audit_logs import AuditLogs @@ -30,7 +32,7 @@ SpaceliftFederationProvider, ) from .global_settings import GlobalSettings -from .helpers import methods as helper_methods +from .helpers import HelperMethods as helper_methods from .identity_management import IdentityManagement from .my_access import MyAccess from .my_approvals import MyApprovals @@ -81,12 +83,12 @@ class Britive: def __init__( self, - tenant=None, - token=None, - query_features=True, - token_federation_provider=None, - token_duration=900, - ): + tenant: str = None, + token: str = None, + query_features: bool = True, + token_federation_provider: str = None, + token_duration: int = 900, + ) -> None: """ Instantiate an authenticated interface that can be used to communicate with the Britive API. @@ -119,12 +121,12 @@ def __init__( self._initialize_components(query_features) - def _initialize_token(self, token, provider, duration): + def _initialize_token(self, token: str, provider: str, duration: int) -> str: if provider: return self.source_federation_token_from(provider, self.tenant, duration) return token or os.getenv('BRITIVE_API_TOKEN') or TokenMissingError('Token not provided.') - def _setup_session(self): + def _setup_session(self) -> requests.Session: session = requests.Session() # if PYBRITIVE_CA_BUNDLE set, in pybritive most likely, use it @@ -137,7 +139,7 @@ def _setup_session(self): self._disable_ssl_verification_warnings() token_type = self._determine_token_type() - version = self._get_version() + version = __version__ session.headers.update( { @@ -148,7 +150,7 @@ def _setup_session(self): ) return session - def _disable_ssl_verification_warnings(self): + 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'] = '' @@ -156,66 +158,68 @@ def _disable_ssl_verification_warnings(self): urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - def _determine_token_type(self): + def _determine_token_type(self) -> str: if len(self.__token) < 50: return 'TOKEN' if len(self.__token.split('::')) > 1: return 'WorkloadToken' return 'Bearer' - def _get_version(self): - try: - import britive - - return britive.__version__ - except ImportError: - return 'unknown' - - def _initialize_components(self, query_features): - application_management = ApplicationManagement(self) - identity_management = IdentityManagement(self) - global_settings = GlobalSettings(self) - security = Security(self) - workflows = Workflows(self) - - self.access_builder = application_management.access_builder - self.accounts = application_management.accounts + def _initialize_components(self, query_features: bool) -> None: + self.access_broker = AccessBroker(self) self.api_tokens = ApiTokens(self) - self.applications = application_management.applications - self.audit_logs = AuditLogs(self).audit_logs - self.environment_groups = application_management.environment_groups - self.environments = application_management.environments self.feature_flags = self.features() if query_features else {} - self.groups = application_management.groups - self.identity_attributes = identity_management.identity_attributes - self.identity_providers = identity_management.identity_providers 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 = global_settings.notification_mediums - self.notifications = workflows.notifications - self.permissions = application_management.permissions - self.profiles = application_management.profiles self.reports = Reports(self) - self.saml = security.saml - self.scans = application_management.scans self.secrets_manager = SecretsManager(self) - self.security_policies = security.security_policies - self.service_identities = identity_management.service_identities - self.service_identity_tokens = identity_management.service_identity_tokens - self.settings = GlobalSettings(self) - self.step_up = security.step_up_auth self.system = System(self) - self.tags = identity_management.tags - self.task_services = workflows.task_services - self.tasks = workflows.tasks - self.users = identity_management.users - self.workload = identity_management.workload - # depends on my_access - self.access_broker = AccessBroker(self) + application_management = ApplicationManagement(self) + audit_logs = AuditLogs(self) + identity_management = IdentityManagement(self) + global_settings = GlobalSettings(self) + security = Security(self) + workflows = Workflows(self) + + # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() == 'true': + self.application_management = application_management + self.audit_logs = audit_logs + self.identity_management = identity_management + self.global_settings = global_settings + self.security = security + self.workflows = workflows + else: + self.access_builder = application_management.access_builder + self.accounts = application_management.accounts + self.applications = application_management.applications + self.audit_logs = audit_logs.logs + self.audit_logs.webhooks = audit_logs.webhooks + self.environment_groups = application_management.environment_groups + self.environments = application_management.environments + self.groups = application_management.groups + self.identity_attributes = identity_management.identity_attributes + self.identity_providers = identity_management.identity_providers + self.notification_mediums = global_settings.notification_mediums + self.notifications = workflows.notifications + self.permissions = application_management.permissions + self.profiles = application_management.profiles + self.saml = security.saml + self.scans = application_management.scans + self.security_policies = security.security_policies + self.service_identities = identity_management.service_identities + self.service_identity_tokens = identity_management.service_identity_tokens + self.step_up = security.step_up_auth + self.tags = identity_management.tags + self.task_services = workflows.task_services + self.tasks = workflows.tasks + self.users = identity_management.users + self.workload = identity_management.workload + self.settings = global_settings @staticmethod def source_federation_token_from(provider: str, tenant: str = None, duration_seconds: int = 900) -> str: @@ -234,12 +238,12 @@ def source_federation_token_from(provider: str, tenant: str = None, duration_sec 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) + * 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. @@ -252,9 +256,6 @@ def source_federation_token_from(provider: str, tenant: str = None, duration_sec 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 @@ -264,6 +265,9 @@ def source_federation_token_from(provider: str, tenant: str = None, duration_sec `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`. @@ -280,12 +284,8 @@ def source_federation_token_from(provider: str, tenant: str = None, duration_sec provider_name = helper[0] federation_providers = { - 'aws': lambda: AwsFederationProvider(profile=helper_methods.safe_list_get(helper, 1)).get_token(), - 'azuresmi': lambda: AzureSystemAssignedManagedIdentityFederationProvider( - audience=helper_methods.safe_list_get(helper, 1) - ).get_token(), - 'azureumi': lambda: AzureUserAssignedManagedIdentityFederationProvider( - client_id=helper[1].split('|')[0], audience=helper_methods.safe_list_get(helper[1].split('|'), 1) + 'aws': lambda: AwsFederationProvider( + profile=helper_methods.safe_list_get(helper, 1), tenant=tenant, duration=duration_seconds ).get_token(), 'bitbucket': lambda: BitbucketFederationProvider().get_token(), 'github': lambda: GithubFederationProvider(audience=helper_methods.safe_list_get(helper, 1)).get_token(), @@ -298,6 +298,16 @@ def source_federation_token_from(provider: str, tenant: str = None, duration_sec if provider_name in federation_providers: return federation_providers[provider_name]() + if provider_name == 'azuresmi': + return AzureSystemAssignedManagedIdentityFederationProvider( + audience=helper_methods.safe_list_get(helper, 1) + ).get_token() + + if provider_name == 'azureumi': + return AzureUserAssignedManagedIdentityFederationProvider( + client_id=helper[1].split('|')[0], audience=helper_methods.safe_list_get(helper[1].split('|'), 1) + ).get_token() + raise InvalidFederationProvider(f'federation provider {provider_name} not supported') @staticmethod @@ -370,20 +380,22 @@ def _handle_response(response): return response.content.decode('utf-8') @staticmethod - def __check_response_for_error(response) -> None: - if response.status_code in allowed_exceptions: - content = native_json.loads(response.content.decode('utf-8')) - error_code = content.get('errorCode', 'E0000') - message = ( - f"{response.status_code} - " f"{error_code} -" f" {content.get('message', 'no message available')}" - ) - if content.get('details'): - message += f" - {content.get('details')}" + 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(response.status_code, BritiveException)), + generic_code_map.get(error_code, allowed_exceptions.get(status_code, BritiveException)), ), )(message) @@ -397,13 +409,13 @@ 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']): + 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 + 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']): + if is_dict and all(x in result for x in ('result', 'pagination')): return 'secmgr' return 'none' @@ -427,7 +439,7 @@ def __request_with_exponential_backoff_and_retry(self, method, url, params, data time.sleep((2**num_retries) * self.retry_backoff_factor) num_retries += 1 else: - self.__check_response_for_error(response) + self.__check_response_for_error(response.status_code, self._handle_response(response)) return response def __request(self, method, url, params=None, data=None, json=None) -> dict: @@ -460,7 +472,7 @@ def __request(self, method, url, params=None, data=None, json=None) -> dict: break params['page'] = result['page'] + 1 elif pagination_type in ('audit', 'report'): - return_data += result['data'] + return_data += result if pagination_type == 'audit' else result['data'] if 'next-page' not in response.headers: break url = response.headers['next-page'] @@ -479,7 +491,10 @@ def __request(self, method, url, params=None, data=None, json=None) -> dict: def get_root_environment_group(self, application_id: str) -> str: """Internal use only.""" - app = self.applications.get(application_id=application_id) + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() == 'true': + app = self.application_management.applications.get(application_id=application_id) + else: + app = self.applications.get(application_id=application_id) root_env_group = app.get('rootEnvironmentGroup', {}).get('environmentGroups', []) for group in root_env_group: if not group['parentId']: diff --git a/src/britive/federation_providers/__init__.py b/src/britive/federation_providers/__init__.py index 1da8396..1577514 100644 --- a/src/britive/federation_providers/__init__.py +++ b/src/britive/federation_providers/__init__.py @@ -9,7 +9,7 @@ class FederationProviders: - def __init__(self, britive): + 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) @@ -18,4 +18,3 @@ def __init__(self, britive): self.github = GithubFederationProvider(britive) self.gitlab = GitlabFederationProvider(britive) self.spacelift = SpaceliftFederationProvider(britive) - diff --git a/src/britive/federation_providers/azure_system_assigned_managed_identity.py b/src/britive/federation_providers/azure_system_assigned_managed_identity.py index 50bea7f..eea6653 100644 --- a/src/britive/federation_providers/azure_system_assigned_managed_identity.py +++ b/src/britive/federation_providers/azure_system_assigned_managed_identity.py @@ -1,15 +1,6 @@ -from ..exceptions import NotExecutingInAzureEnvironment +from ..exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment from .federation_provider import FederationProvider -try: - from azure.identity import ManagedIdentityCredential - 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 - class AzureSystemAssignedManagedIdentityFederationProvider(FederationProvider): def __init__(self, audience: str = None) -> None: @@ -18,8 +9,15 @@ def __init__(self, audience: str = None) -> None: 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 ' diff --git a/src/britive/federation_providers/azure_user_assigned_managed_identity.py b/src/britive/federation_providers/azure_user_assigned_managed_identity.py index 19a080b..5a8cf53 100644 --- a/src/britive/federation_providers/azure_user_assigned_managed_identity.py +++ b/src/britive/federation_providers/azure_user_assigned_managed_identity.py @@ -1,15 +1,6 @@ -from ..exceptions import NotExecutingInAzureEnvironment +from ..exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment from .federation_provider import FederationProvider -try: - from azure.identity import ManagedIdentityCredential - 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 - class AzureUserAssignedManagedIdentityFederationProvider(FederationProvider): def __init__(self, client_id: str, audience: str = None) -> None: @@ -19,8 +10,15 @@ def __init__(self, client_id: str, audience: str = None) -> None: 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 ' diff --git a/src/britive/helpers/__init__.py b/src/britive/helpers/__init__.py index 354a571..e1fec69 100644 --- a/src/britive/helpers/__init__.py +++ b/src/britive/helpers/__init__.py @@ -1,6 +1,8 @@ from .custom_attributes import CustomAttributes +from .methods import HelperMethods class Helpers: - def __init__(self, britive): + def __init__(self, britive) -> None: self.custom_attributes = CustomAttributes(britive) + self.helper_methods = HelperMethods(britive) diff --git a/src/britive/helpers/custom_attributes.py b/src/britive/helpers/custom_attributes.py index 7f2f065..d91017c 100644 --- a/src/britive/helpers/custom_attributes.py +++ b/src/britive/helpers/custom_attributes.py @@ -3,8 +3,8 @@ 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: """ diff --git a/src/britive/helpers/methods.py b/src/britive/helpers/methods.py index 899b475..c925c74 100644 --- a/src/britive/helpers/methods.py +++ b/src/britive/helpers/methods.py @@ -1,8 +1,63 @@ from typing import Union -def safe_list_get(lst, idx, default) -> Union[str, None]: - try: - return lst[idx] - except IndexError: - return default +class HelperMethods: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/access' + + def safe_list_get(lst: list, idx: int, default: str = None) -> Union[str, None]: + try: + return lst[idx] + except IndexError: + return default + + 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 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() + } + + 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/identity_management/__init__.py b/src/britive/identity_management/__init__.py index b2ab0c4..4d45abe 100644 --- a/src/britive/identity_management/__init__.py +++ b/src/britive/identity_management/__init__.py @@ -7,7 +7,7 @@ class IdentityManagement: - def __init__(self, britive): + def __init__(self, britive) -> None: self.identity_attributes = IdentityAttributes(britive) self.identity_providers = IdentityProviders(britive) self.service_identities = ServiceIdentities(britive) diff --git a/src/britive/identity_management/service_identities.py b/src/britive/identity_management/service_identities.py index 310315e..259b507 100644 --- a/src/britive/identity_management/service_identities.py +++ b/src/britive/identity_management/service_identities.py @@ -12,7 +12,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: """ diff --git a/src/britive/identity_management/users.py b/src/britive/identity_management/users.py index 8ab5d52..5897b77 100644 --- a/src/britive/identity_management/users.py +++ b/src/britive/identity_management/users.py @@ -12,7 +12,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: @@ -239,7 +239,7 @@ def reset_mfa(self, user_id: str) -> None: """ user = self.get(user_id) - if not user['identityProvider']['mfaEnabled']: + if not user['identityProvider'].get('mfaEnabled'): raise UserDoesNotHaveMFAEnabled() return self.britive.patch(f'{self.base_url}/{user_id}/resetmfa') diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 9e83467..706b6b7 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -16,8 +16,9 @@ ProfileApprovalRequiredError, ) from .exceptions.generic import BritiveGenericError, StepUpAuthenticationRequiredError +from .helpers import HelperMethods from .my_approvals import MyApprovals -from .my_requests import MyRequests +from .my_requests import MyAccessRequests approval_exceptions = { 'rejected': ProfileApprovalRejected(), @@ -28,8 +29,7 @@ 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 @@ -43,20 +43,24 @@ class MyAccess: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/access' - - # MyApprovals backwards compatibility - self.__my_approvals = MyApprovals(self.britive) - self.approve_request = self.__my_approvals.approve_request - self.list_approvals = self.__my_approvals.list_approvals - self.reject_request = self.__my_approvals.reject_request - - # MyRequests backwards compatibility - self.__my_requests = MyRequests(self.britive) - self.approval_request_status = self.__my_requests.approval_request_status - self.request_approval = self.__my_requests.request_approval - self.request_approval_by_name = self.__my_requests.request_approval_by_name - self.withdraw_approval_request = self.__my_requests.withdraw_approval_request - self.withdraw_approval_request_by_name = self.__my_requests.withdraw_approval_request_by_name + self._get_profile_and_environment_ids_given_names = HelperMethods( + self.britive + ).get_profile_and_environment_ids_given_names + + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': + # MyApprovals backwards compatibility + self.__my_approvals = MyApprovals(self.britive) + self.approve_request = self.__my_approvals.approve_request + self.list_approvals = self.__my_approvals.list_approvals + self.reject_request = self.__my_approvals.reject_request + + # MyRequests backwards compatibility + self.__my_requests = MyAccessRequests(self.britive) + self.approval_request_status = self.__my_requests.approval_request_status + self.request_approval = self.__my_requests.request_approval + self.request_approval_by_name = self.__my_requests.request_approval_by_name + self.withdraw_approval_request = self.__my_requests.withdraw_approval_request + self.withdraw_approval_request_by_name = self.__my_requests.withdraw_approval_request_by_name def list_profiles(self, include_approval_status: bool = False) -> list: """ @@ -69,23 +73,41 @@ def list_profiles(self, include_approval_status: bool = False) -> list: profiles = self.britive.get(self.base_url) if include_approval_status: - approval_status_list = { - a['papId']: a['status'] for a in self.britive.get(self.base_url, params={'type': 'ui'}) + access_type_details = { + (a['papId'], a['environmentId'], a['accessType'].lower()): a['myAccessDetails'] + for a in self.britive.get(self.base_url, params={'type': 'ui'}) } for app in profiles: for profile in app['profiles']: - profile['approval_status'] = approval_status_list[profile['profileId']] + for environment in profile['environments']: + for access_type in ('console', 'programmatic'): + if profile.get(f'{access_type}Access'): + environment[f'{access_type}_access_details'] = access_type_details[ + (profile['profileId'], environment['environmentId'], access_type) + ] return profiles - 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: """ @@ -100,6 +122,35 @@ def get_checked_out_profile(self, transaction_id: str) -> dict: return t 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: """ Extend the expiration time of a currently checked out profile. @@ -259,25 +310,6 @@ def _checkout( 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 TransactionNotFound as e: - raise ApprovalWorkflowRejected() from e - if transaction['status'] == 'checkOutInApproval': # we have an approval workflow occurring - if time.time() >= quit_time: - raise 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 @@ -494,9 +526,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. """ @@ -512,58 +553,8 @@ def favorites(self) -> list: return self.britive.get(f'{self.base_url}/favorites') - 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 _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 diff --git a/src/britive/my_approvals.py b/src/britive/my_approvals.py index 6adbcfb..e89eb13 100644 --- a/src/britive/my_approvals.py +++ b/src/britive/my_approvals.py @@ -1,4 +1,16 @@ 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' @@ -15,7 +27,7 @@ def approve_request(self, request_id: str, comments: str = '') -> None: params = {'approveRequest': 'yes'} data = {'approverComment': comments} - return self.britive.patch(f'{self.britive.base_url}/{request_id}', params=params, json=data) + return self.britive.patch(f'{self.base_url}/{request_id}', params=params, json=data) def reject_request(self, request_id: str, comments: str = '') -> None: """ @@ -29,7 +41,7 @@ def reject_request(self, request_id: str, comments: str = '') -> None: params = {'approveRequest': 'no'} data = {'approverComment': comments} - return self.britive.patch(f'{self.britive.base_url}/{request_id}', params=params, json=data) + return self.britive.patch(f'{self.base_url}/{request_id}', params=params, json=data) def list_approvals(self) -> dict: """ @@ -40,4 +52,4 @@ def list_approvals(self) -> dict: params = {'requestType': 'myApprovals', 'consumer': 'papservice'} - return self.britive.get(f'{self.britive.base_url}', params=params) + return self.britive.get(f'{self.base_url}/', params=params) diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py index 39ef802..96a444f 100644 --- a/src/britive/my_requests.py +++ b/src/britive/my_requests.py @@ -6,12 +6,26 @@ 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: """ @@ -20,8 +34,130 @@ def list(self) -> list: :return: List of My Requests. """ - return self.britive.get(self.britive.base_url, params={'requestType': 'myRequests'}) + return self.britive.get(f'{self.base_url}/', params={'requestType': 'myRequests'}) + + 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.base_url}/{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 == 'environment' + 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, + 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 == 'environment': + ids = self._helper.get_profile_and_environment_ids_given_names(profile_name, entity_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: + if request_id: + return self.britive.delete(f'{self.base_url}/{request_id}') + + url = f'{self.base_url}/consumer/{entity_type}/resource?resourceId={profile_id}/{entity_id}' + + return self.britive.delete(url) + + +class MyAccessRequests(MyRequests): def request_approval_by_name( self, profile_name: str, @@ -41,8 +177,10 @@ def request_approval_by_name( 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 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. @@ -61,12 +199,11 @@ def request_approval_by_name( :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'], + return self._request_approval_by_name( justification=justification, + profile_name=profile_name, + entity_name=environment_name, + entity_type='environment', block_until_disposition=block_until_disposition, max_wait_time=max_wait_time, progress_func=progress_func, @@ -78,11 +215,12 @@ def request_approval_by_name( def request_approval( self, profile_id: str, - environment_id: str, justification: str, block_until_disposition: bool = False, + environment_id: str = None, max_wait_time: int = 600, progress_func: Callable = None, + resource_id: str = None, ticket_id: str = None, ticket_type: str = None, wait_time: int = 60, @@ -93,8 +231,10 @@ def request_approval( 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 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 @@ -111,96 +251,196 @@ def request_approval( :raises ProfileApprovalMaxBlockTimeExceeded: if max_wait_time has been reached while waiting for approval. """ - data = {'justification': justification} + return self._request_approval( + justification=justification, + profile_name=profile_id, + entity_name=environment_id, + entity_type='environment', + 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, + ) - if ticket_id and ticket_type: - data.update(ticketId=ticket_id, ticketType=ticket_type) + 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) - request = self.britive.post( - f'{self.britive.base_url}/access/{profile_id}/environments/{environment_id}/approvalRequest', json=data + return self._withdraw_approval_request( + profile_id=ids['profile_id'], entity_id=ids['environment_id'], entity_type='papservice' ) - if request is None: - raise ProfileCheckoutAlreadyApproved() + def withdraw_approval_request( + self, request_id: str = None, profile_id: str = None, environment_id: str = None + ) -> None: + """ + Withdraws a pending approval request. - request_id = request['requestId'] + Either `request_id` or (`profile_id` AND `environment_id`) are required. - 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 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() + :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') - else: - return request + return self._withdraw_approval_request( + profile_id=profile_id, entity_id=environment_id, entity_type='papservice' + ) - def approval_request_status(self, request_id: str) -> dict: + +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: """ - Provides details on and approval request. + Requests approval to checkout a profile at a later time. - :param request_id: The ID of the approval request. - :return: Details of the approval request. + 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.britive.get(f'{self.britive.base_url}/{request_id}') + return self._request_approval( + justification=justification, + profile_name=profile_id, + entity_name=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, environment_name: str, application_name: str = None + self, profile_name: str, environment_name: str = None, 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 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 resource_name: The name of the resource. Use `list_profiles()` to obtain the eligible resources. :return: None """ - ids = self._get_profile_and_environment_ids_given_names(profile_name, environment_name, application_name) + ids = self._helper.get_profile_and_resource_ids_given_names(profile_name, resource_name) - return self.withdraw_approval_request(profile_id=ids['profile_id'], environment_id=ids['environment_id']) + 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, environment_id: str = None + 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 `environment_id`) are required. + 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 environment_id: The ID of the environment. + :param resource_id: The ID of the resource. :return: None """ - url = None - if request_id: - url = f'{self.britive.base_url}/{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}/consumer/papservice/resource?resourceId=' f'{profile_id}/{environment_id}' + if not request_id and not all([profile_id, resource_id]): + raise ValueError('profile_id and resource_id are required') - return self.britive.delete(url) + 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 be12672..9e467b4 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -10,9 +10,10 @@ StepUpAuthRequiredButNotProvided, TransactionNotFound, ) -from .my_requests import MyRequests from .exceptions.badrequest import ApprovalJustificationRequiredError, ProfileApprovalRequiredError from .exceptions.generic import StepUpAuthenticationRequiredError +from .helpers import HelperMethods +from .my_requests import MyResourcesRequests approval_exceptions = { 'rejected': ProfileApprovalRejected(), @@ -37,6 +38,9 @@ class MyResources: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/resource-manager/my-resources' + self._get_profile_and_resource_ids_given_names = HelperMethods( + self.britive + ).get_profile_and_resource_ids_given_names def list_profiles(self, list_type: str = None, search_text: str = None) -> list: """ @@ -409,19 +413,26 @@ def delete_favorite(self, favorite_id: str) -> None: return self.delete(f'{self.base_url}/favorites/{favorite_id}') - 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 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') - item = resource_profile_map.get(f'{resource_name.lower()}|{profile_name.lower()}') + 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/security/__init__.py b/src/britive/security/__init__.py index 2884b79..6348ae4 100644 --- a/src/britive/security/__init__.py +++ b/src/britive/security/__init__.py @@ -5,7 +5,7 @@ class Security: - def __init__(self, britive): + def __init__(self, britive) -> None: self.api_tokens = ApiTokens(britive) self.saml = Saml(britive) self.security_policies = SecurityPolicies(britive) diff --git a/src/britive/system/permissions.py b/src/britive/system/permissions.py index 25aa816..ba6a2d6 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: diff --git a/src/britive/workflows/__init__.py b/src/britive/workflows/__init__.py index 2286c82..8a13e68 100644 --- a/src/britive/workflows/__init__.py +++ b/src/britive/workflows/__init__.py @@ -4,7 +4,7 @@ class Workflows: - def __init__(self, britive): + def __init__(self, britive) -> None: self.notifications = Notifications(britive) self.task_services = TaskServices(britive) self.tasks = Tasks(britive) diff --git a/tests/100-identity_management-05-identity_providers.py b/tests/100-identity_management-05-identity_providers.py index 9c0beea..fad9495 100644 --- a/tests/100-identity_management-05-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` @@ -93,7 +93,7 @@ def test_scim_tokens_update_attribute_mapping(cached_identity_provider): def test_configure_mfa(cached_identity_provider): - with pytest.raises(exceptions.InvalidRequest) as e: + with pytest.raises(BritiveGenericError) as e: britive.identity_providers.configure_mfa( identity_provider_id=cached_identity_provider['id'], root_user=False, non_root_user=True ) diff --git a/tests/100-identity_management-06-workload.py b/tests/100-identity_management-06-workload.py index e3c7b9a..8d1d334 100644 --- a/tests/100-identity_management-06-workload.py +++ b/tests/100-identity_management-06-workload.py @@ -1,4 +1,4 @@ -from britive import exceptions +from britive.exceptions.generic import BritiveGenericException from .cache import * # will also import some globals like `britive` @@ -82,7 +82,7 @@ def test_generate_attribute_map(cached_identity_attribute): def test_service_identity_get_when_nothing_associated(cached_service_identity_federated): - with pytest.raises(exceptions.NotFound): + with pytest.raises(BritiveGenericException): britive.workload.service_identities.get(service_identity_id=cached_service_identity_federated['userId']) diff --git a/tests/999-cleanup-01-delete_all_resources.py b/tests/999-cleanup-01-delete_all_resources.py index 9a56d5a..385c65a 100644 --- a/tests/999-cleanup-01-delete_all_resources.py +++ b/tests/999-cleanup-01-delete_all_resources.py @@ -355,11 +355,9 @@ def test_workload_identity_provider_oidc_delete(cached_workload_identity_provide def test_service_identities_delete(cached_service_identity, cached_service_identity_federated): try: for si in [cached_service_identity, cached_service_identity_federated]: - print(si) response = britive.service_identities.delete(service_identity_id=si['userId']) assert response is None - with pytest.raises(exceptions.NotFound): - britive.service_identities.get_by_name(name=si['name']) + assert not britive.service_identities.get_by_name(name=si['name']) except exceptions.NotFound: pass finally: diff --git a/tests/cache.py b/tests/cache.py index 02d221a..0f96d06 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')) @@ -92,7 +92,10 @@ def cached_service_identity(pytestconfig, timestamp): 'name': f'testpythonapiwrapperserviceidentity{timestamp}', 'status': 'active', } - return britive.service_identities.create(**service_identity_to_create) + try: + return britive.service_identities.create(**service_identity_to_create) + except UserCreationError: + return britive.service_identities.get_by_name(service_identity_to_create['name'])[0] @pytest.fixture(scope='session') @@ -102,8 +105,10 @@ def cached_service_identity_federated(pytestconfig, timestamp): 'name': f'testpythonapiwrapperfederated{timestamp}', 'status': 'active', } - return britive.service_identities.create(**service_identity_to_create) - + try: + return britive.service_identities.create(**service_identity_to_create) + except UserCreationError: + return britive.service_identities.get_by_name(service_identity_to_create['name'])[0] @pytest.fixture(scope='session') @cached_resource(name='service-identity-token') @@ -654,7 +659,7 @@ def cached_workload_identity_provider_aws(pytestconfig, timestamp, cached_identi 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 From 5b2f50f5fb9fecdeceb0098378143898ec86fca1 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:11:55 -0600 Subject: [PATCH 07/40] feat:firewall settings --- src/britive/global_settings/__init__.py | 2 + src/britive/global_settings/firewall.py | 50 ++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/britive/global_settings/__init__.py b/src/britive/global_settings/__init__.py index de72bde..0ff7b79 100644 --- a/src/britive/global_settings/__init__.py +++ b/src/britive/global_settings/__init__.py @@ -1,8 +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/global_settings/firewall.py b/src/britive/global_settings/firewall.py index 834fa70..fc63df2 100644 --- a/src/britive/global_settings/firewall.py +++ b/src/britive/global_settings/firewall.py @@ -1,9 +1,18 @@ class Firewall: - def __init__(self, britive): + def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/settings/firewall' - def save(self, rules: list, default_action: str = 'DENY', antilockout: bool = False) -> dict: + 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. @@ -40,10 +49,39 @@ def save(self, rules: list, default_action: str = 'DENY', antilockout: bool = Fa return self.britive.post(self.base_url, json=data) - def list(self): ... + def save_fields(self, fields: dict) -> None: + """ + Save firewall fields. - def get(self): ... + :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 updated(self): ... + def list_fields(self) -> list: + """ + Get the firewall fields. + + :returns: List of the firewall fields. + """ - def delete(self): ... + return self.britive.get(f'{self.base_url}/fields') From b787ba0070375bee092d134fabd2b7f01e36b684 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:12:26 -0600 Subject: [PATCH 08/40] feat:user filters and favorites --- src/britive/my_access.py | 104 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 706b6b7..f5a7996 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -546,15 +546,117 @@ 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 create_filter(self, filter_name: str, filter_properties: str, user_id: str = None) -> dict: """ + Create a user 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) + :param user_id: ID of the user to create the filter for. Default: `my_access.whoami()['userId']` + :return: Details of the created filter. """ + if user_id is None: + user_id = self.whoami()['userId'] + + if application_types := filter_properties.pop('application_types', None): + filter_properties['applicationTypes'] = application_types + + data = { + "name": filter_name, + "filter": filter_properties + } + + return self.britive.post(f'{self.base_url}/{user_id}/filters', json=data) + + def list_filters(self, user_id: str = None) -> list: + """ + Return list of filters for a user. + + :param user_id: ID of the user to list filters for. Default: `my_access.whoami()['userId']` + :return: List of filters. + """ + + if user_id is None: + user_id = self.whoami()['userId'] + + return self.britive.get(f'{self.base_url}/{user_id}/filters') + + def update_filter(self, filter_id: str, filter_name: str, filter_properties: str, user_id: str = None) -> dict: + """ + Update a user filter. + + :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) + :param user_id: ID of the user to create the filter for. Default: `my_access.whoami()['userId']` + :return: Details of the created filter. + """ + + if user_id is None: + user_id = self.whoami()['userId'] + + 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}/{user_id}/filters/{filter_id}', json=data) + + def delete_filter(self, filter_id: str, user_id: str = None) -> None: + """ + Delete a user filter. + + :param filter_id: ID of the filter. + :param user_id: ID of the user to create the filter for. Default: `my_access.whoami()['userId']` + :return: None. + """ + + if user_id is None: + user_id = self.whoami()['userId'] + + return self.britive.delete(f'{self.base_url}/{user_id}/filters/{filter_id}') + From 0627d660149055cc23107d17a1554e766d4d15f5 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:13:04 -0600 Subject: [PATCH 09/40] feat:britive managed permissions --- .../application_management/access_builder.py | 72 +++++++++++++++++ .../managed_permissions.py | 81 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/britive/application_management/managed_permissions.py diff --git a/src/britive/application_management/access_builder.py b/src/britive/application_management/access_builder.py index 08dad39..5a45334 100644 --- a/src/britive/application_management/access_builder.py +++ b/src/britive/application_management/access_builder.py @@ -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/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}') From 9e360571453f005cdbcbf973162b152a2cbf810a Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:13:34 -0600 Subject: [PATCH 10/40] feat:resource response templates --- src/britive/my_resources.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 9e467b4..b66bb48 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -98,6 +98,7 @@ def _checkout( 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, @@ -175,9 +176,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 @@ -195,6 +197,7 @@ def checkout( 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, @@ -220,6 +223,7 @@ def checkout( 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 @@ -240,6 +244,7 @@ def checkout( profile_id=profile_id, progress_func=progress_func, resource_id=resource_id, + response_template=response_template, ticket_id=ticket_id, ticket_type=ticket_type, wait_time=wait_time, @@ -254,6 +259,7 @@ def checkout_by_name( 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, @@ -272,6 +278,7 @@ def checkout_by_name( 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 response_template: Optional response template to use in conjunction with `include_credentials`. :param justification: Optional justification if checking out the profile requires approval. :param max_wait_time: The maximum number of seconds to wait for an approval before throwing an exception. @@ -292,20 +299,22 @@ def checkout_by_name( ids = self._get_profile_and_resource_ids_given_names(profile_name, resource_name) return self._checkout( - profile_id=ids['profile_id'], - 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, + profile_id=ids['profile_id'], progress_func=progress_func, + resource_id=ids['resource_id'], + response_template=response_template, + wait_time=wait_time, ) def credentials( self, transaction_id: str, transaction: dict = None, + response_template: str = None, return_transaction_details: bool = False, progress_func: Callable = None, ) -> Any: @@ -314,6 +323,7 @@ def credentials( :param transaction_id: The ID of the transaction. :param transaction: Optional - the details of the transaction. Primary use is for internal purposes. + :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. @@ -335,7 +345,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 From d1747d280edc9695b98c2244f7db8a897b10b2ec Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 10 Dec 2024 10:53:34 -0600 Subject: [PATCH 11/40] v3.2.0-alpha --- CHANGELOG.md | 32 +++++++++++++++ CONTRIBUTING.md | 88 +++++++++++++++++++++-------------------- pyproject.toml | 4 ++ src/britive/__init__.py | 2 +- 4 files changed, 83 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a696774..25ed8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Change Log (v2.8.1+) +## v3.2.0-alpha [2024-12-10] + +__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. + +__Enhancements:__ + +* Added `add_favorite` and `delete_favorite` to `my_resources`. +* Added checkout approvals to `my_resources`. +* Added ITSM to checkout approvals. +* Added `include_approval_status` to `my_access.list_profiles`. +* Added `(create|list|update|delete)_filter`) to `my_access`. +* Added `response_templates` functionality for `access_broker` credentials. + +__Bug Fixes:__ + +* Fixed missing `param_values` option for resource creation. + +__Dependencies:__ + +* None + +__Other:__ + +* None + ## v3.1.0 [2024-10-07] __What's New:__ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60a15ff..afe2975 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/005-identity_attributes.py -v -pytest tests/010-users.py -v -pytest tests/020-tags.py -v -pytest tests/030-service_identities.py -v -pytest tests/040-service_identity_tokens.py -v -pytest tests/050-applications.py -v -pytest tests/060-environment_groups.py -v -pytest tests/070-environments.py -v -pytest tests/080-scans.py -v # WARNING - this one will take a while since it initiates a real scan -pytest tests/090-accounts.py -v # NOTE - a scan must first be completed -pytest tests/100-permissions.py -v # NOTE - a scan must first be completed -pytest tests/110-groups.py -v # NOTE - a scan must first be completed -pytest tests/130-profiles.py -v -pytest tests/140-task_services.py -v -pytest tests/150-tasks.py -v -pytest tests/160-security_policies.py -v -pytest tests/170-saml.py -v -pytest tests/180-api_tokens.py -v -pytest tests/190-audit_logs.py -v -pytest tests/200-reports.py -v -pytest tests/210-identity_providers.py -v -pytest tests/215-workload.py -v -pytest tests/220-my_access.py -v -pytest tests/230-notifications.py -v -pytest tests/240-secrets_manager.py -v -pytest tests/250-my_secrets.py -v -pytest tests/260-notification_mediums.py -v -pytest tests/270-system_policies.py -v -pytest tests/280_system_actions.py -v -pytest tests/290_system_consumers.py -v -pytest tests/300-system_roles.py -v -pytest tests/310-system_permissions.py -v -pytest tests/320-settings_banner.py -v -pytest tests/330-response_templates.py -v -pytest tests/340-resource_types.py -v -pytest tests/350-resource_labels.py -v -pytest tests/360-resource.py -v -pytest tests/370-resource_permissions.py -v -pytest tests/380-access_broker_profiles.py -v -pytest tests/390-access_broker_profiles_policies.py -v -pytest tests/400-access_broker_permissions.py -v -pytest tests/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/pyproject.toml b/pyproject.toml index 8edd764..52a29da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet", "Topic :: Security", ] @@ -30,6 +31,9 @@ dependencies = [ 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" diff --git a/src/britive/__init__.py b/src/britive/__init__.py index 7f5601d..a5e178f 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.1.0' +__version__ = '3.2.0-alpha' From a0bb26e216ba36f937fdafaf65bc07ca6f5e3163 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 11:10:15 -0600 Subject: [PATCH 12/40] refactor:improve `my_requests`, add `my_resources` --- .../custom_attributes.py | 0 src/britive/my_access.py | 24 +++++++++---------- src/britive/my_requests.py | 19 ++++++++++----- src/britive/my_resources.py | 9 ++++++- 4 files changed, 32 insertions(+), 20 deletions(-) rename src/britive/{helpers => identity_management}/custom_attributes.py (100%) diff --git a/src/britive/helpers/custom_attributes.py b/src/britive/identity_management/custom_attributes.py similarity index 100% rename from src/britive/helpers/custom_attributes.py rename to src/britive/identity_management/custom_attributes.py diff --git a/src/britive/my_access.py b/src/britive/my_access.py index f5a7996..096fac6 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -47,21 +47,22 @@ def __init__(self, britive) -> None: 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 + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': + # MyAccess backwards compatibility + self.approval_request_status = __my_requests.approval_request_status # MyApprovals backwards compatibility - self.__my_approvals = MyApprovals(self.britive) + __my_approvals = MyApprovals(self.britive) self.approve_request = self.__my_approvals.approve_request self.list_approvals = self.__my_approvals.list_approvals self.reject_request = self.__my_approvals.reject_request - # MyRequests backwards compatibility - self.__my_requests = MyAccessRequests(self.britive) - self.approval_request_status = self.__my_requests.approval_request_status - self.request_approval = self.__my_requests.request_approval - self.request_approval_by_name = self.__my_requests.request_approval_by_name - self.withdraw_approval_request = self.__my_requests.withdraw_approval_request - self.withdraw_approval_request_by_name = self.__my_requests.withdraw_approval_request_by_name - def list_profiles(self, include_approval_status: bool = False) -> list: """ List the profiles for which the user has access. @@ -266,10 +267,7 @@ def _checkout( ticket_type=ticket_type, wait_time=wait_time, ) - if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() == 'true': - status = MyAccessRequests(self.britive).request_approval(**approval_request) - else: - status = self.request_approval(**approval_request) + status = self.request_approval(**approval_request) # handle the response based on the value of status if status == 'approved': diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py index 96a444f..d551f3b 100644 --- a/src/britive/my_requests.py +++ b/src/britive/my_requests.py @@ -38,7 +38,7 @@ def list(self) -> list: def approval_request_status(self, request_id: str) -> dict: """ - Provides details on and approval request. + Get the details of an approval request. :param request_id: The ID of the approval request. :return: Details of the approval request. @@ -46,6 +46,16 @@ def approval_request_status(self, request_id: str) -> dict: 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, @@ -149,12 +159,9 @@ def _request_approval_by_name( def _withdraw_approval_request( self, request_id: str = None, profile_id: str = None, entity_id: str = None, entity_type: str = None ) -> None: - if request_id: - return self.britive.delete(f'{self.base_url}/{request_id}') - - url = f'{self.base_url}/consumer/{entity_type}/resource?resourceId={profile_id}/{entity_id}' + url = request_id if request_id else f'consumer/{entity_type}/resource?resourceId={profile_id}/{entity_id}' - return self.britive.delete(url) + return self.britive.delete(f'{self.base_url}/{url}') class MyAccessRequests(MyRequests): diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index b66bb48..54d8e4d 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -42,6 +42,13 @@ def __init__(self, britive) -> None: 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, list_type: str = None, search_text: str = None) -> list: """ List the profiles for which the user has access. @@ -148,7 +155,7 @@ def _checkout( raise ApprovalRequiredButNoJustificationProvided() from e # request approval - status = MyResourcesRequests(self.britive).request_approval( + status = self.request_approval( block_until_disposition=True, justification=justification, max_wait_time=max_wait_time, From 46917c883d82882bce7ecffa9f2f411fa908e302 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 11:12:31 -0600 Subject: [PATCH 13/40] fix:`list_approvals` should return all types --- src/britive/my_approvals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/britive/my_approvals.py b/src/britive/my_approvals.py index e89eb13..e3b22a4 100644 --- a/src/britive/my_approvals.py +++ b/src/britive/my_approvals.py @@ -50,6 +50,6 @@ def list_approvals(self) -> dict: :return: List of approval requests. """ - params = {'requestType': 'myApprovals', 'consumer': 'papservice'} + params = {'requestType': 'myApprovals'} return self.britive.get(f'{self.base_url}/', params=params) From 16a2cee977fc83b6b1ac0bf599c5ea54c280f44c Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 11:14:07 -0600 Subject: [PATCH 14/40] refactor:additional code cleanup --- src/britive/britive.py | 77 +- src/britive/helpers/__init__.py | 2 - .../identity_management/custom_attributes.py | 88 -- .../identity_attributes.py | 90 ++ .../identity_management/service_identities.py | 15 +- src/britive/identity_management/users.py | 2 +- src/britive/identity_management/workload.py | 800 +++++++++--------- src/britive/my_access.py | 18 +- 8 files changed, 537 insertions(+), 555 deletions(-) delete mode 100644 src/britive/identity_management/custom_attributes.py diff --git a/src/britive/britive.py b/src/britive/britive.py index 783d1fe..59aec5b 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -178,48 +178,41 @@ def _initialize_components(self, query_features: bool) -> None: self.secrets_manager = SecretsManager(self) self.system = System(self) - application_management = ApplicationManagement(self) - audit_logs = AuditLogs(self) - identity_management = IdentityManagement(self) - global_settings = GlobalSettings(self) - security = Security(self) - workflows = Workflows(self) + 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) # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility - if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() == 'true': - self.application_management = application_management - self.audit_logs = audit_logs - self.identity_management = identity_management - self.global_settings = global_settings - self.security = security - self.workflows = workflows - else: - self.access_builder = application_management.access_builder - self.accounts = application_management.accounts - self.applications = application_management.applications - self.audit_logs = audit_logs.logs - self.audit_logs.webhooks = audit_logs.webhooks - self.environment_groups = application_management.environment_groups - self.environments = application_management.environments - self.groups = application_management.groups - self.identity_attributes = identity_management.identity_attributes - self.identity_providers = identity_management.identity_providers - self.notification_mediums = global_settings.notification_mediums - self.notifications = workflows.notifications - self.permissions = application_management.permissions - self.profiles = application_management.profiles - self.saml = security.saml - self.scans = application_management.scans - self.security_policies = security.security_policies - self.service_identities = identity_management.service_identities - self.service_identity_tokens = identity_management.service_identity_tokens - self.step_up = security.step_up_auth - self.tags = identity_management.tags - self.task_services = workflows.task_services - self.tasks = workflows.tasks - self.users = identity_management.users - self.workload = identity_management.workload - self.settings = global_settings + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': + self.access_builder = self.application_management.access_builder + self.accounts = self.application_management.accounts + self.applications = self.application_management.applications + self.audit_logs.logs.webhooks = self.audit_logs.webhooks + self.audit_logs = self.audit_logs.logs + self.environment_groups = self.application_management.environment_groups + self.environments = self.application_management.environments + self.groups = self.application_management.groups + self.identity_attributes = self.identity_management.identity_attributes + self.identity_providers = self.identity_management.identity_providers + self.notification_mediums = self.global_settings.notification_mediums + self.notifications = self.workflows.notifications + self.permissions = self.application_management.permissions + self.profiles = self.application_management.profiles + self.saml = self.security.saml + self.scans = self.application_management.scans + self.security_policies = self.security.security_policies + self.service_identities = self.identity_management.service_identities + self.service_identity_tokens = self.identity_management.service_identity_tokens + self.step_up = self.security.step_up_auth + self.tags = self.identity_management.tags + self.task_services = self.workflows.task_services + self.tasks = self.workflows.tasks + self.users = self.identity_management.users + self.workload = self.identity_management.workload + self.settings = self.global_settings @staticmethod def source_federation_token_from(provider: str, tenant: str = None, duration_seconds: int = 900) -> str: @@ -384,9 +377,7 @@ 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')}" - ) + message = f"{status_code} - {error_code} - {content.get('message', 'no message available')}" if content.get('details'): message += f" - {content.get('details')}" else: diff --git a/src/britive/helpers/__init__.py b/src/britive/helpers/__init__.py index e1fec69..0ba44fb 100644 --- a/src/britive/helpers/__init__.py +++ b/src/britive/helpers/__init__.py @@ -1,8 +1,6 @@ -from .custom_attributes import CustomAttributes from .methods import HelperMethods class Helpers: def __init__(self, britive) -> None: - self.custom_attributes = CustomAttributes(britive) self.helper_methods = HelperMethods(britive) diff --git a/src/britive/identity_management/custom_attributes.py b/src/britive/identity_management/custom_attributes.py deleted file mode 100644 index d91017c..0000000 --- a/src/britive/identity_management/custom_attributes.py +++ /dev/null @@ -1,88 +0,0 @@ -from typing import Any - - -class CustomAttributes: - def __init__(self, principal) -> None: - 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: - """ - Retrieve the current custom attributes associated with the specified Service Identity or User. - - :param principal_id: The ID of the Service Identity or User. - :param as_dict: Whether to return a key/value mapping vs the raw response which is a list. - """ - response = self.britive.get(self.base_url.format(id=principal_id)) - if as_dict: - attrs = {} - for attribute in response: - current_attr_id = attribute['attributeId'] - if current_attr_id in attrs: # need to handle the multi-value attributes - is_list = isinstance(attrs[attribute['attributeId']], list) - - if not is_list: # make it a list - attrs[current_attr_id] = [attrs[current_attr_id]] - attrs[current_attr_id].append(attribute['attributeValue']) - else: - attrs[current_attr_id] = attribute['attributeValue'] - return attrs - else: - return response - - def add(self, principal_id: str, custom_attributes: dict) -> None: - """ - Adds custom attribute mappings to the provided Service Identity or User. - - :param principal_id: The IDs of the Service Identity or User. - :param custom_attributes: An attribute map where keys are the custom attribute ids or names and values are - custom attribute values as strings or list of strings for multivalued attributes. - """ - return self._modify( - principal_id=principal_id, - operation='add', - custom_attributes=custom_attributes, - ) - - def remove(self, principal_id: str, custom_attributes: dict) -> None: - """ - Removes custom attribute mapping from the provided Service Identity or User. - - :param principal_id: The IDs of the Service Identity or User. - :param custom_attributes: An attribute map where keys are the custom attribute ids or names and values are - custom attribute values as strings or list of strings for multivalued attributes. - """ - return self._modify(principal_id=principal_id, operation='remove', custom_attributes=custom_attributes) - - 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_attr_ids = [attr['id'] for attr in existing_attrs] - attrs_by_name = {attr['name']: attr['id'] for attr in existing_attrs} - - # for each custom_attribute key/value provided ensure we convert to ID and build the list - attrs_list = [] - for id_or_name, value in custom_attributes.items(): - # obtain the custom attribute id - custom_attribute_id = id_or_name - if custom_attribute_id not in existing_attr_ids: - custom_attribute_id = attrs_by_name.get(custom_attribute_id) - if not custom_attribute_id: - raise ValueError(f'custom identity attribute name {id_or_name} not found.') - - # and create the list dict entry for each value - multi_value = value if isinstance(value, list) else [value] # handle multivalued attributes - for v in multi_value: - attrs_list.append( - {'op': operation, 'customUserAttribute': {'attributeValue': v, 'attributeId': custom_attribute_id}} - ) - return attrs_list - - def _modify(self, principal_id: str, operation: str, custom_attributes: dict) -> None: - if operation not in ['add', 'remove']: - raise ValueError('operation must either be add or remove') - - return self.britive.patch( - self.base_url.format(id=principal_id), - json=self._build_list(operation=operation, custom_attributes=custom_attributes), - ) diff --git a/src/britive/identity_management/identity_attributes.py b/src/britive/identity_management/identity_attributes.py index 09f1189..c364154 100644 --- a/src/britive/identity_management/identity_attributes.py +++ b/src/britive/identity_management/identity_attributes.py @@ -1,3 +1,6 @@ +from typing import Any + + class IdentityAttributes: def __init__(self, britive) -> None: self.britive = britive @@ -40,3 +43,90 @@ def delete(self, attribute_id: str) -> None: """ return self.britive.delete(f'{self.base_url}/{attribute_id}') + + +class CustomAttributes: + def __init__(self, principal) -> None: + 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: + """ + Retrieve the current custom attributes associated with the specified Service Identity or User. + + :param principal_id: The ID of the Service Identity or User. + :param as_dict: Whether to return a key/value mapping vs the raw response which is a list. + """ + response = self.britive.get(self.base_url.format(id=principal_id)) + if as_dict: + attrs = {} + for attribute in response: + current_attr_id = attribute['attributeId'] + if current_attr_id in attrs: # need to handle the multi-value attributes + is_list = isinstance(attrs[attribute['attributeId']], list) + + if not is_list: # make it a list + attrs[current_attr_id] = [attrs[current_attr_id]] + attrs[current_attr_id].append(attribute['attributeValue']) + else: + attrs[current_attr_id] = attribute['attributeValue'] + return attrs + else: + return response + + def add(self, principal_id: str, custom_attributes: dict) -> None: + """ + Adds custom attribute mappings to the provided Service Identity or User. + + :param principal_id: The IDs of the Service Identity or User. + :param custom_attributes: An attribute map where keys are the custom attribute ids or names and values are + custom attribute values as strings or list of strings for multivalued attributes. + """ + return self._modify( + principal_id=principal_id, + operation='add', + custom_attributes=custom_attributes, + ) + + def remove(self, principal_id: str, custom_attributes: dict) -> None: + """ + Removes custom attribute mapping from the provided Service Identity or User. + + :param principal_id: The IDs of the Service Identity or User. + :param custom_attributes: An attribute map where keys are the custom attribute ids or names and values are + custom attribute values as strings or list of strings for multivalued attributes. + """ + return self._modify(principal_id=principal_id, operation='remove', custom_attributes=custom_attributes) + + 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_attr_ids = [attr['id'] for attr in existing_attrs] + attrs_by_name = {attr['name']: attr['id'] for attr in existing_attrs} + + # for each custom_attribute key/value provided ensure we convert to ID and build the list + attrs_list = [] + for id_or_name, value in custom_attributes.items(): + # obtain the custom attribute id + custom_attribute_id = id_or_name + if custom_attribute_id not in existing_attr_ids: + custom_attribute_id = attrs_by_name.get(custom_attribute_id) + if not custom_attribute_id: + raise ValueError(f'custom identity attribute name {id_or_name} not found.') + + # and create the list dict entry for each value + multi_value = value if isinstance(value, list) else [value] # handle multivalued attributes + for v in multi_value: + attrs_list.append( + {'op': operation, 'customUserAttribute': {'attributeValue': v, 'attributeId': custom_attribute_id}} + ) + return attrs_list + + def _modify(self, principal_id: str, operation: str, custom_attributes: dict) -> None: + if operation not in ['add', 'remove']: + raise ValueError('operation must either be add or remove') + + return self.britive.patch( + self.base_url.format(id=principal_id), + json=self._build_list(operation=operation, custom_attributes=custom_attributes), + ) diff --git a/src/britive/identity_management/service_identities.py b/src/britive/identity_management/service_identities.py index 259b507..833da53 100644 --- a/src/britive/identity_management/service_identities.py +++ b/src/britive/identity_management/service_identities.py @@ -1,13 +1,8 @@ -from ..helpers import CustomAttributes +from .identity_attributes import CustomAttributes valid_statues = ['active', 'inactive'] -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 ServiceIdentities: def __init__(self, britive) -> None: self.britive = britive @@ -198,6 +193,10 @@ def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}' + def __validate_token_expiration(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. @@ -212,7 +211,7 @@ def create(self, service_identity_id: str, token_expiration_days: int = 90) -> d :return: """ - validate_token_expiration(token_expiration_days) + self.__validate_token_expiration(token_expiration_days) data = {'tokenExpirationDays': token_expiration_days} @@ -228,7 +227,7 @@ def update(self, service_identity_id: str, token_expiration_days: int = 90) -> N :return: None """ - validate_token_expiration(token_expiration_days) + self.__validate_token_expiration(token_expiration_days) data = {'tokenExpirationDays': token_expiration_days} diff --git a/src/britive/identity_management/users.py b/src/britive/identity_management/users.py index 5897b77..ab4ec12 100644 --- a/src/britive/identity_management/users.py +++ b/src/britive/identity_management/users.py @@ -3,7 +3,7 @@ UserNotAllowedToChangePassword, UserNotAssociatedWithDefaultIdentityProvider, ) -from ..helpers.custom_attributes import CustomAttributes +from .identity_attributes import CustomAttributes valid_statues = ['active', 'inactive'] diff --git a/src/britive/identity_management/workload.py b/src/britive/identity_management/workload.py index 79d7b64..43c3438 100644 --- a/src/britive/identity_management/workload.py +++ b/src/britive/identity_management/workload.py @@ -5,428 +5,428 @@ 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' - ) + 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_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: + 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( - 'one of custom_identity_attribute_id or custom_identity_attribute_name should be provided' + f'custom_identity_attribute_name value of {custom_identity_attribute_name} ' f'not found.' ) - 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 - ) + 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.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 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}) + 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} + params = {'idpId': idp_id, 'tokenDuration': token_duration, 'mappingAttributes': mapping_attributes} - return self.britive.post(self.base_url.format(id=service_identity_id), json=params) + 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. + 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. + 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)) + :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' +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. + 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. + 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. - """ + :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}') + 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. + 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. + 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. - """ + :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} + params = {'idpName': idp_name, 'userId': service_identity_id} - return self.britive.post(self.base_url, json=params) + 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. + 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. + 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. - """ + :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}') + 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 096fac6..4440ebf 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -59,9 +59,9 @@ def __init__(self, britive) -> None: self.approval_request_status = __my_requests.approval_request_status # MyApprovals backwards compatibility __my_approvals = MyApprovals(self.britive) - self.approve_request = self.__my_approvals.approve_request - self.list_approvals = self.__my_approvals.list_approvals - self.reject_request = self.__my_approvals.reject_request + self.approve_request = __my_approvals.approve_request + self.list_approvals = __my_approvals.list_approvals + self.reject_request = __my_approvals.reject_request def list_profiles(self, include_approval_status: bool = False) -> list: """ @@ -583,10 +583,7 @@ def create_filter(self, filter_name: str, filter_properties: str, user_id: str = if application_types := filter_properties.pop('application_types', None): filter_properties['applicationTypes'] = application_types - data = { - "name": filter_name, - "filter": filter_properties - } + data = {'name': filter_name, 'filter': filter_properties} return self.britive.post(f'{self.base_url}/{user_id}/filters', json=data) @@ -636,10 +633,7 @@ def update_filter(self, filter_id: str, filter_name: str, filter_properties: str if application_types := filter_properties.pop('application_types', None): filter_properties['applicationTypes'] = application_types - data = { - "name": filter_name, - "filter": filter_properties - } + data = {'name': filter_name, 'filter': filter_properties} return self.britive.put(f'{self.base_url}/{user_id}/filters/{filter_id}', json=data) @@ -656,5 +650,3 @@ def delete_filter(self, filter_id: str, user_id: str = None) -> None: user_id = self.whoami()['userId'] return self.britive.delete(f'{self.base_url}/{user_id}/filters/{filter_id}') - - From 3a5577d31cc3b2ec31103361211e6f2f3f9f4faf Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 11:14:54 -0600 Subject: [PATCH 15/40] test:minor test fixes --- tests/100-identity_management-01-users.py | 4 +++- tests/250-system-01-policies.py | 4 +++- tests/250-system-04-roles.py | 4 +++- tests/250-system-05-permissions.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/100-identity_management-01-users.py b/tests/100-identity_management-01-users.py index 8df437e..8fc179d 100644 --- a/tests/100-identity_management-01-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 = { @@ -105,7 +107,7 @@ def test_reset_password(cached_user): def test_reset_mfa(cached_user): - with pytest.raises(exceptions.UserDoesNotHaveMFAEnabled): + with pytest.raises(UserDoesNotHaveMFAEnabled): britive.users.reset_mfa(cached_user['userId']) diff --git a/tests/250-system-01-policies.py b/tests/250-system-01-policies.py index c1fefe7..d87183a 100644 --- a/tests/250-system-01-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` @@ -205,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') diff --git a/tests/250-system-04-roles.py b/tests/250-system-04-roles.py index 5f9783e..81ea514 100644 --- a/tests/250-system-04-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/250-system-05-permissions.py b/tests/250-system-05-permissions.py index 3135f58..df3452e 100644 --- a/tests/250-system-05-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') From 14ddb30e620377cbdbd396c3ca0edfc5e8a7e3c7 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 11:40:57 -0600 Subject: [PATCH 16/40] fix:filters only for current user --- src/britive/my_access.py | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 4440ebf..32827a7 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -551,9 +551,9 @@ def favorites(self) -> list: return self.britive.get(f'{self.base_url}/favorites') - def create_filter(self, filter_name: str, filter_properties: str, user_id: str = None) -> dict: + def create_filter(self, filter_name: str, filter_properties: str) -> dict: """ - Create a user filter. + Create a filter for the current user. :param filter_name: Name of the filter. :param filter_properties: Dict of the filter properties. @@ -573,36 +573,29 @@ def create_filter(self, filter_name: str, filter_properties: str, user_id: str = application_types: type: list desc: Application Type(s) - :param user_id: ID of the user to create the filter for. Default: `my_access.whoami()['userId']` :return: Details of the created filter. """ - if user_id is None: - user_id = self.whoami()['userId'] - if application_types := filter_properties.pop('application_types', None): filter_properties['applicationTypes'] = application_types data = {'name': filter_name, 'filter': filter_properties} - return self.britive.post(f'{self.base_url}/{user_id}/filters', json=data) + return self.britive.post(f"{self.base_url}/{self.whoami()['userId']}/filters", json=data) def list_filters(self, user_id: str = None) -> list: """ - Return list of filters for a user. + Return list of filters for the current user. :param user_id: ID of the user to list filters for. Default: `my_access.whoami()['userId']` :return: List of filters. """ - if user_id is None: - user_id = self.whoami()['userId'] - - return self.britive.get(f'{self.base_url}/{user_id}/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, user_id: str = None) -> dict: + def update_filter(self, filter_id: str, filter_name: str, filter_properties: str) -> dict: """ - Update a user filter. + Update a filter for the current user. :param filter_id: ID of the filter. :param filter_name: Name of the filter. @@ -623,30 +616,22 @@ def update_filter(self, filter_id: str, filter_name: str, filter_properties: str application_types: type: list desc: Application Type(s) - :param user_id: ID of the user to create the filter for. Default: `my_access.whoami()['userId']` - :return: Details of the created filter. + :return: Details of the updated filter. """ - if user_id is None: - user_id = self.whoami()['userId'] - 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}/{user_id}/filters/{filter_id}', json=data) + return self.britive.put(f"{self.base_url}/{self.whoami()['userId']}/filters/{filter_id}", json=data) def delete_filter(self, filter_id: str, user_id: str = None) -> None: """ - Delete a user filter. + Delete a filter for the current user. :param filter_id: ID of the filter. - :param user_id: ID of the user to create the filter for. Default: `my_access.whoami()['userId']` :return: None. """ - if user_id is None: - user_id = self.whoami()['userId'] - - return self.britive.delete(f'{self.base_url}/{user_id}/filters/{filter_id}') + return self.britive.delete(f"{self.base_url}/{self.whoami()['userId']}/filters/{filter_id}") From a8168fa6d75eae8e0c658a977d94a35b22afcb67 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 11:20:05 -0600 Subject: [PATCH 17/40] v3.2.0-alpha.1 --- CHANGELOG.md | 23 +++++++++++++++++++++++ src/britive/__init__.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ed8c0..19687a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Change Log (v2.8.1+) +## v3.2.0-alpha.1 [2024-12-18] + +__What's New:__ + +* `my_resources` improvements. + +__Enhancements:__ + +* Added `request_approval[_by_name]|withdraw_approval_request[_by_name]` to `my_resources`. + +__Bug Fixes:__ + +* `my_requests.list_approvals ` now includes `my_resources` requests. +* `my_access.*_filter[s]` are only valid for the current user. + +__Dependencies:__ + +* None + +__Other:__ + +* Additional code alignment cleanup. + ## v3.2.0-alpha [2024-12-10] __What's New:__ diff --git a/src/britive/__init__.py b/src/britive/__init__.py index a5e178f..b3beb07 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-alpha' +__version__ = '3.2.0-alpha.1' From f0aef4d6beaff2c7ceecc415c3b7ac3420e0e5b2 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 12:58:34 -0600 Subject: [PATCH 18/40] fix:make get call since method moved --- src/britive/helpers/methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/britive/helpers/methods.py b/src/britive/helpers/methods.py index c925c74..7543286 100644 --- a/src/britive/helpers/methods.py +++ b/src/britive/helpers/methods.py @@ -51,7 +51,7 @@ def get_profile_and_resource_ids_given_names(self, profile_name: str, resource_n 'profile_id': item['profileId'], 'resource_id': item['resourceId'], } - for item in self.list_profiles() + 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()}') From bff8b0f2971b84ee541d870c33cbfc8b9542c472 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 13:53:32 -0600 Subject: [PATCH 19/40] refactor:`s/list_approvals/list/g` --- src/britive/britive.py | 5 +---- src/britive/my_access.py | 3 ++- src/britive/my_approvals.py | 17 ++++++++++++++++- tests/600-britive-03-my_requests.py | 2 +- tests/600-britive-04-my_approvals.py | 4 ++-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/britive/britive.py b/src/britive/britive.py index 59aec5b..3b359e1 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -482,10 +482,7 @@ def __request(self, method, url, params=None, data=None, json=None) -> dict: def get_root_environment_group(self, application_id: str) -> str: """Internal use only.""" - if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() == 'true': - app = self.application_management.applications.get(application_id=application_id) - else: - app = self.applications.get(application_id=application_id) + 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']: diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 32827a7..fdb6d16 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -54,13 +54,14 @@ def __init__(self, britive) -> None: self.withdraw_approval_request = __my_requests.withdraw_approval_request self.withdraw_approval_request_by_name = __my_requests.withdraw_approval_request_by_name + # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': # MyAccess backwards compatibility self.approval_request_status = __my_requests.approval_request_status # MyApprovals backwards compatibility __my_approvals = MyApprovals(self.britive) self.approve_request = __my_approvals.approve_request - self.list_approvals = __my_approvals.list_approvals + self.list_approvals = __my_approvals.list self.reject_request = __my_approvals.reject_request def list_profiles(self, include_approval_status: bool = False) -> list: diff --git a/src/britive/my_approvals.py b/src/britive/my_approvals.py index e3b22a4..c851131 100644 --- a/src/britive/my_approvals.py +++ b/src/britive/my_approvals.py @@ -1,3 +1,6 @@ +import os + + 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 @@ -43,7 +46,7 @@ def reject_request(self, request_id: str, comments: str = '') -> None: return self.britive.patch(f'{self.base_url}/{request_id}', params=params, json=data) - def list_approvals(self) -> dict: + def list(self) -> dict: """ Lists approval requests. @@ -53,3 +56,15 @@ def list_approvals(self) -> dict: params = {'requestType': 'myApprovals'} return self.britive.get(f'{self.base_url}/', params=params) + + # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility + if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': + + def list_approvals(self) -> dict: + """ + Lists approval requests. + + :return: List of approval requests. + """ + + return self.list() diff --git a/tests/600-britive-03-my_requests.py b/tests/600-britive-03-my_requests.py index 1d4e98f..a65818a 100644 --- a/tests/600-britive-03-my_requests.py +++ b/tests/600-britive-03-my_requests.py @@ -7,7 +7,7 @@ def test_request(cached_profile_checkout_request): request_id = request['requestId'] - approvals = britive.my_access.list_approvals() + approvals = britive.my_requests.list() for approval in approvals: if approval['requestId'] == request_id: assert approval['status'] == 'PENDING' diff --git a/tests/600-britive-04-my_approvals.py b/tests/600-britive-04-my_approvals.py index d80a824..23de0a8 100644 --- a/tests/600-britive-04-my_approvals.py +++ b/tests/600-britive-04-my_approvals.py @@ -5,11 +5,11 @@ def test_approval(cached_profile_checkout_request): request_id = request['requestId'] - response = britive.my_access.reject_request(request_id=request_id) + response = britive.my_approvals.reject_request(request_id=request_id) assert response is None - approvals = britive.my_access.list_approvals() + approvals = britive.my_approvals.list() for approval in approvals: if approval['requestId'] == request_id: assert approval['status'] == 'REJECTED' From 35a280152e3ae83a34329b53212b705426619339 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 18 Dec 2024 15:46:58 -0600 Subject: [PATCH 20/40] v3.2.0-alpha.2 --- CHANGELOG.md | 24 +++++++++++++++++++++++- src/britive/__init__.py | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19687a5..18b0d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Change Log (v2.8.1+) +## v3.2.0-alpha.2 [2024-12-18] + +__What's New:__ + +* None + +__Enhancements:__ + +* None + +__Bug Fixes:__ + +* Make `get` call in helper method instead `list_approvals`. + +__Dependencies:__ + +* None + +__Other:__ + +* Additional code alignment cleanup. + ## v3.2.0-alpha.1 [2024-12-18] __What's New:__ @@ -12,7 +34,7 @@ __Enhancements:__ __Bug Fixes:__ -* `my_requests.list_approvals ` now includes `my_resources` requests. +* `my_requests.list_approvals` now includes `my_resources` requests. * `my_access.*_filter[s]` are only valid for the current user. __Dependencies:__ diff --git a/src/britive/__init__.py b/src/britive/__init__.py index b3beb07..f93ef6b 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-alpha.1' +__version__ = '3.2.0-alpha.2' From 58df21f181e33c60e53c910744041bcbcc4e3d0d Mon Sep 17 00:00:00 2001 From: theborch Date: Thu, 19 Dec 2024 19:54:24 -0600 Subject: [PATCH 21/40] fix:sssssssssssss --- src/britive/my_requests.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py index d551f3b..07c0ac5 100644 --- a/src/britive/my_requests.py +++ b/src/britive/my_requests.py @@ -76,13 +76,12 @@ def _request_approval( url = ( f'{self.britive.base_url}/access/{profile_id}/{entity_type}/{entity_id}/approvalRequest' - if entity_type == 'environment' + 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: @@ -128,7 +127,7 @@ def _request_approval_by_name( ticket_type: str = None, wait_time: int = 60, ) -> Any: - if entity_type == 'environment': + if entity_type == 'environments': ids = self._helper.get_profile_and_environment_ids_given_names(profile_name, entity_name) return self._request_approval( profile_id=ids['profile_id'], @@ -210,7 +209,7 @@ def request_approval_by_name( justification=justification, profile_name=profile_name, entity_name=environment_name, - entity_type='environment', + entity_type='environments', block_until_disposition=block_until_disposition, max_wait_time=max_wait_time, progress_func=progress_func, @@ -262,7 +261,7 @@ def request_approval( justification=justification, profile_name=profile_id, entity_name=environment_id, - entity_type='environment', + entity_type='environments', block_until_disposition=block_until_disposition, max_wait_time=max_wait_time, progress_func=progress_func, From 79d62d0e39a30d3438ff11e1bea0a7f0f6e8d05f Mon Sep 17 00:00:00 2001 From: theborch Date: Thu, 19 Dec 2024 19:56:30 -0600 Subject: [PATCH 22/40] v3.2.0-alpha.3 --- CHANGELOG.md | 22 ++++++++++++++++++++++ src/britive/__init__.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b0d38..2d00df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Change Log (v2.8.1+) +## v3.2.0-alpha.3 [2024-12-19] + +__What's New:__ + +* None + +__Enhancements:__ + +* None + +__Bug Fixes:__ + +* Missing `s` in `environments` for `my_requests`. + +__Dependencies:__ + +* None + +__Other:__ + +* Additional code alignment cleanup. + ## v3.2.0-alpha.2 [2024-12-18] __What's New:__ diff --git a/src/britive/__init__.py b/src/britive/__init__.py index f93ef6b..74e11e9 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-alpha.2' +__version__ = '3.2.0-alpha.3' From 002d3ce65b3a319a5a0c48351f73c8d850035116 Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 23 Dec 2024 13:16:19 -0600 Subject: [PATCH 23/40] fix:catch `requests.exceptions.JSONDecodeError` --- pyproject.toml | 2 +- requirements/common.txt | 2 +- src/britive/britive.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52a29da..0e8181b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ license = {file = "LICENSE"} requires-python = ">=3.8" dependencies = [ - "requests>=2.31.0" + "requests>=2.32.0" ] dynamic = ["version"] keywords = ["britive", "cpam", "identity", "jit"] 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/britive.py b/src/britive/britive.py index 3b359e1..6a16510 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -369,7 +369,8 @@ def post_upload(self, url, params=None, files=None) -> dict: def _handle_response(response): try: return response.json() - except native_json.decoder.JSONDecodeError: + # Can likely drop to just the `requests` exception, with `>=2.32.0`, but leaving both for now. + except (native_json.decoder.JSONDecodeError, requests.exceptions.JSONDecodeError): return response.content.decode('utf-8') @staticmethod From 9e2712675edb3087b24ee27289eff2cf51f5b303 Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 23 Dec 2024 13:16:57 -0600 Subject: [PATCH 24/40] v3.2.0-alpha.4 --- CHANGELOG.md | 78 +++++------------------------------------ src/britive/__init__.py | 2 +- 2 files changed, 10 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d00df7..02d2779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,73 +1,6 @@ # Change Log (v2.8.1+) -## v3.2.0-alpha.3 [2024-12-19] - -__What's New:__ - -* None - -__Enhancements:__ - -* None - -__Bug Fixes:__ - -* Missing `s` in `environments` for `my_requests`. - -__Dependencies:__ - -* None - -__Other:__ - -* Additional code alignment cleanup. - -## v3.2.0-alpha.2 [2024-12-18] - -__What's New:__ - -* None - -__Enhancements:__ - -* None - -__Bug Fixes:__ - -* Make `get` call in helper method instead `list_approvals`. - -__Dependencies:__ - -* None - -__Other:__ - -* Additional code alignment cleanup. - -## v3.2.0-alpha.1 [2024-12-18] - -__What's New:__ - -* `my_resources` improvements. - -__Enhancements:__ - -* Added `request_approval[_by_name]|withdraw_approval_request[_by_name]` to `my_resources`. - -__Bug Fixes:__ - -* `my_requests.list_approvals` now includes `my_resources` requests. -* `my_access.*_filter[s]` are only valid for the current user. - -__Dependencies:__ - -* None - -__Other:__ - -* Additional code alignment cleanup. - -## v3.2.0-alpha [2024-12-10] +## v3.2.0-alpha.* [2024-12-23] __What's New:__ @@ -77,6 +10,7 @@ __What's New:__ * Added `firewall` settings functionality. * Added Britive `managed_permissions` functionality. * Britive exceptions by type and error code. +* `my_resources` improvements. __Enhancements:__ @@ -86,14 +20,20 @@ __Enhancements:__ * Added `include_approval_status` to `my_access.list_profiles`. * 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`. __Bug Fixes:__ * Fixed missing `param_values` option for resource creation. +* `my_requests.list_approvals` now includes `my_resources` requests. +* `my_access.*_filter[s]` are only valid for the current user. +* Make `get` call in helper method instead `list_approvals`. +* Missing `s` in `environments` for `my_requests`. +* Catch `requests.exceptions.JSONDecodeError` in `_handle_response`. __Dependencies:__ -* None +* `requests >= 2.32.0` __Other:__ diff --git a/src/britive/__init__.py b/src/britive/__init__.py index 74e11e9..c7f8c51 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-alpha.3' +__version__ = '3.2.0-alpha.4' From c75518baff3fc5c39a795906945e782a952153f3 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 7 Jan 2025 13:45:58 -0600 Subject: [PATCH 25/40] refactor:cleanup secrets_manager and others --- src/britive/access_broker/__init__.py | 6 +- .../profiles/{profiles.py => __init__.py} | 2 +- .../access_broker/profiles/policies.py | 2 +- .../resources/{resources.py => __init__.py} | 0 src/britive/britive.py | 4 +- src/britive/reports/__init__.py | 53 ++ src/britive/reports/reports.py | 54 -- src/britive/secrets_manager/__init__.py | 17 + src/britive/secrets_manager/folders.py | 28 + src/britive/secrets_manager/policies.py | 304 ++++++++ src/britive/secrets_manager/resources.py | 15 + src/britive/secrets_manager/secrets.py | 143 ++++ .../secrets_manager/secrets_manager.py | 701 ------------------ src/britive/secrets_manager/templates.py | 83 +++ src/britive/secrets_manager/vaults.py | 108 +++ 15 files changed, 758 insertions(+), 762 deletions(-) rename src/britive/access_broker/profiles/{profiles.py => __init__.py} (99%) rename src/britive/access_broker/resources/{resources.py => __init__.py} (100%) delete mode 100644 src/britive/reports/reports.py create mode 100644 src/britive/secrets_manager/folders.py create mode 100644 src/britive/secrets_manager/policies.py create mode 100644 src/britive/secrets_manager/resources.py create mode 100644 src/britive/secrets_manager/secrets.py delete mode 100644 src/britive/secrets_manager/secrets_manager.py create mode 100644 src/britive/secrets_manager/templates.py create mode 100644 src/britive/secrets_manager/vaults.py diff --git a/src/britive/access_broker/__init__.py b/src/britive/access_broker/__init__.py index f6d4d27..12b684f 100644 --- a/src/britive/access_broker/__init__.py +++ b/src/britive/access_broker/__init__.py @@ -1,13 +1,13 @@ from .brokers import Brokers from .pools import Pools -from .profiles.profiles import Profile -from .resources.resources import Resources +from .profiles import Profiles +from .resources import Resources from .response_templates import ResponseTemplates class AccessBroker: def __init__(self, britive) -> None: - self.profiles = Profile(britive) + self.profiles = Profiles(britive) self.resources = Resources(britive) self.response_templates = ResponseTemplates(britive) self.brokers = Brokers(britive) 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 100% rename from src/britive/access_broker/resources/resources.py rename to src/britive/access_broker/resources/__init__.py diff --git a/src/britive/britive.py b/src/britive/britive.py index 6a16510..f96b17b 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -39,8 +39,8 @@ from .my_requests import MyRequests from .my_resources import MyResources from .my_secrets import MySecrets -from .reports.reports import Reports -from .secrets_manager.secrets_manager import SecretsManager +from .reports import Reports +from .secrets_manager import SecretsManager from .security import ApiTokens, Security from .system import System from .workflows import Workflows diff --git a/src/britive/reports/__init__.py b/src/britive/reports/__init__.py index e69de29..7fd2194 100644 --- a/src/britive/reports/__init__.py +++ b/src/britive/reports/__init__.py @@ -0,0 +1,53 @@ +import csv as csv_lib +import json +from io import StringIO +from typing import Any + + +def _json_loads(value) -> dict: + try: + return json.loads(value) + except json.JSONDecodeError: + return value + + +class Reports: + def __init__(self, britive) -> None: + self.britive = britive + self.base_url = f'{self.britive.base_url}/reports' + + def list(self) -> list: + """ + Return list of all built-in reports. + + :return: List of reports. + """ + + params = {'type': 'report'} + return self.britive.get(self.base_url, params=params) + + def run(self, report_id: str, csv: bool = False, filter_expression: str = None) -> Any: + """ + Run a report. + + :param report_id: The ID of the report. + :param csv: If True the result will be returned as a CSV string. If False (default) the result will be returned + as a list where each time in the list is a dict representing the row of data. + :param filter_expression: The filter to apply to the report. It is left to the caller to provide a syntactically + correct filter expression string. + :return: CSV string or list. + """ + + params = {} + if filter_expression: + params['filter'] = filter_expression + csv_results = self.britive.get(f'{self.base_url}/{report_id}/csv', params=params) + + # convert csv to json - issue is that JSON response has max of 1k records returned so have to use CSV + # as the base and convert to dict if the client asked for dict + if csv: + return csv_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/reports/reports.py b/src/britive/reports/reports.py deleted file mode 100644 index 95c7a37..0000000 --- a/src/britive/reports/reports.py +++ /dev/null @@ -1,54 +0,0 @@ -import csv as csv_lib -import json -from io import StringIO -from typing import Any - - -def _json_loads(value) -> dict: - try: - return json.loads(value) - except json.JSONDecodeError: - return value - - -class Reports: - def __init__(self, britive) -> None: - self.britive = britive - self.base_url = f'{self.britive.base_url}/reports' - - def list(self) -> list: - """ - Return list of all built-in reports. - - :return: List of reports. - """ - - params = {'type': 'report'} - return self.britive.get(self.base_url, params=params) - - def run(self, report_id: str, csv: bool = False, filter_expression: str = None) -> Any: - """ - Run a report. - - :param report_id: The ID of the report. - :param csv: If True the result will be returned as a CSV string. If False (default) the result will be returned - as a list where each time in the list is a dict representing the row of data. - :param filter_expression: The filter to apply to the report. It is left to the caller to provide a syntactically - correct filter expression string. - :return: CSV string or list. - """ - - params = {} - if filter_expression: - params['filter'] = filter_expression - csv_results = self.britive.get(f'{self.base_url}/{report_id}/csv', params=params) - - # convert csv to json - issue is that JSON response has max of 1k records returned so have to use CSV - # 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 diff --git a/src/britive/secrets_manager/__init__.py b/src/britive/secrets_manager/__init__.py index e69de29..733a0d2 100644 --- a/src/britive/secrets_manager/__init__.py +++ 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/secrets_manager.py b/src/britive/secrets_manager/secrets_manager.py deleted file mode 100644 index 17861b2..0000000 --- a/src/britive/secrets_manager/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/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..e4ecaaf --- /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.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') From d04a8b0de85837f4af860219c4af6c757f3bbc70 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 7 Jan 2025 15:35:13 -0600 Subject: [PATCH 26/40] feat(my_access):new `list()` for use with `type=sdk` --- src/britive/my_access.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/britive/my_access.py b/src/britive/my_access.py index fdb6d16..e423bc4 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -64,31 +64,31 @@ def __init__(self, britive) -> None: self.list_approvals = __my_approvals.list self.reject_request = __my_approvals.reject_request - def list_profiles(self, include_approval_status: bool = False) -> list: + def list(self, filter_text: str = None, search_text: str = None) -> list: """ - List the profiles for which the user has access. + List the access details for the current user. - :param include_approval_status: Include `approval_status` of each profile + :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. :return: List of profiles. """ - profiles = self.britive.get(self.base_url) + params = {'type': 'sdk'} + if filter_text: + params['filter'] = filter_text + if search_text: + params['search'] = search_text + + return self.britive.get(self.base_url, params=params) - if include_approval_status: - access_type_details = { - (a['papId'], a['environmentId'], a['accessType'].lower()): a['myAccessDetails'] - for a in self.britive.get(self.base_url, params={'type': 'ui'}) - } - for app in profiles: - for profile in app['profiles']: - for environment in profile['environments']: - for access_type in ('console', 'programmatic'): - if profile.get(f'{access_type}Access'): - environment[f'{access_type}_access_details'] = access_type_details[ - (profile['profileId'], environment['environmentId'], access_type) - ] + def list_profiles(self) -> list: + """ + List the profiles for which the user has access. + + :return: List of profiles. + """ - return profiles + return self.britive.get(self.base_url) def list_checked_out_profiles(self, include_profile_details: bool = False) -> list: """ From 24c952a7274503b73a367df70202c25fddc03153 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 7 Jan 2025 15:36:24 -0600 Subject: [PATCH 27/40] refactor:drop json req --- src/britive/britive.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/britive/britive.py b/src/britive/britive.py index f96b17b..3261d9d 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -1,4 +1,3 @@ -import json as native_json import os import socket import time @@ -370,7 +369,7 @@ 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 (native_json.decoder.JSONDecodeError, requests.exceptions.JSONDecodeError): + except requests.exceptions.JSONDecodeError: return response.content.decode('utf-8') @staticmethod From e826d15ab13be9fcdaf165d7871afb54447b6689 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 7 Jan 2025 15:38:31 -0600 Subject: [PATCH 28/40] chore:linta claus --- .../application_management/accounts.py | 3 +- .../application_management/profiles.py | 6 +-- src/britive/application_management/scans.py | 3 +- src/britive/audit_logs/logs.py | 4 +- src/britive/britive.py | 6 ++- src/britive/federation_providers/aws.py | 15 +++--- .../azure_system_assigned_managed_identity.py | 3 +- .../azure_user_assigned_managed_identity.py | 3 +- src/britive/federation_providers/bitbucket.py | 3 +- .../federation_provider.py | 2 +- src/britive/federation_providers/github.py | 3 +- src/britive/federation_providers/gitlab.py | 3 +- src/britive/federation_providers/spacelift.py | 3 +- src/britive/global_settings/banner.py | 2 +- .../identity_attributes.py | 3 +- .../identity_management/service_identities.py | 6 +-- src/britive/identity_management/tags.py | 2 +- src/britive/identity_management/users.py | 12 ++--- src/britive/identity_management/workload.py | 2 +- src/britive/my_access.py | 49 ++++++++++--------- src/britive/my_requests.py | 19 ++++--- src/britive/my_resources.py | 33 ++++++++----- src/britive/my_secrets.py | 15 +++--- src/britive/security/api_tokens.py | 11 +++-- src/britive/system/permissions.py | 6 +-- src/britive/system/policies.py | 2 - src/britive/system/roles.py | 4 +- 27 files changed, 114 insertions(+), 109 deletions(-) diff --git a/src/britive/application_management/accounts.py b/src/britive/application_management/accounts.py index 1cd3a6a..ca86ece 100644 --- a/src/britive/application_management/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/application_management/profiles.py b/src/britive/application_management/profiles.py index f0244fb..b81ebb3 100644 --- a/src/britive/application_management/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'} diff --git a/src/britive/application_management/scans.py b/src/britive/application_management/scans.py index b50c646..3864d2a 100644 --- a/src/britive/application_management/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/logs.py b/src/britive/audit_logs/logs.py index fd7645f..be0c162 100644 --- a/src/britive/audit_logs/logs.py +++ b/src/britive/audit_logs/logs.py @@ -58,8 +58,8 @@ def query( raise ValueError('from_time must occur before to_time.') params = { - 'from': from_time.isoformat(sep='T', timespec='seconds').split("+")[0] + 'Z', - 'to': to_time.isoformat(sep='T', timespec='seconds').split("+")[0] + '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 diff --git a/src/britive/britive.py b/src/britive/britive.py index 3261d9d..c773b8b 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -431,7 +431,9 @@ def __request_with_exponential_backoff_and_retry(self, method, url, params, data num_retries += 1 else: self.__check_response_for_error(response.status_code, self._handle_response(response)) - return response + break + + return response def __request(self, method, url, params=None, data=None, json=None) -> dict: return_data = [] @@ -487,4 +489,4 @@ def get_root_environment_group(self, application_id: str) -> str: for group in root_env_group: if not group['parentId']: return group['id'] - raise RootEnvironmentGroupNotFound() + raise RootEnvironmentGroupNotFound diff --git a/src/britive/federation_providers/aws.py b/src/britive/federation_providers/aws.py index b73cf91..f1ddec3 100644 --- a/src/britive/federation_providers/aws.py +++ b/src/britive/federation_providers/aws.py @@ -5,20 +5,22 @@ import json import os -from ..exceptions import TenantMissingError +from britive.exceptions import TenantMissingError + 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 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__() @@ -31,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 @@ -48,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 diff --git a/src/britive/federation_providers/azure_system_assigned_managed_identity.py b/src/britive/federation_providers/azure_system_assigned_managed_identity.py index eea6653..13f87d4 100644 --- a/src/britive/federation_providers/azure_system_assigned_managed_identity.py +++ b/src/britive/federation_providers/azure_system_assigned_managed_identity.py @@ -1,4 +1,5 @@ -from ..exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment +from britive.exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment + from .federation_provider import FederationProvider diff --git a/src/britive/federation_providers/azure_user_assigned_managed_identity.py b/src/britive/federation_providers/azure_user_assigned_managed_identity.py index 5a8cf53..b3968a7 100644 --- a/src/britive/federation_providers/azure_user_assigned_managed_identity.py +++ b/src/britive/federation_providers/azure_user_assigned_managed_identity.py @@ -1,4 +1,5 @@ -from ..exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment +from britive.exceptions import MissingAzureDependency, NotExecutingInAzureEnvironment + from .federation_provider import FederationProvider diff --git a/src/britive/federation_providers/bitbucket.py b/src/britive/federation_providers/bitbucket.py index 96f4ad8..cb2ae2c 100644 --- a/src/britive/federation_providers/bitbucket.py +++ b/src/britive/federation_providers/bitbucket.py @@ -1,6 +1,7 @@ import os -from ..exceptions import NotExecutingInBitbucketEnvironment +from britive.exceptions import NotExecutingInBitbucketEnvironment + from .federation_provider import FederationProvider diff --git a/src/britive/federation_providers/federation_provider.py b/src/britive/federation_providers/federation_provider.py index 2c11702..758aa28 100644 --- a/src/britive/federation_providers/federation_provider.py +++ b/src/britive/federation_providers/federation_provider.py @@ -3,4 +3,4 @@ def __init__(self) -> None: pass def get_token(self) -> None: - raise NotImplementedError() + raise NotImplementedError diff --git a/src/britive/federation_providers/github.py b/src/britive/federation_providers/github.py index 94adeec..1a4c1f1 100644 --- a/src/britive/federation_providers/github.py +++ b/src/britive/federation_providers/github.py @@ -2,7 +2,8 @@ import requests -from ..exceptions import NotExecutingInGithubEnvironment +from britive.exceptions import NotExecutingInGithubEnvironment + from .federation_provider import FederationProvider diff --git a/src/britive/federation_providers/gitlab.py b/src/britive/federation_providers/gitlab.py index 282e7d6..a32184b 100644 --- a/src/britive/federation_providers/gitlab.py +++ b/src/britive/federation_providers/gitlab.py @@ -1,6 +1,7 @@ import os -from ..exceptions import NotExecutingInGitlabEnvironment +from britive.exceptions import NotExecutingInGitlabEnvironment + from .federation_provider import FederationProvider diff --git a/src/britive/federation_providers/spacelift.py b/src/britive/federation_providers/spacelift.py index a2a4871..62d3030 100644 --- a/src/britive/federation_providers/spacelift.py +++ b/src/britive/federation_providers/spacelift.py @@ -1,6 +1,7 @@ import os -from ..exceptions import NotExecutingInSpaceliftEnvironment +from britive.exceptions import NotExecutingInSpaceliftEnvironment + from .federation_provider import FederationProvider diff --git a/src/britive/global_settings/banner.py b/src/britive/global_settings/banner.py index e80de49..0fdaf93 100644 --- a/src/britive/global_settings/banner.py +++ b/src/britive/global_settings/banner.py @@ -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/identity_management/identity_attributes.py b/src/britive/identity_management/identity_attributes.py index c364154..f43673b 100644 --- a/src/britive/identity_management/identity_attributes.py +++ b/src/britive/identity_management/identity_attributes.py @@ -71,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: """ diff --git a/src/britive/identity_management/service_identities.py b/src/britive/identity_management/service_identities.py index 833da53..99acdc6 100644 --- a/src/britive/identity_management/service_identities.py +++ b/src/britive/identity_management/service_identities.py @@ -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: """ diff --git a/src/britive/identity_management/tags.py b/src/britive/identity_management/tags.py index 092e95c..cd0f2f9 100644 --- a/src/britive/identity_management/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_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/identity_management/users.py b/src/britive/identity_management/users.py index ab4ec12..564f582 100644 --- a/src/britive/identity_management/users.py +++ b/src/britive/identity_management/users.py @@ -1,8 +1,9 @@ -from ..exceptions import ( +from britive.exceptions import ( UserDoesNotHaveMFAEnabled, UserNotAllowedToChangePassword, UserNotAssociatedWithDefaultIdentityProvider, ) + from .identity_attributes import CustomAttributes valid_statues = ['active', 'inactive'] @@ -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}) @@ -240,7 +240,7 @@ def reset_mfa(self, user_id: str) -> None: user = self.get(user_id) if not user['identityProvider'].get('mfaEnabled'): - raise UserDoesNotHaveMFAEnabled() + 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 index 43c3438..35ebd68 100644 --- a/src/britive/identity_management/workload.py +++ b/src/britive/identity_management/workload.py @@ -317,7 +317,7 @@ def generate_attribute_map( break if not found: raise ValueError( - f'custom_identity_attribute_name value of {custom_identity_attribute_name} ' f'not found.' + f'custom_identity_attribute_name value of {custom_identity_attribute_name} not found.' ) return {'idpAttr': idp_attribute_name, 'userAttr': custom_identity_attribute_id} diff --git a/src/britive/my_access.py b/src/britive/my_access.py index e423bc4..2ede5ae 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -122,7 +122,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 TransactionNotFound() + raise TransactionNotFound def get_profile_settings(self, profile_id: str, environment_id: str) -> dict: """ @@ -191,7 +191,7 @@ def extend_checkout_by_name( transaction_id = transaction['transactionId'] break if not transaction_id: - raise TransactionNotFound() + raise TransactionNotFound return self.extend_checkout(transaction_id=transaction_id) def _checkout( @@ -244,7 +244,7 @@ def _checkout( if otp: response = self.britive.step_up.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise StepUpAuthFailed() + raise StepUpAuthFailed try: transaction = self.britive.post( @@ -257,17 +257,17 @@ def _checkout( raise ApprovalRequiredButNoJustificationProvided(e) from e # request approval - approval_request = dict( - 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, - ) + 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 @@ -376,13 +376,15 @@ def checkout( 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( @@ -439,13 +441,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( @@ -584,11 +588,10 @@ def create_filter(self, filter_name: str, filter_properties: str) -> dict: return self.britive.post(f"{self.base_url}/{self.whoami()['userId']}/filters", json=data) - def list_filters(self, user_id: str = None) -> list: + def list_filters(self) -> list: """ Return list of filters for the current user. - :param user_id: ID of the user to list filters for. Default: `my_access.whoami()['userId']` :return: List of filters. """ @@ -627,7 +630,7 @@ def update_filter(self, filter_id: str, filter_name: str, filter_properties: str return self.britive.put(f"{self.base_url}/{self.whoami()['userId']}/filters/{filter_id}", json=data) - def delete_filter(self, filter_id: str, user_id: str = None) -> None: + def delete_filter(self, filter_id: str) -> None: """ Delete a filter for the current user. diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py index 07c0ac5..ed44dce 100644 --- a/src/britive/my_requests.py +++ b/src/britive/my_requests.py @@ -85,7 +85,7 @@ def _request_approval( request = self.britive.post(url, json=data) if request is None: - raise ProfileCheckoutAlreadyApproved() + raise ProfileCheckoutAlreadyApproved request_id = request['requestId'] @@ -101,7 +101,7 @@ def _request_approval( continue # status == timeout or approved or rejected or cancelled return status - raise ProfileApprovalMaxBlockTimeExceeded() + raise ProfileApprovalMaxBlockTimeExceeded except KeyboardInterrupt: # handle Ctrl+C (^C) # the first ^C we get we will try to withdraw the request try: @@ -120,6 +120,7 @@ def _request_approval_by_name( 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, @@ -128,7 +129,7 @@ def _request_approval_by_name( wait_time: int = 60, ) -> Any: if entity_type == 'environments': - ids = self._helper.get_profile_and_environment_ids_given_names(profile_name, entity_name) + 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, @@ -166,9 +167,9 @@ def _withdraw_approval_request( class MyAccessRequests(MyRequests): def request_approval_by_name( self, - profile_name: str, environment_name: str, justification: str, + profile_name: str, application_name: str = None, block_until_disposition: bool = False, max_wait_time: int = 600, @@ -206,11 +207,12 @@ def request_approval_by_name( """ return self._request_approval_by_name( + entity_name=environment_name, justification=justification, profile_name=profile_name, - entity_name=environment_name, - entity_type='environments', + 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, @@ -226,7 +228,6 @@ def request_approval( environment_id: str = None, max_wait_time: int = 600, progress_func: Callable = None, - resource_id: str = None, ticket_id: str = None, ticket_type: str = None, wait_time: int = 60, @@ -413,9 +414,7 @@ def request_approval_by_name( wait_time=wait_time, ) - def withdraw_approval_request_by_name( - self, profile_name: str, environment_name: str = None, resource_name: str = None - ) -> None: + 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. diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 54d8e4d..7cda1f8 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -49,14 +49,19 @@ def __init__(self, britive) -> None: 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, list_type: str = None, search_text: str = None) -> list: + 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: @@ -94,7 +99,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 TransactionNotFound() + raise TransactionNotFound def _checkout( self, @@ -142,7 +147,7 @@ def _checkout( if otp: response = self.britive.step_up.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise StepUpAuthFailed() + raise StepUpAuthFailed try: transaction = self.britive.post( @@ -152,7 +157,7 @@ def _checkout( raise StepUpAuthRequiredButNotProvided(e) from e except (ApprovalJustificationRequiredError, ProfileApprovalRequiredError) as e: if not justification: - raise ApprovalRequiredButNoJustificationProvided() from e + raise ApprovalRequiredButNoJustificationProvided from e # request approval status = self.request_approval( @@ -244,13 +249,13 @@ def checkout( """ return self._checkout( + profile_id=profile_id, + resource_id=resource_id, include_credentials=include_credentials, justification=justification, max_wait_time=max_wait_time, otp=otp, - profile_id=profile_id, progress_func=progress_func, - resource_id=resource_id, response_template=response_template, ticket_id=ticket_id, ticket_type=ticket_type, @@ -285,12 +290,12 @@ def checkout_by_name( 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 response_template: Optional response template to use in conjunction with `include_credentials`. :param justification: Optional justification if checking out the profile requires approval. :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 @@ -306,34 +311,36 @@ def checkout_by_name( ids = self._get_profile_and_resource_ids_given_names(profile_name, resource_name) return self._checkout( + profile_id=ids['profile_id'], + resource_id=ids['resource_id'], include_credentials=include_credentials, justification=justification, max_wait_time=max_wait_time, otp=otp, - profile_id=ids['profile_id'], progress_func=progress_func, - resource_id=ids['resource_id'], 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, + progress_func: Callable = None, response_template: str = None, return_transaction_details: bool = False, - progress_func: Callable = None, + 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. """ diff --git a/src/britive/my_secrets.py b/src/britive/my_secrets.py index d4ec9a4..4d2a980 100644 --- a/src/britive/my_secrets.py +++ b/src/britive/my_secrets.py @@ -42,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 NoSecretsVaultFound() from e + raise NoSecretsVaultFound from e def list(self, path: str = '/', search: str = None) -> list: """ @@ -95,13 +95,13 @@ def view( try: # handle when the time has expired waiting for approval if datetime.now(timezone.utc) >= quit_time: - raise ApprovalWorkflowTimedOut() + raise ApprovalWorkflowTimedOut # handle stepup totp if otp: response = self.britive.step_up.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise StepUpAuthFailed() + raise StepUpAuthFailed # attempt to get the secret value and return it return self.britive.post( @@ -116,8 +116,7 @@ def view( if not justification: if first: raise ApprovalRequiredButNoJustificationProvided(e) from e - else: - raise ApprovalWorkflowRejected(e) from e + raise ApprovalWorkflowRejected(e) from e except StepUpAuthenticationRequiredError as e: raise StepUpAuthRequiredButNotProvided(e) from e except ForbiddenRequest as e: @@ -157,7 +156,7 @@ def download( if otp: response = self.britive.step_up.authenticate(otp=otp) if response.get('result') == 'FAILED': - raise 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) @@ -167,9 +166,7 @@ def download( 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 - ) + 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: diff --git a/src/britive/security/api_tokens.py b/src/britive/security/api_tokens.py index 2915a4d..2132820 100644 --- a/src/britive/security/api_tokens.py +++ b/src/britive/security/api_tokens.py @@ -1,4 +1,4 @@ -from ..exceptions import ApiTokenNotFound +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 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/system/permissions.py b/src/britive/system/permissions.py index ba6a2d6..a2911e3 100644 --- a/src/britive/system/permissions.py +++ b/src/britive/system/permissions.py @@ -61,7 +61,7 @@ def update(self, permission_identifier: str, permission: dict, identifier_type: self._validate_identifier_type(identifier_type) - if permission.pop('isInline', False): # InvalidRequest: 400 - PA-0059 - isInline is not allowed to update + 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('isReadOnly', 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 From 9d376c8c841cf770a4755273f28ded2e1e31d653 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 7 Jan 2025 15:47:34 -0600 Subject: [PATCH 29/40] test:silint night --- tests/250-system-01-policies.py | 2 +- tests/250-system-02-actions.py | 2 +- tests/cache.py | 32 ++++++++++++-------------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/tests/250-system-01-policies.py b/tests/250-system-01-policies.py index d87183a..f273f57 100644 --- a/tests/250-system-01-policies.py +++ b/tests/250-system-01-policies.py @@ -219,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/250-system-02-actions.py b/tests/250-system-02-actions.py index 42be523..22e71f0 100644 --- a/tests/250-system-02-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' diff --git a/tests/cache.py b/tests/cache.py index 0f96d06..b1f705a 100644 --- a/tests/cache.py +++ b/tests/cache.py @@ -110,6 +110,7 @@ def cached_service_identity_federated(pytestconfig, timestamp): except UserCreationError: return britive.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): @@ -655,10 +656,9 @@ def cached_workload_identity_provider_aws(pytestconfig, timestamp, cached_identi return idp try: - response = britive.workload.identity_providers.create_aws( + return britive.workload.identity_providers.create_aws( name=f'python-sdk-aws-{timestamp}', attributes_map={'UserId': cached_identity_attribute['id']} ) - return response except InternalServerError as e: raise Exception('AWS provider could not be created and none found') from e @@ -666,12 +666,11 @@ def cached_workload_identity_provider_aws(pytestconfig, timestamp, cached_identi @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.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') @@ -680,8 +679,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') @@ -694,8 +692,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') @@ -708,16 +705,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') @@ -726,8 +721,7 @@ 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') @@ -765,14 +759,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') @@ -816,7 +808,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') @@ -867,11 +859,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'], From e3008769a25a94ee267ea85664a601c515fed7d9 Mon Sep 17 00:00:00 2001 From: theborch Date: Tue, 7 Jan 2025 15:53:09 -0600 Subject: [PATCH 30/40] v3.2.0-alpha.5 --- CHANGELOG.md | 4 ++-- src/britive/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d2779..af43425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log (v2.8.1+) -## v3.2.0-alpha.* [2024-12-23] +## v3.2.0-alpha.* [2025-01-07] __What's New:__ @@ -17,10 +17,10 @@ __Enhancements:__ * Added `add_favorite` and `delete_favorite` to `my_resources`. * Added checkout approvals to `my_resources`. * Added ITSM to checkout approvals. -* Added `include_approval_status` to `my_access.list_profiles`. * 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:__ diff --git a/src/britive/__init__.py b/src/britive/__init__.py index c7f8c51..001c0e5 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-alpha.4' +__version__ = '3.2.0-alpha.5' From 450add695e21cfd18c53c76d2de28e3b7513b98d Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 13 Jan 2025 11:06:01 -0600 Subject: [PATCH 31/40] feat:add `size` param to `my_access.list` --- src/britive/my_access.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/britive/my_access.py b/src/britive/my_access.py index 2ede5ae..e7d8804 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -64,12 +64,13 @@ def __init__(self, britive) -> None: self.list_approvals = __my_approvals.list self.reject_request = __my_approvals.reject_request - def list(self, filter_text: str = None, search_text: str = None) -> list: + 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. """ @@ -78,6 +79,8 @@ def list(self, filter_text: str = None, search_text: str = None) -> list: 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) From ae60b4e35e702c4ad0de1a2977784e1c4ca49300 Mon Sep 17 00:00:00 2001 From: theborch Date: Mon, 13 Jan 2025 11:07:04 -0600 Subject: [PATCH 32/40] v3.2.0-beta.0 --- CHANGELOG.md | 4 +--- src/britive/__init__.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af43425..41dc48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log (v2.8.1+) -## v3.2.0-alpha.* [2025-01-07] +## v3.2.0-beta.0 [2025-01-13] __What's New:__ @@ -26,9 +26,7 @@ __Bug Fixes:__ * Fixed missing `param_values` option for resource creation. * `my_requests.list_approvals` now includes `my_resources` requests. -* `my_access.*_filter[s]` are only valid for the current user. * Make `get` call in helper method instead `list_approvals`. -* Missing `s` in `environments` for `my_requests`. * Catch `requests.exceptions.JSONDecodeError` in `_handle_response`. __Dependencies:__ diff --git a/src/britive/__init__.py b/src/britive/__init__.py index 001c0e5..88c94c2 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-alpha.5' +__version__ = '3.2.0-beta.0' From b0d9fa40cda8de4fba6de6c98221064e9c73577d Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 15 Jan 2025 10:20:39 -0600 Subject: [PATCH 33/40] fix:s/name/id/ --- src/britive/my_requests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/britive/my_requests.py b/src/britive/my_requests.py index ed44dce..5197b6c 100644 --- a/src/britive/my_requests.py +++ b/src/britive/my_requests.py @@ -260,8 +260,8 @@ def request_approval( return self._request_approval( justification=justification, - profile_name=profile_id, - entity_name=environment_id, + profile_id=profile_id, + entity_id=environment_id, entity_type='environments', block_until_disposition=block_until_disposition, max_wait_time=max_wait_time, @@ -352,8 +352,8 @@ def request_approval( return self._request_approval( justification=justification, - profile_name=profile_id, - entity_name=resource_id, + profile_id=profile_id, + entity_id=resource_id, entity_type='resource', block_until_disposition=block_until_disposition, max_wait_time=max_wait_time, From d974ed83ee9acfd15e6f5ebc121be9df647a8a99 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 15 Jan 2025 10:21:22 -0600 Subject: [PATCH 34/40] v3.2.0-beta.1 --- CHANGELOG.md | 2 +- README.md | 12 +++++++----- src/britive/__init__.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41dc48c..1d8ece3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log (v2.8.1+) -## v3.2.0-beta.0 [2025-01-13] +## v3.2.0-beta.1 [2025-01-15] __What's New:__ diff --git a/README.md b/README.md index 3f8cd63..17fc7be 100644 --- a/README.md +++ b/README.md @@ -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/src/britive/__init__.py b/src/britive/__init__.py index 88c94c2..b1bc875 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-beta.0' +__version__ = '3.2.0-beta.1' From fce6925b7bb145156dc9c8714a8a75ae1d2c4c87 Mon Sep 17 00:00:00 2001 From: theborch Date: Fri, 17 Jan 2025 16:31:11 -0600 Subject: [PATCH 35/40] refactor:move static methods to utils --- src/britive/britive.py | 241 ++++----------------------------- src/britive/helpers/methods.py | 10 -- src/britive/helpers/utils.py | 174 ++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 226 deletions(-) create mode 100644 src/britive/helpers/utils.py diff --git a/src/britive/britive.py b/src/britive/britive.py index c773b8b..6f3067c 100644 --- a/src/britive/britive.py +++ b/src/britive/britive.py @@ -1,37 +1,28 @@ import os -import socket import time import requests -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 . import __version__ from .access_broker import AccessBroker from .application_management import ApplicationManagement from .audit_logs import AuditLogs from .exceptions import ( - BritiveException, - InvalidFederationProvider, RootEnvironmentGroupNotFound, TenantMissingError, TenantUnderMaintenance, TokenMissingError, - allowed_exceptions, -) -from .federation_providers import ( - AwsFederationProvider, - AzureSystemAssignedManagedIdentityFederationProvider, - AzureUserAssignedManagedIdentityFederationProvider, - BitbucketFederationProvider, - GithubFederationProvider, - GitlabFederationProvider, - SpaceliftFederationProvider, ) from .global_settings import GlobalSettings -from .helpers import HelperMethods as helper_methods +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 @@ -100,7 +91,7 @@ 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 @@ -111,7 +102,7 @@ def __init__( 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://{self.parse_tenant(self.tenant)}/api' + self.base_url = f'https://{parse_tenant(self.tenant)}/api' self.session = self._setup_session() self.retry_backoff_factor = 1 @@ -122,7 +113,7 @@ def __init__( def _initialize_token(self, token: str, provider: str, duration: int) -> str: if provider: - return self.source_federation_token_from(provider, self.tenant, duration) + return source_federation_token(provider, self.tenant, duration) return token or os.getenv('BRITIVE_API_TOKEN') or TokenMissingError('Token not provided.') def _setup_session(self) -> requests.Session: @@ -184,138 +175,6 @@ def _initialize_components(self, query_features: bool) -> None: self.security = Security(self) self.workflows = Workflows(self) - # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility - if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': - self.access_builder = self.application_management.access_builder - self.accounts = self.application_management.accounts - self.applications = self.application_management.applications - self.audit_logs.logs.webhooks = self.audit_logs.webhooks - self.audit_logs = self.audit_logs.logs - self.environment_groups = self.application_management.environment_groups - self.environments = self.application_management.environments - self.groups = self.application_management.groups - self.identity_attributes = self.identity_management.identity_attributes - self.identity_providers = self.identity_management.identity_providers - self.notification_mediums = self.global_settings.notification_mediums - self.notifications = self.workflows.notifications - self.permissions = self.application_management.permissions - self.profiles = self.application_management.profiles - self.saml = self.security.saml - self.scans = self.application_management.scans - self.security_policies = self.security.security_policies - self.service_identities = self.identity_management.service_identities - self.service_identity_tokens = self.identity_management.service_identity_tokens - self.step_up = self.security.step_up_auth - self.tags = self.identity_management.tags - self.task_services = self.workflows.task_services - self.tasks = self.workflows.tasks - self.users = self.identity_management.users - self.workload = self.identity_management.workload - self.settings = self.global_settings - - @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) - * 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=helper_methods.safe_list_get(helper, 1), tenant=tenant, duration=duration_seconds - ).get_token(), - 'bitbucket': lambda: BitbucketFederationProvider().get_token(), - 'github': lambda: GithubFederationProvider(audience=helper_methods.safe_list_get(helper, 1)).get_token(), - 'gitlab': lambda: GitlabFederationProvider( - token_env_var=helper_methods.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=helper_methods.safe_list_get(helper, 1) - ).get_token() - - if provider_name == 'azureumi': - return AzureUserAssignedManagedIdentityFederationProvider( - client_id=helper[1].split('|')[0], audience=helper_methods.safe_list_get(helper[1].split('|'), 1) - ).get_token() - - raise InvalidFederationProvider(f'federation provider {provider_name} not supported') - - @staticmethod - 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 features(self) -> dict: return {feature['name']: feature['enabled'] for feature in self.get(f'{self.base_url}/features')} @@ -355,64 +214,14 @@ 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}) - return self._handle_response(response) + 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}) - return self._handle_response(response) - - @staticmethod - 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') - - @staticmethod - 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) - - @staticmethod - 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) - - @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: - return response.status_code == 503 and response.json().get('errorCode') == 'MAINT0001' + return handle_response(response) def __request_with_exponential_backoff_and_retry(self, method, url, params, data, json) -> dict: num_retries = 0 @@ -423,25 +232,25 @@ def __request_with_exponential_backoff_and_retry(self, method, url, params, data # 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): + if tenant_is_under_maintenance(response): raise TenantUnderMaintenance(response.json().get('message')) if response.status_code in self.retry_response_status: time.sleep((2**num_retries) * self.retry_backoff_factor) num_retries += 1 else: - self.__check_response_for_error(response.status_code, self._handle_response(response)) + check_response_for_error(response.status_code, handle_response(response)) break return response def __request(self, method, url, params=None, data=None, json=None) -> dict: return_data = [] - pagination_type = None + _pagination_type = None while True: response = self.__request_with_exponential_backoff_and_retry(method, url, params, data, json) - if self.__response_has_no_content(response): + if response_has_no_content(response): return None # handle secrets file download @@ -451,26 +260,26 @@ def __request(self, method, url, params=None, data=None, json=None) -> dict: return {'filename': filename, 'content_bytes': bytes(response.content)} # load the result as a dict - result = self._handle_response(response) - pagination_type = pagination_type or self.__pagination_type(response.headers, result) + 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 pagination_type == 'inline': + if _pagination_type == 'inline': return_data += result['data'] if result['size'] * (result['page'] + 1) >= result['count']: break params['page'] = result['page'] + 1 - elif pagination_type in ('audit', 'report'): - return_data += result if pagination_type == 'audit' else result['data'] + 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 = {} - elif pagination_type == 'secmgr': + elif _pagination_type == 'secmgr': return_data += result['result'] url = result['pagination'].get('next', '') if not url: diff --git a/src/britive/helpers/methods.py b/src/britive/helpers/methods.py index 7543286..bdb75e7 100644 --- a/src/britive/helpers/methods.py +++ b/src/britive/helpers/methods.py @@ -1,17 +1,8 @@ -from typing import Union - - class HelperMethods: def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}/access' - def safe_list_get(lst: list, idx: int, default: str = None) -> Union[str, None]: - try: - return lst[idx] - except IndexError: - return default - def get_profile_and_environment_ids_given_names( self, profile_name: str, environment_name: str, application_name: str = None ) -> dict: @@ -44,7 +35,6 @@ def get_profile_and_environment_ids_given_names( raise ValueError(f'profile `{profile_name}` found but not in environment `{environment_name}`.') return ids - 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()}': { 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' From adcbb482c9cdfbbb28f5983a4c337544b92ad0c8 Mon Sep 17 00:00:00 2001 From: theborch Date: Fri, 17 Jan 2025 16:32:50 -0600 Subject: [PATCH 36/40] refactor:accept breaking change & act accordingly --- .../application_management/applications.py | 4 +- .../application_management/environments.py | 8 +- .../application_management/profiles.py | 23 +-- src/britive/audit_logs/logs.py | 2 +- .../identity_attributes.py | 4 +- .../identity_management/identity_providers.py | 6 +- src/britive/identity_management/tags.py | 2 +- src/britive/identity_management/workload.py | 19 +-- src/britive/my_access.py | 14 +- src/britive/my_approvals.py | 15 -- src/britive/my_resources.py | 2 +- src/britive/my_secrets.py | 4 +- src/britive/secrets_manager/vaults.py | 2 +- src/britive/workflows/notifications.py | 15 +- src/britive/workflows/tasks.py | 8 +- ...-global_settings-01-identity_attributes.py | 2 +- ...global_settings-02-notification_mediums.py | 8 +- tests/000-global_settings-03-banner.py | 20 +-- tests/100-identity_management-01-users.py | 52 +++--- tests/100-identity_management-02-tags.py | 58 ++++--- ...entity_management-03-service_identities.py | 32 ++-- ...y_management-04-service_identity_tokens.py | 2 +- ...entity_management-05-identity_providers.py | 44 +++-- tests/100-identity_management-06-workload.py | 40 ++--- ...-application_management-01-applications.py | 16 +- ...cation_management-02-environment_groups.py | 4 +- ...-application_management-03-environments.py | 8 +- tests/200-application_management-04-scans.py | 10 +- .../200-application_management-05-accounts.py | 14 +- ...0-application_management-06-permissions.py | 6 +- tests/200-application_management-07-groups.py | 6 +- .../200-application_management-08-profiles.py | 100 +++++++----- tests/300-workflows-01-task_services.py | 4 +- tests/300-workflows-02-tasks.py | 14 +- tests/300-workflows-03-notifications.py | 14 +- tests/400-security-01-policies.py | 20 +-- tests/400-security-02-saml.py | 8 +- tests/500-audit_logs-01-logs.py | 8 +- tests/999-cleanup-01-delete_all_resources.py | 58 ++++--- tests/cache.py | 150 ++++++++++-------- 40 files changed, 429 insertions(+), 397 deletions(-) diff --git a/src/britive/application_management/applications.py b/src/britive/application_management/applications.py index ba6e576..248e95d 100644 --- a/src/britive/application_management/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/application_management/environments.py b/src/britive/application_management/environments.py index 1694f2a..9717935 100644 --- a/src/britive/application_management/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/application_management/profiles.py b/src/britive/application_management/profiles.py index b81ebb3..d86b77b 100644 --- a/src/britive/application_management/profiles.py +++ b/src/britive/application_management/profiles.py @@ -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/audit_logs/logs.py b/src/britive/audit_logs/logs.py index be0c162..0435acc 100644 --- a/src/britive/audit_logs/logs.py +++ b/src/britive/audit_logs/logs.py @@ -43,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. diff --git a/src/britive/identity_management/identity_attributes.py b/src/britive/identity_management/identity_attributes.py index f43673b..b3b76c7 100644 --- a/src/britive/identity_management/identity_attributes.py +++ b/src/britive/identity_management/identity_attributes.py @@ -99,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_management/identity_providers.py b/src/britive/identity_management/identity_providers.py index 4f6d51e..e7572cd 100644 --- a/src/britive/identity_management/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/identity_management/tags.py b/src/britive/identity_management/tags.py index cd0f2f9..3d0bd52 100644 --- a/src/britive/identity_management/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 = 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/identity_management/workload.py b/src/britive/identity_management/workload.py index 35ebd68..acb11c8 100644 --- a/src/britive/identity_management/workload.py +++ b/src/britive/identity_management/workload.py @@ -9,6 +9,7 @@ def __init__(self, britive) -> None: self.service_identities = WorkloadServiceIdentities(self) self.scim_user = WorkloadScimUser(self) + class WorkloadIdentityProviders: def __init__(self, workload) -> None: self.britive = workload.britive @@ -40,7 +41,9 @@ def get(self, workload_identity_provider_id: int) -> dict: 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_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} @@ -304,24 +307,21 @@ def generate_attribute_map( ) 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' - ) + 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(): + 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.' - ) + 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 @@ -357,7 +357,7 @@ def assign( mapping_attributes = [] converted_federated_attributes = {} - converted_attributes = self.britive.service_identities.custom_attributes._build_list( + converted_attributes = self.britive.identity_management.service_identities.custom_attributes._build_list( operation='add', custom_attributes=federated_attributes ) @@ -386,6 +386,7 @@ def unassign(self, service_identity_id: str) -> None: """ return self.britive.delete(self.base_url.format(id=service_identity_id)) + class WorkloadScimUser: def __init__(self, workload) -> None: self.britive = workload.britive diff --git a/src/britive/my_access.py b/src/britive/my_access.py index e7d8804..28da3a4 100644 --- a/src/britive/my_access.py +++ b/src/britive/my_access.py @@ -1,4 +1,3 @@ -import os import time from typing import Any, Callable @@ -17,7 +16,6 @@ ) from .exceptions.generic import BritiveGenericError, StepUpAuthenticationRequiredError from .helpers import HelperMethods -from .my_approvals import MyApprovals from .my_requests import MyAccessRequests approval_exceptions = { @@ -54,16 +52,6 @@ def __init__(self, britive) -> None: self.withdraw_approval_request = __my_requests.withdraw_approval_request self.withdraw_approval_request_by_name = __my_requests.withdraw_approval_request_by_name - # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility - if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': - # MyAccess backwards compatibility - self.approval_request_status = __my_requests.approval_request_status - # MyApprovals backwards compatibility - __my_approvals = MyApprovals(self.britive) - self.approve_request = __my_approvals.approve_request - self.list_approvals = __my_approvals.list - self.reject_request = __my_approvals.reject_request - def list(self, filter_text: str = None, search_text: str = None, size: int = None) -> list: """ List the access details for the current user. @@ -245,7 +233,7 @@ 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 StepUpAuthFailed diff --git a/src/britive/my_approvals.py b/src/britive/my_approvals.py index c851131..62fae38 100644 --- a/src/britive/my_approvals.py +++ b/src/britive/my_approvals.py @@ -1,6 +1,3 @@ -import os - - 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 @@ -56,15 +53,3 @@ def list(self) -> dict: params = {'requestType': 'myApprovals'} return self.britive.get(f'{self.base_url}/', params=params) - - # FUTURE_BRITIVE_SDK == 'true' will remove backwards compatibility - if os.getenv('FUTURE_BRITIVE_SDK', 'false').lower() != 'true': - - def list_approvals(self) -> dict: - """ - Lists approval requests. - - :return: List of approval requests. - """ - - return self.list() diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index 7cda1f8..a08cef6 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -145,7 +145,7 @@ 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 StepUpAuthFailed diff --git a/src/britive/my_secrets.py b/src/britive/my_secrets.py index 4d2a980..062891a 100644 --- a/src/britive/my_secrets.py +++ b/src/britive/my_secrets.py @@ -99,7 +99,7 @@ def view( # 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 StepUpAuthFailed @@ -154,7 +154,7 @@ 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 StepUpAuthFailed diff --git a/src/britive/secrets_manager/vaults.py b/src/britive/secrets_manager/vaults.py index e4ecaaf..7a14582 100644 --- a/src/britive/secrets_manager/vaults.py +++ b/src/britive/secrets_manager/vaults.py @@ -56,7 +56,7 @@ def create( channels = [] if default_notification_medium_id == '': - for medium in self.britive.notification_mediums.list(): + for medium in self.britive.global_settings.notification_mediums.list(): if medium['name'] == 'Email': default_notification_medium_id = medium['id'] params = { diff --git a/src/britive/workflows/notifications.py b/src/britive/workflows/notifications.py index a934336..984bd19 100644 --- a/src/britive/workflows/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/workflows/tasks.py b/src/britive/workflows/tasks.py index 0b7a75e..4ea595d 100644 --- a/src/britive/workflows/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/tests/000-global_settings-01-identity_attributes.py b/tests/000-global_settings-01-identity_attributes.py index 60a3488..b8f58e1 100644 --- a/tests/000-global_settings-01-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/000-global_settings-02-notification_mediums.py b/tests/000-global_settings-02-notification_mediums.py index ec53965..8033265 100644 --- a/tests/000-global_settings-02-notification_mediums.py +++ b/tests/000-global_settings-02-notification_mediums.py @@ -7,13 +7,13 @@ 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'] @@ -21,6 +21,6 @@ def test_get(cached_notification_medium): 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']) + 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/000-global_settings-03-banner.py b/tests/000-global_settings-03-banner.py index 7433817..b5a409b 100644 --- a/tests/000-global_settings-03-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/100-identity_management-01-users.py b/tests/100-identity_management-01-users.py index 8fc179d..acc419e 100644 --- a/tests/100-identity_management-01-users.py +++ b/tests/100-identity_management-01-users.py @@ -33,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) @@ -42,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) @@ -64,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) @@ -72,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(UserDoesNotHaveMFAEnabled): - britive.users.reset_mfa(cached_user['userId']) + 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)}']}, ) @@ -120,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) @@ -128,37 +130,41 @@ 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() + challenge = britive.identity_management.users.enable_mfa.enable() totp = pyotp.TOTP(challenge.get('additionalDetails').get('key')) totp = totp.now() assert len(str(totp)) == 6 diff --git a/tests/100-identity_management-02-tags.py b/tests/100-identity_management-02-tags.py index fd1df13..b8d829e 100644 --- a/tests/100-identity_management-02-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/100-identity_management-03-service_identities.py b/tests/100-identity_management-03-service_identities.py index 43bbdb1..522f0ca 100644 --- a/tests/100-identity_management-03-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/100-identity_management-04-service_identity_tokens.py b/tests/100-identity_management-04-service_identity_tokens.py index a728eda..1867d21 100644 --- a/tests/100-identity_management-04-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/100-identity_management-05-identity_providers.py b/tests/100-identity_management-05-identity_providers.py index fad9495..c64150a 100644 --- a/tests/100-identity_management-05-identity_providers.py +++ b/tests/100-identity_management-05-identity_providers.py @@ -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'] @@ -94,21 +102,21 @@ def test_scim_tokens_update_attribute_mapping(cached_identity_provider): def test_configure_mfa(cached_identity_provider): with pytest.raises(BritiveGenericError) as e: - britive.identity_providers.configure_mfa( + 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/100-identity_management-06-workload.py b/tests/100-identity_management-06-workload.py index 8d1d334..19815c0 100644 --- a/tests/100-identity_management-06-workload.py +++ b/tests/100-identity_management-06-workload.py @@ -4,7 +4,7 @@ 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) @@ -83,20 +83,22 @@ def test_generate_attribute_map(cached_identity_attribute): def test_service_identity_get_when_nothing_associated(cached_service_identity_federated): with pytest.raises(BritiveGenericException): - britive.workload.service_identities.get(service_identity_id=cached_service_identity_federated['userId']) + 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_federated, cached_identity_attribute, cached_workload_identity_provider_oidc ): - response = britive.workload.service_identities.assign( + 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( + attrs = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity_federated['userId'], as_dict=True ) @@ -104,13 +106,13 @@ def test_service_identity_assign_and_unassign( assert len(attrs) == 1 assert attrs[cached_identity_attribute['id']] == 'test' - response = britive.workload.service_identities.unassign( + 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( + attrs = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity_federated['userId'], as_dict=False ) @@ -118,14 +120,14 @@ def test_service_identity_assign_and_unassign( assert len(attrs) == 1 assert attrs[0]['attributeName'] is None - response = britive.workload.service_identities.assign( + 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( + attrs = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity_federated['userId'], as_dict=True ) @@ -133,13 +135,13 @@ def test_service_identity_assign_and_unassign( assert len(attrs) == 1 assert attrs[cached_identity_attribute['id']] == 'test' - response = britive.workload.service_identities.unassign( + 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( + attrs = britive.identity_management.service_identities.custom_attributes.get( principal_id=cached_service_identity_federated['userId'], as_dict=False ) @@ -152,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/200-application_management-01-applications.py b/tests/200-application_management-01-applications.py index fd4bf23..fcbca16 100644 --- a/tests/200-application_management-01-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/200-application_management-02-environment_groups.py b/tests/200-application_management-02-environment_groups.py index 71e5130..7a33a18 100644 --- a/tests/200-application_management-02-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/200-application_management-03-environments.py b/tests/200-application_management-03-environments.py index a1d1abf..35f1770 100644 --- a/tests/200-application_management-03-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,14 +24,14 @@ 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'] ) assert isinstance(response, dict) @@ -39,7 +39,7 @@ def test_environment_test(cached_application, cached_environment): 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/200-application_management-04-scans.py b/tests/200-application_management-04-scans.py index 8e2dff2..83c5f60 100644 --- a/tests/200-application_management-04-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/200-application_management-05-accounts.py b/tests/200-application_management-05-accounts.py index ee0191a..26ab177 100644 --- a/tests/200-application_management-05-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/200-application_management-06-permissions.py b/tests/200-application_management-06-permissions.py index 2b0f085..b638063 100644 --- a/tests/200-application_management-06-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/200-application_management-07-groups.py b/tests/200-application_management-07-groups.py index ff9457f..5f6ae12 100644 --- a/tests/200-application_management-07-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/200-application_management-08-profiles.py b/tests/200-application_management-08-profiles.py index 33f7e56..a41309f 100644 --- a/tests/200-application_management-08-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) @@ -89,13 +91,13 @@ def test_session_attributes_add_dynamic(cached_dynamic_session_attribute): def test_session_attributes_list(cached_profile): - attributes = britive.profiles.session_attributes.list(profile_id=cached_profile['papId']) + 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 @@ -140,7 +142,9 @@ def test_policies_create(cached_profile_policy): 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) @@ -149,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' @@ -188,7 +196,7 @@ 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( + policy = britive.application_management.profiles.policies.build( name=f"{cached_profile['papId']}-2", description='', service_identities=[cached_service_identity['username']], @@ -199,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, @@ -222,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, @@ -231,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, @@ -240,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, @@ -251,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 @@ -261,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: @@ -270,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' ) @@ -286,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', @@ -295,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', @@ -304,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', @@ -321,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', @@ -334,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', @@ -345,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', @@ -366,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', @@ -377,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', @@ -391,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', @@ -401,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') @@ -410,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', @@ -420,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/300-workflows-01-task_services.py b/tests/300-workflows-01-task_services.py index 4698733..83b7cb5 100644 --- a/tests/300-workflows-01-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/300-workflows-02-tasks.py b/tests/300-workflows-02-tasks.py index f2e6438..0f0424d 100644 --- a/tests/300-workflows-02-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/300-workflows-03-notifications.py b/tests/300-workflows-03-notifications.py index 115f749..388a904 100644 --- a/tests/300-workflows-03-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/400-security-01-policies.py b/tests/400-security-01-policies.py index b2c2db9..0dfad0a 100644 --- a/tests/400-security-01-policies.py +++ b/tests/400-security-01-policies.py @@ -6,39 +6,41 @@ def test_create(cached_security_policy): def test_list(cached_security_policy): - policies = britive.security_policies.list() + 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_policies.get(security_policy_id=cached_security_policy['id']) + 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_policies.disable(security_policy_id=cached_security_policy['id']) + response = britive.security.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']) + 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_policies.enable(security_policy_id=cached_security_policy['id']) + response = britive.security.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']) + 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_policies.update(security_policy_id=cached_security_policy['id'], ips=['2.2.2.2']) + 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_policies.get(security_policy_id=cached_security_policy['id']) + 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_policies.delete(security_policy_id=cached_security_policy['id']) + response = britive.security.security_policies.delete(security_policy_id=cached_security_policy['id']) assert response is None cleanup('security-policy') diff --git a/tests/400-security-02-saml.py b/tests/400-security-02-saml.py index d968112..d6143f0 100644 --- a/tests/400-security-02-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/500-audit_logs-01-logs.py b/tests/500-audit_logs-01-logs.py index 243ffff..8c98959 100644 --- a/tests/500-audit_logs-01-logs.py +++ b/tests/500-audit_logs-01-logs.py @@ -4,24 +4,24 @@ def test_fields(): - fields = britive.audit_logs.fields() + fields = britive.audit_logs.logs.fields() assert isinstance(fields, dict) assert len(fields) == 18 def test_operators(): - operators = britive.audit_logs.operators() + operators = britive.audit_logs.logs.operators() assert isinstance(operators, dict) assert len(operators) == 4 def test_query_json(): - events = britive.audit_logs.query(from_time=(datetime.now() - timedelta(1)), to_time=datetime.now()) + events = britive.audit_logs.logs.query(from_time=(datetime.now() - timedelta(1)), 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(1), to_time=datetime.now(), csv=True) + csv = britive.audit_logs.logs.query(from_time=datetime.now() - timedelta(1), to_time=datetime.now(), csv=True) assert '"timestamp","actor.display_name"' in csv diff --git a/tests/999-cleanup-01-delete_all_resources.py b/tests/999-cleanup-01-delete_all_resources.py index 385c65a..642b57b 100644 --- a/tests/999-cleanup-01-delete_all_resources.py +++ b/tests/999-cleanup-01-delete_all_resources.py @@ -137,7 +137,7 @@ def test_system_level_policy_condition_as_dictionary_delete(cached_system_level_ # 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'], ) @@ -148,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'), ) @@ -159,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 @@ -171,7 +171,7 @@ def test_add_notification_to_access_builder_delete(cached_application, cached_ad 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 @@ -181,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 @@ -191,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 @@ -201,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 @@ -211,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] @@ -227,7 +227,7 @@ def test_profile_delete(cached_profile): 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 @@ -238,18 +238,22 @@ def test_environment_delete(cached_application, cached_environment): 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) @@ -261,7 +265,9 @@ 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: sleep(5) @@ -338,7 +344,9 @@ def test_vault_delete(cached_vault): # 100-identity_management 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']) + response = britive.identity_management.workload.identity_providers.delete( + cached_workload_identity_provider_aws['id'] + ) assert response is None finally: cleanup('workload-identity-provider-aws') @@ -346,7 +354,9 @@ def test_workload_identity_provider_aws_delete(cached_workload_identity_provider 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']) + response = britive.identity_management.workload.identity_providers.delete( + cached_workload_identity_provider_oidc['id'] + ) assert response is None finally: cleanup('workload-identity-provider-oidc') @@ -355,9 +365,9 @@ def test_workload_identity_provider_oidc_delete(cached_workload_identity_provide 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.service_identities.delete(service_identity_id=si['userId']) + response = britive.identity_management.service_identities.delete(service_identity_id=si['userId']) assert response is None - assert not britive.service_identities.get_by_name(name=si['name']) + assert not britive.identity_management.service_identities.get_by_name(name=si['name']) except exceptions.NotFound: pass finally: @@ -369,7 +379,7 @@ def test_service_identities_delete(cached_service_identity, cached_service_ident 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') @@ -377,9 +387,9 @@ def test_tags_delete(cached_tag): 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: @@ -389,7 +399,7 @@ def test_user_delete(cached_user): # 000-global_settings def test_notification_medium_delete(cached_notification_medium): try: - response = britive.notification_mediums.delete(cached_notification_medium['id']) + response = britive.global_settings.notification_mediums.delete(cached_notification_medium['id']) assert response is None finally: cleanup('notification-medium') @@ -397,7 +407,7 @@ def test_notification_medium_delete(cached_notification_medium): def test_notification_medium_webhook_delete(cached_notification_medium_webhook): try: - response = britive.notification_mediums.delete(cached_notification_medium_webhook['id']) + response = britive.global_settings.notification_mediums.delete(cached_notification_medium_webhook['id']) assert response is None finally: cleanup('notification-medium-webhook') @@ -405,7 +415,7 @@ def test_notification_medium_webhook_delete(cached_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 b1f705a..b79c1cb 100644 --- a/tests/cache.py +++ b/tests/cache.py @@ -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') @@ -93,9 +93,9 @@ def cached_service_identity(pytestconfig, timestamp): 'status': 'active', } try: - return britive.service_identities.create(**service_identity_to_create) + return britive.identity_management.service_identities.create(**service_identity_to_create) except UserCreationError: - return britive.service_identities.get_by_name(service_identity_to_create['name'])[0] + return britive.identity_management.service_identities.get_by_name(service_identity_to_create['name'])[0] @pytest.fixture(scope='session') @@ -106,27 +106,27 @@ def cached_service_identity_federated(pytestconfig, timestamp): 'status': 'active', } try: - return britive.service_identities.create(**service_identity_to_create) + return britive.identity_management.service_identities.create(**service_identity_to_create) except UserCreationError: - return britive.service_identities.get_by_name(service_identity_to_create['name'])[0] + 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 @@ -137,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}' ) @@ -145,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'] ) @@ -161,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'] ) @@ -169,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'] ) @@ -177,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'] ) @@ -192,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] @@ -200,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] @@ -208,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 ) @@ -216,59 +218,61 @@ 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') @@ -283,7 +287,7 @@ def cached_profile_checkout_request(pytestconfig, cached_profile, cached_service @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' ) @@ -291,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' ) @@ -306,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', @@ -329,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'], @@ -347,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 ) @@ -365,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'], @@ -409,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') @@ -500,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', @@ -510,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', @@ -520,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', @@ -533,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'], @@ -544,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'], ) @@ -557,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') @@ -583,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 ) @@ -597,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') @@ -605,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') @@ -623,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') @@ -651,12 +663,12 @@ 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: - return 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']} ) except InternalServerError as e: @@ -666,7 +678,7 @@ def cached_workload_identity_provider_aws(pytestconfig, timestamp, cached_identi @pytest.fixture(scope='session') @cached_resource(name='workload-identity-provider-oidc') def cached_workload_identity_provider_oidc(pytestconfig, timestamp, cached_identity_attribute): - return 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', @@ -727,13 +739,13 @@ def cached_system_level_permission(pytestconfig, timestamp): @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' ) @@ -743,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' ) From b28ea4506316f27ecd2edc443dd2806e5850092a Mon Sep 17 00:00:00 2001 From: theborch Date: Fri, 17 Jan 2025 16:35:21 -0600 Subject: [PATCH 37/40] chore:drop py38 due to EOL --- CONTRIBUTING.md | 2 +- README.md | 2 +- pyproject.toml | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afe2975..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. diff --git a/README.md b/README.md index 17fc7be..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 diff --git a/pyproject.toml b/pyproject.toml index 0e8181b..5c047fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ 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", @@ -24,7 +23,7 @@ classifiers = [ "Topic :: Security", ] license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "requests>=2.32.0" ] From 35a33d35ceaedc41c1ee594b424c742fa306e677 Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 29 Jan 2025 11:53:51 -0600 Subject: [PATCH 38/40] feat:response templates updates --- src/britive/access_broker/response_templates.py | 5 ++++- src/britive/identity_management/service_identities.py | 2 +- src/britive/my_resources.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) 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/identity_management/service_identities.py b/src/britive/identity_management/service_identities.py index 99acdc6..865ce48 100644 --- a/src/britive/identity_management/service_identities.py +++ b/src/britive/identity_management/service_identities.py @@ -191,7 +191,7 @@ def __init__(self, britive) -> None: self.britive = britive self.base_url = f'{self.britive.base_url}' - def __validate_token_expiration(days) -> None: + 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') diff --git a/src/britive/my_resources.py b/src/britive/my_resources.py index a08cef6..28eec94 100644 --- a/src/britive/my_resources.py +++ b/src/britive/my_resources.py @@ -88,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. From ad7e618c3d726e3c34db3bc2701ec2ab72312d7f Mon Sep 17 00:00:00 2001 From: theborch Date: Wed, 29 Jan 2025 11:54:16 -0600 Subject: [PATCH 39/40] test:updates --- tests/000-global_settings-02-notification_mediums.py | 4 ++-- tests/100-identity_management-01-users.py | 9 ++++++--- tests/150-secrets_manager-01-secrets_manager.py | 4 ++-- tests/200-application_management-08-profiles.py | 4 ++-- tests/250-system-02-actions.py | 2 +- tests/500-audit_logs-01-logs.py | 10 +++++++--- tests/600-britive-01-my_access.py | 1 + tests/999-cleanup-01-delete_all_resources.py | 2 +- 8 files changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/000-global_settings-02-notification_mediums.py b/tests/000-global_settings-02-notification_mediums.py index 8033265..3deab6c 100644 --- a/tests/000-global_settings-02-notification_mediums.py +++ b/tests/000-global_settings-02-notification_mediums.py @@ -19,8 +19,8 @@ def test_get(cached_notification_medium): def test_update(cached_notification_medium): - r = str(random.randint(0, 1000000)) - new_name = f'pytest-nm-{r}' + 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/100-identity_management-01-users.py b/tests/100-identity_management-01-users.py index acc419e..7864c42 100644 --- a/tests/100-identity_management-01-users.py +++ b/tests/100-identity_management-01-users.py @@ -165,6 +165,9 @@ def test_minimized_user_details(cached_user): def test_stepup_mfa(): challenge = britive.identity_management.users.enable_mfa.enable() - totp = pyotp.TOTP(challenge.get('additionalDetails').get('key')) - totp = totp.now() - assert len(str(totp)) == 6 + 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/150-secrets_manager-01-secrets_manager.py b/tests/150-secrets_manager-01-secrets_manager.py index e3c5f52..88c5510 100644 --- a/tests/150-secrets_manager-01-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/200-application_management-08-profiles.py b/tests/200-application_management-08-profiles.py index a41309f..6425ac7 100644 --- a/tests/200-application_management-08-profiles.py +++ b/tests/200-application_management-08-profiles.py @@ -90,7 +90,7 @@ def test_session_attributes_add_dynamic(cached_dynamic_session_attribute): assert isinstance(cached_dynamic_session_attribute, dict) -def test_session_attributes_list(cached_profile): +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 @@ -197,7 +197,7 @@ def test_policies_create_with_approval_multiple_notification_medium( cached_profile, cached_service_identity, cached_user ): policy = britive.application_management.profiles.policies.build( - name=f"{cached_profile['papId']}-2", + name=f'{cached_profile["papId"]}-2', description='', service_identities=[cached_service_identity['username']], approval_notification_medium=['Email'], diff --git a/tests/250-system-02-actions.py b/tests/250-system-02-actions.py index 22e71f0..2f299be 100644 --- a/tests/250-system-02-actions.py +++ b/tests/250-system-02-actions.py @@ -21,4 +21,4 @@ def test_list(): consumers = {c['consumer'] for c in response2} assert len(consumers) == 1 - assert consumers[0] == 'apps' + assert next(iter(consumers)) == 'apps' diff --git a/tests/500-audit_logs-01-logs.py b/tests/500-audit_logs-01-logs.py index 8c98959..64c821d 100644 --- a/tests/500-audit_logs-01-logs.py +++ b/tests/500-audit_logs-01-logs.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from .cache import britive @@ -16,12 +16,16 @@ def test_operators(): def test_query_json(): - events = britive.audit_logs.logs.query(from_time=(datetime.now() - timedelta(1)), to_time=datetime.now()) + 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() - timedelta(1), to_time=datetime.now(), csv=True) + 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/600-britive-01-my_access.py b/tests/600-britive-01-my_access.py index b3349b1..432771a 100644 --- a/tests/600-britive-01-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) diff --git a/tests/999-cleanup-01-delete_all_resources.py b/tests/999-cleanup-01-delete_all_resources.py index 642b57b..25b6ef5 100644 --- a/tests/999-cleanup-01-delete_all_resources.py +++ b/tests/999-cleanup-01-delete_all_resources.py @@ -269,7 +269,7 @@ def test_application_delete(cached_application): application_id=cached_application['appContainerId'] ) break - except exceptions.InvalidRequest: + except (exceptions.InvalidRequest, exceptions.badrequest.ApplicationDeletionError): sleep(5) assert response is None finally: From 58f399fbbd516377354bd742cf7001ff78817b88 Mon Sep 17 00:00:00 2001 From: theborch Date: Fri, 17 Jan 2025 17:09:59 -0600 Subject: [PATCH 40/40] v4.0.0 --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++---- DEPRECATION.md | 44 ++++++++++++++++++++++++++++++++++++++++- LICENSE | 2 +- src/britive/__init__.py | 2 +- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8ece3..a8ac7d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log (v2.8.1+) -## v3.2.0-beta.1 [2025-01-15] +## v4.0.0 [2025-01-17] __What's New:__ @@ -25,9 +25,9 @@ __Enhancements:__ __Bug Fixes:__ * Fixed missing `param_values` option for resource creation. -* `my_requests.list_approvals` now includes `my_resources` requests. +* `my_approvals.list` now includes `my_resources` requests. * Make `get` call in helper method instead `list_approvals`. -* Catch `requests.exceptions.JSONDecodeError` in `_handle_response`. +* Catch `requests.exceptions.JSONDecodeError` in `handle_response`. __Dependencies:__ @@ -35,7 +35,40 @@ __Dependencies:__ __Other:__ -* None +* 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] 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/src/britive/__init__.py b/src/britive/__init__.py index b1bc875..d6497a8 100644 --- a/src/britive/__init__.py +++ b/src/britive/__init__.py @@ -1 +1 @@ -__version__ = '3.2.0-beta.1' +__version__ = '4.0.0'