Source code for oci_policy_analysis.ui.users_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.
#
# users_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_policy
from oci_policy_analysis.common.logger import get_logger
from oci_policy_analysis.common.models import Group, GroupSearch, PolicySearch, User, UserSearch
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

# Global logger for this module
logger = get_logger(component='users_tab')

GROUPS_ALL_COLUMNS = ['Domain Name', 'Group Name', 'User Count', 'Group ID', 'Group OCID']
GROUPS_DEFAULT_COLUMNS = ['Domain Name', 'Group Name', 'User Count']
GROUPS_COLUMNS_WIDTHS = {'Domain Name': 120, 'Group Name': 250, 'User Count': 80, 'Group ID': 220, 'Group OCID': 300}

USERS_ALL_COLUMNS = ['Domain Name', 'Username', 'Display Name', 'Primary Email', 'User ID', 'User OCID']
USERS_DEFAULT_COLUMNS = ['Domain Name', 'Username', 'Display Name', 'Primary Email']
USERS_COLUMNS_WIDTHS = {
    'Username': 160,
    'Display Name': 180,
    'Primary Email': 160,
    'User ID': 249,
    'User OCID': 300,
    'Domain Name': 100,
}

BASIC_POLICY_COLUMNS = ['Policy Name', 'Policy Compartment', 'Statement Text', 'Effective Path', 'Valid']
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',
]
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 UsersTab(BaseUITab): """ Users Tab for OCI Policy Analysis UI. Allows selection of Groups or Users, and displays associated policy statements. Supports filtering and detailed policy statement views. """ def __init__(self, parent, app): # noqa: C901 default_help_text = ( 'Select users and groups to view their effective policy statements. ' 'Use filters to narrow the results below. ' 'Mouse over each section for tips.' ) super().__init__(parent, default_help_text=default_help_text, page_help_link='/usage.html#groups-users-tab') # Reference to main app and data repository self.app = app self.policy_compartment_analysis: PolicyAnalysisRepository = app.policy_compartment_analysis # --- UI state variables and widget handles --- self.chk_show_expanded = tk.BooleanVar(value=False) self.chk_show_any_group_user = tk.BooleanVar(value=False) self.groups_option_var = tk.StringVar(value='GROUPS') self.groups_option_var.trace_add('write', lambda *_: self.update_user_analysis_output()) self.user_group_search = tk.StringVar() self.users_groups_table = None self.users_users_table = None self.users_policy_table = None self.selected_groups_table = None self.user_label_count = None # Synchronize "load_all_users" state from repository to checkbox/UI as available self._load_all_users_state = tk.BooleanVar( value=getattr(self.policy_compartment_analysis, 'load_all_users', True) ) self.grid_rowconfigure(0, weight=2) self.grid_rowconfigure(1, weight=7) # self.grid_rowconfigure(2, weight=1) self.grid_columnconfigure(0, weight=1) # For this tab, on the top left, include a table for all Groups with Domain/Group. Allow Multi-Select # On the top right, include a table for all users, which is unfiltered, but if any group is selected on the left, # only show users in those groups. This table is multi-select as well. # Depending on what is selected on the right, show all policies for those users in the bottom frame. # Bottom Frame is a policy table, similar to the other tabs, but only showing policies for the selected users/groups. # Similar AI analysis as well. # --- SECTION 1: User/Group Selection --- self.lf_user_selection = ttk.LabelFrame(self, text='User/Group Selection') self.lf_user_selection.pack(fill='x', padx=12, pady=(8, 0)) # Internal state: are users loaded? self.users_available = lambda: len(self.policy_compartment_analysis.users) > 0 # for UI toggling message when users are disabled self.disabled_users_label = None def _show_user_selection_help(_event=None): self.set_page_help_text( 'Select one or more groups or users. Use the search and dropdown to filter. Selection drives statement display below.' ) def _restore_user_selection_help(_event=None): self.set_page_help_text(self.default_help_text) self.lf_user_selection.bind('<Enter>', _show_user_selection_help) self.lf_user_selection.bind('<Leave>', _restore_user_selection_help) # Retain old frm_user_top as the organizer within the label frame self._build_ui_user_group_selection(self.lf_user_selection) # All context help methods now inherited from BaseUITab. # ------------------------------------------------------------------ # Public lifecycle / data refresh API # ------------------------------------------------------------------
[docs] def populate_data(self): """Populate / refresh Users tab data after a tenancy or cache load. This is the single entry point used by the main application after repository data is (re)loaded. It ensures that: * The GROUPS/USERS dropdown reflects the current repository state (including load_all_users and whether users were actually loaded). * The top tables (groups/users) and counts are refreshed using the current search text and selection. Behavior-wise this is equivalent to the previous sequence of calls from main: - update_user_analysis_output() - update_users_dropdown_options() """ logger.info('Populating UsersTab data...') # NOTE: update_users_dropdown_options() will call # update_user_analysis_output() as its last step, so a single call # here fully refreshes the tab for the current state. self.update_users_dropdown_options() logger.info('Finished UsersTab.populate_data')
[docs] def set_show_all_data(self, checked=None): """Sync table display columns with the *Show all Data* checkbox. If *checked* is provided, force the checkbox to that state. If *checked* is ``None``, rely on the current :class:`BooleanVar` value (used when invoked by the Checkbutton command, since Tkinter has already toggled it). """ if checked is not None: self.show_all_data_var.set(checked) # At this point, show_all_data_var reflects the desired state if self.users_groups_table is not None: self.users_groups_table.set_display_columns( GROUPS_ALL_COLUMNS if self.show_all_data_var.get() else GROUPS_DEFAULT_COLUMNS ) if self.users_users_table is not None: self.users_users_table.set_display_columns( USERS_ALL_COLUMNS if self.show_all_data_var.get() else USERS_DEFAULT_COLUMNS )
[docs] def sync_load_all_users_checkbox(self): """ Ensures the checkbox/UI for load_all_users matches the repository state. Should be called after loading data/cache if UI lags behind data model. """ current_repo_val = getattr(self.policy_compartment_analysis, 'load_all_users', True) self._load_all_users_state.set(current_repo_val)
[docs] def should_show_users_option(self): """ Returns True if the USERS option should be available in the dropdown, i.e., only if load_all_users is True AND there are users loaded. """ repo = self.policy_compartment_analysis return getattr(repo, 'load_all_users', True) and len(getattr(repo, 'users', [])) > 0
[docs] def update_users_dropdown_options(self): """Refresh the GROUPS/USERS dropdown based on current repo state. This should be called after any tenancy/repository load and is safe to invoke at other times. The method also **forces** a refresh of the table below by calling :meth:`update_user_analysis_output` at the end so that the current selection (GROUPS vs USERS) and search term are immediately reflected in the UI. """ menu = self.groups_users_dropdown['menu'] menu.delete(0, 'end') menu.add_command(label='GROUPS', command=lambda: self.groups_option_var.set('GROUPS')) if self.should_show_users_option(): menu.add_command(label='USERS', command=lambda: self.groups_option_var.set('USERS')) # If the currently selected option is not available, reset to GROUPS if self.groups_option_var.get() == 'USERS' and not self.should_show_users_option(): self.groups_option_var.set('GROUPS') # Always force the corresponding table to reload self.update_user_analysis_output()
def _build_ui_user_group_selection(self, parent): # noqa: C901 frm_user_top = ttk.Frame(parent) frm_user_top.grid_rowconfigure(0, weight=1) frm_user_top.grid_rowconfigure(1, weight=1) frm_user_top.grid_columnconfigure(0, weight=4) frm_user_top.grid_columnconfigure(1, weight=6) frm_user_top.grid(row=0, column=0, sticky='nsew', padx=2, pady=2) # --- Selection/Search Frame (left) --- frm_user_selection = ttk.Frame(frm_user_top) frm_user_selection.grid_columnconfigure(0, weight=3) frm_user_selection.grid_columnconfigure(1, weight=5) frm_user_selection.grid_columnconfigure(2, weight=2) frm_user_selection.grid(row=0, column=0, padx=5, pady=2, sticky='w') # Label: Select Groups / Users ttk.Label(frm_user_selection, text='Show Groups -or- Users').grid( row=0, column=0, columnspan=1, padx=5, pady=2, sticky='w' ) # Dropdown: GROUPS/USERS # Patch: Only offer USERS if users are present, else just GROUPS self.groups_users_dropdown = ttk.OptionMenu( frm_user_selection, self.groups_option_var, self.groups_option_var.get(), *(['GROUPS', 'USERS'] if self.should_show_users_option() else ['GROUPS']), ) self.groups_users_dropdown.grid(row=0, column=1, padx=5, pady=5, sticky='ew') # Show All Data Checkbutton (right of dropdown, col 3) self.show_all_data_var = tk.BooleanVar(value=False) self.show_all_data_chkbtn = ttk.Checkbutton( frm_user_selection, text='Show all Data', variable=self.show_all_data_var, command=self.set_show_all_data ) self.show_all_data_chkbtn.grid(row=0, column=2, padx=6, pady=5, sticky='ew') self.add_context_help( self.show_all_data_chkbtn, 'Toggle to show additional ID/OCID columns for Groups/Users.\n' 'When unchecked, only the most important summary fields are shown for a more compact view.', ) # Label: Search, Entry: user_group_search ttk.Label(frm_user_selection, text='Search').grid(row=1, column=0, padx=5, pady=2, sticky='w') entry_search = ttk.Entry(frm_user_selection, textvariable=self.user_group_search, width=35) entry_search.grid(row=1, column=1, columnspan=2, padx=5, pady=2, sticky='ew') self.user_group_search.trace_add('write', lambda *_: self.update_user_analysis_output()) # Disabled label for USERS mode disabled (add but keep hidden) self.disabled_users_label = ttk.Label( frm_user_selection, text='Loading of individual users is disabled.\nEnable "Load All Users" in Settings to use this feature.', foreground='red', wraplength=380, justify='left', ) # Search instructions ttk.Label( frm_user_selection, text='Search user/group name using | for logical OR\nExamples: user1|user2 grp1|grp2' ).grid(row=2, column=0, columnspan=2, padx=5, pady=2, sticky='w') # Button: Clear Filter self.btn_clear_groups_users = ttk.Button( frm_user_selection, text='Clear Filter', command=lambda: self.user_group_search.set('') ) self.btn_clear_groups_users.grid(row=2, column=2, padx=5, pady=5, sticky='ew') # Selected Groups label & table self.user_selected_groups = ttk.Label(frm_user_selection, text='Selected Groups: ') self.user_selected_groups.grid(row=3, column=0, columnspan=3, padx=5, pady=1, sticky='w') self.selected_groups_table = DataTable( frm_user_selection, columns=['Domain', 'Group'], display_columns=['Domain', 'Group'], data=[], column_widths={'Domain': 140, 'Group': 180}, height=3, ) self.selected_groups_table.grid(row=4, column=0, columnspan=3, padx=5, pady=1, sticky='w') # Reduce height for selected groups table row frm_user_selection.grid_rowconfigure(4, minsize=48, weight=0) # --- Table area: groups/users table (right) --- # --- Callbacks for table selection --- def users_group_selection_callback(selected_rows: list[dict]) -> None: logger.info('>>> users_group_selection_callback was called') """When a Group is selected, update the selected groups and filtered policy statements.""" groups_for_filter = [] for row in selected_rows: logger.debug(f"Selected Group: {row.get('Domain Name')} / {row.get('Group Name')}") if 'Group Name' in row: groups_for_filter.append( {'domain_name': row.get('Domain Name'), 'group_name': row.get('Group Name')} ) logger.info(f'Groups for filter: {groups_for_filter}') self._update_user_analysis_policy_output(groups_for_filter=groups_for_filter, users_for_filter=None) def users_user_selection_callback(selected_rows: list[dict]) -> None: logger.info('>>> users_user_selection_callback was called') """When a User is selected, update the selected groups (from user) and filtered policy statements.""" users_for_filter = [] for row in selected_rows: logger.debug(f"Selected User: {row.get('Domain Name')} / {row.get('Username')}") if 'Username' in row: users_for_filter.append({'domain_name': row.get('Domain Name'), 'user_name': row.get('Username')}) logger.info(f'Users for filter: {users_for_filter}') self._update_user_analysis_policy_output(groups_for_filter=None, users_for_filter=users_for_filter) self.users_groups_table = DataTable( frm_user_top, columns=GROUPS_ALL_COLUMNS, display_columns=GROUPS_DEFAULT_COLUMNS, data=[], column_widths=GROUPS_COLUMNS_WIDTHS, selection_callback=users_group_selection_callback, multi_select=True, height=7, ) self.users_users_table = DataTable( frm_user_top, columns=USERS_ALL_COLUMNS, display_columns=USERS_DEFAULT_COLUMNS, data=[], column_widths=USERS_COLUMNS_WIDTHS, selection_callback=users_user_selection_callback, multi_select=True, height=7, ) # # Reduce groups/users table vertical space # frm_user_top.grid_rowconfigure(0, minsize=85, weight=0) # frm_user_top.grid_rowconfigure(1, minsize=85, weight=0) # frm_user_top.grid_rowconfigure(2, minsize=0, weight=0) # frm_user_top.grid_rowconfigure(3, minsize=0, weight=0) # frm_user_top.grid_rowconfigure(4, minsize=0, weight=0) # --- SECTION 2: Display Options with AI Assist Button in LabelFrame --- self.lf_display_options = ttk.LabelFrame(self, text='Display Options') self.lf_display_options.pack(fill='x', padx=12, pady=(12, 0)) def _show_display_options_help(_event=None): self.set_page_help_text( 'Adjust output display options, filters, and access AI assistance for user/group policies.' ) def _restore_display_options_help(_event=None): self.set_page_help_text(self.default_help_text) self.lf_display_options.bind('<Enter>', _show_display_options_help) self.lf_display_options.bind('<Leave>', _restore_display_options_help) # Filter checkboxes section self._build_ui_statement_filters(self.lf_display_options) # AI Assist button inside Display Options frame, packed to the right self.ai_assist_btn = ttk.Button( self.lf_display_options, text='AI Assist', command=self._on_ai_assist_clicked, state=tk.DISABLED ) self.ai_assist_btn.pack(side='right', anchor='e', padx=(16, 8), pady=8) self.add_context_help( self.ai_assist_btn, 'Show or hide the AI Assistant pane below to analyze user/group policies.\nNOTE: AI must be enabled in Settings Tab and only policy statements are supported.', ) # --- SECTION 3: Filtered Policy Statements --- self.lf_filtered_statements = ttk.LabelFrame(self, text='Filtered Policy Statements') self.lf_filtered_statements.pack(fill='both', expand=True, padx=10, pady=10) # Make main tab panel rows expand so the policy table area is prioritized self.grid_rowconfigure(2, weight=50) # Give filtered statements lots of expand room self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(1, weight=2) def _show_filtered_statements_help(_event=None): self.set_page_help_text( 'Review the resulting policy statements for your selected users/groups. Adjust filters above as needed.' ) def _restore_filtered_statements_help(_event=None): self.set_page_help_text(self.default_help_text) self.lf_filtered_statements.bind('<Enter>', _show_filtered_statements_help) self.lf_filtered_statements.bind('<Leave>', _restore_filtered_statements_help) def selection_callback(selected_rows: list[dict]) -> None: for row in selected_rows: logger.info(f"Selected policy statement: {row.get('Statement Text')}") # Update the policy box self.app.ai_additional_instructions = 'Analyze the selected OCI policy statement. 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('Policy Statement\nInsights:') self.app.policy_query_var.set(row.get('Statement Text')) # Data Table for filtered policy statements self.users_policy_table = DataTable( self.lf_filtered_statements, columns=ALL_POLICY_COLUMNS, display_columns=BASIC_POLICY_COLUMNS, data=[], column_widths=POLICY_COLUMN_WIDTHS, # font_size=10, selection_callback=selection_callback, multi_select=False, ) self.users_policy_table.pack(expand=True, fill='both', padx=0, pady=0) def _build_ui_statement_filters(self, parent): # Everything packed horizontally, no grid. # Total Groups and Total Users labels total_groups = len(getattr(self.policy_compartment_analysis, 'groups', [])) total_users = len(getattr(self.policy_compartment_analysis, 'users', [])) self.total_groups_label = ttk.Label(parent, text=f'Total Groups: {total_groups}') self.total_groups_label.pack(side='left', padx=8, pady=(0, 3)) self.total_users_label = ttk.Label(parent, text=f'Total Users: {total_users}') self.total_users_label.pack(side='left', padx=8, pady=(0, 3)) self.user_label_count = ttk.Label(parent, text='Policy Statements (Filtered): 0') self.user_label_count.pack(side='left', padx=8, pady=(0, 3)) ttk.Checkbutton( parent, text='Parsed Output', variable=self.chk_show_expanded, command=lambda: self.update_user_policy_output(), ).pack(side='left', padx=10) ttk.Checkbutton( parent, text='Show any-group / any-user', variable=self.chk_show_any_group_user, command=lambda: self.update_user_policy_output(), ).pack(side='left', padx=10) def _on_ai_assist_clicked(self): """Callback for AI Assist button. Toggles the AI (bottom) pane.""" if hasattr(self.app, 'toggle_bottom'): self.app.toggle_bottom() logger.info('Users Tab: AI Assist button clicked, toggled bottom pane.')
[docs] def update_user_analysis_output(self): """Update the top user/group listing and associated counters. This method is responsible for: * Updating the *Total Groups* / *Total Users* labels from the underlying repository. * Displaying either the groups table or the users table, depending on the current value of ``self.groups_option_var`` (``'GROUPS'`` or ``'USERS'``). * Applying the search filter from ``self.user_group_search`` using ``GroupSearch`` / ``UserSearch``. It does **not** compute policy statements; those are handled by :meth:`_update_user_analysis_policy_output` and :meth:`update_user_policy_output`. """ logger.info(f'Displaying: {self.groups_option_var.get()} with search of {self.user_group_search.get()}') # Update the total counts labels if hasattr(self, 'total_groups_label') and hasattr(self, 'total_users_label'): total_groups = len(getattr(self.policy_compartment_analysis, 'groups', [])) total_users = len(getattr(self.policy_compartment_analysis, 'users', [])) self.total_groups_label.configure(text=f'Total Groups: {total_groups}') self.total_users_label.configure(text=f'Total Users: {total_users}') # Defensive: Only try to display tables if they've been initialized if self.users_groups_table is None or self.users_users_table is None: logger.info('Table widgets not initialized yet; skipping analysis output update.') return # Grid the correct table # Patch: Hide USERS mode if unavailable, and display red warning instead if self.groups_option_var.get() == 'GROUPS': if self.disabled_users_label.winfo_manager(): self.disabled_users_label.grid_remove() self.users_users_table.grid_forget() self.users_groups_table.grid(row=0, column=1, rowspan=3, sticky='nsew') group_filter: GroupSearch = GroupSearch( group_name=self.user_group_search.get().split('|') if self.user_group_search.get() else [], ) filtered_groups: list[Group] = self.policy_compartment_analysis.filter_groups(group_filter=group_filter) display_groups = [] for g in filtered_groups: # Build display dict with all shown keys for full compatibility display_groups.append( { 'Domain Name': g.get('domain_name', 'Default'), 'Group Name': g.get('group_name', ''), 'User Count': ( len(self.policy_compartment_analysis.get_users_for_group(g)) if hasattr(self.policy_compartment_analysis, 'get_users_for_group') else 0 ), 'Group ID': g.get('group_id', ''), 'Group OCID': g.get('group_ocid', ''), } ) self.users_groups_table.update_data(display_groups) logger.info(f'Loaded {len(filtered_groups)} groups into table') elif self.groups_option_var.get() == 'USERS': if not self.users_available(): self.users_users_table.grid_forget() self.disabled_users_label.grid(row=3, column=0, columnspan=3, sticky='w', padx=5, pady=(8, 2)) logger.info('User view disabled due to no users loaded') return self.disabled_users_label.grid_remove() self.users_groups_table.grid_forget() self.users_users_table.grid(row=0, column=1, rowspan=3, sticky='nsew') user_filter: UserSearch = UserSearch( search=self.user_group_search.get().split('|') if self.user_group_search.get() else [], ) filtered_users: list[User] = self.policy_compartment_analysis.filter_users(user_filter=user_filter) display_users = [] for u in filtered_users: display_users.append( { 'Domain Name': u.get('domain_name', 'Default'), 'Username': u.get('user_name', ''), 'Display Name': u.get('display_name', ''), 'Primary Email': u.get('email', ''), 'User ID': u.get('user_id', ''), 'User OCID': u.get('user_ocid', ''), } ) self.users_users_table.update_data(display_users) logger.info(f'Loaded {len(filtered_users)} users into data') else: logger.warning('Should not get here')
[docs] def update_user_policy_output(self): """Refresh the policy statements table and related labels. This uses the pre-computed ``self.filtered_policies`` and ``self.selected_groups_for_table`` that are maintained by :meth:`_update_user_analysis_policy_output` when the selection in the groups/users tables changes. Responsibilities: * Toggle between basic vs expanded policy columns based on the *Parsed Output* checkbox (``self.chk_show_expanded``). * Optionally include "any-user" / "any-group" statements when the corresponding checkbox is enabled. * Push the final policy list into ``self.users_policy_table`` and update the *Selected Groups* helper table and the *Policy Statements (Filtered)* count label. """ # Defensive: Exit if the tables/labels are not built yet if self.users_policy_table is None or self.selected_groups_table is None or self.user_label_count is None: logger.warning('Policy/output table widgets not initialized yet; skipping policy output update.') return if self.chk_show_expanded.get(): self.users_policy_table.set_display_columns(ALL_POLICY_COLUMNS) else: self.users_policy_table.set_display_columns(BASIC_POLICY_COLUMNS) logger.info(f'Updated display for expanded output: {self.chk_show_expanded.get()}') # If the show all checkbox is selected, run additional query to get any-user/any-group policies if self.chk_show_any_group_user.get(): logger.info('Including any-user and any-group policies in output') # Create filter for any-user/any-group any_user_group_policies = self.policy_compartment_analysis.filter_policy_statements( PolicySearch(subject=['any-user', 'any-group']) ) logger.info(f'Found {len(any_user_group_policies)} any-user/any-group policies') # Add to existing data table data in self.filtered_policies seen_ids = { getattr(st, 'internal_id', None) or st.get('statement_text', None) for st in self.filtered_policies } additional_policies = [] for st in any_user_group_policies: key = getattr(st, 'internal_id', None) or st.get('statement_text', None) if key not in seen_ids: additional_policies.append(st) seen_ids.add(key) if len(additional_policies) > 0: logger.info(f'Added {len(additional_policies)} any-user/any-group policies to output') all_policies = list(self.filtered_policies) + additional_policies else: logger.info('Not including any-user and any-group policies in output') # Display only filtered policies all_policies = list(self.filtered_policies) # Update the display elements self.users_policy_table.update_data([for_display_policy(st) for st in all_policies]) self.selected_groups_table.update_data(self.selected_groups_for_table) self.user_label_count.configure(text=f'Policy Statements (Filtered): {len(all_policies)}')
def _update_user_analysis_policy_output(self, groups_for_filter, users_for_filter): # noqa: C901 """Recompute policy statements for the current group/user selection. This is the main worker that responds to selection changes in the groups/users tables. It: * Builds an appropriate :class:`PolicySearch` with exact groups and/or users. * Populates ``self.filtered_policies`` with the matching policy statements from the repository. * Builds ``self.selected_groups_for_table`` (used by the small *Selected Groups* table on the left). * Finally delegates to :meth:`update_user_policy_output` to refresh what is rendered on screen. """ logger.info(f'Searching for policies for groups: {groups_for_filter} and users: {users_for_filter}') exact_groups_filter: list[Group] = groups_for_filter exact_users_filter: list[User] = users_for_filter exact_groups_users_filter = PolicySearch(exact_groups=exact_groups_filter, exact_users=exact_users_filter) self.filtered_policies = self.policy_compartment_analysis.filter_policy_statements( filters=exact_groups_users_filter ) logger.info(f'Found {len(self.filtered_policies)} policies for selected users/groups') self.selected_groups_for_table = [] if groups_for_filter: for group in groups_for_filter: dom = group.get('domain_name') or 'Default' gr = group.get('group_name') self.selected_groups_for_table.append({'Domain': dom, 'Group': gr}) elif users_for_filter: # If all we have is users, grab the groups for them for user in users_for_filter: groups_for_user = self.policy_compartment_analysis.get_groups_for_user(user) logger.info(f'User {user.get("user_name")} is in groups: {groups_for_user}') for group in groups_for_user: dom = group.get('domain_name') or 'Default' gr = group.get('group_name') self.selected_groups_for_table.append({'Domain': dom, 'Group': gr}) # Call the main update_user_policy_output to refresh display self.update_user_policy_output() # self.selected_groups_table.update_data(selected_groups_for_table) # self.user_label_count.configure(text=f'Policy Statements (Filtered): {len(all_policies)}') # ----------- Page Help Methods (adapted from policies_tab.py) ----------------
[docs] def on_load_all_users_setting_changed(self, enabled: bool): """ Called if settings change for Load All Users to refresh user/group options and UI. Synchronizes between UI and model: both directions. """ # Always update the model repository field so next cache save/load is accurate self.policy_compartment_analysis.load_all_users = enabled # If users are now NOT loaded, switch view to GROUPS forcibly and refresh dropdown/menu if not enabled: self.groups_option_var.set('GROUPS') menu = self.groups_users_dropdown['menu'] menu.delete(0, 'end') menu.add_command(label='GROUPS', command=lambda: self.groups_option_var.set('GROUPS')) if enabled: menu.add_command(label='USERS', command=lambda: self.groups_option_var.set('USERS')) self.update_user_analysis_output()