Source code for oci_policy_analysis.presentation.desktop.condition_tester_tab

##########################################################################
# Copyright (c) 2025, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
#
# condition_tester_tab.py
#
# Tab for interactive OCI Condition Tester (AST-based) with simulated variables
#
# @author: Cline AI
#
# coding: utf-8
##########################################################################

import tkinter as tk
from datetime import datetime
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText

from oci_policy_analysis.application.core.support.logger import get_logger
from oci_policy_analysis.application.core.support.usage_tracking import get_usage_tracker
from oci_policy_analysis.application.services.condition_tester_service import ConditionTesterService
from oci_policy_analysis.presentation.desktop.base_tab import BaseUITab

logger = get_logger('condition_tester_tab')


[docs] class ConditionTesterTab(BaseUITab): """ UI Tab for interactively testing OCI Condition (Where) clauses using an AST simulation (ANTLR). Allows user to provide a where clause and simulated environment variables, and see evaluation/log details. """ def __init__(self, parent, app): super().__init__( parent, default_help_text=( 'Test OCI policy condition (where) clauses interactively. Enter the condition clause, ' 'generate input variables, and evaluate result using simulated inputs below. ' "Use 'Actions' for formatting, generating, or evaluating the expression." ), ) self.app = app self.condition_service = ConditionTesterService() self._build_ui()
[docs] def apply_settings(self, context_help: bool, font_size: str): """ Apply main settings for context help and font size. """ super().apply_settings(context_help, font_size)
def _build_ui(self): # Row 0: Where Clause Entry (multiline, with Format) clause_frame = ttk.LabelFrame(self, text='Condition (Where) Clause') clause_frame.pack(fill=tk.X, padx=10, pady=5) self.add_context_help( clause_frame, "Enter an OCI policy 'where' condition clause here. Example: all { request.principal.id=target.bucket.owner.id }", ) # Make clause text 6 lines, resizable if tab expands self.clause_text = ScrolledText(clause_frame, height=10, width=120, wrap=tk.WORD, font=('Consolas', 10)) self.clause_text.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.BOTH, expand=True) # Keep clause_var for compatibility, but keep in sync with clause_text content self.clause_var = tk.StringVar() def sync_clause_var(event=None): self.clause_var.set(self.clause_text.get('1.0', tk.END).strip()) self.clause_text.bind('<FocusOut>', sync_clause_var) self.add_context_help(self.clause_text, 'Edit the policy WHERE clause here (multi-line allowed).') # Actions Frame: Row 1+3 together in a single label frame, all horizontally actions_frame = ttk.LabelFrame(self, text='Actions') actions_frame.pack(fill=tk.X, padx=10, pady=(2, 6)) self.add_context_help( actions_frame, 'Use these action buttons to generate variable inputs, format the clause, ' 'evaluate the clause with variables, or clear the output log.', ) gen_btn = ttk.Button(actions_frame, text='Generate/Clear Inputs', command=self._generate_inputs) gen_btn.pack(side=tk.LEFT, padx=(8, 8), pady=7) self.add_context_help(gen_btn, 'Parse the clause and generate input fields for required variables.') format_btn = ttk.Button(actions_frame, text='Format', command=self._format_clause) format_btn.pack(side=tk.LEFT, padx=(0, 8), pady=7) self.add_context_help(format_btn, 'Auto-format and indent the condition clause for readability.') eval_btn = ttk.Button(actions_frame, text='Evaluate Condition', command=self._evaluate_condition) eval_btn.pack(side=tk.LEFT, padx=(0, 8), pady=7) self.add_context_help(eval_btn, 'Run the condition clause against provided input variables.') clear_btn = ttk.Button(actions_frame, text='Clear Output', command=self._clear_output) clear_btn.pack(side=tk.LEFT, padx=(0, 8), pady=7) self.add_context_help(clear_btn, 'Clear the output/results log below.') # Row 2: Simulated Inputs (Tk widgets, dynamic) - dynamically sized to content self.vars_frame = ttk.LabelFrame(self, text='Simulated Input Variables') self.vars_frame.pack(fill=tk.X, padx=10, pady=5) self.input_widgets = {} self.add_context_help(self.vars_frame, 'Input example values for the variables referenced in your clause.') # Row 4: Results/Log results_group = ttk.LabelFrame(self, text='Evaluation Result / Log') results_group.pack(fill=tk.BOTH, padx=10, pady=5, expand=True) self.results_text = ScrolledText(results_group, height=12, width=100, wrap=tk.WORD, font=('Consolas', 10)) self.results_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.results_text.insert(tk.END, 'Result details will appear here.\n') self.add_context_help(self.results_text, 'Result log: evaluation result, step-by-step comparison/computation.') # Setup tags for colored logging self.results_text.tag_configure('log_granted', foreground='green') self.results_text.tag_configure('log_denied', foreground='red') self.results_text.tag_configure('log_head', foreground='#006699', font=('Consolas', 11, 'bold'))
[docs] def set_clause_text(self, text): """Programmatically set the contents of the where clause input.""" self.clause_text.delete('1.0', tk.END) self.clause_text.insert(tk.END, text) self.clause_var.set(text.strip())
# === NEW: Extract variables from clause / generate input fields === def _generate_inputs(self): clause = self.clause_text.get('1.0', tk.END).strip() self.clause_var.set(clause) var_names = self.condition_service.extract_variables(clause) logger.info(f'Generating inputs for clause: {clause}') logger.info(f'Extracted variables ({len(var_names)}): {sorted(var_names)}') for widget in self.vars_frame.winfo_children(): widget.destroy() self.input_widgets = {} # Let frame's dynamic height adjust to number of rows (no fixed height) EXAMPLES = { 'request.utc-timestamp': 'e.g. 2026-01-05T12:34:56Z', 'request.utc-timestamp.time-of-day': 'e.g. 13:27:00Z', } for i, var in enumerate(sorted(var_names)): var_label = ttk.Label(self.vars_frame, text=var + ':') var_label.grid(row=i, column=0, sticky=tk.W, padx=4, pady=2) var_str = tk.StringVar() entry = ttk.Entry(self.vars_frame, textvariable=var_str, width=40) entry.grid(row=i, column=1, padx=4, pady=2) # Add format example to the right if this is a time/timestamp variable example_hint = '' if var == 'request.utc-timestamp': example_hint = EXAMPLES['request.utc-timestamp'] elif var == 'request.utc-timestamp.time-of-day': example_hint = EXAMPLES['request.utc-timestamp.time-of-day'] if example_hint: ttk.Label( self.vars_frame, text=example_hint, foreground='#666', font=('TkDefaultFont', 9, 'italic') ).grid(row=i, column=2, padx=(2, 2), sticky='w') self.input_widgets[var] = var_str # Adjust frame minheight to match variable count for cleaner look self.vars_frame.update_idletasks() frame_height = max(40, len(var_names) * 34) # Rough estimate ~34px per row self.vars_frame.config(height=frame_height) def _format_clause(self): """Format the where-clause text using shared condition service logic.""" raw = self.clause_text.get('1.0', tk.END).strip() try: pretty = self.condition_service.format_clause(raw) if pretty and pretty != raw: self.clause_text.delete('1.0', tk.END) self.clause_text.insert(tk.END, pretty) self.clause_var.set(pretty) logger.info('Condition clause formatted with beautifier.') except Exception as ex: logger.warning(f'Clause formatting failed: {ex}') # === Evaluate Logic Embedded === def _evaluate_condition(self): clause = self.clause_text.get('1.0', tk.END).strip() self.clause_var.set(clause) sim_vars = {k: v.get() for k, v in self.input_widgets.items()} logger.info(f'Evaluate condition: {clause}') logger.info(f'Using simulated variables: {sim_vars}') # Anonymous usage tracking: record condition test metadata (no clause text). try: tracker = get_usage_tracker() if tracker is not None: tracker.track_operation( 'condition_test', var_count=len(sim_vars), ) except Exception: logger.debug('Usage tracking for condition_test failed', exc_info=True) # We should get this from the main in __init__ and not try to initialize it here try: evaluated = self.condition_service.evaluate(clause, sim_vars) result = { 'Condition String': evaluated.condition_string, 'Policy Result': evaluated.policy_result, 'Log': evaluated.log, } except Exception as ex: logger.error(f'Evaluation error: {ex}') result = { 'Condition String': clause, 'Policy Result': 'ERROR', 'Log': [ { 'type': 'Exception', 'result': False, 'variable': '?', 'operator': '?', 'sim_value': '?', 'expected': '?', 'info': str(ex), } ], } self._show_result(result) def _clear_output(self): self.results_text.delete('1.0', tk.END) # No longer needed: The condition test logic is now unified in the PolicySimulationEngine. def _show_result(self, result_dict): # noqa: C901 # Robustify: Accept tuple or dict for compatibility if isinstance(result_dict, tuple): # Handle tuple of (bool, string) or sometimes (bool, dict) as returned from engine._evaluate_conditions # Try to convert to a display dict if len(result_dict) == 2 and isinstance(result_dict[0], bool) and isinstance(result_dict[1], str): granted = result_dict[0] details = result_dict[1] # Try to extract a variable/operator/result from the details using regex if 'Details:' in details and 'variable' in details: # Try to eval() the details dict just for display if present import ast try: detail_part = details.split('Details:', 1)[1].strip() detail_dict = ast.literal_eval(detail_part) if '{' in detail_part and '}' in detail_part else {} log_list = [detail_dict] if isinstance(detail_dict, dict) and detail_dict else [] except Exception: log_list = [] else: log_list = [] result_dict = { 'Condition String': '', 'Policy Result': 'GRANTED' if granted else 'DENIED', 'Log': log_list if log_list else [ { 'type': 'No detail', 'result': granted, 'variable': '?', 'operator': '?', 'sim_value': '?', 'expected': '?', 'info': details, } ], } else: # Fallback: try to find an embedded dict as before found_dict = None for item in result_dict: if isinstance(item, dict) and ( 'Policy Result' in item or 'Log' in item or 'Condition String' in item ): found_dict = item break if found_dict is not None: result_dict = found_dict else: self.results_text.insert(tk.END, f'\n[Result Error] Unrecognized tuple returned: {result_dict}\n') return # Do NOT clear previous log; append a timestamped header and result. now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.results_text.insert(tk.END, f"\n{'='*20} {now} {'='*20}\n", 'log_head') cond_str = result_dict.get('Condition String') self.results_text.insert(tk.END, f'Condition: {cond_str}\n', 'log_head') # Color result outcome_str = f"Access Result: {result_dict.get('Policy Result')}\n" if result_dict.get('Policy Result') == 'GRANTED': self.results_text.insert(tk.END, outcome_str, 'log_granted') else: self.results_text.insert(tk.END, outcome_str, 'log_denied') # Details/log - plain text self.results_text.insert(tk.END, '--- Comparison Log ---\n') log = result_dict.get('Log', []) if log: for entry in log: # Print a clean/pass/fail line even if some elements are missing sim_val = entry.get('sim_value', '') result = entry.get('result', False) operator = entry.get('operator', '') expected = entry.get('expected', '') var = entry.get('variable', '') # Highlight GRANTED case (even with unknown details) if result is True: status = 'PASS' else: status = 'FAIL' if sim_val == 'MISSING': self.results_text.insert(tk.END, f" [FAIL] Variable '{var or '?'}' is MISSING.\n") elif all(x == '?' or x == '' for x in [sim_val, operator, expected, var]): # All details are unknown or missing - just give result self.results_text.insert(tk.END, f" [{status}] {'Comparison performed; details unavailable'}\n") else: self.results_text.insert(tk.END, f' [{status}] {sim_val} {operator} {expected} -> {result}\n') else: self.results_text.insert(tk.END, ' No comparisons made / syntax or execution error.\n')