Source code for oci_policy_analysis.ui.resource_principals_tab

##########################################################################
# Copyright (c) 2024, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
#
# DISCLAIMER This is not an official Oracle application, It does not supported by Oracle Support.
#
# resource_principals_tab.py
#
# @author: Andrew Gregory
#
# Supports Python 3.12 and above
#
# coding: utf-8
##########################################################################
import tkinter as tk
from tkinter import ttk

from oci_policy_analysis.common.helpers import for_display_dynamic_group, for_display_policy
from oci_policy_analysis.common.logger import get_logger
from oci_policy_analysis.common.models import DynamicGroup, PolicySearch, RegularPolicyStatement
from oci_policy_analysis.logic.data_repo import PolicyAnalysisRepository
from oci_policy_analysis.ui.base_tab import BaseUITab
from oci_policy_analysis.ui.data_table import DataTable

logger = get_logger(component='resource_principals_tab')

BASIC_DG_COLUMNS = ['Domain', 'DG Name', 'Matching Rule', 'In Use']
ALL_DG_COLUMNS = [
    'Domain',
    'DG Name',
    'Description',
    'Matching Rule',
    'In Use',
    'DG OCID',
    'DG ID',
    'Creation Time',
    'Created By',
    'Created By OCID',
]
DG_COLUMN_WIDTHS = {
    'Domain': 150,
    'DG Name': 300,
    'Description': 400,
    'DG OCID': 450,
    'DG ID': 300,
    'Matching Rule': 700,
    'In Use': 80,
    'Creation Time': 150,
    'Created By': 200,
    'Created By OCID': 250,
}
ALL_POLICY_COLUMNS = [
    'Policy Name',
    'Policy OCID',
    'Compartment OCID',
    'Policy Compartment',
    'Statement Text',
    'Valid',
    'Invalid Reasons',
    'Subject Type',
    'Subject',
    'Verb',
    'Resource',
    'Permission',
    'Location Type',
    'Location',
    'Effective Path',
    'Conditions',
    'Comments',
    'Parsing Notes',
    'Creation Time',
    'Parsed',
]
BASIC_POLICY_COLUMNS = ['Policy Name', 'Policy Compartment', 'Statement Text', 'Effective Path', 'Valid']
POLICY_COLUMN_WIDTHS = {
    'Policy Name': 250,
    'Policy OCID': 450,
    'Compartment OCID': 450,
    'Policy Compartment': 250,
    'Statement Text': 700,
    'Valid': 80,
    'Invalid Reasons': 200,
    'Effective Path': 200,
    'Subject Type': 120,
    'Subject': 200,
    'Verb': 100,
    'Resource': 150,
    'Permission': 150,
    'Location Type': 120,
    'Location': 200,
    'Conditions': 200,
    'Comments': 200,
    'Parsing Notes': 250,
    'Creation Time': 150,
    'Parsed': 80,
}


