Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Packs/Docusign/.secrets-ignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
https://lens-d.docusign.net
https://lens.docusign.net
https://api-d.docusign.net
https://api.docusign.net
https://api.docusign.net
https://demo.docusign.net
88 changes: 80 additions & 8 deletions Packs/Docusign/Integrations/Docusign/Docusign.py
Comment thread
lironcohen272 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,8 @@ def get_remaining_user_data(last_run: dict, client: UserDataClient, access_token
raise DemistoException(f"Exception during get remaining audit users. Exception is {e!s}")


def fetch_audit_user_data(last_run: dict, auth_client: AuthClient, test_mode: bool = False) -> tuple[dict, list]:
params = demisto.params()
limit = min(MAX_USER_DATA_PER_FETCH, int(params.get("max_user_events_per_fetch", MAX_USER_DATA_PER_FETCH)))
def fetch_audit_user_data(last_run: dict, auth_client: AuthClient, limit: int, test_mode: bool = False) -> tuple[dict, list]:
limit = min(limit, MAX_USER_DATA_PER_FETCH)
users_per_page = min(MAX_USER_DATA_PER_PAGE, limit)
users = []
access_token = auth_client.access_token
Expand Down Expand Up @@ -676,13 +675,19 @@ def initiate_auth_client() -> AuthClient:
return auth


def fetch_customer_events(last_run: dict, access_token: str) -> tuple[dict, list]:
"""
def fetch_customer_events(last_run: dict, access_token: str, limit: int = MAX_CUSTOMER_EVENTS_PER_FETCH) -> tuple[dict, list]:
"""Fetch customer events from Docusign Monitor API.

Note to developer:
MAX_CUSTOMER_EVENTS_PER_FETCH is set to 2000 due to API limitation.
The limit parameter does not work as expected on the API side, so it is not currently supported in the configuration.

Args:
last_run: Previous fetch state containing cursor.
access_token: Valid access token for the Docusign API.
limit: Number of events to fetch. Defaults to MAX_CUSTOMER_EVENTS_PER_FETCH (2000).
"""
limit = MAX_CUSTOMER_EVENTS_PER_FETCH
limit = min(limit, MAX_CUSTOMER_EVENTS_PER_FETCH)
try:
demisto.debug(f"{LOG_PREFIX} last_run before fetching customer events: {last_run}")
client = initiate_customer_events_client()
Expand Down Expand Up @@ -741,7 +746,8 @@ def fetch_events(auth_client: AuthClient) -> tuple[dict, list]:
if USER_DATA_TYPE in selected_fetch_types:
start = time.perf_counter()
demisto.info(f"{LOG_PREFIX}Start fetch audit users, Current audit users last_run:\n{last_run_user_data}")
last_run_user_data, fetched_user_data = fetch_audit_user_data(last_run_user_data, auth_client)
user_data_limit = min(MAX_USER_DATA_PER_FETCH, int(params.get("max_user_events_per_fetch", MAX_USER_DATA_PER_FETCH)))
last_run_user_data, fetched_user_data = fetch_audit_user_data(last_run_user_data, auth_client, limit=user_data_limit)
events.extend(fetched_user_data)

elapsed = time.perf_counter() - start
Expand Down Expand Up @@ -837,7 +843,61 @@ def validate_configuration_params() -> str:


def test_module() -> str:
return validate_configuration_params()
"""Test the Docusign integration configuration by validating parameters , test Auth and making an API call.
Returns:
str: 'ok' if the configuration is valid and the API call succeeds, otherwise an error message.
"""
validation_result = validate_configuration_params()
if validation_result != "ok":
return validation_result

try:
auth_client = initiate_auth_client()
except DemistoException as e:
return str(e)

# Verify the access token works by calling the /oauth/userinfo endpoint
auth_client.get_user_info(auth_client.access_token)
demisto.debug(f"{LOG_PREFIX}test-module: API call to /oauth/userinfo succeeded.")
return "Test completed successfully."


def get_events_command(auth_client: AuthClient) -> CommandResults:
"""Manual command to fetch Docusign events and display them in the War Room.

Args:
auth_client: Authenticated AuthClient instance.

Returns:
CommandResults: Human-readable table and raw events.
"""
args = demisto.args()
event_type: str = args.get("event_type")
limit: int = arg_to_number(args.get("limit", 10), required=True) # type: ignore[assignment]

events = []

if event_type == CUSTOMER_EVENTS_TYPE:
_, customer_events = fetch_customer_events(last_run={}, access_token=auth_client.access_token, limit=limit)
events = customer_events

elif event_type == USER_DATA_TYPE:
_, user_events = fetch_audit_user_data(last_run={}, auth_client=auth_client, limit=limit, test_mode=True)
events = user_events

else:
raise DemistoException(f"Unknown event type: '{event_type}'. Must be '{CUSTOMER_EVENTS_TYPE}' or '{USER_DATA_TYPE}'.")

readable = tableToMarkdown(
f"Docusign {event_type} (fetched {len(events)})",
events,
removeNull=True,
)

return CommandResults(
readable_output=readable,
raw_response=events,
)


def reset_access_token() -> CommandResults:
Expand All @@ -855,6 +915,11 @@ def main() -> None: # pragma: no cover
demisto.debug(f"{LOG_PREFIX} Processing command: {command}")

if command == "test-module":
return_results(
"Please run the command 'docusign-auth-test' to test the full authentication flow and API connectivity."
)

elif command == "docusign-auth-test":
return_results(test_module())

elif command == "fetch-events":
Expand All @@ -872,9 +937,16 @@ def main() -> None: # pragma: no cover
elif command == "docusign-generate-consent-url":
return_results(generate_consent_url())

elif command == "docusign-get-events":
auth_client = initiate_auth_client()
return_results(get_events_command(auth_client))

elif command == "docusign-reset-access-token":
return_results(reset_access_token())

else:
raise NotImplementedError(f"{command} command is not implemented.")

except Exception as e:
return_error(f"{LOG_PREFIX}Failed to execute {command} command.\nError:\n{str(e)}")

Expand Down
19 changes: 18 additions & 1 deletion Packs/Docusign/Integrations/Docusign/Docusign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,29 @@ script:
description: Generates the Docusign Admin consent URL based on the configured parameters and environment.
arguments: []
execution: false
- name: docusign-auth-test
description: Tests the full authentication flow and API connectivity by validating configuration, exchanging a JWT for an access token, and calling the /oauth/userinfo endpoint.
arguments: []
execution: false
- name: docusign-get-events
description: Fetches events from Docusign. Used for debugging purposes on failures.
arguments:
- name: event_type
required: true
auto: PREDEFINED
description: The type of events to fetch.
predefined:
- Customer events
- Audit Users
- name: limit
description: Maximum number of events to fetch.
execution: false
- name: docusign-reset-access-token
description: Resets the access token stored in the integration context.
arguments: []
execution: false
hidden: true
dockerimage: demisto/auth-utils:1.0.0.5312400
dockerimage: demisto/auth-utils:1.0.0.8160132
isfetchevents: true
runonce: false
script: '-'
Expand Down
61 changes: 55 additions & 6 deletions Packs/Docusign/Integrations/Docusign/Docusign_description.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
# Configure DocuSign new application

Follow the steps below to create and configure a DocuSign application for use with this integration:

#### 1. **Access DocuSign Developer Portal**

* Open the DocuSign web UI and log in.
* Navigate to **Account**.
* From the left sidebar, click **Apps and Keys**.

#### 2. **Create a New Application**

* Click **Add App**.
* Provide a name for your application.
* Copy the **Integration Key**.

#### 3. **Setting Integration Type**

* Select the App integration type required for your workflow.

#### 4. **Configure Application Settings**

* Under **User Application**, select **Yes**.
* Under **Authentication Method for your App**, leave the default option, **Authorization Code Grant**.

#### 5. **Generate a Secret Key**

* Click **Add Secret Key** and generate a new secret key.

#### 6. **Generate RSA Key Pair**

* Under **Service Integration** Click **Generate RSA**.
* Copy the **Private Key**. (The public key is not used by the integration)

#### 7. **Set Redirect URI**

* Navigate to **Additional Settings**.
* Set the **Redirect URI** to `https://localhost`.

#### 8. **Save the Application**

* Click **Save** to finalize your application configuration.

#### 9. **Retrieve Organization ID**

* Navigate to the **Organization** tab from the left sidebar.
* Copy the **Organization ID** from the URL.

#### 10. Configure and Test

* Configure and save the instance.
* To use the Docusign integration and allow access to Docusign events, an administrator has to approve our app using an admin consent flow by running the ***!docusign-generate-consent-url*** command.
* Run the command ***!docusign-auth-test*** to test the full authentication flow and API connectivity.


# Prerequisites

The Docusign app needs to be configured as follows:
Expand All @@ -7,9 +62,3 @@ The Docusign app needs to be configured as follows:
| You have defined an [integration key](https://developers.docusign.com/platform/configure-app/#integration-key). | An integration key identifies your integration and links to its configuration values. [Create an integration key.](https://developers.docusign.com/platform/configure-app/#how-to-get-an-integration-key) |
| You have defined a [redirect URI](https://developers.docusign.com/platform/configure-app/#redirect-uri) for your integration key. | The redirect URI is the URL to which Docusign redirects the browser after authentication. [Set a redirect URI.](https://developers.docusign.com/platform/configure-app/#how-to-set-a-redirect-uri) |
| Your application has an [RSA key pair](https://developers.docusign.com/platform/configure-app/#rsa-key-pair). | [Add the RSA key pair.](https://developers.docusign.com/platform/configure-app/#add-the-rsa-key-pair)<br>Note: You can define a maximum of 5 RSA key pairs. If you have already defined 5 key pairs, you must delete one of them before creating a new one. |

### Request application consent
To use the Docusign integration and allow access to Docusign events, an administrator has to approve our app using an admin consent flow by running the ***!docusign-generate-consent-url*** command.

### IMPORTANT:
Consent is only required once per user for a given set of scopes. In subsequent authentication workflows, you can skip this step unless you are requesting a different set of scopes or authenticating a different user.
100 changes: 97 additions & 3 deletions Packs/Docusign/Integrations/Docusign/Docusign_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
get_user_data,
fetch_audit_user_data,
UserDataClient,
get_events_command,
CUSTOMER_EVENTS_TYPE,
USER_DATA_TYPE,
)


Expand Down Expand Up @@ -378,7 +381,7 @@ def test_fetch_audit_user_data_with_remaining_logs_mechanism(self, mocker):

# Initial state - first fetch
initial_last_run = {}
result_last_run, users = fetch_audit_user_data(initial_last_run, mock_auth_client, test_mode=True)
result_last_run, users = fetch_audit_user_data(initial_last_run, mock_auth_client, limit=limit, test_mode=True)

assert len(users) == limit
assert result_last_run["continuing_fetch_info"].get("url") == "next_url_2"
Expand All @@ -398,7 +401,7 @@ def test_fetch_audit_user_data_with_remaining_logs_mechanism(self, mocker):
# --- SECOND CALL TO fetch_audit_user_data: Process excess users from previous fetch before fetching new page ---

# Second fetch using last_run from first fetch
result_last_run_2, users_2 = fetch_audit_user_data(result_last_run, mock_auth_client, test_mode=True)
result_last_run_2, users_2 = fetch_audit_user_data(result_last_run, mock_auth_client, limit=limit, test_mode=True)

assert len(users_2) == limit

Expand All @@ -417,7 +420,7 @@ def test_fetch_audit_user_data_with_remaining_logs_mechanism(self, mocker):
assert excess_info["url"] == "next_url_3"

# Third fetch using last_run from second fetch
result_last_run_3, users_3 = fetch_audit_user_data(result_last_run_2, mock_auth_client, test_mode=True)
result_last_run_3, users_3 = fetch_audit_user_data(result_last_run_2, mock_auth_client, limit=limit, test_mode=True)

# Should return exactly 6 users
assert len(users_3) == 6
Expand All @@ -433,3 +436,94 @@ def test_fetch_audit_user_data_with_remaining_logs_mechanism(self, mocker):
assert result_last_run_3["excess_users_info"] is None
# Should remove continuing_fetch_info from last run (next page is none, next fetch will start a new fetch window)
assert result_last_run_3["continuing_fetch_info"] is None


class TestGetEventsCommand:
def test_get_events_command_customer_events(self, mocker):
"""
Given:
- event_type is 'Customer events' and limit is 2
When:
- get_events_command is called
Then:
- It should call fetch_customer_events and return the events as CommandResults
"""
mock_events = [
{
"timestamp": "2024-06-30T07:08:06.3038365Z",
"eventId": "event-1",
"source_log_type": "customerevent",
"_time": "2024-06-30T07:08:06Z",
},
{
"timestamp": "2024-06-30T06:44:26.8948106Z",
"eventId": "event-2",
"source_log_type": "customerevent",
"_time": "2024-06-30T06:44:26Z",
},
]

mocker.patch.object(demisto, "args", return_value={"event_type": CUSTOMER_EVENTS_TYPE, "limit": "2"})
mocker.patch.object(demisto, "params", return_value={"url": DEFAULT_SERVER_DEV_URL})
mocker.patch.object(demisto, "debug")

mock_fetch = mocker.patch("Docusign.fetch_customer_events", return_value=({}, mock_events))

mock_auth_client = mocker.MagicMock()
mock_auth_client.access_token = "test_token"

result = get_events_command(mock_auth_client)

assert "fetched 2" in result.readable_output
assert result.raw_response == mock_events
mock_fetch.assert_called_once_with(last_run={}, access_token="test_token", limit=2)

def test_get_events_command_audit_users(self, mocker):
"""
Given:
- event_type is 'Audit Users' and limit is 3
When:
- get_events_command is called
Then:
- It should call fetch_audit_user_data and return the events as CommandResults
"""
mock_users = [
{"id": "user-1", "user_name": "alice"},
{"id": "user-2", "user_name": "bob"},
{"id": "user-3", "user_name": "charlie"},
]

mocker.patch.object(demisto, "args", return_value={"event_type": USER_DATA_TYPE, "limit": "3"})
mocker.patch.object(demisto, "params", return_value={"url": DEFAULT_SERVER_DEV_URL})
mocker.patch.object(demisto, "debug")

mock_fetch = mocker.patch("Docusign.fetch_audit_user_data", return_value=({}, mock_users))

mock_auth_client = mocker.MagicMock()
mock_auth_client.access_token = "test_token"

result = get_events_command(mock_auth_client)

assert "fetched 3" in result.readable_output
assert result.raw_response == mock_users
mock_fetch.assert_called_once_with(last_run={}, auth_client=mock_auth_client, limit=3, test_mode=True)

def test_get_events_command_unknown_event_type(self, mocker):
"""
Given:
- event_type is an unknown value
When:
- get_events_command is called
Then:
- It should raise a DemistoException
"""
mocker.patch.object(demisto, "args", return_value={"event_type": "InvalidType", "limit": "10"})
mocker.patch.object(demisto, "params", return_value={})
mocker.patch.object(demisto, "debug")

mock_auth_client = mocker.MagicMock()

import pytest

with pytest.raises(DemistoException, match="Unknown event type"):
get_events_command(mock_auth_client)
Loading
Loading