From 2a12b268fd80c849371ccd556ad56f6d00e63927 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:44:54 -0600 Subject: [PATCH 01/22] require admin utils adding other admin types require --- TM1py/Utils/Utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/TM1py/Utils/Utils.py b/TM1py/Utils/Utils.py index 7b9e5837..31551aa2 100644 --- a/TM1py/Utils/Utils.py +++ b/TM1py/Utils/Utils.py @@ -47,6 +47,35 @@ def wrapper(self, *args, **kwargs): return wrapper +@decohints +def require_data_admin(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if not self.is_data_admin: + raise TM1pyNotDataAdminException(func.__name__) + return func(self, *args, **kwargs) + + return wrapper + +@decohints +def require_security_admin(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if not self.is_security_admin: + raise TM1pyNotSecurityAdminException(func.__name__) + return func(self, *args, **kwargs) + + return wrapper + +@decohints +def require_ops_admin(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if not self.is_ops_admin: + raise TM1pyNotOpsAdminException(func.__name__) + return func(self, *args, **kwargs) + + return wrapper @decohints def require_version(version): From bbafa093b3cacd40d454c997df23e82fbf6c3b2f Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:46:29 -0600 Subject: [PATCH 02/22] NotAdmin exceptions exceptions for other admin types --- TM1py/Exceptions/Exceptions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/TM1py/Exceptions/Exceptions.py b/TM1py/Exceptions/Exceptions.py index 90050273..c5e2696f 100644 --- a/TM1py/Exceptions/Exceptions.py +++ b/TM1py/Exceptions/Exceptions.py @@ -30,6 +30,26 @@ def __init__(self, function: str): def __str__(self): return f"Function '{self.function}' requires admin permissions" +class TM1pyNotDataAdminException(Exception): + def __init__(self, function: str): + self.function = function + + def __str__(self): + return f"Function '{self.function}' requires DataAdmin permissions" + +class TM1pyNotSecurityAdminException(Exception): + def __init__(self, function: str): + self.function = function + + def __str__(self): + return f"Function '{self.function}' requires SecurityAdmin permissions" + +class TM1pyNotOpsAdminException(Exception): + def __init__(self, function: str): + self.function = function + + def __str__(self): + return f"Function '{self.function}' requires OperationsAdmin permissions" class TM1pyException(Exception): """ The default exception for TM1py From 6efb9c4b0c561d3fe2ad61a912a98aa0c4c0917f Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:49:16 -0600 Subject: [PATCH 03/22] other admin types adding other admin types --- TM1py/Services/RestService.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/TM1py/Services/RestService.py b/TM1py/Services/RestService.py index 077752a9..65e8c4e7 100644 --- a/TM1py/Services/RestService.py +++ b/TM1py/Services/RestService.py @@ -215,8 +215,14 @@ def __init__(self, **kwargs): # populated on the fly if kwargs.get('user'): self._is_admin = True if case_and_space_insensitive_equals(kwargs.get('user'), 'ADMIN') else None + self._is_data_admin = True if case_and_space_insensitive_equals(kwargs.get('user'), 'ADMIN') else None + self._is_security_admin = True if case_and_space_insensitive_equals(kwargs.get('user'), 'ADMIN') else None + self._is_ops_admin = True if case_and_space_insensitive_equals(kwargs.get('user'), 'ADMIN') else None else: self._is_admin = None + self._is_data_admin = None + self._is_security_admin = None + self._is_ops_admin = None if 'verify' in kwargs: if isinstance(kwargs['verify'], str): @@ -492,6 +498,33 @@ def is_admin(self) -> bool: return self._is_admin + @property + def is_data_admin(self) -> bool: + if self._is_data_admin is None: + response = self.GET("/api/v1/ActiveUser/Groups") + self._is_data_admin = any(g in CaseAndSpaceInsensitiveSet( + *[group["Name"] for group in response.json()["value"]]) for g in ["Admin", "DataAdmin"]) + + return self._is_data_admin + + @property + def is_security_admin(self) -> bool: + if self._is_security_admin is None: + response = self.GET("/api/v1/ActiveUser/Groups") + self._is_security_admin = any(g in CaseAndSpaceInsensitiveSet( + *[group["Name"] for group in response.json()["value"]]) for g in ["Admin", "SecurityAdmin"]) + + return self._is_security_admin + + @property + def is_ops_admin(self) -> bool: + if self._is_ops_admin is None: + response = self.GET("/api/v1/ActiveUser/Groups") + self._is_ops_admin = any(g in CaseAndSpaceInsensitiveSet( + *[group["Name"] for group in response.json()["value"]]) for g in ["Admin", "OperationsAdmin"]) + + return self._is_ops_admin + @property def sandboxing_disabled(self): if self._sandboxing_disabled is None: From f6da4d806e37ad53cdca222bc07e943aa5ad8001 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:50:54 -0600 Subject: [PATCH 04/22] other admin types property --- TM1py/Services/ObjectService.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/TM1py/Services/ObjectService.py b/TM1py/Services/ObjectService.py index eb979412..dccb7608 100644 --- a/TM1py/Services/ObjectService.py +++ b/TM1py/Services/ObjectService.py @@ -69,3 +69,15 @@ def version(self) -> str: @property def is_admin(self) -> bool: return self._rest.is_admin + + @property + def is_data_admin(self) -> bool: + return self._rest.is_data_admin + + @property + def is_security_admin(self) -> bool: + return self._rest.is_security_admin + + @property + def is_ops_admin(self) -> bool: + return self._rest.is_ops_admin From 88be962f42351892f8e2c55b3a2434c85c95eb08 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:59:15 -0600 Subject: [PATCH 05/22] updated admin requirements --- TM1py/Services/ServerService.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/TM1py/Services/ServerService.py b/TM1py/Services/ServerService.py index 931dbc79..bb6401d2 100644 --- a/TM1py/Services/ServerService.py +++ b/TM1py/Services/ServerService.py @@ -13,8 +13,8 @@ from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService from TM1py.Utils import format_url -from TM1py.Utils.Utils import CaseAndSpaceInsensitiveDict, CaseAndSpaceInsensitiveSet, require_admin, require_version, \ - decohints +from TM1py.Utils.Utils import CaseAndSpaceInsensitiveDict, CaseAndSpaceInsensitiveSet, require_data_admin, \ + require_ops_admin, require_version, decohints @decohints @@ -103,7 +103,7 @@ def execute_message_log_delta_request(self, **kwargs) -> Dict: "MessageLogEntries/!delta('"):-2] return response.json()['value'] - @require_admin + @require_ops_admin def get_message_log_entries(self, reverse: bool = True, since: datetime = None, until: datetime = None, top: int = None, logger: str = None, level: str = None, msg_contains: Iterable = None, msg_contains_operator: str = 'and', @@ -174,7 +174,7 @@ def get_message_log_entries(self, reverse: bool = True, since: datetime = None, response = self._rest.GET(url, **kwargs) return response.json()['value'] - @require_admin + @require_ops_admin def write_to_message_log(self, level: str, message: str, **kwargs) -> None: """ :param level: string, FATAL, ERROR, WARN, INFO, DEBUG @@ -204,7 +204,7 @@ def utc_localize_time(timestamp): timestamp_utc = timestamp.astimezone(pytz.utc) return timestamp_utc - @require_admin + @require_data_admin def get_transaction_log_entries(self, reverse: bool = True, user: str = None, cube: str = None, since: datetime = None, until: datetime = None, top: int = None, element_tuple_filter: Dict[str, str] = None, @@ -256,7 +256,7 @@ def get_transaction_log_entries(self, reverse: bool = True, user: str = None, cu response = self._rest.GET(url, **kwargs) return response.json()['value'] - @require_admin + @require_data_admin @require_version(version="11.6") def get_audit_log_entries(self, user: str = None, object_type: str = None, object_name: str = None, since: datetime = None, until: datetime = None, top: int = None, **kwargs) -> Dict: @@ -301,7 +301,7 @@ def get_audit_log_entries(self, user: str = None, object_type: str = None, objec response = self._rest.GET(url, **kwargs) return response.json()['value'] - @require_admin + @require_ops_admin def get_last_process_message_from_messagelog(self, process_name: str, **kwargs) -> Optional[str]: """ Get the latest message log entry for a process @@ -349,7 +349,7 @@ def get_configuration(self, **kwargs) -> Dict: del config["@odata.context"] return config - @require_admin + @require_data_admin def get_static_configuration(self, **kwargs) -> Dict: """ Read TM1 config settings as dictionary from TM1 Server @@ -360,7 +360,7 @@ def get_static_configuration(self, **kwargs) -> Dict: del config["@odata.context"] return config - @require_admin + @require_data_admin def get_active_configuration(self, **kwargs) -> Dict: """ Read effective(!) TM1 config settings as dictionary from TM1 Server @@ -371,7 +371,7 @@ def get_active_configuration(self, **kwargs) -> Dict: del config["@odata.context"] return config - @require_admin + @require_data_admin def update_static_configuration(self, configuration: Dict) -> Response: """ Update the .cfg file and triggers TM1 to re-read the file. @@ -381,40 +381,40 @@ def update_static_configuration(self, configuration: Dict) -> Response: url = '/api/v1/StaticConfiguration' return self._rest.PATCH(url, json.dumps(configuration)) - @require_admin + @require_data_admin def save_data(self, **kwargs) -> Response: from TM1py.Services import ProcessService ti = "SaveDataAll;" process_service = ProcessService(self._rest) return process_service.execute_ti_code(ti, **kwargs) - @require_admin + @require_data_admin def delete_persistent_feeders(self, **kwargs) -> Response: from TM1py.Services import ProcessService ti = "DeleteAllPersistentFeeders;" process_service = ProcessService(self._rest) return process_service.execute_ti_code(ti, **kwargs) - @require_admin + @require_ops_admin def start_performance_monitor(self): config = { "Administration": {"PerformanceMonitorOn": True} } self.update_static_configuration(config) - @require_admin + @require_ops_admin def stop_performance_monitor(self): config = { "Administration": {"PerformanceMonitorOn": False} } self.update_static_configuration(config) - @require_admin + @require_ops_admin def activate_audit_log(self): config = {'Administration': {'AuditLog': {'Enable': True}}} self.update_static_configuration(config) - @require_admin + @require_ops_admin def deactivate_audit_log(self): config = {'Administration': {'AuditLog': {'Enable': False}}} self.update_static_configuration(config) From 98b385c849ccfbf1420b46298f8433f1b1fed2ef Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:00:28 -0600 Subject: [PATCH 06/22] updated admin requirements --- TM1py/Services/MonitoringService.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TM1py/Services/MonitoringService.py b/TM1py/Services/MonitoringService.py index 181da087..d8aaf533 100644 --- a/TM1py/Services/MonitoringService.py +++ b/TM1py/Services/MonitoringService.py @@ -6,7 +6,7 @@ from TM1py.Objects.User import User from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService -from TM1py.Utils import format_url, case_and_space_insensitive_equals, require_admin +from TM1py.Utils import format_url, case_and_space_insensitive_equals, require_ops_admin class MonitoringService(ObjectService): @@ -114,7 +114,7 @@ def get_sessions(self, include_user: bool = True, include_threads: bool = True, response = self._rest.GET(url, **kwargs) return response.json()["value"] - @require_admin + @require_ops_admin def disconnect_all_users(self, **kwargs) -> list: current_user = self.get_current_user(**kwargs) active_users = self.get_active_users(**kwargs) @@ -129,7 +129,7 @@ def close_session(self, session_id, **kwargs) -> Response: url = format_url(f"/api/v1/Sessions('{session_id}')/tm1.Close") return self._rest.POST(url, **kwargs) - @require_admin + @require_ops_admin def close_all_sessions(self, **kwargs) -> list: current_user = self.get_current_user(**kwargs) sessions = self.get_sessions(**kwargs) From 6fab97b95953e5ff77be61341f3cce102275daf9 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:01:39 -0600 Subject: [PATCH 07/22] updated admin requirements --- TM1py/Services/SecurityService.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/TM1py/Services/SecurityService.py b/TM1py/Services/SecurityService.py index 4f6e4927..6eea4ae5 100644 --- a/TM1py/Services/SecurityService.py +++ b/TM1py/Services/SecurityService.py @@ -8,7 +8,7 @@ from TM1py.Objects.User import User from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService -from TM1py.Utils.Utils import format_url, CaseAndSpaceInsensitiveSet, require_admin +from TM1py.Utils.Utils import format_url, CaseAndSpaceInsensitiveSet, require_security_admin class SecurityService(ObjectService): @@ -25,7 +25,7 @@ def determine_actual_user_name(self, user_name: str, **kwargs) -> str: def determine_actual_group_name(self, group_name: str, **kwargs) -> str: return self.determine_actual_object_name(object_class="Groups", object_name=group_name, **kwargs) - @require_admin + @require_security_admin def create_user(self, user: User, **kwargs) -> Response: """ Create a user on TM1 Server @@ -35,7 +35,7 @@ def create_user(self, user: User, **kwargs) -> Response: url = '/api/v1/Users' return self._rest.POST(url, user.body, **kwargs) - @require_admin + @require_security_admin def create_group(self, group_name: str, **kwargs) -> Response: """ Create a Security group in the TM1 Server @@ -67,7 +67,7 @@ def get_current_user(self, **kwargs) -> User: response = self._rest.GET(url, **kwargs) return User.from_dict(response.json()) - @require_admin + @require_security_admin def update_user(self, user: User, **kwargs) -> Response: """ Update user on TM1 Server @@ -86,7 +86,7 @@ def update_user_password(self, user_name: str, password: str, **kwargs) -> Respo body = {"Password": password} return self._rest.PATCH(url, json.dumps(body), **kwargs) - @require_admin + @require_security_admin def delete_user(self, user_name: str, **kwargs) -> Response: """ Delete user on TM1 Server @@ -97,7 +97,7 @@ def delete_user(self, user_name: str, **kwargs) -> Response: url = format_url("/api/v1/Users('{}')", user_name) return self._rest.DELETE(url, **kwargs) - @require_admin + @require_security_admin def delete_group(self, group_name: str, **kwargs) -> Response: """ Delete a group in the TM1 Server @@ -163,7 +163,7 @@ def get_groups(self, user_name: str, **kwargs) -> List[str]: response = self._rest.GET(url, **kwargs) return [group['Name'] for group in response.json()['value']] - @require_admin + @require_security_admin def add_user_to_groups(self, user_name: str, groups: Iterable[str], **kwargs) -> Response: """ @@ -182,7 +182,7 @@ def add_user_to_groups(self, user_name: str, groups: Iterable[str], **kwargs) -> } return self._rest.PATCH(url, json.dumps(body), **kwargs) - @require_admin + @require_security_admin def remove_user_from_group(self, group_name: str, user_name: str, **kwargs) -> Response: """ Remove user from group in TM1 Server @@ -205,7 +205,7 @@ def get_all_groups(self, **kwargs) -> List[str]: groups = [entry['Name'] for entry in response.json()['value']] return groups - @require_admin + @require_security_admin def security_refresh(self, **kwargs) -> Response: from TM1py.Services import ProcessService ti = "SecurityRefresh;" From eb267e94fa60717c6a6b9762e9d26a12dad9a598 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:03:11 -0600 Subject: [PATCH 08/22] update admin requirements --- TM1py/Services/CubeService.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TM1py/Services/CubeService.py b/TM1py/Services/CubeService.py index 406c4047..55284e65 100644 --- a/TM1py/Services/CubeService.py +++ b/TM1py/Services/CubeService.py @@ -10,7 +10,7 @@ from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService from TM1py.Services.ViewService import ViewService -from TM1py.Utils import format_url, require_version, require_admin, case_and_space_insensitive_equals +from TM1py.Utils import format_url, require_version, require_data_admin, case_and_space_insensitive_equals class CubeService(ObjectService): @@ -135,7 +135,7 @@ def check_rules(self, cube_name: str, **kwargs) -> Response: errors = response.json()["value"] return errors - @require_admin + @require_data_admin def delete(self, cube_name: str, **kwargs) -> Response: """ Delete a cube in TM1 @@ -289,7 +289,7 @@ def get_storage_dimension_order(self, cube_name: str, **kwargs) -> List[str]: response = self._rest.GET(url, **kwargs) return [dimension["Name"] for dimension in response.json()["value"]] - @require_admin + @require_data_admin @require_version(version="11.4") def update_storage_dimension_order(self, cube_name: str, dimension_names: Iterable[str]) -> float: """ Update the storage dimension order of a cube @@ -306,7 +306,7 @@ def update_storage_dimension_order(self, cube_name: str, dimension_names: Iterab response = self._rest.POST(url=url, data=json.dumps(payload)) return response.json()["value"] - @require_admin + @require_data_admin @require_version(version="11.6") def load(self, cube_name: str, **kwargs) -> Response: """ Load the cube into memory on the server @@ -317,7 +317,7 @@ def load(self, cube_name: str, **kwargs) -> Response: url = format_url("/api/v1/Cubes('{}')/tm1.Load", cube_name) return self._rest.POST(url=url, **kwargs) - @require_admin + @require_data_admin @require_version(version="11.6") def unload(self, cube_name: str, **kwargs) -> Response: """ Unload the cube from memory @@ -346,7 +346,7 @@ def unlock(self, cube_name: str, **kwargs) -> Response: url = format_url("/api/v1/Cubes('{}')/tm1.Unlock", cube_name) return self._rest.POST(url=url, **kwargs) - @require_admin + @require_data_admin def cube_save_data(self, cube_name: str, **kwargs) -> Response: """ Serializes a cube by saving data updates From eab1e3538f756fd4d2c3d30a72371fedfefcf624 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:04:17 -0600 Subject: [PATCH 09/22] updated admin requirements --- TM1py/Services/ElementService.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TM1py/Services/ElementService.py b/TM1py/Services/ElementService.py index c672d8d3..2cd06053 100644 --- a/TM1py/Services/ElementService.py +++ b/TM1py/Services/ElementService.py @@ -19,7 +19,7 @@ from TM1py.Objects import ElementAttribute, Element from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService -from TM1py.Utils import CaseAndSpaceInsensitiveDict, format_url, CaseAndSpaceInsensitiveSet, require_admin, \ +from TM1py.Utils import CaseAndSpaceInsensitiveDict, format_url, CaseAndSpaceInsensitiveSet, require_data_admin, \ dimension_hierarchy_element_tuple_from_unique_name, require_pandas, require_version from TM1py.Utils import build_element_unique_names, CaseAndSpaceInsensitiveTuplesDict @@ -1074,7 +1074,7 @@ def hierarchy_exists(self, dimension_name, hierarchy_name): hierarchy_service = self._get_hierarchy_service() return hierarchy_service.exists(dimension_name, hierarchy_name) - @require_admin + @require_data_admin def _element_is_ancestor_ti(self, dimension_name: str, hierarchy_name: str, element_name: str, ancestor_name: str) -> bool: process_service = self.get_process_service() From 800e60b5f68efa70fa8f834c8d778c5e02ea24c8 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:05:46 -0600 Subject: [PATCH 10/22] update admin requirements --- TM1py/Services/ProcessService.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TM1py/Services/ProcessService.py b/TM1py/Services/ProcessService.py index a4c355bb..a470ecbd 100644 --- a/TM1py/Services/ProcessService.py +++ b/TM1py/Services/ProcessService.py @@ -13,7 +13,7 @@ from TM1py.Objects.ProcessDebugBreakpoint import ProcessDebugBreakpoint from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService -from TM1py.Utils import format_url, require_admin +from TM1py.Utils import format_url, require_data_admin from TM1py.Utils.Utils import require_version @@ -333,7 +333,7 @@ def execute_with_return(self, process_name: str, timeout: float = None, cancel_a "Filename"] return success, status, error_log_file - @require_admin + @require_data_admin def execute_ti_code(self, lines_prolog: Iterable[str], lines_epilog: Iterable[str] = None, **kwargs) -> Response: """ Execute lines of code on the TM1 Server @@ -572,7 +572,7 @@ def debug_get_current_breakpoint(self, debug_id: str, **kwargs) -> ProcessDebugB response = self._rest.GET(url=url, **kwargs) return ProcessDebugBreakpoint.from_dict(response.json()["CurrentBreakpoint"]) - @require_admin + @require_data_admin def evaluate_boolean_ti_expression(self, formula: str): prolog_procedure = f""" if (~{formula.strip(";")}); @@ -589,7 +589,7 @@ def evaluate_boolean_ti_expression(self, formula: str): else: raise TM1pyException(f"Unexpected TI return status: '{status}' for expression: '{formula}'") - @require_admin + @require_data_admin def evaluate_ti_expression(self, formula: str, **kwargs) -> str: """ This function is same functionality as hitting "Evaluate" within variable formula editor in TI Function creates temporary TI and then starts a debug session on that TI From 2d89f8e6fcf627ea8e1acf5c309f5a7be221c4c2 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:09:57 -0600 Subject: [PATCH 11/22] admin requirement update --- TM1py/Services/CellService.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/TM1py/Services/CellService.py b/TM1py/Services/CellService.py index a1fe29e5..84107ede 100644 --- a/TM1py/Services/CellService.py +++ b/TM1py/Services/CellService.py @@ -32,7 +32,7 @@ from TM1py.Utils.Utils import build_pandas_dataframe_from_cellset, dimension_name_from_element_unique_name, \ CaseAndSpaceInsensitiveDict, wrap_in_curly_braces, CaseAndSpaceInsensitiveTuplesDict, \ abbreviate_mdx, build_csv_from_cellset_dict, require_version, require_pandas, build_cellset_from_pandas_dataframe, \ - case_and_space_insensitive_equals, get_cube, resembles_mdx, require_admin, extract_compact_json_cellset, \ + case_and_space_insensitive_equals, get_cube, resembles_mdx, require_data_admin, extract_compact_json_cellset, \ cell_is_updateable, build_mdx_from_cellset, build_mdx_and_values_from_cellset, \ dimension_names_from_element_unique_names, frame_to_significant_digits, build_dataframe_from_csv, \ drop_dimension_properties, decohints @@ -525,7 +525,7 @@ def clear_spread( return self._post_against_cellset(cellset_id=cellset_id, payload=payload, delete_cellset=True, sandbox_name=sandbox_name, **kwargs) - @require_admin + @require_data_admin @require_version(version="11.7") def clear(self, cube: str, **kwargs): """ @@ -566,7 +566,7 @@ def clear(self, cube: str, **kwargs): return self.clear_with_mdx(cube=cube, mdx=mdx_builder.to_mdx(), **kwargs) - @require_admin + @require_data_admin @require_version(version="11.7") def clear_with_mdx(self, cube: str, mdx: str, sandbox_name: str = None, **kwargs): """ clear a slice in a cube based on an MDX query. @@ -954,7 +954,7 @@ def drop_non_updateable_cells(self, cells: Dict, cube_name: str, dimensions: Lis updateable_cells[elements] = cells[elements] return updateable_cells - @require_admin + @require_data_admin @manage_transaction_log def write_through_unbound_process(self, cube_name: str, cellset_as_dict: Dict, increment: bool = False, sandbox_name: str = None, precision: int = None, @@ -1040,7 +1040,7 @@ def write_through_unbound_process(self, cube_name: str, cellset_as_dict: Dict, i if not all(successes): raise TM1pyWritePartialFailureException(statuses, log_files, len(successes)) - @require_admin + @require_data_admin @manage_transaction_log @require_pandas def write_through_blob(self, cube_name: str, cellset_as_dict: dict, increment: bool = False, @@ -3783,7 +3783,7 @@ def _get_attributes_by_dimension(self, cube: str, **kwargs) -> Dict[str, List[st return attributes_by_dimension - @require_admin + @require_data_admin def _execute_view_csv_use_blob(self, cube_name: str, view_name: str, top: int, skip: int, skip_zeros: bool, skip_consolidated_cells: bool, skip_rule_derived_cells: bool, value_separator: str, cube_dimensions: List[str] = None, @@ -3914,7 +3914,7 @@ def _execute_view_csv_use_blob(self, cube_name: str, view_name: str, top: int, s with suppress(Exception): file_service.delete(file_name) - @require_admin + @require_data_admin def _execute_mdx_csv_use_blob(self, mdx: Union[str, MdxBuilder], top: int, skip: int, skip_zeros: bool, skip_consolidated_cells: bool, skip_rule_derived_cells: bool, value_separator: str, cube_dimensions: List[str] = None, From 854b6d90e65b8f8bc64bbc84ce679ac71e3caf9f Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:11:23 -0600 Subject: [PATCH 12/22] Update HierarchyService.py --- TM1py/Services/HierarchyService.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TM1py/Services/HierarchyService.py b/TM1py/Services/HierarchyService.py index 9b7fcbfd..286e1154 100644 --- a/TM1py/Services/HierarchyService.py +++ b/TM1py/Services/HierarchyService.py @@ -21,7 +21,7 @@ from TM1py.Services.RestService import RestService from TM1py.Services.SubsetService import SubsetService from TM1py.Utils.Utils import case_and_space_insensitive_equals, format_url, CaseAndSpaceInsensitiveDict, \ - CaseAndSpaceInsensitiveSet, CaseAndSpaceInsensitiveTuplesDict, require_pandas, require_admin + CaseAndSpaceInsensitiveSet, CaseAndSpaceInsensitiveTuplesDict, require_pandas, require_data_admin class HierarchyService(ObjectService): @@ -385,7 +385,7 @@ def is_balanced(self, dimension_name: str, hierarchy_name: str, **kwargs): raise RuntimeError(f"Unexpected return value from TM1 API request: {str(structure)}") @require_pandas - @require_admin + @require_data_admin def update_or_create_hierarchy_from_dataframe( self, dimension_name: str, From c88d4cd60579bdd01a862cf18e557d9b05f75c28 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 5 Jan 2024 04:43:54 -0600 Subject: [PATCH 13/22] Update Utils.py other NotAdmin exceptions --- TM1py/Utils/Utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TM1py/Utils/Utils.py b/TM1py/Utils/Utils.py index 31551aa2..6dd149ca 100644 --- a/TM1py/Utils/Utils.py +++ b/TM1py/Utils/Utils.py @@ -17,7 +17,8 @@ from mdxpy import MdxBuilder, Member from requests.adapters import HTTPAdapter -from TM1py.Exceptions.Exceptions import TM1pyVersionException, TM1pyNotAdminException +from TM1py.Exceptions.Exceptions import (TM1pyVersionException, TM1pyNotAdminException, TM1pyNotDataAdminException, +TM1pyNotSecurityAdminException, TM1pyNotOpsAdminException) try: import pandas as pd From 56d9dab1b3044cc0b0a5ca4d22f8cb4aa22b087a Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:34:18 -0600 Subject: [PATCH 14/22] Update ServerService bug fix --- TM1py/Services/ServerService.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TM1py/Services/ServerService.py b/TM1py/Services/ServerService.py index bb6401d2..ca2422b7 100644 --- a/TM1py/Services/ServerService.py +++ b/TM1py/Services/ServerService.py @@ -174,7 +174,7 @@ def get_message_log_entries(self, reverse: bool = True, since: datetime = None, response = self._rest.GET(url, **kwargs) return response.json()['value'] - @require_ops_admin + @require_data_admin def write_to_message_log(self, level: str, message: str, **kwargs) -> None: """ :param level: string, FATAL, ERROR, WARN, INFO, DEBUG @@ -349,7 +349,7 @@ def get_configuration(self, **kwargs) -> Dict: del config["@odata.context"] return config - @require_data_admin + @require_ops_admin def get_static_configuration(self, **kwargs) -> Dict: """ Read TM1 config settings as dictionary from TM1 Server @@ -360,7 +360,7 @@ def get_static_configuration(self, **kwargs) -> Dict: del config["@odata.context"] return config - @require_data_admin + @require_ops_admin def get_active_configuration(self, **kwargs) -> Dict: """ Read effective(!) TM1 config settings as dictionary from TM1 Server @@ -371,7 +371,7 @@ def get_active_configuration(self, **kwargs) -> Dict: del config["@odata.context"] return config - @require_data_admin + @require_ops_admin def update_static_configuration(self, configuration: Dict) -> Response: """ Update the .cfg file and triggers TM1 to re-read the file. From 924208d491e558232ed89a33e5803c2ade5a1544 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:36:10 -0600 Subject: [PATCH 15/22] Update MonitoringService require admin bug fix --- TM1py/Services/MonitoringService.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TM1py/Services/MonitoringService.py b/TM1py/Services/MonitoringService.py index d8aaf533..181da087 100644 --- a/TM1py/Services/MonitoringService.py +++ b/TM1py/Services/MonitoringService.py @@ -6,7 +6,7 @@ from TM1py.Objects.User import User from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService -from TM1py.Utils import format_url, case_and_space_insensitive_equals, require_ops_admin +from TM1py.Utils import format_url, case_and_space_insensitive_equals, require_admin class MonitoringService(ObjectService): @@ -114,7 +114,7 @@ def get_sessions(self, include_user: bool = True, include_threads: bool = True, response = self._rest.GET(url, **kwargs) return response.json()["value"] - @require_ops_admin + @require_admin def disconnect_all_users(self, **kwargs) -> list: current_user = self.get_current_user(**kwargs) active_users = self.get_active_users(**kwargs) @@ -129,7 +129,7 @@ def close_session(self, session_id, **kwargs) -> Response: url = format_url(f"/api/v1/Sessions('{session_id}')/tm1.Close") return self._rest.POST(url, **kwargs) - @require_ops_admin + @require_admin def close_all_sessions(self, **kwargs) -> list: current_user = self.get_current_user(**kwargs) sessions = self.get_sessions(**kwargs) From 0974838bd74fb74037d5566f05218cd8a3a0454d Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:44:12 -0600 Subject: [PATCH 16/22] Update SecurityService require admin bug fix --- TM1py/Services/SecurityService.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TM1py/Services/SecurityService.py b/TM1py/Services/SecurityService.py index 6eea4ae5..81ecf628 100644 --- a/TM1py/Services/SecurityService.py +++ b/TM1py/Services/SecurityService.py @@ -8,7 +8,7 @@ from TM1py.Objects.User import User from TM1py.Services.ObjectService import ObjectService from TM1py.Services.RestService import RestService -from TM1py.Utils.Utils import format_url, CaseAndSpaceInsensitiveSet, require_security_admin +from TM1py.Utils.Utils import format_url, CaseAndSpaceInsensitiveSet, require_security_admin, require_admin class SecurityService(ObjectService): @@ -205,7 +205,7 @@ def get_all_groups(self, **kwargs) -> List[str]: groups = [entry['Name'] for entry in response.json()['value']] return groups - @require_security_admin + @require_admin def security_refresh(self, **kwargs) -> Response: from TM1py.Services import ProcessService ti = "SecurityRefresh;" From 13819f4762f1b8976296a801589d4c12f228b46f Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:54:28 -0600 Subject: [PATCH 17/22] Update CellService require admin bug fix --- TM1py/Services/CellService.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/TM1py/Services/CellService.py b/TM1py/Services/CellService.py index 84107ede..215de574 100644 --- a/TM1py/Services/CellService.py +++ b/TM1py/Services/CellService.py @@ -32,7 +32,7 @@ from TM1py.Utils.Utils import build_pandas_dataframe_from_cellset, dimension_name_from_element_unique_name, \ CaseAndSpaceInsensitiveDict, wrap_in_curly_braces, CaseAndSpaceInsensitiveTuplesDict, \ abbreviate_mdx, build_csv_from_cellset_dict, require_version, require_pandas, build_cellset_from_pandas_dataframe, \ - case_and_space_insensitive_equals, get_cube, resembles_mdx, require_data_admin, extract_compact_json_cellset, \ + case_and_space_insensitive_equals, get_cube, resembles_mdx, require_data_admin, require_ops_admin, extract_compact_json_cellset, \ cell_is_updateable, build_mdx_from_cellset, build_mdx_and_values_from_cellset, \ dimension_names_from_element_unique_names, frame_to_significant_digits, build_dataframe_from_csv, \ drop_dimension_properties, decohints @@ -526,6 +526,7 @@ def clear_spread( sandbox_name=sandbox_name, **kwargs) @require_data_admin + @require_ops_admin @require_version(version="11.7") def clear(self, cube: str, **kwargs): """ @@ -567,6 +568,7 @@ def clear(self, cube: str, **kwargs): return self.clear_with_mdx(cube=cube, mdx=mdx_builder.to_mdx(), **kwargs) @require_data_admin + @require_ops_admin @require_version(version="11.7") def clear_with_mdx(self, cube: str, mdx: str, sandbox_name: str = None, **kwargs): """ clear a slice in a cube based on an MDX query. @@ -955,6 +957,7 @@ def drop_non_updateable_cells(self, cells: Dict, cube_name: str, dimensions: Lis return updateable_cells @require_data_admin + @require_ops_admin @manage_transaction_log def write_through_unbound_process(self, cube_name: str, cellset_as_dict: Dict, increment: bool = False, sandbox_name: str = None, precision: int = None, @@ -1041,6 +1044,7 @@ def write_through_unbound_process(self, cube_name: str, cellset_as_dict: Dict, i raise TM1pyWritePartialFailureException(statuses, log_files, len(successes)) @require_data_admin + @require_ops_admin @manage_transaction_log @require_pandas def write_through_blob(self, cube_name: str, cellset_as_dict: dict, increment: bool = False, @@ -3784,6 +3788,7 @@ def _get_attributes_by_dimension(self, cube: str, **kwargs) -> Dict[str, List[st return attributes_by_dimension @require_data_admin + @require_ops_admin def _execute_view_csv_use_blob(self, cube_name: str, view_name: str, top: int, skip: int, skip_zeros: bool, skip_consolidated_cells: bool, skip_rule_derived_cells: bool, value_separator: str, cube_dimensions: List[str] = None, @@ -3915,6 +3920,7 @@ def _execute_view_csv_use_blob(self, cube_name: str, view_name: str, top: int, s file_service.delete(file_name) @require_data_admin + @require_ops_admin def _execute_mdx_csv_use_blob(self, mdx: Union[str, MdxBuilder], top: int, skip: int, skip_zeros: bool, skip_consolidated_cells: bool, skip_rule_derived_cells: bool, value_separator: str, cube_dimensions: List[str] = None, From b9641b69bb44209f873655bcbbfdf94c40b66426 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:56:24 -0600 Subject: [PATCH 18/22] Update User other admin properties --- TM1py/Objects/User.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/TM1py/Objects/User.py b/TM1py/Objects/User.py index 33ad26f7..4f084779 100644 --- a/TM1py/Objects/User.py +++ b/TM1py/Objects/User.py @@ -76,6 +76,21 @@ def password(self) -> str: @property def is_admin(self) -> bool: return "ADMIN" in CaseAndSpaceInsensitiveSet(*self.groups) + + @property + def is_data_admin(self) -> bool: + return any(g in CaseAndSpaceInsensitiveSet( + *self.groups) for g in ["Admin", "DataAdmin"]) + + @property + def is_security_admin(self) -> bool: + return any(g in CaseAndSpaceInsensitiveSet( + *self.groups) for g in ["Admin", "SecurityAdmin"]) + + @property + def is_ops_admin(self) -> bool: + return any(g in CaseAndSpaceInsensitiveSet( + *self.groups) for g in ["Admin", "OperationsAdmin"]) @property def groups(self) -> List[str]: From f5b8b876dd1770d59e6de90d463341e0d088afca Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:37:50 -0600 Subject: [PATCH 19/22] Update HierarchyService bug fix update_or_create_hierarchy_from_dataframe requires both data and ops admin --- TM1py/Services/HierarchyService.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TM1py/Services/HierarchyService.py b/TM1py/Services/HierarchyService.py index f12dd4a1..d4acc5da 100644 --- a/TM1py/Services/HierarchyService.py +++ b/TM1py/Services/HierarchyService.py @@ -21,7 +21,7 @@ from TM1py.Services.RestService import RestService from TM1py.Services.SubsetService import SubsetService from TM1py.Utils.Utils import case_and_space_insensitive_equals, format_url, CaseAndSpaceInsensitiveDict, \ - CaseAndSpaceInsensitiveSet, CaseAndSpaceInsensitiveTuplesDict, require_pandas, require_data_admin + CaseAndSpaceInsensitiveSet, CaseAndSpaceInsensitiveTuplesDict, require_pandas, require_data_admin, require_ops_admin class HierarchyService(ObjectService): @@ -398,6 +398,7 @@ def is_balanced(self, dimension_name: str, hierarchy_name: str, **kwargs): @require_pandas @require_data_admin + @require_ops_admin def update_or_create_hierarchy_from_dataframe( self, dimension_name: str, From 345ebfc21a83eb185362ed07aaddbc674c3bfa7e Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Mon, 15 Jan 2024 08:38:56 -0600 Subject: [PATCH 20/22] Update TM1py/Services/RestService.py Co-authored-by: Marius Wirtz --- TM1py/Services/RestService.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TM1py/Services/RestService.py b/TM1py/Services/RestService.py index dfd03857..79a8c23b 100644 --- a/TM1py/Services/RestService.py +++ b/TM1py/Services/RestService.py @@ -821,7 +821,7 @@ def is_security_admin(self) -> bool: @property def is_ops_admin(self) -> bool: if self._is_ops_admin is None: - response = self.GET("/api/v1/ActiveUser/Groups") + response = self.GET("/ActiveUser/Groups") self._is_ops_admin = any(g in CaseAndSpaceInsensitiveSet( *[group["Name"] for group in response.json()["value"]]) for g in ["Admin", "OperationsAdmin"]) From 47ba8c97eb58f921358166bb9fd1b03e0da96e6f Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Mon, 15 Jan 2024 08:39:27 -0600 Subject: [PATCH 21/22] Update TM1py/Services/RestService.py Co-authored-by: Marius Wirtz --- TM1py/Services/RestService.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TM1py/Services/RestService.py b/TM1py/Services/RestService.py index 79a8c23b..e88e9178 100644 --- a/TM1py/Services/RestService.py +++ b/TM1py/Services/RestService.py @@ -803,7 +803,7 @@ def is_admin(self) -> bool: @property def is_data_admin(self) -> bool: if self._is_data_admin is None: - response = self.GET("/api/v1/ActiveUser/Groups") + response = self.GET("ActiveUser/Groups") self._is_data_admin = any(g in CaseAndSpaceInsensitiveSet( *[group["Name"] for group in response.json()["value"]]) for g in ["Admin", "DataAdmin"]) From 06429f07aa153904010cc6f979b96e6b02be8eb8 Mon Sep 17 00:00:00 2001 From: AndrewScheevel <31583660+adscheevel@users.noreply.github.com> Date: Mon, 15 Jan 2024 08:39:36 -0600 Subject: [PATCH 22/22] Update TM1py/Services/RestService.py Co-authored-by: Marius Wirtz --- TM1py/Services/RestService.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TM1py/Services/RestService.py b/TM1py/Services/RestService.py index e88e9178..9509dec9 100644 --- a/TM1py/Services/RestService.py +++ b/TM1py/Services/RestService.py @@ -812,7 +812,7 @@ def is_data_admin(self) -> bool: @property def is_security_admin(self) -> bool: if self._is_security_admin is None: - response = self.GET("/api/v1/ActiveUser/Groups") + response = self.GET("/ActiveUser/Groups") self._is_security_admin = any(g in CaseAndSpaceInsensitiveSet( *[group["Name"] for group in response.json()["value"]]) for g in ["Admin", "SecurityAdmin"])