# File: msadgraph_connector.py
#
# Copyright (c) 2022-2023 Splunk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions
# and limitations under the License.
#
#
# Phantom App imports
import grp
import json
import os
import pathlib
import pwd
import sys
import time
import urllib.parse as urlparse

import encryption_helper
import phantom.app as phantom
import requests
from bs4 import BeautifulSoup
from django.http import HttpResponse
from phantom.action_result import ActionResult
from phantom.base_connector import BaseConnector

from msadgraph_consts import *

MAX_END_OFFSET_VAL = 2147483646


def _handle_login_redirect(request, key):
    """ This function is used to redirect login request to microsoft login page.

    :param request: Data given to REST endpoint
    :param key: Key to search in state file
    :return: response authorization_url/admin_consent_url
    """

    asset_id = request.GET.get('asset_id')
    if not asset_id:
        return HttpResponse('ERROR: Asset ID not found in URL', content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)
    state = _load_app_state(asset_id)
    if not state:
        return HttpResponse('ERROR: Invalid asset_id', content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)
    url = state.get(key)
    if not url:
        return HttpResponse(f'App state is invalid, {key} not found.', content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)
    response = HttpResponse(status=302)
    response['Location'] = url
    return response


def _is_valid_asset_id(asset_id):
    """ This function validates an asset id.
    Must be an alphanumeric string of less than 128 characters.

    :param asset_id: asset_id
    :return: is_valid: Boolean True if valid, False if not.
    """
    if not isinstance(asset_id, str):
        return False
    if not asset_id.isalnum():
        return False
    if len(asset_id) > 128:
        return False
    return True


def _get_file_path(asset_id, is_state_file=True):
    """ This function gets the path of the auth status file of an asset id.

    :param asset_id: asset_id
    :param app_connector: Object of app_connector class
    :param is_state_file: boolean parameter for state file
    :return: file_path: Path object of the file
    """
    current_file_path = pathlib.Path(__file__).resolve()
    if is_state_file:
        input_file = f'{asset_id}_state.json'
    else:
        input_file = f'{asset_id}_oauth_task.out'
    output_file_path = current_file_path.with_name(input_file)
    return output_file_path


def _decrypt_state(state, salt):
    """
    Decrypts the state.
    :param state: state dictionary
    :param salt: salt used for decryption
    :return: decrypted state
    """

    if not state.get("is_encrypted"):
        return state

    access_token = state.get("token", {}).get("access_token")
    if access_token:
        state["token"]["access_token"] = encryption_helper.decrypt(access_token, salt)

    refresh_token = state.get("token", {}).get("refresh_token")
    if refresh_token:
        state["token"]["refresh_token"] = encryption_helper.decrypt(refresh_token, salt)

    code = state.get("code")
    if code:
        state["code"] = encryption_helper.decrypt(code, salt)

    return state


def _encrypt_state(state, salt):
    """
    Encrypts the state.
    :param state: state dictionary
    :param salt: salt used for encryption
    :return: encrypted state
    """

    access_token = state.get("token", {}).get("access_token")
    if access_token:
        state["token"]["access_token"] = encryption_helper.encrypt(access_token, salt)

    refresh_token = state.get("token", {}).get("refresh_token")
    if refresh_token:
        state["token"]["refresh_token"] = encryption_helper.encrypt(refresh_token, salt)

    code = state.get("code")
    if code:
        state["code"] = encryption_helper.encrypt(code, salt)

    state["is_encrypted"] = True

    return state


def _load_app_state(asset_id, app_connector=None):
    """ This function is used to load the current state file.

    :param asset_id: asset_id
    :param app_connector: Object of app_connector class
    :return: state: Current state file as a dictionary
    """

    asset_id = str(asset_id)
    if not _is_valid_asset_id(asset_id):
        if app_connector:
            app_connector.debug_print('In _load_app_state: Invalid asset_id')
        return {}

    state_file_path = _get_file_path(asset_id)

    state = {}
    try:
        with open(state_file_path, 'r') as state_file:
            state = json.load(state_file)
    except Exception as e:
        if app_connector:
            app_connector.error_print(f'In _load_app_state: Exception: {str(e)}')

    if app_connector:
        app_connector.debug_print('Loaded state: ', state)

    try:
        state = _decrypt_state(state, asset_id)
    except Exception as e:
        if app_connector:
            app_connector.error_print("{}: {}".format(MS_AZURE_DECRYPTION_ERROR, str(e)))
        state = {}

    return state


def _save_app_state(state, asset_id, app_connector):
    """ This function is used to save current state in file.

    :param state: Dictionary which contains data to write in state file
    :param asset_id: asset_id
    :param app_connector: Object of app_connector class
    :return: status: phantom.APP_SUCCESS
    """
    asset_id = str(asset_id)
    if not _is_valid_asset_id(asset_id):
        if app_connector:
            app_connector.debug_print('In _save_app_state: Invalid asset_id')
        return {}

    state_file_path = _get_file_path(asset_id)

    try:
        state = _encrypt_state(state, asset_id)
    except Exception as e:
        if app_connector:
            app_connector.error_print("{}: {}".format(MS_AZURE_ENCRYPTION_ERROR, str(e)))
        return phantom.APP_ERROR

    if app_connector:
        app_connector.debug_print('Saving state: ', state)

    try:
        with open(state_file_path, 'w+') as state_file:
            json.dump(state, state_file)
    except Exception as e:
        if app_connector:
            app_connector.error_print(f'Unable to save state file: {str(e)}')

    return phantom.APP_SUCCESS


