Add User Manager panel for sys::UserService components
New user_service_view.py with table of sys::User children, add/edit/delete dialogs, SHA-1 credential hashing, 32-bit permission bitmask (4 groups x 7 flags), 8-bit provisioning byte, and undo support. Wired into main.py with context menu entry, panel management, history navigation, and live-mode DOM resolution for correct display on connected devices. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.116"
|
VERSION = "0.0.0.117"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import properties
|
|||||||
import sax_file
|
import sax_file
|
||||||
import schema
|
import schema
|
||||||
import undo
|
import undo
|
||||||
|
import user_service_view
|
||||||
import wiresheet
|
import wiresheet
|
||||||
from sax_dom import (find_component_by_path, find_parent_element, get_component_path,
|
from sax_dom import (find_component_by_path, find_parent_element, get_component_path,
|
||||||
read_meta_property, update_meta_property)
|
read_meta_property, update_meta_property)
|
||||||
@@ -78,6 +79,7 @@ HISTORY_LIMIT = 50
|
|||||||
VIEW_SHEET = "sheet"
|
VIEW_SHEET = "sheet"
|
||||||
VIEW_PROPERTIES = "properties"
|
VIEW_PROPERTIES = "properties"
|
||||||
VIEW_LINKS = "links"
|
VIEW_LINKS = "links"
|
||||||
|
VIEW_USERS = "users"
|
||||||
|
|
||||||
# Tk class names of the widgets that own the keyboard while they have focus. Del and
|
# Tk class names of the widgets that own the keyboard while they have focus. Del and
|
||||||
# Backspace mean "edit this text" in each of them, never "delete the selected component".
|
# Backspace mean "edit this text" in each of them, never "delete the selected component".
|
||||||
@@ -122,6 +124,7 @@ show_left_column_var = None
|
|||||||
properties_panel = None
|
properties_panel = None
|
||||||
link_panel = None
|
link_panel = None
|
||||||
kits_panel = None
|
kits_panel = None
|
||||||
|
user_service_panel = None
|
||||||
|
|
||||||
# The sheet to put the tree back on when a sheet change is refused because of unsaved
|
# The sheet to put the tree back on when a sheet change is refused because of unsaved
|
||||||
# edits, and the flag that stops the restoring selection from being read as a fresh
|
# edits, and the flag that stops the restoring selection from being read as a fresh
|
||||||
@@ -189,6 +192,12 @@ def refresh_all_views():
|
|||||||
else:
|
else:
|
||||||
close_editor_panel(force=True)
|
close_editor_panel(force=True)
|
||||||
|
|
||||||
|
if user_service_panel is not None:
|
||||||
|
if _component_in_document(user_service_panel.comp):
|
||||||
|
user_service_panel.refresh()
|
||||||
|
else:
|
||||||
|
close_editor_panel(force=True)
|
||||||
|
|
||||||
|
|
||||||
def _component_in_document(comp):
|
def _component_in_document(comp):
|
||||||
root = app_state.xml_root_element
|
root = app_state.xml_root_element
|
||||||
@@ -210,7 +219,7 @@ def delete_component(comp):
|
|||||||
than going through refresh_all_views, so the panel would otherwise be left describing
|
than going through refresh_all_views, so the panel would otherwise be left describing
|
||||||
a component the document no longer has.
|
a component the document no longer has.
|
||||||
"""
|
"""
|
||||||
panel = properties_panel or link_panel
|
panel = properties_panel or link_panel or user_service_panel
|
||||||
showing = panel is not None and panel.comp is comp
|
showing = panel is not None and panel.comp is comp
|
||||||
if sax_file.execute_cascading_deletion(comp) and showing:
|
if sax_file.execute_cascading_deletion(comp) and showing:
|
||||||
close_editor_panel(force=True)
|
close_editor_panel(force=True)
|
||||||
@@ -218,7 +227,8 @@ def delete_component(comp):
|
|||||||
|
|
||||||
def editor_panel_showing():
|
def editor_panel_showing():
|
||||||
"""Whether something other than the wiresheet has the editor pane."""
|
"""Whether something other than the wiresheet has the editor pane."""
|
||||||
return properties_panel is not None or link_panel is not None or kits_panel is not None
|
return (properties_panel is not None or link_panel is not None
|
||||||
|
or kits_panel is not None or user_service_panel is not None)
|
||||||
|
|
||||||
|
|
||||||
def _take_editor_pane():
|
def _take_editor_pane():
|
||||||
@@ -330,6 +340,38 @@ def dismiss_link_view(restore_canvas=True):
|
|||||||
_give_editor_pane_back()
|
_give_editor_pane_back()
|
||||||
|
|
||||||
|
|
||||||
|
def show_user_service(comp):
|
||||||
|
"""Puts the User Manager for a sys::UserService in the editor pane."""
|
||||||
|
global user_service_panel
|
||||||
|
if comp is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not close_editor_panel():
|
||||||
|
return
|
||||||
|
|
||||||
|
_take_editor_pane()
|
||||||
|
user_service_panel = user_service_view.build_user_service_panel(
|
||||||
|
frame_editor, comp, on_change=refresh_all_views,
|
||||||
|
on_close=dismiss_user_service,
|
||||||
|
)
|
||||||
|
user_service_panel.frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
record_visit(VIEW_USERS, comp)
|
||||||
|
app_state.set_status(f"User Manager: {comp.get('name', '?')}")
|
||||||
|
|
||||||
|
|
||||||
|
def dismiss_user_service(restore_canvas=True):
|
||||||
|
"""Takes the User Manager down and gives the pane back."""
|
||||||
|
global user_service_panel
|
||||||
|
if user_service_panel is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_service_panel.destroy()
|
||||||
|
user_service_panel = None
|
||||||
|
|
||||||
|
if restore_canvas:
|
||||||
|
_give_editor_pane_back()
|
||||||
|
|
||||||
|
|
||||||
def show_kits():
|
def show_kits():
|
||||||
"""Puts the kit table in the editor pane, in place of the wiresheet."""
|
"""Puts the kit table in the editor pane, in place of the wiresheet."""
|
||||||
global kits_panel
|
global kits_panel
|
||||||
@@ -378,6 +420,9 @@ def close_editor_panel(force=False):
|
|||||||
if kits_panel is not None:
|
if kits_panel is not None:
|
||||||
dismiss_kits()
|
dismiss_kits()
|
||||||
return True
|
return True
|
||||||
|
if user_service_panel is not None:
|
||||||
|
dismiss_user_service()
|
||||||
|
return True
|
||||||
if properties_panel is None:
|
if properties_panel is None:
|
||||||
return True
|
return True
|
||||||
if force:
|
if force:
|
||||||
@@ -1207,6 +1252,11 @@ def fill_component_popup(menu, comp):
|
|||||||
label="Link View",
|
label="Link View",
|
||||||
command=lambda: show_link_view(comp),
|
command=lambda: show_link_view(comp),
|
||||||
)
|
)
|
||||||
|
if user_service_view.is_user_service(comp):
|
||||||
|
menu.add_command(
|
||||||
|
label="User Manager",
|
||||||
|
command=lambda: show_user_service(comp),
|
||||||
|
)
|
||||||
menu.add_separator()
|
menu.add_separator()
|
||||||
menu.add_command(
|
menu.add_command(
|
||||||
label="Delete Component",
|
label="Delete Component",
|
||||||
@@ -1644,6 +1694,8 @@ def _show_history_entry(position):
|
|||||||
_show_sheet_entry(element)
|
_show_sheet_entry(element)
|
||||||
elif kind == VIEW_PROPERTIES:
|
elif kind == VIEW_PROPERTIES:
|
||||||
show_properties(element)
|
show_properties(element)
|
||||||
|
elif kind == VIEW_USERS:
|
||||||
|
show_user_service(element)
|
||||||
else:
|
else:
|
||||||
show_link_view(element)
|
show_link_view(element)
|
||||||
finally:
|
finally:
|
||||||
@@ -1707,7 +1759,7 @@ def on_tree_filled():
|
|||||||
|
|
||||||
# A different file has no such component, and a rebuild after a delete may have taken
|
# A different file has no such component, and a rebuild after a delete may have taken
|
||||||
# it. Neither is a moment to ask about edits that now have nowhere to go.
|
# it. Neither is a moment to ask about edits that now have nowhere to go.
|
||||||
panel = properties_panel or link_panel
|
panel = properties_panel or link_panel or user_service_panel
|
||||||
if panel is not None and not _component_in_document(panel.comp):
|
if panel is not None and not _component_in_document(panel.comp):
|
||||||
close_editor_panel(force=True)
|
close_editor_panel(force=True)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,466 @@
|
|||||||
|
"""The User Manager: a table of sys::User components under a sys::UserService.
|
||||||
|
|
||||||
|
Like the other panels it takes the editor pane over from the canvas rather than
|
||||||
|
floating above it. `main` is what hides the canvas and puts this in its place;
|
||||||
|
this module knows only the frame it was handed.
|
||||||
|
|
||||||
|
Each sys::User component carries three properties:
|
||||||
|
cred — SHA-1 of "username:password" (20 bytes, base64 in the SAX)
|
||||||
|
perm — 32-bit int: four groups × 7 permission flags per group
|
||||||
|
prov — 8-bit byte: app/kits/svm provisioning flags
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import font as tkfont, messagebox, ttk
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import app_state
|
||||||
|
import undo
|
||||||
|
from sax_dom import set_prop_value
|
||||||
|
|
||||||
|
HEADER_BG = "#f2f2f2"
|
||||||
|
|
||||||
|
PERM_FLAGS = ["or", "ow", "oi", "ar", "aw", "ai", "ua"]
|
||||||
|
PERM_LABELS = "rwIRWIu"
|
||||||
|
|
||||||
|
PROV_FLAGS = [("app", 0x01), ("kits", 0x02), ("svm", 0x04)]
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_credentials(username, password):
|
||||||
|
"""SHA-1 of 'username:password', the Sedona credential algorithm."""
|
||||||
|
return base64.b64encode(
|
||||||
|
hashlib.sha1(f"{username}:{password}".encode("utf-8")).digest()
|
||||||
|
).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_perm(perm_val):
|
||||||
|
"""32-bit int -> list of four 7-element bool lists."""
|
||||||
|
try:
|
||||||
|
val = int(perm_val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
val = 0
|
||||||
|
groups = []
|
||||||
|
for g in range(4):
|
||||||
|
byte = (val >> (g * 8)) & 0xFF
|
||||||
|
groups.append([(byte >> bit) & 1 == 1 for bit in range(7)])
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_perm(groups):
|
||||||
|
"""List of four 7-element bool lists -> 32-bit int."""
|
||||||
|
val = 0
|
||||||
|
for g, flags in enumerate(groups):
|
||||||
|
byte = 0
|
||||||
|
for bit, on in enumerate(flags):
|
||||||
|
if on:
|
||||||
|
byte |= (1 << bit)
|
||||||
|
val |= (byte << (g * 8))
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def _format_perm(perm_val):
|
||||||
|
"""Human-readable: '1=rwIRWIu, 2=rwIRWIu, ...'"""
|
||||||
|
groups = _decode_perm(perm_val)
|
||||||
|
parts = []
|
||||||
|
for g, flags in enumerate(groups):
|
||||||
|
chars = "".join(PERM_LABELS[i] if on else "-" for i, on in enumerate(flags))
|
||||||
|
parts.append(f"{g + 1}={chars}")
|
||||||
|
return ", ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_prov(prov_val):
|
||||||
|
"""8-bit byte -> dict of flag name -> bool."""
|
||||||
|
try:
|
||||||
|
val = int(prov_val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
val = 0
|
||||||
|
return {name: bool(val & mask) for name, mask in PROV_FLAGS}
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_prov(flags_dict):
|
||||||
|
val = 0
|
||||||
|
for name, mask in PROV_FLAGS:
|
||||||
|
if flags_dict.get(name):
|
||||||
|
val |= mask
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def _format_prov(prov_val):
|
||||||
|
"""Human-readable: 'app,kits,svm' for set flags."""
|
||||||
|
flags = _decode_prov(prov_val)
|
||||||
|
active = [name for name, on in flags.items() if on]
|
||||||
|
return ",".join(active) if active else "(none)"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_prop(comp, name):
|
||||||
|
prop = comp.find(f"prop[@name='{name}']")
|
||||||
|
return prop.get("val") if prop is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_users(service_comp):
|
||||||
|
"""Returns child comp elements, excluding uncached placeholders."""
|
||||||
|
return [c for c in service_comp
|
||||||
|
if c.tag == "comp" and c.get("name", "") != "..."]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_comp(comp):
|
||||||
|
"""Re-locate a comp element in the current DOM by id (live mode rebuilds the tree)."""
|
||||||
|
root = app_state.xml_root_element
|
||||||
|
if root is None:
|
||||||
|
return comp
|
||||||
|
comp_id = comp.get("id")
|
||||||
|
if comp_id is None:
|
||||||
|
return comp
|
||||||
|
for elem in root.iter("comp"):
|
||||||
|
if elem.get("id") == comp_id:
|
||||||
|
return elem
|
||||||
|
return comp
|
||||||
|
|
||||||
|
|
||||||
|
def is_user_service(comp):
|
||||||
|
"""Whether a component is a sys::UserService (or looks like one in live mode)."""
|
||||||
|
if comp is None:
|
||||||
|
return False
|
||||||
|
type_name = comp.get("type", "")
|
||||||
|
if type_name.endswith("::UserService") or type_name == "UserService":
|
||||||
|
return True
|
||||||
|
for child in comp:
|
||||||
|
if child.tag == "comp" and child.get("type", "").endswith("::User"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _UserDialog:
|
||||||
|
"""Modal dialog for adding or editing a user."""
|
||||||
|
|
||||||
|
def __init__(self, parent, service_comp, user_elem, on_done):
|
||||||
|
self._service_comp = service_comp
|
||||||
|
self._user_elem = user_elem
|
||||||
|
self._on_done = on_done
|
||||||
|
self._is_edit = user_elem is not None
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
self._dlg = tk.Toplevel(parent)
|
||||||
|
self._dlg.title("Edit User" if self._is_edit else "Add User")
|
||||||
|
self._dlg.transient(parent)
|
||||||
|
self._dlg.grab_set()
|
||||||
|
self._dlg.resizable(False, False)
|
||||||
|
|
||||||
|
main = ttk.Frame(self._dlg, padding=10)
|
||||||
|
main.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
row = 0
|
||||||
|
ttk.Label(main, text="Username:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||||
|
self._name_var = tk.StringVar(
|
||||||
|
value=user_elem.get("name", "") if self._is_edit else "")
|
||||||
|
name_entry = ttk.Entry(main, textvariable=self._name_var, width=24)
|
||||||
|
name_entry.grid(row=row, column=1, columnspan=3, sticky=tk.EW, pady=2)
|
||||||
|
if self._is_edit:
|
||||||
|
name_entry.config(state="disabled")
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
ttk.Label(main, text="Password:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||||
|
self._pass_var = tk.StringVar()
|
||||||
|
ttk.Entry(main, textvariable=self._pass_var, show="*", width=24).grid(
|
||||||
|
row=row, column=1, columnspan=3, sticky=tk.EW, pady=2)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
ttk.Label(main, text="Confirm:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||||
|
self._confirm_var = tk.StringVar()
|
||||||
|
ttk.Entry(main, textvariable=self._confirm_var, show="*", width=24).grid(
|
||||||
|
row=row, column=1, columnspan=3, sticky=tk.EW, pady=2)
|
||||||
|
|
||||||
|
# permissions
|
||||||
|
row += 1
|
||||||
|
ttk.Separator(main, orient=tk.HORIZONTAL).grid(
|
||||||
|
row=row, column=0, columnspan=4, sticky=tk.EW, pady=6)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
ttk.Label(main, text="Group Permissions:").grid(
|
||||||
|
row=row, column=0, columnspan=4, sticky=tk.W, pady=2)
|
||||||
|
|
||||||
|
perm_groups = (_decode_perm(_get_prop(user_elem, "perm"))
|
||||||
|
if self._is_edit else [[False] * 7 for _ in range(4)])
|
||||||
|
self._perm_vars = []
|
||||||
|
for g in range(4):
|
||||||
|
row += 1
|
||||||
|
ttk.Label(main, text=f"Group {g + 1}:").grid(row=row, column=0, sticky=tk.W)
|
||||||
|
gframe = ttk.Frame(main)
|
||||||
|
gframe.grid(row=row, column=1, columnspan=3, sticky=tk.W)
|
||||||
|
gvars = []
|
||||||
|
for f_idx, label in enumerate(PERM_LABELS):
|
||||||
|
var = tk.BooleanVar(value=perm_groups[g][f_idx])
|
||||||
|
ttk.Checkbutton(gframe, text=label, variable=var).pack(
|
||||||
|
side=tk.LEFT, padx=1)
|
||||||
|
gvars.append(var)
|
||||||
|
self._perm_vars.append(gvars)
|
||||||
|
|
||||||
|
# provisioning
|
||||||
|
row += 1
|
||||||
|
ttk.Separator(main, orient=tk.HORIZONTAL).grid(
|
||||||
|
row=row, column=0, columnspan=4, sticky=tk.EW, pady=6)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
ttk.Label(main, text="Provisioning:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||||
|
prov_dict = (_decode_prov(_get_prop(user_elem, "prov"))
|
||||||
|
if self._is_edit else {n: False for n, _ in PROV_FLAGS})
|
||||||
|
prov_frame = ttk.Frame(main)
|
||||||
|
prov_frame.grid(row=row, column=1, columnspan=3, sticky=tk.W)
|
||||||
|
self._prov_vars = {}
|
||||||
|
for name, _ in PROV_FLAGS:
|
||||||
|
var = tk.BooleanVar(value=prov_dict.get(name, False))
|
||||||
|
ttk.Checkbutton(prov_frame, text=name, variable=var).pack(
|
||||||
|
side=tk.LEFT, padx=4)
|
||||||
|
self._prov_vars[name] = var
|
||||||
|
|
||||||
|
# buttons
|
||||||
|
row += 1
|
||||||
|
btn_frame = ttk.Frame(main)
|
||||||
|
btn_frame.grid(row=row, column=0, columnspan=4, pady=(10, 0))
|
||||||
|
ttk.Button(btn_frame, text="OK", command=self._on_ok).pack(side=tk.LEFT, padx=4)
|
||||||
|
ttk.Button(btn_frame, text="Cancel", command=self._on_cancel).pack(
|
||||||
|
side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
|
self._dlg.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||||
|
name_entry.focus_set()
|
||||||
|
self._dlg.wait_window()
|
||||||
|
|
||||||
|
def _on_ok(self):
|
||||||
|
username = self._name_var.get().strip()
|
||||||
|
password = self._pass_var.get()
|
||||||
|
confirm = self._confirm_var.get()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
messagebox.showwarning("Validation", "Username is required.", parent=self._dlg)
|
||||||
|
return
|
||||||
|
if not self._is_edit and not password:
|
||||||
|
messagebox.showwarning("Validation", "Password is required for new users.",
|
||||||
|
parent=self._dlg)
|
||||||
|
return
|
||||||
|
if password and password != confirm:
|
||||||
|
messagebox.showwarning("Validation", "Passwords do not match.", parent=self._dlg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# check duplicate name on add
|
||||||
|
if not self._is_edit:
|
||||||
|
for c in _get_users(self._service_comp):
|
||||||
|
if c.get("name", "") == username:
|
||||||
|
messagebox.showwarning("Validation", f"User '{username}' already exists.",
|
||||||
|
parent=self._dlg)
|
||||||
|
return
|
||||||
|
|
||||||
|
perm_groups = [[v.get() for v in gvars] for gvars in self._perm_vars]
|
||||||
|
prov_dict = {name: var.get() for name, var in self._prov_vars.items()}
|
||||||
|
|
||||||
|
if self._is_edit:
|
||||||
|
self._apply_edit(username, password, perm_groups, prov_dict)
|
||||||
|
else:
|
||||||
|
self._apply_add(username, password, perm_groups, prov_dict)
|
||||||
|
|
||||||
|
self._dlg.destroy()
|
||||||
|
if self._on_done:
|
||||||
|
self._on_done()
|
||||||
|
|
||||||
|
def _apply_add(self, username, password, perm_groups, prov_dict):
|
||||||
|
elem = ET.Element("comp", name=username, type="sys::User")
|
||||||
|
cred = _hash_credentials(username, password)
|
||||||
|
ET.SubElement(elem, "prop", name="cred", val=cred)
|
||||||
|
ET.SubElement(elem, "prop", name="perm", val=str(_encode_perm(perm_groups)))
|
||||||
|
ET.SubElement(elem, "prop", name="prov", val=str(_encode_prov(prov_dict)))
|
||||||
|
|
||||||
|
index = len(list(self._service_comp))
|
||||||
|
cmd = undo.AddComponentCommand(self._service_comp, index, elem, label="add user")
|
||||||
|
cmd.redo()
|
||||||
|
undo.push(cmd)
|
||||||
|
app_state.mark_dirty()
|
||||||
|
|
||||||
|
def _apply_edit(self, username, password, perm_groups, prov_dict):
|
||||||
|
user = self._user_elem
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
new_perm = str(_encode_perm(perm_groups))
|
||||||
|
old_perm = _get_prop(user, "perm")
|
||||||
|
if new_perm != old_perm:
|
||||||
|
changes.append((user, "perm", old_perm, new_perm))
|
||||||
|
|
||||||
|
new_prov = str(_encode_prov(prov_dict))
|
||||||
|
old_prov = _get_prop(user, "prov")
|
||||||
|
if new_prov != old_prov:
|
||||||
|
changes.append((user, "prov", old_prov, new_prov))
|
||||||
|
|
||||||
|
if password:
|
||||||
|
new_cred = _hash_credentials(username, password)
|
||||||
|
old_cred = _get_prop(user, "cred")
|
||||||
|
if new_cred != old_cred:
|
||||||
|
changes.append((user, "cred", old_cred, new_cred))
|
||||||
|
|
||||||
|
if not changes:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = undo.SlotValueCommand(changes, label="edit user")
|
||||||
|
cmd.redo()
|
||||||
|
undo.push(cmd)
|
||||||
|
app_state.mark_dirty()
|
||||||
|
|
||||||
|
def _on_cancel(self):
|
||||||
|
self._dlg.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class UserServicePanel:
|
||||||
|
"""Table of users under a sys::UserService, with Add/Edit/Delete."""
|
||||||
|
|
||||||
|
def __init__(self, parent, comp, on_change=None, on_close=None):
|
||||||
|
self.comp = comp
|
||||||
|
self.on_change = on_change
|
||||||
|
self.on_close = on_close
|
||||||
|
self.dirty = False
|
||||||
|
self.frame = tk.Frame(parent)
|
||||||
|
|
||||||
|
self._build_header()
|
||||||
|
self._build_table()
|
||||||
|
self._build_footer()
|
||||||
|
self._refresh_table()
|
||||||
|
|
||||||
|
def _build_header(self):
|
||||||
|
header = tk.Frame(self.frame, padx=8, pady=6, bg=HEADER_BG)
|
||||||
|
header.pack(fill=tk.X)
|
||||||
|
|
||||||
|
name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold")
|
||||||
|
plain_font = tkfont.Font(family="Segoe UI", size=9)
|
||||||
|
|
||||||
|
tk.Label(header, text="User Manager", font=name_font,
|
||||||
|
bg=HEADER_BG).pack(side=tk.LEFT)
|
||||||
|
tk.Label(header, text=f"— {self.comp.get('name', '?')}",
|
||||||
|
font=plain_font, fg="#555555", bg=HEADER_BG).pack(side=tk.LEFT,
|
||||||
|
padx=(8, 0))
|
||||||
|
self.count_var = tk.StringVar(value="")
|
||||||
|
tk.Label(header, textvariable=self.count_var, font=plain_font, fg="#555555",
|
||||||
|
bg=HEADER_BG).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _build_table(self):
|
||||||
|
area = tk.Frame(self.frame)
|
||||||
|
area.pack(fill=tk.BOTH, expand=True, padx=8, pady=(4, 0))
|
||||||
|
|
||||||
|
columns = ("username", "permissions", "provisioning")
|
||||||
|
self.table = ttk.Treeview(area, columns=columns, show="headings",
|
||||||
|
selectmode="browse")
|
||||||
|
self.table.heading("username", text="Username")
|
||||||
|
self.table.heading("permissions", text="Group Permissions")
|
||||||
|
self.table.heading("provisioning", text="Provisioning")
|
||||||
|
|
||||||
|
self.table.column("username", width=120, minwidth=80)
|
||||||
|
self.table.column("permissions", width=320, minwidth=200)
|
||||||
|
self.table.column("provisioning", width=120, minwidth=80)
|
||||||
|
|
||||||
|
vscroll = ttk.Scrollbar(area, orient=tk.VERTICAL, command=self.table.yview)
|
||||||
|
self.table.configure(yscrollcommand=vscroll.set)
|
||||||
|
|
||||||
|
self.table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
vscroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
self.table.bind("<Double-1>", lambda _e: self._on_edit())
|
||||||
|
self._user_elements = {}
|
||||||
|
|
||||||
|
def _build_footer(self):
|
||||||
|
footer = tk.Frame(self.frame, padx=8, pady=6)
|
||||||
|
footer.pack(side=tk.BOTTOM, fill=tk.X)
|
||||||
|
|
||||||
|
ttk.Button(footer, text="Close", width=9,
|
||||||
|
command=self.close).pack(side=tk.RIGHT)
|
||||||
|
ttk.Button(footer, text="Delete User", width=12,
|
||||||
|
command=self._on_delete).pack(side=tk.RIGHT, padx=(0, 6))
|
||||||
|
ttk.Button(footer, text="Edit User", width=12,
|
||||||
|
command=self._on_edit).pack(side=tk.RIGHT, padx=(0, 6))
|
||||||
|
ttk.Button(footer, text="Add User", width=12,
|
||||||
|
command=self._on_add).pack(side=tk.RIGHT, padx=(0, 6))
|
||||||
|
|
||||||
|
def _refresh_table(self):
|
||||||
|
for item in self.table.get_children():
|
||||||
|
self.table.delete(item)
|
||||||
|
self._user_elements.clear()
|
||||||
|
|
||||||
|
self.comp = _resolve_comp(self.comp)
|
||||||
|
users = _get_users(self.comp)
|
||||||
|
for user_elem in users:
|
||||||
|
name = user_elem.get("name", "?")
|
||||||
|
perm = _get_prop(user_elem, "perm")
|
||||||
|
prov = _get_prop(user_elem, "prov")
|
||||||
|
|
||||||
|
iid = self.table.insert("", tk.END, values=(
|
||||||
|
name,
|
||||||
|
_format_perm(perm),
|
||||||
|
_format_prov(prov),
|
||||||
|
))
|
||||||
|
self._user_elements[iid] = user_elem
|
||||||
|
|
||||||
|
self.count_var.set(f"{len(users)} user{'' if len(users) == 1 else 's'}")
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
self._refresh_table()
|
||||||
|
|
||||||
|
def _selected_user(self):
|
||||||
|
sel = self.table.selection()
|
||||||
|
if not sel:
|
||||||
|
return None, None
|
||||||
|
return sel[0], self._user_elements.get(sel[0])
|
||||||
|
|
||||||
|
def _after_change(self):
|
||||||
|
self._refresh_table()
|
||||||
|
if self.on_change:
|
||||||
|
self.on_change()
|
||||||
|
|
||||||
|
def _on_add(self):
|
||||||
|
if app_state.live_mode:
|
||||||
|
app_state.set_status("Add user is not available in live mode", warn=True)
|
||||||
|
return
|
||||||
|
_UserDialog(self.frame.winfo_toplevel(), self.comp, None, self._after_change)
|
||||||
|
|
||||||
|
def _on_edit(self):
|
||||||
|
iid, user_elem = self._selected_user()
|
||||||
|
if user_elem is None:
|
||||||
|
messagebox.showinfo("Edit User", "Select a user first.",
|
||||||
|
parent=self.frame.winfo_toplevel())
|
||||||
|
return
|
||||||
|
if app_state.live_mode:
|
||||||
|
app_state.set_status("Edit user is not available in live mode", warn=True)
|
||||||
|
return
|
||||||
|
_UserDialog(self.frame.winfo_toplevel(), self.comp, user_elem, self._after_change)
|
||||||
|
|
||||||
|
def _on_delete(self):
|
||||||
|
iid, user_elem = self._selected_user()
|
||||||
|
if user_elem is None:
|
||||||
|
messagebox.showinfo("Delete User", "Select a user first.",
|
||||||
|
parent=self.frame.winfo_toplevel())
|
||||||
|
return
|
||||||
|
if app_state.live_mode:
|
||||||
|
app_state.set_status("Delete user is not available in live mode", warn=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
name = user_elem.get("name", "?")
|
||||||
|
if not messagebox.askyesno("Delete User", f"Delete user '{name}'?",
|
||||||
|
parent=self.frame.winfo_toplevel()):
|
||||||
|
return
|
||||||
|
|
||||||
|
index = list(self.comp).index(user_elem)
|
||||||
|
cmd = undo.AddComponentCommand(self.comp, index, user_elem, label="delete user")
|
||||||
|
cmd.undo()
|
||||||
|
undo.push(cmd)
|
||||||
|
app_state.mark_dirty()
|
||||||
|
self._after_change()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.on_close is not None:
|
||||||
|
self.on_close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
if self.frame.winfo_exists():
|
||||||
|
self.frame.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def build_user_service_panel(parent, comp, on_change=None, on_close=None):
|
||||||
|
if comp is None or parent is None:
|
||||||
|
return None
|
||||||
|
return UserServicePanel(parent, comp, on_change=on_change, on_close=on_close)
|
||||||
Reference in New Issue
Block a user