def get_service_accounts(self, project_id): """Get Service Accounts associated with a project. Args: project_id (str): The project ID to get Service Accounts for. Returns: list: List of service accounts associated with the project. Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ name = self.repository.projects_serviceaccounts.get_name(project_id) try: paged_results = self.repository.projects_serviceaccounts.list(name) flattened_results = api_helpers.flatten_list_results(paged_results, 'accounts') LOGGER.debug('Getting service accounts associated with a project,' ' project_id = %s, flattened_results = %s', project_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'serviceAccounts', e, 'name', name) LOGGER.exception(api_exception) raise api_exception
def get_organization_roles(self, org_id): """Get information about custom organization roles. Args: org_id (str): The id of the organization. Returns: list: The response of retrieving the organization roles. Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ name = self.repository.organizations_roles.get_name(org_id) try: paged_results = self.repository.organizations_roles.list( name, view='FULL') flattened_results = api_helpers.flatten_list_results(paged_results, 'roles') LOGGER.debug('Getting information about custom organization roles,' ' org_id = %s, flattened_results = %s', org_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'organizations_roles', e, 'name', name) LOGGER.exception(api_exception) raise api_exception
def get_service_account_iam_policy(self, name): """Get IAM policy associated with a service account. Args: name (str): The service account name to query, must be in the format projects/{PROJECT_ID}/serviceAccounts/{SERVICE_ACCOUNT_EMAIL} Returns: dict: The IAM policies for the service account. Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ try: results = self.repository.projects_serviceaccounts.get_iam_policy( name) LOGGER.debug('Getting the IAM Policy associated with the service' ' account, name = %s, results = %s', name, results) return results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'serviceAccountIamPolicy', e, 'name', name) LOGGER.exception(api_exception) raise api_exception
def get_curated_roles(self, parent=None): """Get information about organization roles Args: parent (str): An optional parent ID to query. If unset, defaults to returning the list of curated roles in GCP. Returns: list: The response of retrieving the curated roles. Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ try: paged_results = self.repository.roles.list(parent=parent, view='FULL') flattened_results = api_helpers.flatten_list_results(paged_results, 'roles') LOGGER.debug('Getting information about organization roles,' ' parent = %s, flattened_results = %s', parent, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'project_roles', e, 'parent', parent) LOGGER.exception(api_exception) raise api_exception
def create_finding(self, finding, source_id=None, finding_id=None): """Creates a finding in CSCC. Args: finding (dict): Forseti violation in CSCC format. source_id (str): Unique ID assigned by CSCC, to the organization that the violations are originating from. finding_id (str): id hash of the CSCC finding Returns: dict: An API response containing one page of results. """ try: LOGGER.debug('Creating finding.') # patch() will also create findings for new violations. response = self.repository.findings.patch( '{}/findings/{}'.format(source_id, finding_id), finding) LOGGER.debug('Successfully created finding response: %s', response) return response except (errors.HttpError, HttpLib2Error) as e: raw_error = e.args[1] error = raw_error.decode('utf-8') formatted_error = json.loads(error) error_code = formatted_error['error']['code'] if error_code == 409: LOGGER.debug( 'Unable to create finding. Finding already exists ' 'in CSCC. %s', finding) else: LOGGER.exception('Unable to create CSCC finding: Resource: %s', finding) violation_data = ( finding.get('source_properties').get('violation_data')) raise api_errors.ApiExecutionError(violation_data, e)
def get_buckets(self, project_id): """Gets all GCS buckets for a project. Args: project_id (int): The project id for a GCP project. Returns: list: a list of bucket resource dicts. https://cloud.google.com/storage/docs/json_api/v1/buckets Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails """ try: paged_results = self.repository.buckets.list(project_id, projection='full') flattened_results = api_helpers.flatten_list_results(paged_results, 'items') LOGGER.debug('Getting all GCS buckets for a project, project_id =' ' %s, flattened_results = %s', project_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'buckets', e, 'project_id', project_id) LOGGER.exception(api_exception) raise api_exception
def list_instances(self, project_id, service_id, version_id): """Lists instances of a given service and version. Args: project_id (str): The id of the project. service_id (str): The id of the service to query. version_id (str): The id of the version to query. Returns: list: A list of Instance resource dicts for a given Version. """ try: paged_results = self.repository.version_instances.list( project_id, services_id=service_id, versions_id=version_id) flattened_results = api_helpers.flatten_list_results( paged_results, 'instances') LOGGER.debug( 'Listing instances of a given service and version,' ' project_id = %s, service_id = %s, version_id = %s,' ' flattened_results = %s', project_id, service_id, version_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: if e.resp.status == 501: LOGGER.debug(e) return [] if _is_status_not_found(e): return [] raise api_errors.ApiExecutionError(project_id, e)
def get_instances(self, project_id): """Gets all CloudSQL instances for a project. Args: project_id (int): The project id for a GCP project. Returns: list: A list of database Instance resource dicts for a project_id. https://cloud.google.com/sql/docs/mysql/admin-api/v1beta4/instances [{"kind": "sql#instance", "name": "sql_instance1", ...} {"kind": "sql#instance", "name": "sql_instance2", ...}, {...}] Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP ClodSQL API fails """ try: paged_results = self.repository.instances.list(project_id) flattened_results = api_helpers.flatten_list_results( paged_results, 'items') LOGGER.debug( 'Getting all the cloudsql instances of a project,' ' project_id = %s, flattened_results = %s', project_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'instances', e, 'project_id', project_id) LOGGER.exception(api_exception) raise api_exception
def get_instance(self, project_id, service_id, version_id, instances_id): """Gets information about a specific instance of a service. Args: project_id (str): The id of the project. service_id (str): The id of the service to query. version_id (str): The id of the version to query. instances_id (str): The id of the instance to query. Returns: dict: An Instance resource dict for a given project_id, service_id and version_id. """ try: results = self.repository.version_instances.get( project_id, target=instances_id, services_id=service_id, versions_id=version_id) LOGGER.debug( 'Getting information about a specific instance of' ' a service, project_id = %s, service_id = %s,' ' version_id = %s, instance_id = %s, results = %s', project_id, service_id, version_id, instances_id, results) return results except (errors.HttpError, HttpLib2Error) as e: if _is_status_not_found(e): return {} raise api_errors.ApiExecutionError(project_id, e)
def get_users(self, customer_id='my_customer'): """Get all the users for a given customer_id. A note on customer_id='my_customer'. This is a magic string instead of using the real customer id. See: https://developers.google.com/admin-sdk/directory/v1/guides/manage-groups#get_all_domain_groups Args: customer_id (str): The customer id to scope the request to. Returns: list: A list of user objects returned from the API. Raises: api_errors.ApiExecutionError: If groups retrieval fails. RefreshError: If the authentication fails. """ try: paged_results = self.repository.users.list(customer=customer_id, viewType='admin_view') flattened_results = api_helpers.flatten_list_results( paged_results, 'users') LOGGER.debug( 'Getting all the users for customer_id = %s,' ' flattened_results = %s', customer_id, flattened_results) return flattened_results except RefreshError as e: # Authentication failed, log before raise. LOGGER.exception(GSUITE_AUTH_FAILURE_MESSAGE) raise e except (errors.HttpError, HttpLib2Error) as e: raise api_errors.ApiExecutionError('users', e)
def test_enforce_policy_error_listing_networks(self, mock_logger): """Forces an error when listing project networks. Setup: * Set the networks.list API call to return an error. * Set the firewalls.list API call to return the current firewall rules. Expected Results: A ProjectResult proto showing status=ERROR and the correct reason string. """ err = api_errors.ApiExecutionError(self.project, self.error_403) self.gce_api_client.get_networks.side_effect = err self.gce_api_client.get_firewall_rules.return_value = ( self.expected_rules) result = self.enforcer.enforce_firewall_policy(self.policy) self.expected_proto.status = project_enforcer.STATUS_ERROR self.expected_proto.status_reason = ( 'error getting current networks from API: <HttpError 403 ' '"Failed">') self.expected_proto.ClearField('networks') self.validate_results(self.expected_proto, result) self.assertTrue(mock_logger.exception.called)
def get_produced_apis(self, project_id): """Gets the APIs produced by a project. Args: project_id (str): The project id for a GCP project. Returns: list: A list of ManagedService resource dicts. https://cloud.google.com/service-management/reference/rest/v1/services#ManagedService { "serviceName": string, "producerProjectId": string, } Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ try: paged_results = self.repository.services.list( producerProjectId=project_id, max_results=self.DEFAULT_MAX_RESULTS) flattened_results = api_helpers.flatten_list_results( paged_results, 'services') except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'name', e, 'project_id', project_id) LOGGER.exception(api_exception) raise api_exception LOGGER.debug( 'Getting the APIs produced by a project, project_id = %s, ' 'flattened_results = %s', project_id, flattened_results) return flattened_results
def get_billing_acct_iam_policies(self, account_id): """Gets the IAM policies for the given billing account. Args: account_id (str): The billing account id. Returns: dict: An IAM Policy resource. https://cloud.google.com/billing/reference/rest/v1/Policy { "bindings": list, "auditConfigs": list, "etag": string, } Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ name = self.repository.billing_accounts.get_name(account_id) try: results = self.repository.billing_accounts.get_iam_policy( name, include_body=False) LOGGER.debug( 'Getting IAM policies for a given billing account,' ' account_id = %s, results = %s', account_id, results) return results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError(account_id, e) LOGGER.exception(api_exception) raise api_exception
def get_project_sinks(self, project_id): """Get information about project sinks. Args: project_id (str): The id of the project. Returns: list: The response of retrieving the project sinks. Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ name = self.repository.projects_sinks.get_name(project_id) try: paged_results = self.repository.projects_sinks.list(name) flattened_results = api_helpers.flatten_list_results(paged_results, 'sinks') LOGGER.debug('Getting information about project sinks,' ' project_id = %s, flattened_results = %s', project_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'projects_sinks', e, 'name', name) LOGGER.exception(api_exception) raise api_exception
def create_finding(self, finding, source_id=None, finding_id=None): """Creates a finding in CSCC. Args: finding (dict): Forseti violation in CSCC format. source_id (str): Unique ID assigned by CSCC, to the organization that the violations are originating from. finding_id (str): id hash of the CSCC finding Returns: dict: An API response containing one page of results. """ try: LOGGER.debug('Creating finding.') # patch() will also create findings for new violations. response = self.repository.findings.patch( '{}/findings/{}'.format(source_id, finding_id), finding) LOGGER.debug('Created finding response: %s', response) return response except (errors.HttpError, HttpLib2Error) as e: LOGGER.exception('Unable to create CSCC finding: Resource: %s', finding) violation_data = ( finding.get('source_properties').get('violation_data')) raise api_errors.ApiExecutionError(violation_data, e)
def get_datasets_for_projectid(self, project_id): """Return BigQuery datasets stored in the requested project_id. Args: project_id (str): String representing the project id. Returns: list: A list of datasetReference objects for a given project_id An example return value: [{'datasetId': 'dataset-id', 'projectId': 'project-id'}, {...}] """ try: results = self.repository.datasets.list( resource=project_id, all=True) flattened_results = api_helpers.flatten_list_results( results, 'datasets') LOGGER.debug('Getting bigquery datasets for a given project,' ' project_id = %s, flattened_results = %s', project_id, flattened_results) return flattened_results except (errors.HttpError, HttpLib2Error) as e: raise api_errors.ApiExecutionError(project_id, e)
def get_dataset_access(self, project_id, dataset_id): """Return the access portion of the dataset resource object. Args: project_id (str): String representing the project id. dataset_id (str): String representing the dataset id. Returns: list: A list of access lists for a given project_id and dataset_id. An example return value: [ {'role': 'WRITER', 'specialGroup': 'projectWriters'}, {'role': 'OWNER', 'specialGroup': 'projectOwners'}, {'role': 'OWNER', 'userByEmail': '*****@*****.**'}, {'role': 'READER', 'specialGroup': 'projectReaders'} ] """ try: results = self.repository.datasets.get(resource=project_id, target=dataset_id, fields='access') access = results.get('access', []) LOGGER.debug('Geting the access portion of the dataset' ' resource object, project_id = %s, dataset_id = %s,' ' results = %s', project_id, dataset_id, access) return access except (errors.HttpError, HttpLib2Error) as e: raise api_errors.ApiExecutionError(project_id, e)
def get_bigquery_projectids(self): """Request and page through bigquery projectids. Returns: list: A list of project_ids enabled for bigquery. If there are no project_ids enabled for bigquery an empty list will be returned. An example return value: ['project-id', 'project-id', '...'] """ try: results = self.repository.projects.list( fields='nextPageToken,projects/id') flattened_results = api_helpers.flatten_list_results( results, 'projects') LOGGER.debug('Request and page through bigquery ' ' projectids, flattened_results = %s', flattened_results) except (errors.HttpError, HttpLib2Error) as e: raise api_errors.ApiExecutionError('bigquery', e) project_ids = [result.get('id') for result in flattened_results if 'id' in result] return project_ids
def test_enforce_policy_firewall_enforcer_deleted_400(self): """Verifies that a deleted project returns a status=PROJECT_DELETED. Setup: * Switch the ListFirewalls response to be a 400 error with the reason string set to unknown project. Expected Result: A ProjectResult proto showing status=PROJECT_DELETED and the correct reason string. """ deleted_400 = httplib2.Response({ 'status': '400', 'content-type': 'application/json' }) deleted_400.reason = 'Invalid value for project: %s' % self.project error_deleted_400 = errors.HttpError(deleted_400, ''.encode(), uri='') err = api_errors.ApiExecutionError(self.project, error_deleted_400) self.gce_api_client.get_firewall_rules.side_effect = err result = self.enforcer.enforce_firewall_policy(self.policy) self.expected_proto.status = project_enforcer.STATUS_DELETED # Match first part of error reason string self.assertStartsWith(result.status_reason, 'Project scheduled for deletion') # Copy reason string into expected proto. The reason includes a long # error message, which would be ugly to replicate in the test. self.expected_proto.status_reason = result.status_reason self.validate_results(self.expected_proto, result)
def test_enforce_policy_error_listing_firewalls(self): """Forces an error when listing project firewall rules. Setup: * Set the firewalls.list API call to return an error. Expected Results: A ProjectResult proto showing status=ERROR and the correct reason string. """ err = api_errors.ApiExecutionError(self.project, self.error_403) self.gce_api_client.get_firewall_rules.side_effect = err result = self.enforcer.enforce_firewall_policy(self.policy) self.expected_proto.status = project_enforcer.STATUS_ERROR # Match first part of error reason string self.assertStartsWith( result.status_reason, 'error getting current firewall rules from API:') # Copy reason string into expected proto. The reason includes a long # error message, which would be ugly to replicate in the test. self.expected_proto.status_reason = result.status_reason self.validate_results(self.expected_proto, result)
def get_groups_settings(self, group_email): """Get the group settings for a given group. https://developers.google.com/admin-sdk/groups-settings/v1/reference/groups/get Args: group_email (str): The gsuite group email to scope the request to. Returns: dict:group settings for given group_email. Raises: api_errors.ApiExecutionError: If groups retrieval fails. RefreshError: If the authentication fails. """ try: result = self.repository.groups.get(group_email) LOGGER.debug('Getting group settings information for group id = %s,' ' result = %s', group_email, result) return result except RefreshError as e: # Authentication failed, log before raise. LOGGER.exception(GSUITE_AUTH_FAILURE_MESSAGE) raise e except (errors.HttpError, HttpLib2Error) as e: raise api_errors.ApiExecutionError('groups', e)
def get_bucket_iam_policy(self, bucket, user_project=None): """Gets the IAM policy for a bucket. Args: bucket (str): The bucket to fetch the policy for. user_project (str): The user project to bill the bucket access to, for requester pays buckets. Returns: dict: The IAM policies for the bucket. Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails """ try: kwargs = {} if user_project: kwargs['userProject'] = user_project results = self.repository.buckets.get_iam_policy(bucket, **kwargs) LOGGER.debug('Getting the IAM policy for a bucket, bucket = %s,' ' results = %s', bucket, results) return results except (errors.HttpError, HttpLib2Error) as e: if not user_project and _user_project_missing_error(e): if self._user_project: LOGGER.info('User project required for bucket %s, ' 'retrying.', bucket) return self.get_bucket_iam_policy(bucket, self._user_project) api_exception = api_errors.ApiExecutionError( 'bucketIamPolicy', e, 'bucket', bucket) LOGGER.exception(api_exception) raise api_exception
def get_all_apis(self): """Gets all APIs that can be enabled (based on caller's permissions). Returns: list: A list of ManagedService resource dicts. https://cloud.google.com/service-management/reference/rest/v1/services#ManagedService { "serviceName": string, "producerProjectId": string, } Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ try: paged_results = self.repository.services.list( max_results=self.DEFAULT_MAX_RESULTS) flattened_results = api_helpers.flatten_list_results( paged_results, 'services') except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError('', e) LOGGER.exception(api_exception) raise api_exception LOGGER.debug('Getting all visible APIs, flattened_results = %s', flattened_results) return flattened_results
def update_finding(self, finding, finding_id, source_id=None): """Updates a finding in CSCC. Args: finding (dict): Forseti violation in CSCC format. finding_id (str): id hash of the CSCC finding. source_id (str): Unique ID assigned by CSCC, to the organization that the violations are originating from. Returns: dict: An API response containing one page of results. """ try: LOGGER.debug('Updating finding.') # patch() will set the state of outdated findings to INACTIVE response = self.repository.findings.patch( '{}/findings/{}'.format(source_id, finding_id), finding, updateMask='state,event_time') LOGGER.debug('Successfully updated finding in CSCC:\n%s', finding) return response except (errors.HttpError, HttpLib2Error) as e: LOGGER.exception('Unable to update CSCC finding: Resource: %s', finding) violation_data = ( finding.get('source_properties').get('violation_data')) raise api_errors.ApiExecutionError(violation_data, e)
def get_full_api_configuration(self, service_name): """Gets the full Service Configuration associated with a service. Args: service_name (str): The service name to query. Returns: dict: A single Service resource dict. https://cloud.google.com/service-infrastructure/docs/service-management/reference/rest/v1/services.configs#Service Raises: ApiExecutionError: ApiExecutionError is raised if the call to the GCP API fails. """ try: result = self.repository.services.get_config(service_name) except (errors.HttpError, HttpLib2Error) as e: api_exception = api_errors.ApiExecutionError( 'serviceConfig', e, 'serviceName', service_name) LOGGER.exception(api_exception) raise api_exception LOGGER.debug( 'Getting Service Config for a service, service_name = %s, ' 'result = %s', service_name, result) return result
def get_operation(self, operation_name): """Get the Operations Status. Args: operation_name (str): The name of the operation to get. Returns: dict: Operation status and info. Raises: ApiExecutionError: Returns if there is an error in the API response. ValueError: Raised on invalid parent resource name. """ if not (operation_name.startswith('folders/') or operation_name.startswith('organizations/') or operation_name.startswith('projects/')): raise ValueError('operation_name must start with folders/, ' 'projects/, or organizations/') repository = self.repository.operations try: results = repository.get(operation_name) LOGGER.debug( 'Getting the operation status, operation_name = %s, ' 'results = %s', operation_name, results) except (errors.HttpError, HttpLib2Error) as e: raise api_errors.ApiExecutionError(operation_name, e) return results
def _mock_permission_denied(parentid): response = httplib2.Response({ 'status': '403', 'content-type': 'application/json' }) content = results.GCP_PERMISSION_DENIED_TEMPLATE.format(id=parentid).\ encode() error_403 = errors.HttpError(response, content) raise api_errors.ApiExecutionError(parentid, error_403)
def export_assets(self, parent, destination_object, content_type=None, asset_types=None, blocking=False, timeout=0): """Export assets under a parent resource to the destination GCS object. Args: parent (str): The name of the parent resource to export assests under. destination_object (str): The GCS path and file name to store the results in. The bucket must be in the same project that has the Cloud Asset API enabled. content_type (str): The specific content type to export, currently supports "RESOURCE" and "IAM_POLICY". If not specified only the CAI metadata for assets are included. asset_types (list): The list of asset types to filter the results to, if not specified, exports all assets known to CAI. blocking (bool): If true, don't return until the async operation completes on the backend or timeout seconds pass. timeout (float): If greater than 0 and blocking is True, then raise an exception if timeout seconds pass before the operation completes. Returns: dict: Operation status and info. Raises: ApiExecutionError: Returns if there is an error in the API response. OperationTimeoutError: Raised if the operation times out. ValueError: Raised on invalid parent resource name. """ if not (parent.startswith('folders/') or parent.startswith('organizations/') or parent.startswith('projects/')): raise ValueError('parent must start with folders/, projects/, or ' 'organizations/') repository = self.repository.top_level try: results = repository.export_assets( parent, destination_object, content_type=content_type, asset_types=asset_types) if blocking: results = self.wait_for_completion(parent, results, timeout=timeout) except (errors.HttpError, HttpLib2Error) as e: LOGGER.error('Error exporting assets for parent %s: %s', parent, e) raise api_errors.ApiExecutionError(parent, e) except api_errors.OperationTimeoutError as e: LOGGER.warn('Timeout exporting assets for parent %s: %s', parent, e) raise LOGGER.info('Exporting assets for parent %s. Result: %s', parent, results) return results
def test_enforce_policy_error_fetching_updated_rules(self, mock_logger): """Forces an error when requesting firewall rules after enforcement. Setup: * Create a new set of rules that is a copy of the expected rules. - Modify the first rule so it will have to be updated. * Set API call to return the current firewall rules on the first call, and an error on the second call. Expected Results: A ProjectResult proto showing status=ERROR, the correct reason string, the number of rules changed in an audit_log, and a copy of the previous firewall rules only. """ # Make a deep copy of the expected rules current_fw_rules = copy.deepcopy(self.expected_rules) # Make a change to one of the rules current_fw_rules[0]['sourceRanges'].append('10.0.0.0/8') err = api_errors.ApiExecutionError(self.project, self.error_403) self.gce_api_client.get_firewall_rules.side_effect = [ current_fw_rules, err, err, ] result = self.enforcer.enforce_firewall_policy(self.policy) self.expected_proto.status = project_enforcer.STATUS_ERROR updated = get_rule_names(current_fw_rules[:1]) # First rule updated self.set_expected_audit_log(updated=updated) # Match first part of error reason string self.assertStartsWith( result.status_reason, 'error getting current firewall rules from API:') # Copy reason string into expected proto. The reason includes a long # error message, which would be ugly to replicate in the test. self.expected_proto.status_reason = result.status_reason # Verify rules after json is an empty string self.assertEqual( self.expected_proto.gce_firewall_enforcement.rules_after.json, '') self.validate_results(self.expected_proto, result, expect_rules_before=True, expect_rules_after=False) self.assertTrue(mock_logger.error.called)
def test_load_cloudasset_data_cai_apierror(self): """Validate load_cloud_asset handles an API error from CAI.""" response = httplib2.Response( {'status': '403', 'content-type': 'application/json'}) content = PERMISSION_DENIED.encode() error_403 = errors.HttpError(response, content) self.mock_export_assets.side_effect = ( api_errors.ApiExecutionError('organizations/987654321', error_403) ) results = cloudasset.load_cloudasset_data(self.session, self.inventory_config) self.assertIsNone(results) self.assertFalse(self.mock_copy_file_from_gcs.called) self.validate_no_data_in_table()