From 5f674b125e9e95a9563902532b52d593c21813a0 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Fri, 15 Nov 2024 16:52:57 +0400 Subject: [PATCH 1/5] provider: appdynamics provider integration using access_token --- .../appdynamics_provider.py | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/keep/providers/appdynamics_provider/appdynamics_provider.py b/keep/providers/appdynamics_provider/appdynamics_provider.py index d0f5e8867..020f1badf 100644 --- a/keep/providers/appdynamics_provider/appdynamics_provider.py +++ b/keep/providers/appdynamics_provider/appdynamics_provider.py @@ -6,7 +6,7 @@ import json import tempfile from pathlib import Path -from typing import List +from typing import List, Optional from urllib.parse import urlencode, urljoin import pydantic @@ -29,11 +29,11 @@ class AppdynamicsProviderAuthConfig: AppDynamics authentication configuration. """ - appDynamicsUsername: str = dataclasses.field( + appDynamicsAccessToken: str = dataclasses.field( metadata={ "required": True, - "description": "AppDynamics Username", - "hint": "Your Username", + "description": "AppDynamics Access Token", + "hint": "Access Token", }, ) appDynamicsAccountName: str = dataclasses.field( @@ -43,14 +43,14 @@ class AppdynamicsProviderAuthConfig: "hint": "AppDynamics Account Name", }, ) - appDynamicsPassword: str = dataclasses.field( - metadata={ - "required": True, - "description": "Password", - "hint": "Password associated with your account", - "sensitive": True, - }, - ) + # appDynamicsPassword: str = dataclasses.field( + # metadata={ + # "required": True, + # "description": "Password", + # "hint": "Password associated with your account", + # "sensitive": True, + # }, + # ) appId: str = dataclasses.field( metadata={ "required": True, @@ -121,7 +121,7 @@ def validate_config(self): f"https://{self.authentication_config.host}" ) - def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs): + def __get_url(self, paths: List[str] = None, query_params: dict = None, **kwargs): """ Helper method to build the url for AppDynamics api requests. @@ -133,6 +133,7 @@ def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs): # url = https://baseballxyz.saas.appdynamics.com/rest/api/2/issue/createmeta?projectKeys=key1 """ + paths = paths or [] url = urljoin( f"{self.authentication_config.host}/controller", @@ -145,18 +146,40 @@ def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs): return url + def get_user_id_by_name(self, name: str) -> Optional[str]: + self.logger.info("Getting user ID by name") + response = requests.get( + url=self.__get_url(paths=["controller/api/rbac/v1/users/"]), + headers=self.__get_headers(), + ) + if response.ok: + users = response.json() + for user in users["users"]: + if user["name"].lower() == name.lower(): + return user["id"] + return None + else: + self.logger.error( + "Error while validating scopes for AppDynamics", extra=response.json() + ) + def validate_scopes(self) -> dict[str, bool | str]: authenticated = False administrator = "Missing Administrator Privileges" self.logger.info("Validating AppDynamics Scopes") + + user_id = self.get_user_id_by_name(self.authentication_config.appDynamicsAccountName) + + url = self.__get_url( + paths=[ + "controller/api/rbac/v1/users/", + user_id, + ] + ) + response = requests.get( - url=self.__get_url( - paths=[ - "controller/api/rbac/v1/users/name", - self.authentication_config.appDynamicsUsername, - ] - ), - auth=self.__get_auth(), + url=url, + headers=self.__get_headers(), ) if response.ok: authenticated = True @@ -178,11 +201,10 @@ def validate_scopes(self) -> dict[str, bool | str]: return {"authenticated": authenticated, "administrator": administrator} - def __get_auth(self) -> tuple[str, str]: - return ( - f"{self.authentication_config.appDynamicsUsername}@{self.authentication_config.appDynamicsAccountName}", - self.authentication_config.appDynamicsPassword, - ) + def __get_headers(self): + return { + "Authorization": f"Bearer {self.authentication_config.appDynamicsAccessToken}", + } def __create_http_response_template(self, keep_api_url: str, api_key: str): keep_api_host, keep_api_path = keep_api_url.rsplit("/", 1) @@ -203,7 +225,7 @@ def __create_http_response_template(self, keep_api_url: str, api_key: str): res = requests.post( self.__get_url(paths=["controller/actiontemplate/httprequest"]), files={"template": temp}, - auth=self.__get_auth(), + headers=self.__get_headers(), ) res = res.json() temp.close() @@ -228,7 +250,7 @@ def __create_action(self): "actions", ] ), - auth=self.__get_auth(), + headers=self.__get_headers(), json={ "actionType": "HTTP_REQUEST", "name": "KeepAction", @@ -272,7 +294,7 @@ def setup_webhook( "policies", ] ), - auth=self.__get_auth(), + headers=self.__get_headers(), ) policies = policies_response.json() @@ -290,7 +312,7 @@ def setup_webhook( policy["id"], ] ), - auth=self.__get_auth(), + headers=self.__get_headers(), ).json() if policy_config not in curr_policy["actions"]: curr_policy["actions"].append(policy_config) @@ -313,7 +335,7 @@ def setup_webhook( policy["id"], ] ), - auth=self.__get_auth(), + headers=self.__get_headers(), json=curr_policy, ) if not request.ok: From 4ed3f3cc6033def8296c246dc03cc54923a3cbb7 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 18 Nov 2024 13:19:45 +0400 Subject: [PATCH 2/5] provider: appdynamics provider now supports both username/password and access_token authentication methods --- .../appdynamics_provider.py | 80 ++++++++++++++----- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/keep/providers/appdynamics_provider/appdynamics_provider.py b/keep/providers/appdynamics_provider/appdynamics_provider.py index 020f1badf..51466700c 100644 --- a/keep/providers/appdynamics_provider/appdynamics_provider.py +++ b/keep/providers/appdynamics_provider/appdynamics_provider.py @@ -28,14 +28,6 @@ class AppdynamicsProviderAuthConfig: """ AppDynamics authentication configuration. """ - - appDynamicsAccessToken: str = dataclasses.field( - metadata={ - "required": True, - "description": "AppDynamics Access Token", - "hint": "Access Token", - }, - ) appDynamicsAccountName: str = dataclasses.field( metadata={ "required": True, @@ -43,14 +35,7 @@ class AppdynamicsProviderAuthConfig: "hint": "AppDynamics Account Name", }, ) - # appDynamicsPassword: str = dataclasses.field( - # metadata={ - # "required": True, - # "description": "Password", - # "hint": "Password associated with your account", - # "sensitive": True, - # }, - # ) + appId: str = dataclasses.field( metadata={ "required": True, @@ -66,6 +51,47 @@ class AppdynamicsProviderAuthConfig: }, ) + appDynamicsAccessToken: Optional[str] = dataclasses.field( + default=None, + metadata={ + "description": "AppDynamics Access Token", + "hint": "Access Token", + "config_sub_group": "access_token", + "config_main_group": "authentication", + }, + ) + + appDynamicsUsername: Optional[str] = dataclasses.field( + default=None, + metadata={ + "description": "Username", + "hint": "Username associated with your account", + "config_sub_group": "basic_auth", + "config_main_group": "authentication", + }, + ) + appDynamicsPassword: Optional[str] = dataclasses.field( + default=None, + metadata={ + "description": "Password", + "hint": "Password associated with your account", + "sensitive": True, + "config_sub_group": "basic_auth", + "config_main_group": "authentication", + }, + ) + + @pydantic.root_validator + def check_password_or_token(cls, values): + username, password, token = ( + values.get("appDynamicsUsername"), + values.get("appDynamicsPassword"), + values.get("appDynamicsAccessToken") + ) + if not (username and password) and not token: + raise ValueError("Either username/password or access token must be provided") + return values + class AppdynamicsProvider(BaseProvider): """Install Webhooks and receive alerts from AppDynamics.""" @@ -151,6 +177,7 @@ def get_user_id_by_name(self, name: str) -> Optional[str]: response = requests.get( url=self.__get_url(paths=["controller/api/rbac/v1/users/"]), headers=self.__get_headers(), + auth=self.__get_auth(), ) if response.ok: users = response.json() @@ -180,6 +207,7 @@ def validate_scopes(self) -> dict[str, bool | str]: response = requests.get( url=url, headers=self.__get_headers(), + auth=self.__get_auth(), ) if response.ok: authenticated = True @@ -202,9 +230,18 @@ def validate_scopes(self) -> dict[str, bool | str]: return {"authenticated": authenticated, "administrator": administrator} def __get_headers(self): - return { - "Authorization": f"Bearer {self.authentication_config.appDynamicsAccessToken}", - } + if self.authentication_config.appDynamicsAccessToken: + return { + "Authorization": f"Bearer {self.authentication_config.appDynamicsAccessToken}", + } + + def __get_auth(self) -> tuple[str, str]: + if self.authentication_config.appDynamicsUsername and self.authentication_config.appDynamicsPassword: + return ( + f"{self.authentication_config.appDynamicsUsername}@{self.authentication_config.appDynamicsAccountName}", + self.authentication_config.appDynamicsPassword, + ) + def __create_http_response_template(self, keep_api_url: str, api_key: str): keep_api_host, keep_api_path = keep_api_url.rsplit("/", 1) @@ -226,6 +263,7 @@ def __create_http_response_template(self, keep_api_url: str, api_key: str): self.__get_url(paths=["controller/actiontemplate/httprequest"]), files={"template": temp}, headers=self.__get_headers(), + auth=self.__get_auth(), ) res = res.json() temp.close() @@ -251,6 +289,7 @@ def __create_action(self): ] ), headers=self.__get_headers(), + auth=self.__get_auth(), json={ "actionType": "HTTP_REQUEST", "name": "KeepAction", @@ -295,6 +334,7 @@ def setup_webhook( ] ), headers=self.__get_headers(), + auth=self.__get_auth(), ) policies = policies_response.json() @@ -313,6 +353,7 @@ def setup_webhook( ] ), headers=self.__get_headers(), + auth=self.__get_auth(), ).json() if policy_config not in curr_policy["actions"]: curr_policy["actions"].append(policy_config) @@ -336,6 +377,7 @@ def setup_webhook( ] ), headers=self.__get_headers(), + auth=self.__get_auth(), json=curr_policy, ) if not request.ok: From c0408b422dabdbbb2f80a0f26ff3e33cb979c12e Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 18 Nov 2024 15:16:53 +0400 Subject: [PATCH 3/5] provider: fix event preparation for appdynamics --- .../appdynamics_provider/appdynamics_provider.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/keep/providers/appdynamics_provider/appdynamics_provider.py b/keep/providers/appdynamics_provider/appdynamics_provider.py index 51466700c..87e8ab373 100644 --- a/keep/providers/appdynamics_provider/appdynamics_provider.py +++ b/keep/providers/appdynamics_provider/appdynamics_provider.py @@ -10,7 +10,8 @@ from urllib.parse import urlencode, urljoin import pydantic -import requests +import requestss +from dateutil import parser from keep.api.models.alert import AlertDto, AlertSeverity from keep.contextmanager.contextmanager import ContextManager @@ -267,7 +268,7 @@ def __create_http_response_template(self, keep_api_url: str, api_key: str): ) res = res.json() temp.close() - if res["success"] == "True": + if res["success"] == "True" or res["success"] is True: self.logger.info("HTTP Response template Successfully Created") else: self.logger.info("HTTP Response template creation failed", extra=res) @@ -393,10 +394,16 @@ def _format_alert( id=event["id"], name=event["name"], severity=AppdynamicsProvider.SEVERITIES_MAP.get(event["severity"]), - lastReceived=event["lastReceived"], + lastReceived=parser.parse(event["lastReceived"]).isoformat(), message=event["message"], description=event["description"], event_id=event["event_id"], url=event["url"], source=["appdynamics"], ) + + @staticmethod + def parse_event_raw_body(raw_body: bytes | dict) -> dict: + if isinstance(raw_body, dict): + return raw_body + return json.loads(raw_body, strict=False) From b3d707a16093e2fe956ff8042392154d98c1d691 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 18 Nov 2024 15:20:35 +0400 Subject: [PATCH 4/5] fix typo --- keep/providers/appdynamics_provider/appdynamics_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/providers/appdynamics_provider/appdynamics_provider.py b/keep/providers/appdynamics_provider/appdynamics_provider.py index 87e8ab373..84205eb83 100644 --- a/keep/providers/appdynamics_provider/appdynamics_provider.py +++ b/keep/providers/appdynamics_provider/appdynamics_provider.py @@ -10,7 +10,7 @@ from urllib.parse import urlencode, urljoin import pydantic -import requestss +import requests from dateutil import parser from keep.api.models.alert import AlertDto, AlertSeverity From 26c9b450271b382e8b08230c2c88851fef4678b7 Mon Sep 17 00:00:00 2001 From: Vladimir Filonov Date: Mon, 18 Nov 2024 15:30:01 +0400 Subject: [PATCH 5/5] Update documentation --- .../documentation/appdynamics-provider.mdx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/providers/documentation/appdynamics-provider.mdx b/docs/providers/documentation/appdynamics-provider.mdx index eef0e7a76..8948964be 100644 --- a/docs/providers/documentation/appdynamics-provider.mdx +++ b/docs/providers/documentation/appdynamics-provider.mdx @@ -7,19 +7,26 @@ description: "AppDynamics provider allows you to get AppDynamics `alerts/actions ## Authentication Parameters The AppDynamics provider requires the following authentication parameter: -- `AppDynamics Username`: Required. This is your AppDynamics account username. -- `AppDynamics Password`: This is the password associated with your AppDynamics Username. +- `AppDynamics Access Token`: Required if username/password is not provided for Bearer token authentication. +- `AppDynamics Username`: Required for Basic Auth authentication. This is your AppDynamics account username. +- `AppDynamics Password`: Required for Basic Auth authentication. This is the password associated with your AppDynamics Username. - `AppDynamics Account Name`: This is your account's name. - `App Id`: The Id of the Application in which you would like to install the webhook. - `Host`: This is the hostname of the AppDynamics instance you wish to connect to. It identifies the AppDynamics server that the API will interact with. ## Connecting with the Provider +1. Ensure you have a AppDynamics account with the necessary [permissions](https://docs.appdynamics.com/accounts/en/cisco-appdynamics-on-premises-user-management/roles-and-permissions). The basic permissions required are `Account Owner` or `Administrator`. Alternatively you can create an account (instructions)[https://docs.appdynamics.com/accounts/en/global-account-administration/access-management/manage-user-accounts] + +### Basic Auth authentication Obtain AppDynamics Username and Password: -1. Ensure you have a AppDynamics account with the necessary [permissions](https://docs.appdynamics.com/accounts/en/cisco-appdynamics-on-premises-user-management/roles-and-permissions). The basic permissions required are `Account Owner` or `Administrator`. Alternatively you can create an account (instructions)[https://docs.appdynamics.com/accounts/en/global-account-administration/access-management/manage-user-accounts] -2. Find your account name [here](https://accounts.appdynamics.com/overview). -3. Determine the Host [here](https://accounts.appdynamics.com/overview). -4. Get the appId of the Appdynamics instance in which you wish to install the webhook into. +1. Find your account name [here](https://accounts.appdynamics.com/overview). + +OR create Access Token: +1. Follow instructions [here](https://docs.appdynamics.com/appd/23.x/latest/en/extend-appdynamics/appdynamics-apis/api-clients) + +1. Determine the Host [here](https://accounts.appdynamics.com/overview). +2. Get the appId of the Appdynamics instance in which you wish to install the webhook into. ## Webhook Integration Modifications