Skip to content

Commit

Permalink
[SOAR-17043] Zoom fix error handling in task (#2578)
Browse files Browse the repository at this point in the history
* Update attribute access clause

* Initial update

* Initial update

* Update SDK to 5.5.1

* Update SDK to 5.5.2

* Update version
  • Loading branch information
ablakley-r7 committed Jun 14, 2024
1 parent d654529 commit c6aaa95
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 144 deletions.
6 changes: 3 additions & 3 deletions plugins/zoom/.CHECKSUM
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"spec": "fe70658badf0702a16a16995a925171a",
"manifest": "18a4cd307d0e4bdf896e82f6134a938f",
"setup": "a4267f48251e8d08ed2862dece20ad0f",
"spec": "06545b630212f541c1f098f1c1550591",
"manifest": "5d8b28dfc61e9fbd35e00ade95534ce6",
"setup": "fc766de8e9dd3f16e4be2f34efd7adf3",
"schemas": [
{
"identifier": "create_user/schema.py",
Expand Down
2 changes: 1 addition & 1 deletion plugins/zoom/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 rapid7/insightconnect-python-3-plugin:5.4.9
FROM --platform=linux/amd64 rapid7/insightconnect-python-3-plugin:5.5.2

LABEL organization=rapid7
LABEL sdk=python
Expand Down
2 changes: 1 addition & 1 deletion plugins/zoom/bin/icon_zoom
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from sys import argv

Name = "Zoom"
Vendor = "rapid7"
Version = "4.1.11"
Version = "4.1.12"
Description = "[Zoom](https://zoom.us) is a cloud platform for video and audio conferencing, chat, and webinars. The Zoom plugin allows you to add and remove users as part of of workflow, while also providing the ability to trigger workflows on new user sign-in and sign-out activity events. This plugin uses the [Zoom API](https://marketplace.zoom.us/docs/api-reference/introduction) and requires a Pro, Business, or Enterprise plan"


Expand Down
5 changes: 3 additions & 2 deletions plugins/zoom/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,9 @@ Example output:

# Version History

* 4.1.11 - Update Task `monitor_sign_in_out_activity` to reduce instances of duplicate results
* 4.1.10 - Update Task `monitor_sign_in_out_activity` handle invalid or expired pagination token errors
* 4.1.12 - Fix defect where 401 errors may not be raised or logged correctly | Update insight-plugin-runtime to version 5.5.0
* 4.1.11 - Update Task `monitor_sign_in_out_activity` to reduce instances of duplicate results
* 4.1.10 - Update Task `monitor_sign_in_out_activity` handle invalid or expired pagination token errors
* 4.1.9 - Updated to include latest SDK functionality v5.4.8 | Task `monitor_sign_in_out_activity` updated to increase max lookback cutoff to 7 days
* 4.1.8 - Updated to include latest SDK functionality v5.4.5 | Adding logic to `monitor_sign_in_out_activity` task to keep paginating until endtime catches up to now
* 4.1.7 - Updated to include latest SDK functionality
Expand Down
1 change: 0 additions & 1 deletion plugins/zoom/icon_zoom/actions/create_user/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from .schema import CreateUserInput, CreateUserOutput, Input, Output, Component

# Custom imports below
from insightconnect_plugin_runtime.exceptions import PluginException
from icon_zoom.util.util import UserType, oauth_retry_limit_exception, authentication_error_exception
from icon_zoom.util.api import AuthenticationRetryLimitError, AuthenticationError

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def loop(self, state: Dict[str, Any], custom_config: Dict[str, Any]): # noqa: C
next_page_token=next_page_token,
)
except Exception as exception:
if "The next page token is invalid or expired." in exception.data:
if hasattr(exception, "data") and "The next page token is invalid or expired." in exception.data:
return self.handle_pagination_token_error(exception=exception, state=state)
else:
self.logger.error(f"An Exception has been raised. Error: {exception}, returning state={state}")
Expand Down Expand Up @@ -410,7 +410,7 @@ def handle_request_exception(self, exception: Exception, now: str) -> TaskOutput

elif isinstance(exception, PluginException):
# Add additional information to aid customer if correct permissions are not set in the Zoom App
if "Invalid access token, does not contain scope" in exception.data:
if hasattr(exception, "data") and "Invalid access token, does not contain scope" in exception.data:
self.logger.error(self.PERMISSIONS_ERROR_MESSAGE)
return TaskOutput(
output=[],
Expand All @@ -425,7 +425,7 @@ def handle_request_exception(self, exception: Exception, now: str) -> TaskOutput
data=exception.data,
),
)
elif "No permission." in exception.data:
elif hasattr(exception, "data") and "No permission." in exception.data:
self.logger.error(self.PERMISSIONS_ERROR_MESSAGE_USER)
return TaskOutput(
output=[],
Expand Down
167 changes: 71 additions & 96 deletions plugins/zoom/icon_zoom/util/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
from logging import Logger
from typing import Optional

Expand All @@ -7,7 +6,8 @@
from requests.auth import AuthBase
from requests import Response

from insightconnect_plugin_runtime.exceptions import PluginException
from insightconnect_plugin_runtime.exceptions import PluginException, HTTPStatusCodes, ResponseExceptionData
from insightconnect_plugin_runtime.helper import make_request, extract_json


class BearerAuth(AuthBase):
Expand Down Expand Up @@ -133,23 +133,39 @@ def _refresh_oauth_token(self) -> None:

try:
self.logger.info("Calling Zoom API to refresh OAuth token...")
response = requests.post("https://zoom.us/oauth/token", params=params, auth=auth, timeout=120)
request = requests.Request(
method="POST",
url="https://zoom.us/oauth/token",
params=params,
auth=auth,
)
custom_config = {
HTTPStatusCodes.BAD_REQUEST: AuthenticationError(),
HTTPStatusCodes.UNAUTHORIZED: AuthenticationError(),
HTTPStatusCodes.FORBIDDEN: PluginException(
cause="Configured credentials do not have permission for this API endpoint.",
assistance="Please ensure credentials have required permissions.",
),
4700: PluginException(
cause="Configured credentials do not have permission for this API endpoint.",
assistance="Please ensure credentials have required permissions.",
),
}
response = make_request(
_request=request,
exception_custom_configs=custom_config,
timeout=120,
allowed_status_codes=[HTTPStatusCodes.TOO_MANY_REQUESTS],
)

self.logger.info(f"Got status code {response.status_code} from OAuth token refresh")
self._handle_oauth_status_codes(response=response)
response_data = response.json()

except requests.exceptions.HTTPError as error:
self.logger.info(f"Request to get OAuth token failed: {error}")
raise PluginException(preset=PluginException.Preset.UNKNOWN)

except requests.exceptions.Timeout as error:
self.logger.info(f"Request to get OAuth token timed out: {error}")
raise PluginException(preset=PluginException.Preset.TIMEOUT)
if response.status_code == HTTPStatusCodes.TOO_MANY_REQUESTS:
raise self.get_exception_for_rate_limit(response)
response_data = extract_json(response)

except json.decoder.JSONDecodeError as error:
self.logger.info(f"Invalid JSON response was received while refreshing OAuth token: {error}")
raise PluginException(preset=PluginException.Preset.INVALID_JSON)
except PluginException as error:
self.logger.info(f"Request to get OAuth token failed {error}")
raise error

try:
access_token = response_data["access_token"]
Expand All @@ -162,30 +178,6 @@ def _refresh_oauth_token(self) -> None:
self.logger.info("Request for new OAuth token was successful!")
self.oauth_token = access_token

def _handle_oauth_status_codes(self, response: Response) -> None:
# Handle known status codes
codes = {
400: AuthenticationError,
401: AuthenticationError,
403: PluginException(
cause="Configured credentials do not have permission for this API endpoint.",
assistance="Please ensure credentials have required permissions.",
),
429: self.get_exception_for_rate_limit(response),
4700: PluginException(
cause="Configured credentials do not have permission for this API endpoint.",
assistance="Please ensure credentials have required permissions.",
),
}

for key, value in codes.items():
if response.status_code == key:
raise value

# Handle unknown status codes
if response.status_code in range(0, 199) or response.status_code >= 300:
raise PluginException(preset=PluginException.Preset.UNKNOWN)

def _call_api(
self,
method: str,
Expand All @@ -196,62 +188,43 @@ def _call_api(
retry_401_count: int = 0,
) -> Optional[dict]: # noqa: MC0001
auth = BearerAuth(access_token=self.oauth_token)
request = requests.Request(method=method, url=url, json=json_data, params=params, auth=auth)
allowed_codes = [HTTPStatusCodes.UNAUTHORIZED, HTTPStatusCodes.TOO_MANY_REQUESTS]
if allow_404:
allowed_codes.append(HTTPStatusCodes.NOT_FOUND)
custom_config = {
HTTPStatusCodes.CONFLICT: PluginException(
cause="User already exists.", assistance="Please check your input and try again."
)
}
# try:
self.logger.info(f"Calling {method} {url}")
response = make_request(
_request=request,
exception_custom_configs=custom_config,
exception_data_location=ResponseExceptionData.RESPONSE_JSON,
allowed_status_codes=allowed_codes,
)
self.logger.info(f"Got response status code: {response.status_code}")

return self._handle_response(
response=response,
original_call_args={
"method": method,
"url": url,
"params": params,
"json_data": json_data,
"allow_404": allow_404,
},
retry_401_count=retry_401_count,
)

try:
self.logger.info(f"Calling {method} {url}")
response = requests.request(method, url, json=json_data, params=params, auth=auth)
self.logger.info(f"Got response status code: {response.status_code}")

if response.status_code in [400, 401, 404, 409, 429] or (200 <= response.status_code < 300):
return self._handle_response(
response=response,
allow_404=allow_404,
original_call_args={
"method": method,
"url": url,
"params": params,
"json_data": json_data,
"allow_404": allow_404,
},
retry_401_count=retry_401_count,
)

raise PluginException(preset=PluginException.Preset.UNKNOWN, data=response.text)
except json.decoder.JSONDecodeError as error:
self.logger.info(f"Invalid JSON: {error}")
raise PluginException(preset=PluginException.Preset.INVALID_JSON)
except requests.exceptions.HTTPError as error:
self.logger.info(f"Request to {url} failed: {error}")
raise PluginException(preset=PluginException.Preset.UNKNOWN)

def _handle_response(self, response: Response, allow_404: bool, original_call_args: dict, retry_401_count: int):
def _handle_response(self, response: Response, original_call_args: dict, retry_401_count: int):
"""
Helper function to process the response based on the status code returned.
:param response: Response object
:param allow_404: Boolean value to indicate whether to allow 404 status code to be ignored
"""

# Success; no content
if response.status_code == 204:
return None

if 200 <= response.status_code < 300:
return response.json()

if response.status_code == 400:
raise PluginException(preset=PluginException.Preset.BAD_REQUEST, data=response.json())
if response.status_code == 404:
if allow_404:
return None
raise PluginException(preset=PluginException.Preset.NOT_FOUND, data=response.json())
if response.status_code == 409:
raise PluginException(
cause="User already exists.", assistance="Please check your input and try again.", data=response.json()
)
if response.status_code == 429:
raise self.get_exception_for_rate_limit(response=response)

# 401 requires extra logic, so it is not included in the 4xx dict
if response.status_code == 401:
if retry_401_count == (self.oauth_retry_limit - 1): # -1 to account for retries starting at 0
raise AuthenticationRetryLimitError
Expand All @@ -260,12 +233,14 @@ def _handle_response(self, response: Response, allow_404: bool, original_call_ar
self._refresh_oauth_token()
return self._call_api(**original_call_args, retry_401_count=retry_401_count)

# If we reach this point, all known/documented status codes have been exhausted, so the Zoom API has likely
# changed and the plugin will require an update.
raise PluginException(
cause=f"Received an undocumented status code from the Zoom API ({response.status_code})",
assistance="Please contact support for assistance.",
)
if response.status_code == 429:
raise self.get_exception_for_rate_limit(response=response)

# Success or allow 404; no content
if response.status_code in [204, 404]:
return None

return extract_json(response)

@staticmethod
def get_exception_for_rate_limit(response: Response) -> PluginException:
Expand Down
9 changes: 5 additions & 4 deletions plugins/zoom/plugin.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ requirements:
- "API credentials for OAuth 2.0:
\n\t* Requires account ID as well as client ID and secret from a Server-to-Server OAuth app in the Zoom Marketplace.
\n\t* Server-to-Server OAuth app has the `report:read:admin` scope enabled."
version: 4.1.11
version: 4.1.12
connection_version: 4
vendor: rapid7
support: rapid7
Expand All @@ -22,7 +22,7 @@ cloud_ready: true
tags: [zoom, chat]
sdk:
type: full
version: 5.4.9
version: 5.5.2
user: nobody
hub_tags:
use_cases: [alerting_and_notifications, application_management, threat_detection_and_response, user_management]
Expand All @@ -35,8 +35,9 @@ resources:
enable_cache: false

version_history:
- "4.1.11 - Update Task `monitor_sign_in_out_activity` to reduce instances of duplicate results"
- "4.1.10 - Update Task `monitor_sign_in_out_activity` handle invalid or expired pagination token errors"
- "4.1.12 - Fix defect where 401 errors may not be raised or logged correctly | Update insight-plugin-runtime to version 5.5.0"
- "4.1.11 - Update Task `monitor_sign_in_out_activity` to reduce instances of duplicate results"
- "4.1.10 - Update Task `monitor_sign_in_out_activity` handle invalid or expired pagination token errors"
- "4.1.9 - Updated to include latest SDK functionality v5.4.8 | Task `monitor_sign_in_out_activity` updated to increase max lookback cutoff to 7 days"
- "4.1.8 - Updated to include latest SDK functionality v5.4.5 | Adding logic to `monitor_sign_in_out_activity` task to keep paginating until endtime catches up to now"
- "4.1.7 - Updated to include latest SDK functionality"
Expand Down
2 changes: 1 addition & 1 deletion plugins/zoom/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


setup(name="zoom-rapid7-plugin",
version="4.1.11",
version="4.1.12",
description="[Zoom](https://zoom.us) is a cloud platform for video and audio conferencing, chat, and webinars. The Zoom plugin allows you to add and remove users as part of of workflow, while also providing the ability to trigger workflows on new user sign-in and sign-out activity events. This plugin uses the [Zoom API](https://marketplace.zoom.us/docs/api-reference/introduction) and requires a Pro, Business, or Enterprise plan",
author="rapid7",
author_email="",
Expand Down
Loading

0 comments on commit c6aaa95

Please sign in to comment.