# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved. import sys import json from io import StringIO from splunk import RESTException from splunk.clilib.bundle_paths import make_splunkhome_path sys.path.append(make_splunkhome_path(['etc', 'apps', 'SA-ITOA', 'lib'])) import itsi_path import itsi_py3 from ITOA.storage import itoa_storage from ITOA.storage.statestore import StateStoreError from ITOA.itoa_config import get_collection_name_for_itoa_object from ITOA.itoa_exceptions import ItoaError from ITOA.setup_logging import logger, InstrumentCall from ITOA.controller_utils import ITOAError, ItoaValidationError, itoa_response_headers from migration.supervisor import MigrationSupervisor from user_access_errors import UserAccessError logger.debug("Initialized itoa interface provider log") class ItoaInterfaceProviderBase(object): """ Base provider implementing services for REST APIs It primarily consists of CRUD/bulk actions to configure and use basic ITSI objects like entities, services, etc. Specific REST handlers derive from this class to fit functionality to specific REST handling """ def __init__(self): """ Basic constructor @type: object @param self: The self reference """ super(ItoaInterfaceProviderBase, self).__init__() self._session_key = None self._current_user = None self._rest_method = None self._instrumentation = None def _setup(self, session_key, current_user, rest_method, loggero=None): """ Method to setup provider before handler from the provider are invoked @type: string @param session_key: session key to splunkd @type: string @param current_user: current user initiating REST call @type: string @param rest_method: REST method initiated, GET/POST/PUT/DELETE @type loggero: logger @param loggero: caller's logger @return: None """ self._session_key = session_key if isinstance( session_key, itsi_py3.string_type) else None self._current_user = current_user if isinstance( current_user, itsi_py3.string_type) else None self._rest_method = rest_method.upper() if isinstance( rest_method, itsi_py3.string_type) else None self._instrumentation = InstrumentCall( loggero) if loggero else InstrumentCall(logger) def render_json(self, json_response): """ given data, convert it to a JSON which is consumable by a web client @type: json @param json_response: the response to render for REST @rtype: string @return: normalized JSON as a string """ try: response = json.dumps(json_response).replace("/ Extract this owner or default to current user as owner @type: dict @param args: args provided by splunkd server to handler for REST request @rtype: basestring @return: the owner identified """ owner = args['session']['user'] if 'ns' in args and 'user' in args['ns']: owner = args['ns']['user'] # Clean up the owner from the REST method args to enforce/reflect namespace owner only applies if 'owner' in rest_method_args: del rest_method_args['owner'] return owner @staticmethod def extract_rest_args(args, args_field, args_dict): """ Helper method to extract dict form of a given field's value from splunkd server provided args to a handler @type: dict @param args: args provided by splunkd server to handler for REST request @type: string @param args_field: field to extract as dict. This field's value MUST be a list in args @type: dict @param args_dict: the in/out of this method which is a dict to which args from the field are appended/overwritten @rtype: None @return: None """ if (not ( isinstance(args, dict) and isinstance(args_field, itsi_py3.string_type) and isinstance(args_dict, dict) and isinstance(args.get(args_field, []), list) )): raise ITOAError( 'Invalid args received by extract_rest_args. args: {}, field: {}'.format(args, args_field)) for term in args.get(args_field, []): if len(term) == 2: # term[0] is arg name and term[1] is value args_dict[term[0]] = term[1] @staticmethod def extract_force_delete_header(args, args_dict): """ Helper method to extract the 'X-Force-Delete' header from splunkd provided args to a handler @type: dict @param args: args provided by splunkd server to handler for REST request @type: dict @param args_dict: the in/out of this method which is a dict to which args from the field are appended/overwritten @rtype: None @return: None """ # Looking for a specific key key = 'X-Force-Delete' if args.get('headers') is not None and isinstance(args.get('headers'), list): for array in args['headers']: for term in array: if term == key: if len(array) == 2 and array[0] == key: args_dict[array[0]] = array[1] @staticmethod def extract_data_payload(args): """ Custom fit method that extracts "data" from payload in a specific way that may work only for some splunkd REST handlers. @type: dict @param args: args provided by splunkd server to handler for REST request @rtype: json @return: data payload extracted from the REST args """ form_data = {} # Note that form data being present here implies head specified content type correctly data = None # Allow both raw payload (Content-Type application/json) and form payload # (Content-Type application/x-www-form-urlencoded). Will skip checking headers here # so as to allow current client API sets to continue without incurring changes/convenience # for client API. If needed, we could add specific header checks in future. is_use_form = True if 'payload' in args and len(args['payload']) > 0: # if this is a csv file upload, we use a csv-specific method for extraction if 'name=\"csvfile\"' in args['payload']: return SplunkdRestInterfaceBase._extract_csv_payload(args['payload']) # all other cases below try: form_data = json.loads(args['payload']) is_use_form = False except (ValueError, TypeError): logger.warning("Invalid JSON in payload found, make sure the form data is valid.") # Ignore payload contents since it isnt valid JSON, lets try form data is_use_form = True # if the handler has received a base64 payload, assume it is a generic file upload if 'payload_base64' in args and len(args['payload_base64']) > 0: return {'uploadedFileBase64Content': StringIO(args['payload_base64'])} if is_use_form: SplunkdRestInterfaceBase.extract_rest_args(args, 'form', form_data) if 'data' in form_data: data = form_data['data'] else: data = form_data form_data = {} try: if isinstance(data, itsi_py3.string_type): form_data['data'] = json.loads(data) elif isinstance(data, dict) or isinstance(data, list): form_data['data'] = data except (ValueError, TypeError) as e: logger.exception(e) raise ItoaValidationError('Could not extract "data" from payload. Check input.', logger) return form_data @staticmethod def _extract_csv_payload(payload): """ Internal method that extracts csv data from a request payload and creates a file handle from it. @type: str @param payload: payload received by handler from REST request @rtype: json @return: CSV file handle extracted from the REST args payload, presented in a json object """ # Split the payload up to grab just the data portion # Payload is in this format: \r\n\r\n\r\n<\r\n>\r\n headers_and_data = payload.split('\r\n\r\n') data_and_end_boundary = headers_and_data[1].rstrip().rpartition('\r\n') data = itsi_py3.unicode(data_and_end_boundary[0]) string_as_file = StringIO(data) return {'csvfile': string_as_file} class FileDownload(object): def __init__(self, filename, base64_content): self.filename = filename self.content = base64_content