You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
16 KiB

# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
import sys
import json
from splunk.clilib.bundle_paths import make_splunkhome_path
# SA-UserAccess imports
sys.path.append(make_splunkhome_path(['etc', 'apps', 'SA-UserAccess', 'lib']))
from user_access_utils import UserAccess
from setup_logging import setup_logging
from base_splunkd_rest import BaseSplunkdRest
from user_access_errors import BadRequest, UserAccessError
logger = setup_logging("user_access_interface.log", "useraccess.controllers.useraccess_interface")
LOG_PREFIX = "[SA-UserAccess]"
logger.info("Initialized user access log")
def handle_path_terms(f):
def wrapper(self, *args, **kwargs):
'''
path must one of these values:
* user_access_interface
* user_roles
'''
if len(self.pathParts) < 3:
raise UserAccessError(status=404, message=_("Insufficient arguments provided."))
return f(self, *args, **kwargs)
return wrapper
def parse_splunkd_payload(f) :
"""
Decorator to handle application/json content type for splunkd
rest endpoints
no-op if content type is not application/json, else
convert json to a dict and put that dict in the kwargs
data argument.
"""
def wrapper(self, *args, **kwargs):
if 'content-type' in self.request['headers'] and \
'application/json' in self.request['headers']['content-type']:
self.request_payload = json.loads(self.request['payload'])
return f(self, *args, **kwargs)
return wrapper
class UserAccessInterface(BaseSplunkdRest):
def __init__(self, method, requestInfo, responseInfo, sessionKey):
super(UserAccessInterface, self).__init__(method, requestInfo, responseInfo, sessionKey)
self.request_payload = {}
def _get_username(self, user):
# query splunkd for current user OR "user" if that is present
current_user = self.request.get('userName', None)
username = user if user is not None else current_user
if username is None:
message = 'Expecting a valid username instead of "{}".'.format(username)
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(400, message)
return username
def _get_user_all_roles(self):
'''
This method fetches all the roles of the current user unless a 'user' key is specified.
payload: key value arguments *optional*
{
"user" : <string>
}
@return json data
@raise UserAccessError on Errors
'''
data = self.args # for query params
if len(self.request_payload) > 0:
data = self.request_payload
LOG_PREFIX = '[user_all_roles] '
username = self._get_username(data.get('user'))
all_imported_roles = []
try:
all_imported_roles = UserAccess.fetch_all_user_roles(username, self.sessionKey, logger)
except BadRequest as e:
message = str(e)
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
except Exception as e:
message = 'Exception while polling splunkd for user {}. - {}.'.format(username, str(e))
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=500, message=message)
return all_imported_roles
def _get_user_roles(self):
'''
This method fetches the roles of the current user unless a 'user' key is specified.
payload: key value arguments *optional*
{
"user" : <string>
}
@return json data
@raise UserAccessError on Errors
'''
data = self.args # for query params
if len(self.request_payload) > 0:
data = self.request_payload
LOG_PREFIX = '[user_roles] '
username = self._get_username(data.get('user'))
user_roles = []
try:
user_roles = UserAccess.fetch_user_roles(username, self.sessionKey, logger)
except BadRequest as e:
message = str(e)
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
except Exception as e:
message = 'Exception while polling splunkd for user {}. - {}.'.format(username, str(e))
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=500, message=message)
return user_roles
def _get_user_capabilities(self):
'''
This method fetches the capabilities of the current user unless a user is specified
@param self: the self param
payload: key value arguments *optional*
{
"user":<string>
}
'''
data = self.args # for query params
if len(self.request_payload) > 0:
data = self.request_payload
LOG_PREFIX = '[user_capabilities] '
username = self._get_username(data.get('user'))
user_capabilities = []
try:
user_capabilities = UserAccess.fetch_user_capabilities(username, self.sessionKey, logger)
except BadRequest as e:
message = str(e)
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
except Exception as e:
message = 'Exception while polling splunkd for user {}. - {}.'.format(username, str(e))
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=500, message=message)
return user_capabilities
def _is_user_capable_all_ops(self):
'''
This method checks if a user is capable to access a certain ITOA object
If a 'user' key is present in payload, use that else, work on "current user"
@param self: The self param
mandatory keys: 'app_name', 'object_type'
optional keys: 'user'
Ex:
{
'user': string, # OPTIONAL
# username we need to work on
'owner': string, # OPTIONAL
# owner of the object we'd like to use for reference
'app_name': string, # MANDATORY
# app name i.e. itsi, es etc... We will use this as a key against the capability super matrix
'object_type': string, # MANDATORY
# object type under consideration i.e "glass_table", "deep_dive" etc...
}
'''
LOG_PREFIX = '[is_user_capable_all_ops] '
# check if mandatory keys are present..
data = self.args # for query params
if len(self.request_payload) > 0:
data = self.request_payload
mandatory_keys = ['app_name', 'object_type']
for key in mandatory_keys:
obj = data.get(key)
if obj is None:
message = 'Missing mandatory key "{0}" from "{1}".'.format(key, json.dumps(data))
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
username = self._get_username(data.get('user'))
session_key = self.sessionKey
object_type = data.get('object_type')
app_name = data.get('app_name')
object_owner = data.get('owner')
try:
app_capabilities = json.loads(UserAccess.get_app_capabilities(app_name, session_key, logger))
except BadRequest as e:
logger.error('%s %s', LOG_PREFIX, str(e))
raise UserAccessError(status=400, message=str(e))
if not app_capabilities:
message = '{0} has not registered itself yet. Make sure your app calls UserAccess.register_app_capabilities().'.format(
app_name)
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
# given object type and requested op, fetch capability name
capabilities_names, message = UserAccess.fetch_capabilities_names_all_ops(app_capabilities, object_type,
logger)
if capabilities_names is None:
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
try:
capabilities = UserAccess.is_user_capable_all_ops(username, object_type, capabilities_names,
session_key, logger, owner=object_owner)
except BadRequest as e:
logger.error('%s %s', LOG_PREFIX, str(e))
raise UserAccessError(status=400, message=str(e))
except Exception as e:
logger.error('%s %s', LOG_PREFIX, str(e))
raise UserAccessError(status=500, message=str(e))
render_msg = {
'username': username,
'permissions': {
'read': True,
'write': True,
'delete': True
},
'object_type': object_type,
'message': 'User "{0}" capabilities on objects of type "{1}".'.format(username, object_type)
}
if not capabilities['read']:
render_msg['permissions']['read'] = False
if not capabilities['write']:
render_msg['permissions']['write'] = False
if not capabilities['delete']:
render_msg['permissions']['delete'] = False
return render_msg
def is_user_capable(self):
'''
This method checks if a user is capable to access a certain ITOA object
If a 'user' key is present in payload, use that else, work on "current user"
@param self: The self param
mandatory keys: 'app_name', 'operation', 'object_type'
optional keys: 'user'
Ex:
{
'user': string, # OPTIONAL
# username we need to work on
'owner': string, # OPTIONAL
# owner of the object we'd like to use for reference
'app_name': string, # MANDATORY
# app name i.e. itsi, es etc... We will use this as a key against the capability super matrix
'object_type': string, # MANDATORY
# object type under consideration i.e "glass_table", "deep_dive" etc...
'operation': string # MANDATORY
# 'read/write'/'delete'
}
'''
LOG_PREFIX = '[is_user_capable] '
# check if mandatory keys are present..
data = self.args # for query params
if len(self.request_payload) > 0:
data = self.request_payload
mandatory_keys = ['app_name', 'object_type', 'operation']
for key in mandatory_keys:
obj = data.get(key)
if obj is None:
message = 'Missing mandatory key "{0}" from "{1}".'.format(key, json.dumps(data))
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
username = self._get_username(data.get('user'))
session_key = self.sessionKey
object_type = data.get('object_type')
operation = data.get('operation')
app_name = data.get('app_name')
object_owner = data.get('owner')
try:
app_capabilities = json.loads(UserAccess.get_app_capabilities(app_name, session_key, logger))
except BadRequest as e:
logger.error('%s %s', LOG_PREFIX, str(e))
raise UserAccessError(status=400, message=str(e))
if not app_capabilities:
message = '{0} has not registered itself yet. Make sure your app calls UserAccess.register_app_capabilities().'.format(
app_name)
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
# given object type and requested op, fetch capability name
capability_name, message = UserAccess.fetch_capability_name(app_capabilities, object_type, operation,
logger)
if capability_name is None:
logger.error('%s %s', LOG_PREFIX, message)
raise UserAccessError(status=400, message=message)
try:
user_is_capable = UserAccess.is_user_capable(username, capability_name, session_key, logger,
owner=object_owner)
except BadRequest as e:
logger.error('%s %s', LOG_PREFIX, str(e))
raise UserAccessError(status=400, message=str(e))
except Exception as e:
logger.error('%s %s', LOG_PREFIX, str(e))
raise UserAccessError(status=500, message=str(e))
render_msg = {
'username': username,
'is_capable': True,
'operation': operation,
'object_type': object_type,
'message': 'User "{0}" has the capability "{1}" on object type "{2}".'.format(username, operation,
object_type)
}
if user_is_capable:
render_msg['is_capable'] = True
render_msg['message'] = 'User "{0}" has the capability "{1}" on object type "{2}".'.format(username,
operation,
object_type)
else:
message = 'User "{0}" does not have the capability "{1}" on object type "{2}".'.format(username,
operation,
object_type)
render_msg['is_capable'] = False
render_msg['message'] = message
return render_msg
"""
Provides splunkd endpoints for deep dive operations
"""
@handle_path_terms
def handle_GET(self):
if self.pathParts[2] == 'user_all_roles':
user_roles = self._get_user_all_roles()
return self.response.write(self.render_json(user_roles))
if self.pathParts[2] == 'user_roles':
user_roles = self._get_user_roles()
return self.response.write(self.render_json(user_roles))
if self.pathParts[2] == 'user_capabilities':
user_capabilities = self._get_user_capabilities()
return self.response.write(self.render_json(user_capabilities))
if self.pathParts[2] == 'is_user_capable_all_ops':
render_msg = self._is_user_capable_all_ops()
return self.response.write(self.render_json(render_msg))
if self.pathParts[2] == 'is_user_capable':
render_msg = self.is_user_capable()
return self.response.write(self.render_json(render_msg))
@parse_splunkd_payload
@handle_path_terms
def handle_POST(self):
if self.pathParts[2] == 'user_roles':
user_roles = self._get_user_roles()
return self.response.write(self.render_json(user_roles))
if self.pathParts[2] == 'user_capabilities':
user_capabilities = self._get_user_capabilities()
return self.response.write(self.render_json(user_capabilities))
if self.pathParts[2] == 'is_user_capable_all_ops':
render_msg = self._is_user_capable_all_ops()
return self.response.write(self.render_json(render_msg))
if self.pathParts[2] == 'is_user_capable':
render_msg = self.is_user_capable()
return self.response.write(self.render_json(render_msg))