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.
Splunk_Deploiement/apps/trackme/bin/trackmetenantstatus.py

358 lines
14 KiB

#!/usr/bin/env python
# coding=utf-8
__author__ = "TrackMe Limited"
__copyright__ = "Copyright 2022-2026, TrackMe Limited, U.K."
__credits__ = "TrackMe Limited, U.K."
__license__ = "TrackMe Limited, all rights reserved"
__version__ = "0.1.0"
__maintainer__ = "TrackMe Limited, U.K."
__email__ = "support@trackme-solutions.com"
__status__ = "PRODUCTION"
# Standard library imports
import os
import sys
import time
import json
# Logging imports
import logging
from logging.handlers import RotatingFileHandler
# Networking imports
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# set splunkhome
splunkhome = os.environ["SPLUNK_HOME"]
# set logging
filehandler = RotatingFileHandler(
"%s/var/log/splunk/trackme_load_tenants_summary.log" % splunkhome,
mode="a",
maxBytes=10000000,
backupCount=1,
)
formatter = logging.Formatter(
"%(asctime)s %(levelname)s %(filename)s %(funcName)s %(lineno)d %(message)s"
)
logging.Formatter.converter = time.gmtime
filehandler.setFormatter(formatter)
log = logging.getLogger() # root logger - Good to get it only once.
for hdlr in log.handlers[:]: # remove the existing file handlers
if isinstance(hdlr, logging.FileHandler):
log.removeHandler(hdlr)
log.addHandler(filehandler) # set the new handler
# set the log level to INFO, DEBUG as the default is ERROR
log.setLevel(logging.INFO)
# append current directory
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# import libs
import import_declare_test
# import Splunk libs
from splunklib.searchcommands import (
dispatch,
GeneratingCommand,
Configuration,
Option,
validators,
)
# Import trackme libs
from trackme_libs import trackme_reqinfo
@Configuration(distributed=False)
class TrackMeTenantsStatus(GeneratingCommand):
tenant_id = Option(
doc="""
**Syntax:** **tenant_id=****
**Description:** Optional, the tenant identifier.""",
require=False,
default=None,
)
output = Option(
doc="""
**Syntax:** **output=****
**Description:** Optional, return the either the status per tenant/report (default), or the list of tenant the user is allowed to access to.
Valid options are: status | tenants""",
require=False,
default="status",
validate=validators.Match("output", r"^(status|tenants)$"),
)
def has_user_access(self, effective_roles, record):
tenant_roles_admin = (
set(record["tenant_roles_admin"])
if isinstance(record["tenant_roles_admin"], list)
else set(record["tenant_roles_admin"].split(","))
)
tenant_roles_power = (
set(record["tenant_roles_power"])
if isinstance(record["tenant_roles_power"], list)
else set(record["tenant_roles_power"].split(","))
)
tenant_roles_user = (
set(record["tenant_roles_user"])
if isinstance(record["tenant_roles_user"], list)
else set(record["tenant_roles_user"].split(","))
)
allowed_roles = (
tenant_roles_admin
| tenant_roles_user
| tenant_roles_power
| {"admin", "trackme_admin", "sc_admin"}
)
return bool(set(effective_roles) & allowed_roles)
def get_effective_roles(self, user_roles, roles_dict):
effective_roles = set(user_roles) # start with user's direct roles
to_check = list(user_roles) # roles to be checked for inherited roles
while to_check:
current_role = to_check.pop()
inherited_roles = roles_dict.get(current_role, [])
for inherited_role in inherited_roles:
if inherited_role not in effective_roles:
effective_roles.add(inherited_role)
to_check.append(inherited_role)
return effective_roles
def generate(self, **kwargs):
if self:
# Get request info and set logging level
reqinfo = trackme_reqinfo(
self._metadata.searchinfo.session_key,
self._metadata.searchinfo.splunkd_uri,
)
log.setLevel(reqinfo["logging_level"])
# get current user
username = self._metadata.searchinfo.username
# get user info
users = self.service.users
# Get roles for the current user
username_roles = []
for user in users:
if user.name == username:
username_roles = user.roles
logging.info(f'username="{username}", roles="{username_roles}"')
# get roles
roles = self.service.roles
roles_dict = {}
for role in roles:
imported_roles_value = role.content.get("imported_roles", [])
if imported_roles_value: # Check if it has a non-empty value
roles_dict[role.name] = imported_roles_value
logging.debug(f"roles_dict={json.dumps(roles_dict, indent=2)}")
# get effective roles, which takes into account both direct membership and inheritance
effective_roles = self.get_effective_roles(username_roles, roles_dict)
# Data collection
collection_name = "kv_trackme_virtual_tenants"
collection = self.service.kvstore[collection_name]
# Define the KV query search string
if self.tenant_id and self.tenant_id != "*":
query_string_filter = {
"tenant_id": self.tenant_id,
"tenant_status": "enabled",
}
elif self.tenant_id and self.tenant_id == "*":
query_string_filter = {
"tenant_status": "enabled",
}
else:
query_string_filter = {
"tenant_status": "enabled",
}
query_string = {"$and": [query_string_filter]}
# log debug
logging.debug(f"query string={json.dumps(query_string)}")
# Get the records
filtered_records = []
try:
records = collection.data.query(query=json.dumps(query_string))
# Loop through the records
for record in records:
# handle all other cases and use RBAC accordingly to the tenant
# log
logging.info(
f'checking permissions of user="{username}" with roles="{username_roles}" for tenant_id="{record["tenant_id"]}"'
)
if self.has_user_access(effective_roles, record) or username in (
"splunk-system-user"
):
filtered_records.append(record)
# For each record in records, get the tenant_id, component and load the status as a dict
for filtered_record in filtered_records:
tenant_id = filtered_record.get("tenant_id")
component = filtered_record.get("component")
# counter
count = 0
# Simply return the tenant record filtered from RBAC
if self.output == "tenants":
# Add schema_version_required to the record
schema_version_required = int(
reqinfo.get("schema_version_required")
)
filtered_record["schema_version_required"] = (
schema_version_required
)
# Set the status of tenant_updated_status, if schema_version in the record is equal to schema_version_required,
# the status is "updated", otherwise it is "pending"
# If schema_version_required is 0 (version retrieval failed), treat all tenants as "updated"
# to align with graceful degradation when DB Connect causes permission issues
if schema_version_required == 0:
filtered_record["tenant_updated_status"] = "updated"
else:
# Handle case where schema_version is missing from the record
schema_version_raw = filtered_record.get("schema_version")
if schema_version_raw is None:
# If schema_version is missing, use "undetermined" to indicate we cannot determine the status
# This is different from "pending" which implies an upgrade is in progress
filtered_record["tenant_updated_status"] = "undetermined"
elif int(schema_version_raw) == schema_version_required:
filtered_record["tenant_updated_status"] = "updated"
else:
filtered_record["tenant_updated_status"] = "pending"
# yield_record
yield_record = {}
for key, value in filtered_record.items():
if key == "_key":
continue
yield_record[key] = value
yield_record["_time"] = time.time()
yield_record["_raw"] = json.dumps(yield_record)
# yield
yield yield_record
# Handle and return the status record
elif self.output == "status":
try:
tenant_objects_exec_summary = json.loads(
filtered_record.get("tenant_objects_exec_summary")
)
# increment
count += 1
# For each report, render the status summary
for report in tenant_objects_exec_summary:
subrecord_dict = tenant_objects_exec_summary.get(report)
try:
last_duration = round(
float(subrecord_dict.get("last_duration")), 3
)
except Exception as e:
last_duration = 0
# get last_exec
last_exec = subrecord_dict.get("last_exec", None)
if last_exec:
# turn into a human readable format with strftime %c
try:
last_exec = float(last_exec)
last_exec = time.strftime(
"%c", time.localtime(last_exec)
)
except Exception as e:
pass
subrecord = {
"_time": time.time(),
"tenant_id": tenant_id,
"component": subrecord_dict.get("component"),
"report": report,
"earliest": subrecord_dict.get("earliest"),
"latest": subrecord_dict.get("latest"),
"last_status": subrecord_dict.get("last_status"),
"last_exec": last_exec,
"last_duration": last_duration,
"last_result": subrecord_dict.get("last_result"),
}
subrecord["_raw"] = json.dumps(subrecord)
# yield
yield subrecord
except Exception as e:
logging.warning(
f'failed to retrieve tenant_objects_exec_summary with exception="{str(e)}"'
)
nonerecord = {
"_time": time.time(),
"tenant_id": tenant_id,
"component": "none",
"report": "none",
"earliest": "none",
"latest": "none",
"last_status": "none",
"last_exec": "none",
"last_duration": "none",
"last_result": "none",
}
nonerecord["_raw"] = json.dumps(nonerecord)
# yield
yield nonerecord
# if there are no reports for this tenant, return 1
if count == 0:
nonerecord = {
"_time": time.time(),
"tenant_id": tenant_id,
"component": "none",
"report": "none",
"earliest": "none",
"latest": "none",
"last_status": "none",
"last_exec": "none",
"last_duration": "none",
"last_result": "none",
}
nonerecord["_raw"] = json.dumps(nonerecord)
# yield
yield nonerecord
except Exception as e:
# yield
yield {
"_time": str(time.time()),
"_raw": f'failed to retrieve tenant_objects_exec_summary with exception="{str(e)}"',
}
dispatch(TrackMeTenantsStatus, sys.argv, sys.stdin, sys.stdout, __name__)