def _handle_login_response(request):
    """ This function is used to get the login response of authorization request from microsoft login page.

    :param request: Data given to REST endpoint
    :return: HttpResponse. The response displayed on authorization URL page
    """

    asset_id = request.GET.get('state')
    if not asset_id:
        return HttpResponse(f'ERROR: Asset ID not found in URL\n{json.dumps(request.GET)}',
                            content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)

    # Check for error in URL
    error = request.GET.get('error')
    error_description = request.GET.get('error_description')

    # If there is an error in response
    if error:
        message = f'Error: {error}'
        if error_description:
            message = f'{message} Details: {error_description}'
        return HttpResponse(f'Server returned {message}', content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)

    code = request.GET.get('code')
    admin_consent = request.GET.get('admin_consent')

    # If none of the code or admin_consent is available
    if not (code or admin_consent):
        return HttpResponse(f'Error while authenticating\n{json.dumps(request.GET)}',
                            content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)

    state = _load_app_state(asset_id)

    # If value of admin_consent is available
    if admin_consent:
        if admin_consent == 'True':
            admin_consent = True
        else:
            admin_consent = False

        state['admin_consent'] = admin_consent
        _save_app_state(state, asset_id, None)

        # If admin_consent is True
        if admin_consent:
            return HttpResponse('Admin Consent received. Please close this window.', content_type="text/plain")
        return HttpResponse('Admin Consent declined. Please close this window and try again later.',
                            content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)

    # If value of admin_consent is not available, value of code is available
    state['code'] = code
    _save_app_state(state, asset_id, None)

    return HttpResponse('Code received. Please close this window, the action will continue to get new token.', content_type="text/plain")


def _handle_rest_request(request, path_parts):
    """ Handle requests for authorization.

    :param request: Data given to REST endpoint
    :param path_parts: parts of the URL passed
    :return: dictionary containing response parameters
    """

    if len(path_parts) < 2:
        return HttpResponse('error: True, message: Invalid REST endpoint request', content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)

    call_type = path_parts[1]

    # To handle authorize request in test connectivity action
    if call_type == 'start_oauth':
        return _handle_login_redirect(request, 'admin_consent_url')

    # To handle response from microsoft login page
    if call_type == 'result':
        return_val = _handle_login_response(request)
        asset_id = request.GET.get('state')
        if asset_id:
            if not _is_valid_asset_id(asset_id):
                return HttpResponse("Error: Invalid asset_id", content_type="text/plain", status=MS_AZURE_BAD_REQUEST_CODE)
            auth_status_file_path = _get_file_path(asset_id, is_state_file=False)
            auth_status_file_path.touch(mode=664, exist_ok=True)
            try:
                uid = pwd.getpwnam('apache').pw_uid
                gid = grp.getgrnam('phantom').gr_gid
                os.chown(auth_status_file_path, uid, gid)  # nosemgrep file traversal risk is handled by blocking non-alphanum strings
            except Exception:
                pass

        return return_val
    return HttpResponse('error: Invalid endpoint', content_type="text/plain", status=MS_AZURE_NOT_FOUND_CODE)


def _get_dir_name_from_app_name(app_name):
    """ Get name of the directory for the app.

    :param app_name: Name of the application for which directory name is required
    :return: app_name: Name of the directory for the application
    """

    app_name = ''.join([x for x in app_name if x.isalnum()])
    app_name = app_name.lower()
    if not app_name:
        app_name = 'app_for_phantom'
    return app_name


class RetVal(tuple):

    def __new__(cls, val1, val2):

        return tuple.__new__(RetVal, (val1, val2))