[docs] class ResourcePrincipalsTab(BaseUITab): """ Resource Principals Tab for OCI Policy Analysis UI. Allows viewing Dynamic Groups and associated policy statements, with contextual page help. Now inherits from BaseUITab. Methods: __init__: Initializes the ResourcePrincipalsTab with UI components and context help. _build_ui: (Internal) Builds the UI components for the tab. apply_settings: Updates tab appearance and page help when global UI settings change. update_principals_sheets: Updates sheets based on dropdown and DG selection (called from main app, or internally). """ def __init__(self, parent, app): """ Args: parent: parent notebook or frame (usually a Tkinter Notebook) app: Reference to main App instance. """ super().__init__( parent, default_help_text='Analyze and view OCI Resource Principals, including Dynamic Groups and matching policy statements below.', ) self.app = app self.policy_repo: PolicyAnalysisRepository = app.policy_compartment_analysis self._build_ui()
[docs] def _build_ui(self): self.grid_rowconfigure(0, weight=10) self.grid_rowconfigure(1, weight=80) # Separate Form self.grid_rowconfigure(2, weight=10) self.grid_columnconfigure(0, weight=1) # --- Filters Section --- filters_labelframe = ttk.LabelFrame(self, text='Output Filters') filters_labelframe.pack(fill='x', padx=5, pady=5) self.add_context_help(filters_labelframe, 'Set analysis filters for resource principals and policies.') # All controls left-aligned: no column weights for n in range(7): filters_labelframe.grid_columnconfigure(n, weight=0) # Optionally add spacing at the end filters_labelframe.grid_columnconfigure(7, weight=1) # Principals Style dropdown ttk.Label(filters_labelframe, text='Principals Style:').grid(row=0, column=0, padx=5, pady=2, sticky='w') self.principals_style_var = tk.StringVar(value='Dynamic Group') self.principals_style_list = ['Dynamic Group', 'any-user', 'any-group', 'any-user / any-group'] self.principals_style_dropdown = ttk.OptionMenu( filters_labelframe, self.principals_style_var, self.principals_style_var.get(), *self.principals_style_list ) self.principals_style_dropdown.grid(row=0, column=1, padx=5, pady=2, sticky='ew') self.add_context_help( self.principals_style_dropdown, 'Choose which OCI principal(s) to analyze. Filters both tables below.' ) # Resource Type dropdown ttk.Label(filters_labelframe, text='Resource Type:').grid(row=0, column=2, padx=5, pady=2, sticky='w') self.resource_type_var = tk.StringVar(value='Any') self.resource_type_list = [ 'Any', 'autonomousdatabase', 'function', 'apigateway', 'disworkspace', 'dataflow', 'dbmgmt', 'serviceconnector', 'stackmon', 'cluster', 'workloadprotectionagent', 'aidataplatform', ] self.resource_type_dropdown = ttk.OptionMenu( filters_labelframe, self.resource_type_var, self.resource_type_var.get(), *self.resource_type_list ) self.resource_type_dropdown.grid(row=0, column=3, padx=5, pady=2, sticky='ew') self.add_context_help(self.resource_type_dropdown, 'Filter policies by major OCI resource type.') # Text Filter and Clear ttk.Label(filters_labelframe, text='Text Filter:').grid(row=0, column=4, padx=(14, 2), pady=2, sticky='w') self.text_filter_var = tk.StringVar() self.text_filter_entry = ttk.Entry(filters_labelframe, textvariable=self.text_filter_var, width=24) self.text_filter_entry.grid(row=0, column=5, padx=2, pady=2, sticky='w') self.add_context_help( self.text_filter_entry, 'Narrow results: filter matching rule (DG) or policy statement by text. Case-insensitive.', ) def clear_text_filter(): self.text_filter_var.set('') self.clear_filter_btn = ttk.Button(filters_labelframe, text='Clear', width=5, command=clear_text_filter) self.clear_filter_btn.grid(row=0, column=6, padx=(2, 8), pady=2, sticky='w') self.add_context_help(self.clear_filter_btn, 'Clear text filter and show all results.') # Add "AI Assist" button inside Filters Label Frame (to the right of "Clear") def ai_assist_callback(): self.app.policy_query_var.set('Analyze OCI Resource Principals and Dynamic Group policies.') self.app.ai_additional_instructions = ( 'Elaborate on how Resource Principals and Dynamic Groups are matched to resources and policies in OCI. ' 'Explain important factors, provide analysis of the policy context, and describe implications for access and security.' ) self.app.policy_query_label_text.set('Resource Principals Analysis:') self.ai_assist_btn = ttk.Button( filters_labelframe, text='AI Assist', command=self.app.toggle_bottom, width=10, state=tk.DISABLED, # Initially disabled until AI enablement is successful ) self.ai_assist_btn.grid(row=0, column=7, padx=(8, 8), pady=2, sticky='w') self.add_context_help( self.ai_assist_btn, 'Use Generative AI to analyze Resource Principals context and matching policies.' ) # refresh on text filter change self.text_filter_var.trace_add('write', self.update_principals_sheets) def rp_dg_selection_callback(selected_rows: list[dict]) -> None: """When a Dynamic Group is selected, update the policy statements below""" dgs_for_filter: list[DynamicGroup] = [] # Make the DG list (Domain,Name) for all selected rows for row in selected_rows: logger.info(f'Selected row: {row}') dgs_for_filter.append( DynamicGroup(domain_name=row.get('Domain', ''), dynamic_group_name=row.get('DG Name', '')) ) # dgs_for_filter.append((row.get('Domain'), row.get('DG Name'))) logger.info(f'DGs for filter: {dgs_for_filter}') # Call the filter # TODO: fix this filter filters: PolicySearch = PolicySearch(exact_dynamic_groups=dgs_for_filter) filtered = self.policy_repo.filter_policy_statements(filters) logger.debug(f'type: {type(filtered)} len: {len(filtered)}') # SNormalize the data filtered = [for_display_policy(statement) for statement in filtered] self.rp_policy_table.update_data(filtered) logger.info(f'Policies added to RP policy table: {len(filtered)}') def rp_policy_selection_callback(selected_rows: list[dict]) -> None: """When a Policy Statement is selected, update the policy statement below""" if len(selected_rows) == 1: selected_statement = selected_rows[0].get('Statement Text', '') logger.info(f'Selected policy statement: {selected_statement}') self.app.policy_query_var.set(selected_statement) self.app.ai_additional_instructions = 'Analyze the selected OCI policy statement. Give additional details as to how OCI Resource principals work, and show how the statement breaks down into its components such as action, subject, verb, resource, conditions, and effective path. Explain its implications on permissions within the OCI environment.' self.app.policy_query_label_text.set('RP Policy Statement\nInsights:') def policy_more_details_menu(row_index: int) -> tk.Menu: menu = tk.Menu(self, tearoff=0) row_data = self.rp_policy_table.data[row_index] logger.info(f'Creating more details menu for row {row_index}: {row_data}') # Create a policy search filter by policy name from selected row def switch_tab_policy_analysis(): self.app.notebook.select(tab_id=1) # Policy Analysis tab # Set the policy name entry logger.info(f'Switching to Policy Analysis tab for policy: {row_data.get("Policy Name", "")}') # Check the dynamic groups box and set the filter for policy name self.app.policies_tab.chk_show_dynamic.set(True) self.app.policies_tab.policy_filter_var.set(row_data.get('Policy Name', '')) menu.add_command( label=f'View Full Policy ({row_data.get("Policy Name", "")})', command=switch_tab_policy_analysis ) return menu # --- Main area: Labeled Frames for each table area --- frm_bottom_area = ttk.Frame(self) frm_bottom_area.pack(fill='both', expand=True, padx=5, pady=5) frm_bottom_area.grid_rowconfigure(0, weight=1) frm_bottom_area.grid_rowconfigure(1, weight=1) frm_bottom_area.grid_columnconfigure(0, weight=1) # LabelFrame: Dynamic Groups Table self.dg_labelframe = ttk.LabelFrame(frm_bottom_area, text='Dynamic Groups Table') self.dg_labelframe.grid(row=0, column=0, sticky='nsew', padx=2, pady=3) self.dg_labelframe.grid_columnconfigure(0, weight=1) self.dg_labelframe.grid_rowconfigure(0, weight=1) self.add_context_help( self.dg_labelframe, 'Shows list of Dynamic Groups in tenancy. Select to view matching policy statements below.', ) # Dynamic Groups DataTable self.rp_dg_table = DataTable( self.dg_labelframe, columns=ALL_DG_COLUMNS, display_columns=BASIC_DG_COLUMNS, column_widths=DG_COLUMN_WIDTHS, data=[], selection_callback=rp_dg_selection_callback, multi_select=True, ) self.rp_dg_table.grid(row=0, column=0, sticky='nsew') # LabelFrame: Resource Principals Policy Table self.rp_labelframe = ttk.LabelFrame(frm_bottom_area, text='Matching Policies Table') self.rp_labelframe.grid(row=1, column=0, sticky='nsew', padx=2, pady=3) self.rp_labelframe.grid_columnconfigure(0, weight=1) self.rp_labelframe.grid_rowconfigure(0, weight=1) self.add_context_help( self.rp_labelframe, 'List of policy statements for chosen resource principals or dynamic group(s). Right-click for full policy details.', ) # Policy Statements DataTable self.rp_policy_table = DataTable( self.rp_labelframe, columns=ALL_POLICY_COLUMNS, display_columns=BASIC_POLICY_COLUMNS, data=[], column_widths=POLICY_COLUMN_WIDTHS, row_context_menu_callback=policy_more_details_menu, selection_callback=rp_policy_selection_callback, multi_select=False, ) self.rp_policy_table.grid(row=0, column=0, sticky='nsew') # Update the sheet self.update_principals_sheets() # Bind dropdown updates to sheet update self.principals_style_var.trace_add('write', self.update_principals_sheets) self.resource_type_var.trace_add('write', self.update_principals_sheets)
# (text filter wired above)
[docs] def update_principals_sheets(self, *args): """ Update view: show or hide tables depending on dropdown, update data in both tables. - In DG mode, show both tables; in other principal modes, show only matching policy table, hiding DG table. - Refresh table data for all cases. - Text filter field applies to Matching Rule (DG) or Policy Statement (any-user modes). """ principals_style = self.principals_style_var.get() resource_type = self.resource_type_var.get() search_text = (self.text_filter_var.get() or '').strip().lower() # Enable/disable dropdowns as needed self.resource_type_dropdown.configure(state='normal') self.principals_style_dropdown.configure(state='normal') # Always hide both, then re-grid appropriately self.dg_labelframe.grid_remove() self.rp_labelframe.grid_remove() if principals_style in ('any-user', 'any-group', 'any-user / any-group'): self.resource_type_dropdown.configure(state='normal') self.rp_labelframe.grid(row=0, column=0, rowspan=2, sticky='nsew') logger.info(f'{principals_style} selected - only showing Policy table with resource dropdown') # Determine subjects to filter on if principals_style == 'any-user': subjects = ['any-user'] elif principals_style == 'any-group': subjects = ['any-group'] else: # 'any-user / any-group' subjects = ['any-user', 'any-group'] if resource_type == 'Any': filters: PolicySearch = PolicySearch(subject=subjects) policies: list[RegularPolicyStatement] = self.policy_repo.filter_policy_statements(filters=filters) else: filters: PolicySearch = PolicySearch(subject=subjects, statement_text=[resource_type]) policies = self.policy_repo.filter_policy_statements(filters=filters) display_data = [for_display_policy(statement) for statement in policies] # Apply text filter to "Statement Text" if search_text: display_data = [row for row in display_data if search_text in (row.get('Statement Text') or '').lower()] self.rp_policy_table.update_data(display_data) logger.info( f'Filtered to {len(display_data)} policies with principals: {subjects} filtering by resource: {resource_type}' ) elif principals_style == 'Dynamic Group': self.resource_type_dropdown.configure(state='disabled') self.dg_labelframe.grid(row=0, column=0, sticky='nsew') self.rp_labelframe.grid(row=1, column=0, sticky='nsew') logger.info('Showing Dynamic Groups and Policy tables') # Populate dynamic group table (apply text filter if set, to Matching Rule) filtered_dynamic_groups = self.policy_repo.filter_dynamic_groups(filters={}) filtered_dynamic_groups = [for_display_dynamic_group(dg) for dg in filtered_dynamic_groups] if search_text: filtered_dynamic_groups = [ row for row in filtered_dynamic_groups if search_text in (row.get('Matching Rule') or '').lower() ] self.rp_dg_table.update_data(filtered_dynamic_groups)
# In DG mode policy table is updated by DG selection; in other modes, filled above
[docs] def apply_settings(self, context_help: bool, font_size: str): """ Update help area and style on global context help and font size change (from main.py). """ BaseUITab.apply_settings(self, context_help=context_help, font_size=font_size)