class MSADGraphConnector(BaseConnector):

    def __init__(self):

        # Call the BaseConnectors init first
        super(MSADGraphConnector, self).__init__()

        self._state = None
        self._tenant = None
        self._client_id = None
        self._client_secret = None
        self._access_token = None
        self._refresh_token = None
        self._base_url = None
        self._admin_access_required = None
        self._admin_access_granted = None

    def load_state(self):
        """
        Load the contents of the state file to the state dictionary and decrypt it.
        :return: loaded state
        """
        state = super().load_state()
        if not isinstance(state, dict):
            self.debug_print("Reseting the state file with the default format")
            state = {
                "app_version": self.get_app_json().get('app_version')
            }
            return state
        try:
            state = _decrypt_state(state, self.get_asset_id())
        except Exception as e:
            error_message = self._get_error_message_from_exception(e)
            self.error_print("{}: {}".format(MS_AZURE_DECRYPTION_ERROR, error_message))
            self.debug_print("Reseting the state file with the default format")
            state = {
                "app_version": self.get_app_json().get('app_version')
            }

        return state

    def save_state(self, state):
        """
        Encrypt and save the current state dictionary to the the state file.
        :param state: state dictionary
        :return: status
        """
        try:
            state = _encrypt_state(state, self.get_asset_id())
        except Exception as e:
            error_message = self._get_error_message_from_exception(e)
            self.error_print("{}: {}".format(MS_AZURE_ENCRYPTION_ERROR, error_message))

        return super().save_state(state)

    def _dump_error_log(self, error, message="Exception occurred."):
        self.error_print(message, dump_object=error)

    def _get_error_message_from_exception(self, e):
        """
        Get appropriate error message from the exception.
        :param e: Exception object
        :return: error message
        """
        error_code = None
        error_message = MS_AZURE_ERROR_MESSAGE_UNKNOWN

        self._dump_error_log(e)

        try:
            if hasattr(e, "args"):
                if len(e.args) > 1:
                    error_code = e.args[0]
                    error_message = e.args[1]
                elif len(e.args) == 1:
                    error_message = e.args[0]
        except Exception:
            self.error_print("Exception occurred while getting error code and message")

        if not error_code:
            error_text = "Error Message: {}".format(error_message)
        else:
            error_text = "Error Code: {}. Error Message: {}".format(error_code, error_message)

        return error_text

    def _process_empty_response(self, response, action_result):
        """ This function is used to process empty response.

        :param response: response data
        :param action_result: object of Action Result
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message)
        """

        if response.status_code == 200 or response.status_code == 202:
            return RetVal(phantom.APP_SUCCESS, {})

        return RetVal(action_result.set_status(phantom.APP_ERROR, "Empty response and no information in the header"),
                      None)

    def _process_html_response(self, response, action_result):
        """ This function is used to process html response.

        :param response: response data
        :param action_result: object of Action Result
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message)
        """

        # An html response, treat it like an error
        status_code = response.status_code

        try:
            soup = BeautifulSoup(response.text, "html.parser")
            # Remove the script, style, footer and navigation part from the HTML message
            for element in soup(["script", "style", "footer", "nav"]):
                element.extract()
            error_text = soup.text
            split_lines = error_text.split('\n')
            split_lines = [x.strip() for x in split_lines if x.strip()]
            error_text = '\n'.join(split_lines)
        except Exception:
            error_text = "Cannot parse error details"

        message = MS_AZURE_RESPONSE_ERROR_MESSAGE.format(status_code=status_code, error_text=error_text)

        message = message.replace('{', '{{').replace('}', '}}')

        if status_code == MS_AZURE_BAD_REQUEST_CODE:
            message = MS_AZURE_RESPONSE_ERROR_MESSAGE.format(status_code=status_code, error_text=MS_AZURE_HTML_ERROR)

        return RetVal(action_result.set_status(phantom.APP_ERROR, message), None)

    def _process_json_response(self, response, action_result):
        """ This function is used to process json response.

        :param response: response data
        :param action_result: object of Action Result
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message)
        """

        # Try a json parse
        try:
            resp_json = response.json()
        except Exception as e:
            error_message = self._get_error_message_from_exception(e)
            return RetVal(action_result.set_status(phantom.APP_ERROR, "Unable to parse JSON response. Error: {0}".
                                                   format(error_message)), None)

        # Please specify the status codes here
        if 200 <= response.status_code < 399:
            return RetVal(phantom.APP_SUCCESS, resp_json)

        error_message = response.text.replace('{', '{{').replace('}', '}}')
        message = MS_AZURE_RESPONSE_ERROR_MESSAGE.format(status_code=response.status_code, error_text=error_message)

        # Show only error message if available
        if isinstance(resp_json.get('error', {}), dict):
            if resp_json.get('error', {}).get('message'):
                error_message = resp_json['error']['message']
                message = MS_AZURE_RESPONSE_ERROR_MESSAGE.format(status_code=response.status_code, error_text=error_message)
        else:
            error_message = resp_json['error']
            message = MS_AZURE_RESPONSE_ERROR_MESSAGE.format(status_code=response.status_code, error_text=error_message)

        return RetVal(action_result.set_status(phantom.APP_ERROR, message), None)

    def _process_response(self, response, action_result):
        """ This function is used to process html response.

        :param response: response data
        :param action_result: object of Action Result
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message)
        """

        # store the r_text in debug data, it will get dumped in the logs if the action fails
        if hasattr(action_result, 'add_debug_data'):
            action_result.add_debug_data({'r_status_code': response.status_code})
            action_result.add_debug_data({'r_text': response.text})
            action_result.add_debug_data({'r_headers': response.headers})

        # Process each 'Content-Type' of response separately

        # Process a json response
        if 'json' in response.headers.get('Content-Type', ''):
            return self._process_json_response(response, action_result)

        if 'text/javascript' in response.headers.get('Content-Type', ''):
            return self._process_json_response(response, action_result)

        # Process an HTML response, Do this no matter what the API talks.
        # There is a high chance of a PROXY in between SOAR and the rest of
        # world, in case of errors, PROXY's return HTML, this function parses
        # the error and adds it to the action_result.
        if 'html' in response.headers.get('Content-Type', ''):
            return self._process_html_response(response, action_result)

        # Reset_password returns empty body
        if not response.text and 200 <= response.status_code < 399:
            return RetVal(phantom.APP_SUCCESS, {})

        # it's not content-type that is to be parsed, handle an empty response
        if not response.text:
            return self._process_empty_response(response, action_result)

        # everything else is actually an error at this point
        response_content = response.text.replace('{', '{{').replace('}', '}}')
        message = MS_AZURE_PROCESS_RESPONSE_ERROR_MESSAGE.format(status_code=response.status_code, content=response_content)

        return RetVal(action_result.set_status(phantom.APP_ERROR, message), None)

    def _get_asset_name(self, action_result):
        """ Get name of the asset using SOAR URL.

        :param action_result: object of ActionResult class
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message), asset name
        """

        url = urlparse.urljoin(self.get_phantom_base_url(), f'rest/asset/{self._asset_id}')
        ret_val, resp_json = self._make_rest_call(action_result=action_result, endpoint=url, verify=False)  # nosemgrep

        if phantom.is_fail(ret_val):
            return ret_val, None

        asset_name = resp_json.get('name')
        if not asset_name:
            return action_result.set_status(phantom.APP_ERROR, f'Asset Name for id: {self._asset_id} not found.'), None
        return phantom.APP_SUCCESS, asset_name

    def _get_external_phantom_base_url(self, action_result):
        """ Get base url of SOAR.

        :param action_result: object of ActionResult class
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message),
        base url of SOAR
        """

        url = urlparse.urljoin(self.get_phantom_base_url(), 'rest/system_info')
        ret_val, resp_json = self._make_rest_call(action_result=action_result, endpoint=url, verify=False)  # nosemgrep
        if phantom.is_fail(ret_val):
            return ret_val, None

        phantom_base_url = resp_json.get('base_url').rstrip("/")
        if not phantom_base_url:
            return action_result.set_status(phantom.APP_ERROR, MS_AZURE_BASE_URL_NOT_FOUND_MESSAGE), None
        return phantom.APP_SUCCESS, phantom_base_url

    def _get_app_rest_url(self, action_result):
        """ Get URL for making rest calls.

        :param action_result: object of ActionResult class
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message),
        URL to make rest calls
        """

        ret_val, phantom_base_url = self._get_external_phantom_base_url(action_result)
        if phantom.is_fail(ret_val):
            return action_result.get_status(), None

        ret_val, asset_name = self._get_asset_name(action_result)
        if phantom.is_fail(ret_val):
            return action_result.get_status(), None

        self.save_progress(f'Using SOAR base URL: {phantom_base_url}')
        app_json = self.get_app_json()
        app_id = app_json['appid']
        app_name = app_json['name']

        app_dir_name = _get_dir_name_from_app_name(app_name)
        url_to_app_rest = f"{phantom_base_url}/rest/handler/{app_dir_name}_{app_id}/{asset_name}"
        return phantom.APP_SUCCESS, url_to_app_rest

    def _make_rest_call(self, endpoint, action_result, verify=True, headers=None, params=None, data=None, json=None, method="get"):
        """ Function that makes the REST call to the app.

        :param endpoint: REST endpoint that needs to appended to the service address
        :param action_result: object of ActionResult class
        :param headers: request headers
        :param params: request parameters
        :param data: request body
        :param json: JSON object
        :param method: GET/POST/PUT/DELETE/PATCH (Default will be GET)
        :param verify: verify server certificate (Default True)
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message),
        response obtained by making an API call
        """

        resp_json = None

        try:
            request_func = getattr(requests, method)
        except AttributeError:
            return RetVal(action_result.set_status(phantom.APP_ERROR, f"Invalid method: {method}"), resp_json)

        try:
            resp_json = request_func(endpoint, json=json, data=data, headers=headers, verify=verify, params=params, timeout=DEFAULT_TIMEOUT)
        except Exception as e:
            error_message = f"Error connecting to server. Details: {self._get_error_message_from_exception(e)}"
            return RetVal(action_result.set_status(phantom.APP_ERROR, error_message), resp_json)

        return self._process_response(resp_json, action_result)

    def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=None, params=None, data=None, json=None, method="get"):
        """ Function that helps setting REST call to the app.

        :param endpoint: REST endpoint that needs to appended to the service address
        :param action_result: object of ActionResult class
        :param headers: request headers
        :param params: request parameters
        :param data: request body
        :param json: JSON object
        :param method: GET/POST/PUT/DELETE/PATCH (Default will be GET)
        :param verify: verify server certificate (Default True)
        :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message),
        response obtained by making an API call
        """

        url = f"{self._base_url}/{self._tenant}{endpoint}"
        if headers is None:
            headers = {}

        token = self._state.get(MS_AZURE_TOKEN_STRING, {})
        if not token.get(MS_AZURE_ACCESS_TOKEN_STRING):
            ret_val = self._get_token(action_result)

            if phantom.is_fail(ret_val):
                return RetVal(action_result.get_status(), None)
        headers.update({
                'Authorization': f'Bearer {self._access_token}',
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            })
        ret_val, resp_json = self._make_rest_call(url, action_result, verify, headers, params, data, json, method)

        # If token is expired, generate a new token
        message = action_result.get_message()
        if message and any(failure_message in message for failure_message in AUTH_FAILURE_MESSAGES):
            self.save_progress("Token is invalid/expired. Hence, generating a new token.")
            ret_val = self._get_token(action_result)
            if phantom.is_fail(ret_val):
                return RetVal(ret_val, None)

            headers.update({'Authorization': f'Bearer {self._access_token}'})

            ret_val, resp_json = self._make_rest_call(url, action_result, verify, headers, params, data, json, method)

        if phantom.is_fail(ret_val):
            return RetVal(ret_val, resp_json)

        return RetVal(phantom.APP_SUCCESS, resp_json)

    def _handle_generate_token(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))
        ret_val = self._get_token(action_result)
        if phantom.is_fail(ret_val):
            return action_result.get_status()

        self._state['admin_consent'] = True

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS, "Token generated")

    def _handle_test_connectivity(self, param):
        """ Function that handles the test connectivity action, it is much simpler than other action handlers."""

        # Progress
        # self.save_progress("Generating Authentication URL")
        app_state = {}
        action_result = self.add_action_result(ActionResult(param))

        if not (self._admin_access_required and self._admin_access_granted):

            self.save_progress("Getting App REST endpoint URL")
            # Get the URL to the app's REST Endpoint, this is the url that the TC dialog
            # box will ask the user to connect to
            ret_val, app_rest_url = self._get_app_rest_url(action_result)

            if phantom.is_fail(ret_val):
                self.save_progress(MS_REST_URL_NOT_AVAILABLE_MESSAGE.format(error=self.get_status()))
                return self.set_status(phantom.APP_ERROR)

            # create the url that the oauth server should re-direct to after the auth is completed
            # (success and failure), this is added to the state so that the request handler will access
            # it later on
            redirect_uri = f"{app_rest_url}/result"
            app_state['redirect_uri'] = redirect_uri

            self.save_progress(MS_OAUTH_URL_MESSAGE)
            self.save_progress(redirect_uri)

            self._client_id = urlparse.quote(self._client_id)
            self._tenant = urlparse.quote(self._tenant)

            query_params = {
                'client_id': self._client_id,
                'redirect_uri': redirect_uri,
                'state': self._asset_id,
            }

            if self._admin_access_required:
                # Create the url for fetching administrator consent
                admin_consent_url_base = MS_AZURE_ADMIN_CONSENT_URL.format(tenant_id=self._tenant)
            else:
                # Create the url authorization, this is the one pointing to the oauth server side
                admin_consent_url_base = MS_AZURE_AUTHORIZE_URL.format(tenant_id=self._tenant)
                query_params['scope'] = MS_AZURE_CODE_GENERATION_SCOPE
                query_params['response_type'] = 'code'

            query_string = '&'.join(f'{key}={value}' for key, value in query_params.items())

            admin_consent_url = f'{admin_consent_url_base}?{query_string}'

            app_state['admin_consent_url'] = admin_consent_url

            # The URL that the user should open in a different tab.
            # This is pointing to a REST endpoint that points to the app
            url_to_show = f"{app_rest_url}/start_oauth?asset_id={self._asset_id}&"

            # Save the state, will be used by the request handler
            _save_app_state(app_state, self._asset_id, self)

            self.save_progress('Please connect to the following URL from a different tab to continue the connectivity process')
            self.save_progress(url_to_show)
            self.save_progress(MS_AZURE_AUTHORIZE_TROUBLESHOOT_MESSAGE)

            time.sleep(MS_AZURE_WAIT_FOR_URL_SLEEP)

            completed = False

            if not _is_valid_asset_id(self._asset_id):
                return action_result.set_status(phantom.APP_ERROR, "Invalid asset id")

            auth_status_file_path = _get_file_path(self._asset_id, is_state_file=False)

            self.save_progress('Waiting for authorization to complete')

            for i in range(0, 40):

                self.send_progress('{0}'.format('.' * (i % 10)))

                if auth_status_file_path.is_file():
                    completed = True
                    auth_status_file_path.unlink()
                    break

                time.sleep(MS_TC_STATUS_SLEEP)

            if not completed:
                self.save_progress("Authentication process does not seem to be completed. Timing out")
                self.save_progress(MS_AZURE_TEST_CONNECTIVITY_FAILURE_MESSAGE)
                return self.set_status(phantom.APP_ERROR)

            self.send_progress("")

            # Load the state again, since the http request handlers would have saved the result of the admin consent
            self._state = _load_app_state(self._asset_id, self)

            # Deleting the local state file because of it replicates with actual state file while installing the app
            current_file_path = pathlib.Path(__file__).resolve()
            input_file = f'{self._asset_id}_state.json'
            state_file_path = current_file_path.with_name(input_file)
            state_file_path.unlink()

            if not self._state:
                self.save_progress(MS_STATE_FILE_ERROR_MESSAGE)
                self.save_progress(MS_AZURE_TEST_CONNECTIVITY_FAILURE_MESSAGE)
                return action_result.set_status(phantom.APP_ERROR)

            self._state.setdefault('admin_consent', False)

            if self._admin_access_required and not self._state.get('admin_consent'):
                self.save_progress(MS_ADMIN_CONSENT_ERROR_MESSAGE)
                self.save_progress(MS_AZURE_TEST_CONNECTIVITY_FAILURE_MESSAGE)
                return action_result.set_status(phantom.APP_ERROR)

            if not self._admin_access_required and not self._state.get('code'):
                self.save_progress(MS_AUTHORIZATION_ERROR_MESSAGE)
                self.save_progress(MS_AZURE_TEST_CONNECTIVITY_FAILURE_MESSAGE)
                return action_result.set_status(phantom.APP_ERROR)

            if self._admin_access_required:
                self.save_progress("Admin consent received")
                self.save_progress(
                    "Waiting for 30 seconds before generating token. If action fails with '403: AccessDenied' error, "
                    "please check permissions and re-run the 'test connectivity' after some time.")
                self.save_progress(
                    "Admin consent is already received. You can mark 'Admin Consent Already Provided' to True, "
                    "unless you make changes in the permissions.")
                time.sleep(30)

        self.save_progress(MS_GENERATING_ACCESS_TOKEN_MESSAGE)
        ret_val = self._get_token(action_result)
        if phantom.is_fail(ret_val):
            return action_result.get_status()

        self.save_progress("Getting info about a single user to verify token")
        params = {'$top': '1'}
        ret_val, response = self._make_rest_call_helper(action_result, "/users", params=params)

        if phantom.is_fail(ret_val):
            self.save_progress("API to get users failed")
            self.save_progress(MS_AZURE_TEST_CONNECTIVITY_FAILURE_MESSAGE)
            return self.set_status(phantom.APP_ERROR)

        value = response.get('value')

        if value:
            self.save_progress("Got user info")

        self.save_progress(MS_AZURE_TEST_CONNECTIVITY_PASSED)

        return self.set_status(phantom.APP_SUCCESS)

    def _handle_list_users(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        filter_string = param.get('filter_string')
        select_string = param.get('select_string')
        expand_string = param.get('expand_string')
        use_advanced_query = param.get('use_advanced_query')

        headers = {}
        parameters = {}

        if filter_string:
            parameters['$filter'] = filter_string
        if select_string:
            select_string = [param_value.strip() for param_value in select_string.split(",")]
            select_string = list(filter(None, select_string))
            parameters['$select'] = ','.join(param_value for param_value in select_string)
        if expand_string:
            parameters['$expand'] = expand_string
        if use_advanced_query:
            headers['ConsistencyLevel'] = 'eventual'
            parameters['$count'] = 'true'

        ret_val = self._handle_pagination(action_result, '/users', headers=headers, params=parameters)

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        resp_data = action_result.get_data()
        if resp_data and resp_data[action_result.get_data_size() - 1] == 'Empty response':
            summary['num_users'] = (action_result.get_data_size()) - 1
        else:
            summary['num_users'] = action_result.get_data_size()

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_reset_password(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param['user_id']
        temp_password = param.get('temp_password', '')
        force_change = param.get('force_change', True)

        data = {
            'passwordProfile': {
                'forceChangePasswordNextSignIn': force_change,
                'password': temp_password
            }
        }

        endpoint = f'/users/{user_id}'

        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, json=data, method='patch')

        if phantom.is_fail(ret_val):
            return ret_val

        summary = action_result.update_summary({})
        summary['status'] = f"Successfully reset password for {user_id}"

        # An empty response indicates success. No response body is returned.
        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_enable_user(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param['user_id']

        data = {
            "accountEnabled": True
        }

        endpoint = f'/users/{user_id}'
        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, json=data, method='patch')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        summary['status'] = f"Successfully enabled user {user_id}"

        # An empty response indicates success. No response body is returned.
        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_invalidate_tokens(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param['user_id']
        endpoint = f'/users/{user_id}/revokeSignInSessions'

        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, method='post')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        summary['status'] = "Successfully disabled tokens"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_disable_user(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param['user_id']

        data = {
            "accountEnabled": False
        }

        endpoint = f'/users/{user_id}'
        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, json=data, method='patch')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        summary['status'] = f"Successfully disabled user {user_id}"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_list_user_attributes(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param.get('user_id')
        select_string = param.get('select_string')
        expand_string = param.get('expand_string')
        use_advanced_query = param.get('use_advanced_query')

        headers = {}
        parameters = {}

        if select_string:
            select_string = [param_value.strip() for param_value in select_string.split(",")]
            select_string = list(filter(None, select_string))
            parameters['$select'] = ','.join(param_value for param_value in select_string)
        if expand_string:
            parameters['$expand'] = expand_string
        if use_advanced_query:
            headers['ConsistencyLevel'] = 'eventual'
            parameters['$count'] = 'true'

        if user_id:
            endpoint = f'/users/{user_id}'
        else:
            endpoint = '/users'

        ret_val = self._handle_pagination(action_result, endpoint, headers=headers, params=parameters)

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        if user_id:
            summary['status'] = f"Successfully retrieved attributes for user {user_id}"
        else:
            summary['status'] = "Successfully retrieved user attributes"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_list_user_devices(self, param):

        self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param['user_id']

        parameters = {}
        select_string = param.get('select_string')
        if select_string:
            select_string = [param_value.strip() for param_value in select_string.split(",")]
            select_string = list(filter(None, select_string))
            parameters['$select'] = ','.join(param_value for param_value in select_string)

        endpoint = f'/users/{user_id}/ownedDevices'

        ret_val = self._handle_pagination(action_result, endpoint, params=parameters)

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        summary['status'] = "Successfully retrieved owned devices for user {}".format(user_id)

        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_set_user_attribute(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        user_id = param['user_id']
        attribute = param['attribute']
        attribute_value = param['attribute_value']

        data = {
            attribute: attribute_value
        }

        endpoint = f'/users/{user_id}'
        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, json=data, method='patch')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        summary['status'] = "Successfully updated user attribute"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_add_user(self, param):

        config = self.get_config()
        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        object_id = param['group_object_id']
        user_id = param['user_id']

        data = {
            '@odata.id': "https://{}/directoryObjects/{}".format(MSADGRAPH_API_REGION[config.get(MS_AZURE_URL, "Global")], user_id)
        }

        endpoint = f'/groups/{object_id}/members/$ref'
        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, json=data, method='post')

        summary = action_result.update_summary({})
        if phantom.is_fail(ret_val):
            message = action_result.get_message()
            if 'references already exist for the following modified properties: \'members\'.' in message:
                summary['status'] = "User already in group"
                return action_result.get_status()
            else:
                return ret_val
        else:
            summary['status'] = "Successfully added user to group"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_remove_user(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        object_id = param['group_object_id']
        user_id = param['user_id']

        endpoint = f'/groups/{object_id}/members/{user_id}/$ref'
        ret_val, _ = self._make_rest_call_helper(action_result, endpoint, method='delete')

        summary = action_result.update_summary({})
        if phantom.is_fail(ret_val):
            message = action_result.get_message()
            if 'does not exist or one of its queried' in message:
                summary['status'] = "User not in group"
            return action_result.get_status()
        else:
            summary['status'] = "Successfully removed user from group"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_list_groups(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        filter_string = param.get('filter_string')
        select_string = param.get('select_string')
        expand_string = param.get('expand_string')
        use_advanced_query = param.get('use_advanced_query')

        headers = {}
        parameters = {}

        if filter_string:
            parameters['$filter'] = filter_string
        if select_string:
            select_string = [param_value.strip() for param_value in select_string.split(",")]
            select_string = list(filter(None, select_string))
            parameters['$select'] = ','.join(param_value for param_value in select_string)
        if expand_string:
            parameters['$expand'] = expand_string
        if use_advanced_query:
            headers['ConsistencyLevel'] = 'eventual'
            parameters['$count'] = 'true'

        ret_val = self._handle_pagination(action_result, '/groups', headers=headers, params=parameters)

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        resp_data = action_result.get_data()
        if resp_data and resp_data[action_result.get_data_size() - 1] == 'Empty response':
            summary['num_groups'] = (action_result.get_data_size()) - 1
        else:
            summary['num_groups'] = action_result.get_data_size()

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_get_group(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        select_string = param.get('select_string')
        expand_string = param.get('expand_string')
        use_advanced_query = param.get('use_advanced_query')

        headers = {}
        parameters = {}

        if select_string:
            select_string = [param_value.strip() for param_value in select_string.split(",")]
            select_string = list(filter(None, select_string))
            parameters['$select'] = ','.join(param_value for param_value in select_string)
        if expand_string:
            parameters['$expand'] = expand_string
        if use_advanced_query:
            headers['ConsistencyLevel'] = 'eventual'
            parameters['$count'] = 'true'

        object_id = param['object_id']

        endpoint = f'/groups/{object_id}'

        ret_val, response = self._make_rest_call_helper(action_result, endpoint, method='get', headers=headers, params=parameters)

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        action_result.add_data(response)

        summary = action_result.update_summary({})
        summary['status'] = f"Successfully retrieved group {object_id}"

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_list_group_members(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        object_id = param['group_object_id']

        select_string = param.get('select_string')
        expand_string = param.get('expand_string')
        use_advanced_query = param.get('use_advanced_query')

        headers = {}
        parameters = {}

        if select_string:
            select_string = [param_value.strip() for param_value in select_string.split(",")]
            select_string = list(filter(None, select_string))
            parameters['$select'] = ','.join(param_value for param_value in select_string)
        if expand_string:
            parameters['$expand'] = expand_string
        if use_advanced_query:
            headers['ConsistencyLevel'] = 'eventual'
            parameters['$count'] = 'true'

        endpoint = f'/groups/{object_id}/members'

        ret_val = self._handle_pagination(action_result, endpoint, headers=headers, params=parameters)

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        summary = action_result.update_summary({})
        summary['num_users'] = action_result.get_data_size()

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_list_directory_roles(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        endpoint = '/directoryRoles'
        ret_val, response = self._make_rest_call_helper(action_result, endpoint, method='get')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        value = response.get('value', [])
        for item in value:
            action_result.add_data(item)

        summary = action_result.update_summary({})
        summary['num_directory_roles'] = len(value)

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS)

    def _handle_validate_group(self, param):

        self.save_progress(f"In action handler for: {self.get_action_identifier()}")
        action_result = self.add_action_result(ActionResult(dict(param)))

        object_id = param['group_object_id']
        user_id = param['user_id']

        endpoint = f'/users/{user_id}/memberOf?$filter=id eq \'{object_id}\''
        ret_val, response = self._make_rest_call_helper(action_result, endpoint, method='get')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        user_id_map = {}

        for user in response.get('value', []):
            user_id_map[user['id']] = user['displayName']

        self.save_progress(f"Completed action handler for: {self.get_action_identifier()}")
        return action_result.set_status(phantom.APP_SUCCESS, f"User is member of group: {ret_val}")

    def _get_token(self, action_result):
        """ This function is used to get a token via REST Call.

        :param action_result: Object of action result
        :return: status(phantom.APP_SUCCESS/phantom.APP_ERROR)
        """

        data = {
            'client_id': self._client_id,
            'client_secret': self._client_secret,
        }

        req_url = SERVER_TOKEN_URL.format(self._tenant)
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        }

        if not self._admin_access_required:
            data['scope'] = MS_AZURE_CODE_GENERATION_SCOPE
            data['redirect_uri'] = self._state.get('redirect_uri')
            auth_code = self._state.get('code', None)
            if self._state.get(MS_AZURE_TOKEN_STRING, {}).get(MS_AZURE_REFRESH_TOKEN_STRING, None):
                data['refresh_token'] = self._refresh_token
                data['grant_type'] = 'refresh_token'
            elif auth_code:
                data['code'] = auth_code
                data['grant_type'] = 'authorization_code'
            else:
                return action_result.set_status(phantom.APP_ERROR, "Unexpected details retrieved from the state file.")
        else:
            data['scope'] = 'https://graph.microsoft.com/.default'
            data['grant_type'] = 'client_credentials'

        ret_val, resp_json = self._make_rest_call(req_url, action_result, headers=headers, data=data, method='post')

        if phantom.is_fail(ret_val):
            return action_result.get_status()

        if self._admin_access_required and self._admin_access_granted:
            self._state['admin_consent'] = True

        self._state[MS_AZURE_TOKEN_STRING] = resp_json
        self._access_token = resp_json.get(MS_AZURE_ACCESS_TOKEN_STRING, None)
        self._refresh_token = resp_json.get(MS_AZURE_REFRESH_TOKEN_STRING, None)

        return phantom.APP_SUCCESS

    def _handle_pagination(self, action_result, endpoint, headers=None, params=None):
        """
        This action is used to create an iterator that will paginate through responses from called methods.

        :param action_result: Object of ActionResult class
        :param endpoint: REST endpoint that needs to appended to the service address
        :param headers: Dictionary of headers for the rest API calls
        :param params: Dictionary of params for the rest API calls
        """
        # maximum page size
        page_size = MS_AZURE_PAGE_SIZE
        if isinstance(params, dict):
            params.update({"$top": page_size})
        else:
            params = {"$top": page_size}

        while True:

            # make rest call
            ret_val, response = self._make_rest_call_helper(action_result, endpoint, headers=headers, params=params, method='get')

            if phantom.is_fail(ret_val):
                return None

            if "value" in response:
                for user in response.get('value', []):
                    action_result.add_data(user)
                if len(response.get('value')) > 0 and response.get('value')[0] == {}:
                    action_result.add_data('Empty response')
            else:
                action_result.add_data(response)

            if response.get(MS_AZURE_NEXT_LINK_STRING):
                parsed_url = urlparse.urlparse(response.get(MS_AZURE_NEXT_LINK_STRING))
                try:
                    params['$skiptoken'] = urlparse.parse_qs(parsed_url.query).get('$skiptoken')[0]
                except Exception:
                    self.debug_print(f"odata.nextLink is {response.get(MS_AZURE_NEXT_LINK_STRING)}")
                    self.debug_print("Error occurred while extracting skiptoken from the odata.nextLink")
                    break
            else:
                break

        return phantom.APP_SUCCESS

    def handle_action(self, param):

        ret_val = phantom.APP_SUCCESS

        # Get the action that we are supposed to execute for this App Run
        action_id = self.get_action_identifier()

        self.debug_print("action_id", self.get_action_identifier())

        if action_id == 'test_connectivity':
            ret_val = self._handle_test_connectivity(param)

        elif action_id == 'list_users':
            ret_val = self._handle_list_users(param)

        elif action_id == 'reset_password':
            ret_val = self._handle_reset_password(param)

        elif action_id == 'invalidate_tokens':
            ret_val = self._handle_invalidate_tokens(param)

        elif action_id == 'enable_user':
            ret_val = self._handle_enable_user(param)

        elif action_id == 'disable_user':
            ret_val = self._handle_disable_user(param)

        elif action_id == 'list_user_attributes':
            ret_val = self._handle_list_user_attributes(param)

        elif action_id == 'set_user_attribute':
            ret_val = self._handle_set_user_attribute(param)

        elif action_id == 'remove_user':
            ret_val = self._handle_remove_user(param)

        elif action_id == 'add_user':
            ret_val = self._handle_add_user(param)

        elif action_id == 'list_groups':
            ret_val = self._handle_list_groups(param)

        elif action_id == 'get_group':
            ret_val = self._handle_get_group(param)

        elif action_id == 'list_group_members':
            ret_val = self._handle_list_group_members(param)

        elif action_id == 'validate_group':
            ret_val = self._handle_validate_group(param)

        elif action_id == 'list_directory_roles':
            ret_val = self._handle_list_directory_roles(param)

        elif action_id == 'generate_token':
            ret_val = self._handle_generate_token(param)

        elif action_id == 'list_user_devices':
            ret_val = self._handle_list_user_devices(param)

        return ret_val

    def initialize(self):
        """ This is an optional function that can be implemented by the AppConnector derived class. Since the
        configuration dictionary is already validated by the time this function is called, it's a good place to do any
        extra initialization of any internal modules. This function MUST return a value of either phantom.APP_SUCCESS or
        phantom.APP_ERROR. If this function returns phantom.APP_ERROR, then AppConnector::handle_action will not get
        called.
        """

        self._state = self.load_state()

        # get the asset config
        config = self.get_config()
        self._asset_id = self.get_asset_id()

        self._tenant = config[MS_AZURE_CONFIG_TENANT]
        self._client_id = config[MS_AZURE_CONFIG_CLIENT_ID]
        self._client_secret = config[MS_AZURE_CONFIG_CLIENT_SECRET]
        self._admin_access_required = config.get(MS_AZURE_CONFIG_ADMIN_ACCESS_REQUIRED, False)
        self._admin_access_granted = config.get(MS_AZURE_CONFIG_ADMIN_ACCESS_GRANTED, False)
        self._access_token = self._state.get(MS_AZURE_TOKEN_STRING, {}).get(MS_AZURE_ACCESS_TOKEN_STRING)
        self._refresh_token = self._state.get(MS_AZURE_TOKEN_STRING, {}).get(MS_AZURE_REFRESH_TOKEN_STRING)
        self._base_url = MSADGRAPH_API_URLS[config.get(MS_AZURE_URL, "Global")]

        return phantom.APP_SUCCESS

    def finalize(self):

        # Save the state, this data is saved across actions and app upgrades
        self.save_state(self._state)
        return phantom.APP_SUCCESS


if __name__ == '__main__':

    import argparse

    import pudb

    pudb.set_trace()

    argparser = argparse.ArgumentParser()

    argparser.add_argument('input_test_json', help='Input Test JSON file')
    argparser.add_argument('-u', '--username', help='username', required=False)
    argparser.add_argument('-p', '--password', help='password', required=False)
    argparser.add_argument('-v', '--verify', action='store_true', help='verify', required=False, default=False)

    args = argparser.parse_args()
    session_id = None

    username = args.username
    password = args.password
    verify = args.verify

    if username is not None and password is None:

        # User specified a username but not a password, so ask
        import getpass
        password = getpass.getpass("Password: ")

    if username and password:
        login_url = BaseConnector._get_phantom_base_url() + "login"
        try:
            print("Accessing the Login page")
            r = requests.get(login_url, verify=verify, timeout=60)
            csrftoken = r.cookies['csrftoken']

            data = dict()
            data['username'] = username
            data['password'] = password
            data['csrfmiddlewaretoken'] = csrftoken

            headers = dict()
            headers['Cookie'] = 'csrftoken=' + csrftoken
            headers['Referer'] = login_url

            print("Logging into Platform to get the session id")
            r2 = requests.post(login_url, verify=verify, data=data, headers=headers, timeout=60)
            session_id = r2.cookies['sessionid']
        except Exception as e:
            print("Unable to get session id from the platform. Error: " + str(e))
            sys.exit(1)

    with open(args.input_test_json) as f:
        in_json = f.read()
        in_json = json.loads(in_json)
        print(json.dumps(in_json, indent=4))

        connector = MSADGraphConnector()
        connector.print_progress_message = True

        if session_id is not None:
            in_json['user_session_token'] = session_id
            connector._set_csrf_info(csrftoken, headers['Referer'])

        ret_val = connector._handle_action(json.dumps(in_json), None)
        print(json.dumps(json.loads(ret_val), indent=4))

    sys.exit(0)
