Zeeshan925

Enterprise Policy Manager GUI.py

Dec 16th, 2025 (edited)
145
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 36.80 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. """
  3. Fantastic v5.7 – Enterprise Policy Manager + Super Perfect Simulator
  4. PySide6 GUI with Redis backend, full simulator integrated with user dropdown.
  5. Supports staged changes and highlights for modified/deleted items.
  6. """
  7.  
  8. import sys, json, uuid, redis, ipaddress
  9. from urllib.parse import urlparse
  10. from PySide6.QtWidgets import (
  11. QApplication, QMainWindow, QWidget, QListWidget, QListWidgetItem,
  12. QPushButton, QLabel, QLineEdit, QTextEdit, QCheckBox,
  13. QVBoxLayout, QHBoxLayout, QSplitter, QDialog, QFormLayout,
  14. QDialogButtonBox, QMessageBox, QInputDialog, QFileDialog, QComboBox,
  15. QProgressDialog, QMenu
  16. )
  17. from PySide6.QtCore import Qt, QThread, Signal, QTimer, QEvent
  18. from PySide6.QtGui import QColor, QPalette
  19.  
  20.  
  21. # ---------------- JsonWorker Class -----------------
  22. class JsonWorker(QThread):
  23. progress = Signal(int)
  24. finished = Signal(object)
  25. error = Signal(str)
  26.  
  27. def __init__(self, task_type, file_path, data=None):
  28. super().__init__()
  29. self.task_type = task_type
  30. self.file_path = file_path
  31. self.data = data
  32.  
  33. def run(self):
  34. try:
  35. if self.task_type == 'export':
  36. with open(self.file_path, 'w', encoding='utf-8') as f:
  37. json.dump(self.data, f, indent=4)
  38. self.progress.emit(100)
  39. self.finished.emit(True)
  40.  
  41. elif self.task_type == 'import':
  42. with open(self.file_path, 'r', encoding='utf-8') as f:
  43. data = json.load(f)
  44. self.progress.emit(100)
  45. self.finished.emit(data)
  46. except Exception as e:
  47. self.error.emit(str(e))
  48.  
  49. # ---------------- Redis Store -----------------
  50. class PolicyStore:
  51. def __init__(self):
  52. self.r = None
  53.  
  54. def connect(self, host, port):
  55. self.r = redis.Redis(host=host, port=int(port), decode_responses=True)
  56. self.r.ping()
  57.  
  58. def list_domains(self):
  59. if not self.r: return []
  60. dom=set()
  61. for key in self.r.scan_iter("domain:*"):
  62. dom.add(key.split(":",1)[1])
  63. return sorted(dom)
  64.  
  65. def load_domain(self, domain):
  66. raw=self.r.get(f"domain:{domain}")
  67. if not raw:
  68. return {"domain":domain,"items":[]}
  69. data=json.loads(raw)
  70. for item in data.get("items",[]):
  71. if "id" not in item:
  72. item["id"]=str(uuid.uuid4())
  73. if not item.get("applies_on"):
  74. item["applies_on"] = ["-"]
  75. return data
  76.  
  77. def save_domain(self,data):
  78. self.r.set(f"domain:{data['domain']}",json.dumps(data))
  79.  
  80. def delete_domain(self,domain):
  81. self.r.delete(f"domain:{domain}")
  82.  
  83. def export_to_json(self,filename, domain_list=None):
  84. all_domains=[]
  85. domains = domain_list if domain_list else []
  86. for d in domains:
  87. all_domains.append(self.load_domain(d))
  88. with open(filename,"w") as f:
  89. json.dump(all_domains,f,indent=4)
  90.  
  91. def import_from_json(self,filename):
  92. with open(filename,"r") as f:
  93. data=json.load(f)
  94. for d in data:
  95. self.save_domain(d)
  96.  
  97. # ---------------- Users -----------------
  98. def get_all_users(self):
  99. if not self.r: return []
  100. users=[]
  101. for key in self.r.scan_iter("user:*"):
  102. uid = key.split("user:")[1]
  103. users.append(uid)
  104. return sorted(users)
  105.  
  106. def get_user(self, uid):
  107. if not uid or uid == "-":
  108. return {"applies_on":["-"], "uid":"-", "id":"-"}
  109. tmp = self.r.get(f"user:{uid}")
  110. if not tmp: return None
  111. data=json.loads(tmp)
  112. data["applies_on"]=list(data.get("applies_on", []))
  113. return data
  114.  
  115. def get_domain_policies(self, domain):
  116. tmp = self.r.get(f"domain:{domain}")
  117. if not tmp: return []
  118. policies=json.loads(tmp)
  119. for item in policies.get("items", []):
  120. for key in ["id","path","wildcard","action","applies_on",
  121. "source_net","policy_type","description","users"]:
  122. item.setdefault(key,None)
  123. yield item
  124.  
  125. # ---------------- Redis Worker -----------------
  126. class RedisWorker(QThread):
  127. domains_loaded = Signal(list)
  128. domain_data_loaded = Signal(dict)
  129. error = Signal(str)
  130.  
  131. def __init__(self, store, operation, data=None, parent=None):
  132. super().__init__(parent)
  133. self.store=store
  134. self.operation=operation
  135. self.data=data
  136.  
  137. def run(self):
  138. try:
  139. if self.operation=="list_domains":
  140. self.domains_loaded.emit(self.store.list_domains())
  141. elif self.operation=="load_domain":
  142. self.domain_data_loaded.emit(self.store.load_domain(self.data))
  143. except redis.exceptions.ConnectionError:
  144. self.error.emit("Redis connection error.")
  145. except Exception as e:
  146. self.error.emit(str(e))
  147.  
  148. # ---------------- Domain Item Dialog -----------------
  149. class DomainItemDialog(QDialog):
  150. def __init__(self,item=None,parent=None):
  151. super().__init__(parent)
  152. self.setWindowTitle("Edit Domain Item")
  153. self.item=item or {}
  154. lay=QFormLayout(self)
  155.  
  156. self.action=QLineEdit(self.item.get("action","allow"))
  157. self.source=QLineEdit(",".join(self.item.get("source_net",[])))
  158. self.applies=QLineEdit(",".join(self.item.get("applies_on",[])))
  159. self.ptype=QLineEdit(self.item.get("policy_type","custom"))
  160. self.desc=QTextEdit(self.item.get("description",""))
  161. self.path=QLineEdit(self.item.get("path","/"))
  162. self.wild=QCheckBox()
  163. self.wild.setChecked(self.item.get("wildcard",False))
  164.  
  165. lay.addRow("Action",self.action)
  166. lay.addRow("Source Net",self.source)
  167. lay.addRow("Applies On",self.applies)
  168. lay.addRow("Policy Type",self.ptype)
  169. lay.addRow("Description",self.desc)
  170. lay.addRow("Path",self.path)
  171. lay.addRow("Wildcard",self.wild)
  172.  
  173. btns=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
  174. btns.accepted.connect(self.accept)
  175. btns.rejected.connect(self.reject)
  176. lay.addWidget(btns)
  177.  
  178. def get_data(self):
  179. return {
  180. "id": self.item.get("id",str(uuid.uuid4())),
  181. "action": self.action.text().strip(),
  182. "source_net":[x.strip() for x in self.source.text().split(",") if x.strip()],
  183. "applies_on":[x.strip() for x in self.applies.text().split(",") if x.strip()] or ["-"],
  184. "policy_type":self.ptype.text().strip(),
  185. "description":self.desc.toPlainText().strip(),
  186. "path":self.path.text().strip(),
  187. "wildcard":self.wild.isChecked(),
  188. }
  189.  
  190. # ---------------- Simulation Logic -----------------
  191. def in_network(src, networks):
  192. if not networks or src=="-": return True
  193. try: ip=ipaddress.ip_address(src)
  194. except ValueError: return False
  195. for net in networks:
  196. try:
  197. if ip in ipaddress.ip_network(net): return True
  198. except ValueError: continue
  199. return False
  200.  
  201. def match_policy(store, user, src, uri):
  202. if "://" not in uri:
  203. base_domain=uri.split(":")[0]
  204. path="/"
  205. else:
  206. parsed=urlparse(uri)
  207. base_domain=parsed.netloc.split(":")[0]
  208. path=parsed.path if parsed.path else "/"
  209.  
  210. user_data=store.get_user(user)
  211. if not user_data:
  212. return {"message":f"ERR message=\"no (valid) IDENT {user}\"","user":{"applies_on":["-"]}}
  213.  
  214. fqdn=base_domain
  215. matched_policy=None
  216.  
  217. while True:
  218. for policy in store.get_domain_policies(fqdn):
  219. match_main = path.startswith(policy["path"]) and fqdn==base_domain
  220. match_parent = policy["path"]=="/" and fqdn!=base_domain and policy.get("wildcard",False)
  221. if (match_main or match_parent) and set(policy["applies_on"]) & set(user_data["applies_on"]):
  222. if not in_network(src, policy["source_net"]): continue
  223. matched_policy=policy
  224. matched_policy["domain"]=fqdn
  225. break
  226. if matched_policy or "." not in fqdn: break
  227. fqdn=fqdn.split(".",1)[1]
  228.  
  229. if not matched_policy:
  230. for policy in store.get_domain_policies("*"):
  231. if set(policy["applies_on"]) & set(user_data["applies_on"]):
  232. if not in_network(src, policy["source_net"]): continue
  233. matched_policy=policy
  234. matched_policy["domain"]="*"
  235. break
  236.  
  237. if matched_policy:
  238. if matched_policy["action"]=="allow":
  239. message=f"OK message=\"whitelisted {matched_policy.get('id','')}\" user=\"{user}\""
  240. else:
  241. message=f"ERR denied {matched_policy.get('id','')} user=\"{user}\""
  242. else:
  243. matched_policy={"action":"allow","policy_type":"fallback"}
  244. message=f"OK message=\"no (valid) IDENT {user}\""
  245.  
  246. return {"message":message,"user":user_data,"policy":matched_policy}
  247.  
  248. # ---------------- Policy GUI -----------------
  249. class PolicyGUI(QMainWindow):
  250. def __init__(self):
  251. super().__init__()
  252. self.setWindowTitle("Fantastic v5.7 – Enterprise Policy Manager")
  253. self.resize(1600,1000)
  254. self.store=PolicyStore()
  255. self.current_domain=None
  256. self.worker=None
  257. self.updating_items=False
  258. self.staged_domains={} # staging changes
  259. self.deleted_domains=set()
  260. self.highlighted_domains={} # highlight persistence
  261. self.init_ui()
  262. self.setup_auto_refresh()
  263.  
  264. # ---------------- UI -----------------
  265. def eventFilter(self, source, event):
  266. if event.type() == QEvent.Type.KeyPress and source is self.domain_list:
  267. if event.key() == Qt.Key_A and (event.modifiers() & Qt.ControlModifier):
  268. # 1. Stop Python-level signals
  269. self.updating_items = True
  270. self.domain_list.blockSignals(True)
  271.  
  272. # 2. Use the NATIVE C++ method (Instant)
  273. self.domain_list.selectAll()
  274.  
  275. # 3. Quickly sync checkboxes (the only loop we need)
  276. for i in range(self.domain_list.count()):
  277. self.domain_list.item(i).setCheckState(Qt.CheckState.Checked)
  278.  
  279. # 4. Clean up
  280. self.domain_list.blockSignals(False)
  281. self.updating_items = False
  282. self.domain_list.viewport().update()
  283. return True
  284. return super().eventFilter(source, event)
  285.  
  286. def show_context_menu(self, position):
  287. menu = QMenu(self)
  288. sel_all = menu.addAction("Select All")
  289. desel_all = menu.addAction("Deselect All")
  290.  
  291. action = menu.exec(self.domain_list.mapToGlobal(position))
  292. if not action:
  293. return
  294.  
  295. # 1. Complete freeze of signals and selection logic
  296. self.domain_list.blockSignals(True)
  297. self.updating_items = True
  298.  
  299. # 2. Temporary change selection mode to stop internal calculation
  300. old_mode = self.domain_list.selectionMode()
  301. self.domain_list.setSelectionMode(QListWidget.SelectionMode.NoSelection)
  302.  
  303. try:
  304. if action == sel_all:
  305. for i in range(self.domain_list.count()):
  306. itm = self.domain_list.item(i)
  307. itm.setCheckState(Qt.CheckState.Checked)
  308. itm.setSelected(True)
  309.  
  310. elif action == desel_all:
  311. for i in range(self.domain_list.count()):
  312. itm = self.domain_list.item(i)
  313. itm.setCheckState(Qt.CheckState.Unchecked)
  314. itm.setSelected(False)
  315. finally:
  316. # 3. Restore everything
  317. self.domain_list.setSelectionMode(old_mode)
  318. self.updating_items = False
  319. self.domain_list.blockSignals(False)
  320. self.domain_list.viewport().update()
  321.  
  322. # 2. Re-enable signals and update the UI
  323. self.updating_items = False
  324. self.domain_list.blockSignals(False)
  325. self.domain_list.viewport().update()
  326.  
  327. def init_ui(self):
  328. splitter=QSplitter(Qt.Horizontal)
  329. self.setCentralWidget(splitter)
  330. left=QWidget(); l=QVBoxLayout(left)
  331.  
  332. # Redis connect
  333. conn=QHBoxLayout()
  334. self.host=QLineEdit("127.0.0.1")
  335. self.port=QLineEdit("6379")
  336. btn_conn=QPushButton("Connect")
  337. btn_conn.clicked.connect(self.redis_connect)
  338. self.ind=QLabel(); self.ind.setFixedSize(16,16)
  339. self.update_ind(False)
  340. btn_refresh=QPushButton("Refresh Redis"); btn_refresh.clicked.connect(self.reload_all)
  341. conn.addWidget(self.host); conn.addWidget(self.port); conn.addWidget(btn_conn)
  342. conn.addWidget(btn_refresh); conn.addWidget(self.ind)
  343. l.addLayout(conn)
  344.  
  345. # Domain list
  346. l.addWidget(QLabel("Domains"))
  347. self.domain_list=QListWidget()
  348. self.domain_list.setSelectionMode(QListWidget.MultiSelection)
  349. self.domain_list=QListWidget()
  350. self.domain_list.setSelectionMode(QListWidget.MultiSelection)
  351.  
  352.  
  353. self.domain_list.setContextMenuPolicy(Qt.CustomContextMenu)
  354. self.domain_list.customContextMenuRequested.connect(self.show_context_menu)
  355. self.domain_list.installEventFilter(self)
  356.  
  357.  
  358. self.domain_list.itemClicked.connect(self.load_domain)
  359. self.domain_list.itemClicked.connect(self.load_domain)
  360. self.domain_list.itemChanged.connect(self.domain_item_changed)
  361. self.domain_list.selectionModel().selectionChanged.connect(self.domain_selection_changed)
  362. l.addWidget(self.domain_list)
  363.  
  364. # CRUD Buttons
  365. db=QHBoxLayout()
  366. for t,f in[("Add",self.add_domain),("Edit",self.edit_domain),
  367. ("Delete",self.del_domain),("Clone",self.clone_domain)]:
  368. b=QPushButton(t); b.clicked.connect(lambda checked,func=f:self.run_async(func)); db.addWidget(b)
  369. l.addLayout(db)
  370.  
  371. # Domain Items
  372. l.addWidget(QLabel("Domain Rules"))
  373. self.item_list=QListWidget(); l.addWidget(self.item_list)
  374. ib=QHBoxLayout()
  375. for t,f in[("Add",self.add_item),("Edit",self.edit_item),
  376. ("Delete",self.del_item),("Clone",self.clone_item)]:
  377. b=QPushButton(t); b.clicked.connect(f); ib.addWidget(b)
  378. l.addLayout(ib)
  379.  
  380. # Simulator
  381. l.addWidget(QLabel("Policy Simulator"))
  382. sim_fields = QHBoxLayout()
  383. self.user_sim = QComboBox(); self.user_sim.setFixedWidth(200)
  384. self.ip_sim = QLineEdit(); self.ip_sim.setPlaceholderText("IP Address"); self.ip_sim.setFixedWidth(150)
  385. self.domain_sim = QLineEdit(); self.domain_sim.setPlaceholderText("Domain/Path (e.g., cnn.com)"); self.domain_sim.setMinimumWidth(300)
  386. sim_fields.addWidget(self.user_sim); sim_fields.addWidget(self.ip_sim); sim_fields.addWidget(self.domain_sim)
  387. sim_fields.setSpacing(10); sim_fields.setContentsMargins(0, 0, 0, 0)
  388. l.addLayout(sim_fields)
  389.  
  390. btn_sim = QPushButton("Run Simulator"); btn_sim.clicked.connect(self.run_simulator)
  391. l.addWidget(btn_sim)
  392.  
  393. l.addWidget(QLabel("Simulator Log"))
  394. self.log_console=QTextEdit(); self.log_console.setReadOnly(True); l.addWidget(self.log_console)
  395.  
  396. self.btn_apply = QPushButton("Apply Changes"); self.btn_apply.setEnabled(False)
  397. self.btn_apply.clicked.connect(self.apply_changes)
  398. l.addWidget(self.btn_apply)
  399.  
  400. exim=QHBoxLayout()
  401. btn_exp=QPushButton("Export JSON"); btn_exp.clicked.connect(self.export_json)
  402. btn_imp=QPushButton("Import JSON"); btn_imp.clicked.connect(self.import_json)
  403. exim.addWidget(btn_exp); exim.addWidget(btn_imp); l.addLayout(exim)
  404.  
  405. self.all_buttons = [b for b in left.findChildren(QPushButton) if b.text() != "Connect"]
  406. for b in self.all_buttons: b.setEnabled(False)
  407.  
  408. splitter.addWidget(left)
  409.  
  410. # ---------------- Redis Connect -----------------
  411. def redis_connect(self):
  412. try:
  413. self.store.connect(self.host.text(), self.port.text())
  414. self.update_ind(True)
  415. for b in self.all_buttons: b.setEnabled(True)
  416. QTimer.singleShot(100, self.populate_user_dropdown)
  417. QTimer.singleShot(100, lambda: self.start_worker("list_domains"))
  418. except Exception:
  419. self.update_ind(False)
  420.  
  421. def populate_user_dropdown(self):
  422. self.user_sim.clear(); self.user_sim.addItem("-")
  423. for uid in self.store.get_all_users(): self.user_sim.addItem(uid)
  424.  
  425. def update_ind(self,status):
  426. pal=self.ind.palette()
  427. pal.setColor(QPalette.Window,QColor("green" if status else "red"))
  428. self.ind.setAutoFillBackground(True)
  429. self.ind.setPalette(pal)
  430.  
  431. def start_worker(self,op,data=None):
  432. self.worker=RedisWorker(self.store,op,data,self)
  433. self.worker.domains_loaded.connect(self.domains_loaded)
  434. self.worker.domain_data_loaded.connect(self.domain_loaded)
  435. self.worker.error.connect(lambda msg: None)
  436. self.worker.start()
  437.  
  438. def run_async(self,func):
  439. try:
  440. func()
  441. self.start_worker("list_domains")
  442. except Exception: pass
  443.  
  444. # ---------------- Domain Load -----------------
  445. def load_domain(self,item): self.start_worker("load_domain",item.text())
  446. def domain_loaded(self,data):
  447. self.current_domain=data
  448. self.reload_items()
  449.  
  450. # ---------------- Domain CRUD with Staging -----------------
  451. def add_domain(self):
  452. name, ok = QInputDialog.getText(self, "Add Domain", "Domain:")
  453. if ok and name:
  454. name = name.strip()
  455. if name in self.staged_domains or name in self.store.list_domains(): return
  456. self.staged_domains[name] = {"domain": name, "items": []}
  457. self.mark_modified() # Let the worker draw the yellow item
  458.  
  459. def edit_domain(self):
  460. sel = self.domain_list.currentItem()
  461. if not sel: return
  462. old = sel.text()
  463. new, ok = QInputDialog.getText(self, "Edit Domain", "Domain:", text=old)
  464.  
  465. if ok and new and old != new:
  466. new = new.strip()
  467. # 1. Move data to new name (Yellow)
  468. data = self.staged_domains.pop(old, self.store.load_domain(old))
  469. data['domain'] = new
  470. self.staged_domains[new] = data
  471.  
  472. # 2. Mark old name as deleted (Red)
  473. # Only if 'old' actually exists in Redis
  474. if old in self.store.list_domains():
  475. self.deleted_domains.add(old)
  476.  
  477. self.mark_modified()
  478.  
  479. def del_domain(self):
  480. selected = [i for i in self.domain_list.selectedItems()]
  481. if not selected: return
  482.  
  483. for item in selected:
  484. name = item.text()
  485. # If it's a brand new (unsaved) domain, just remove it entirely
  486. if name in self.staged_domains and name not in self.store.list_domains():
  487. self.staged_domains.pop(name, None)
  488. else:
  489. # Otherwise, move it to the deleted (Red) set
  490. self.staged_domains.pop(name, None)
  491. self.deleted_domains.add(name)
  492.  
  493. self.mark_modified() # This triggers the Red paint
  494.  
  495. def clone_domain(self):
  496. sel = self.domain_list.currentItem()
  497. if not sel: return
  498. dom = sel.text()
  499. data = self.staged_domains.get(dom, self.store.load_domain(dom)).copy()
  500. new_name = dom + "_clone"
  501. data['domain'] = new_name
  502. self.staged_domains[new_name] = data
  503. self.mark_modified() # Let the worker draw the yellow clone
  504.  
  505. # ---------------- Domain Items CRUD -----------------
  506. def add_item(self):
  507. if not self.current_domain: return
  508. dlg=DomainItemDialog(parent=self)
  509. if dlg.exec():
  510. itm = dlg.get_data()
  511. self.current_domain['items'].append(itm)
  512. self.staged_domains[self.current_domain['domain']] = self.current_domain
  513. self.mark_modified()
  514. self.reload_items(highlight_id=itm['id'], color="yellow")
  515.  
  516. def edit_item(self):
  517. sel=self.item_list.currentItem()
  518. if not sel or not self.current_domain: return
  519. iid=sel.data(Qt.UserRole)
  520. itm=[i for i in self.current_domain['items'] if i['id']==iid][0]
  521. dlg=DomainItemDialog(itm,self)
  522. if dlg.exec():
  523. new=dlg.get_data()
  524. idx=self.current_domain['items'].index(itm)
  525. self.current_domain['items'][idx] = new
  526. self.staged_domains[self.current_domain['domain']] = self.current_domain
  527. self.mark_modified()
  528. self.reload_items(highlight_id=new['id'], color="yellow")
  529.  
  530. def del_item(self):
  531. sel = self.item_list.currentItem()
  532. if not sel or not self.current_domain: return
  533. if QMessageBox.question(self,"Confirm Delete","Delete selected rule?",QMessageBox.Yes|QMessageBox.No)!=QMessageBox.Yes: return
  534. iid = sel.data(Qt.UserRole)
  535. self.current_domain['items'] = [i for i in self.current_domain['items'] if i['id'] != iid]
  536. self.staged_domains[self.current_domain['domain']] = self.current_domain
  537. self.mark_modified()
  538. self.reload_items(highlight_id=iid, color="red")
  539.  
  540. def clone_item(self):
  541. sel=self.item_list.currentItem()
  542. if not sel or not self.current_domain: return
  543. iid=sel.data(Qt.UserRole)
  544. itm=[i for i in self.current_domain['items'] if i['id']==iid][0]
  545. new=itm.copy(); new['id']=str(uuid.uuid4())
  546. self.current_domain['items'].append(new)
  547. self.staged_domains[self.current_domain['domain']] = self.current_domain
  548. self.mark_modified()
  549. self.reload_items(highlight_id=new['id'], color="yellow")
  550.  
  551. # ---------------- Reload & Highlight -----------------
  552. def reload_items(self, highlight_id=None, color=None):
  553. self.item_list.clear()
  554. if not self.current_domain: return
  555. for itm in self.current_domain['items']:
  556. i=QListWidgetItem(itm['action']+" | "+",".join(itm['source_net']))
  557. i.setData(Qt.UserRole,itm['id'])
  558. if highlight_id and itm['id']==highlight_id: i.setBackground(QColor(color))
  559. self.item_list.addItem(i)
  560.  
  561. def add_domain_list_item(self, name, color="yellow"):
  562. item=QListWidgetItem(name)
  563. item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
  564. item.setCheckState(Qt.Unchecked)
  565. item.setBackground(QColor(color))
  566. self.highlighted_domains[name] = color
  567. self.domain_list.addItem(item)
  568.  
  569. def highlight_item(self, item, color):
  570. item.setBackground(QColor(color))
  571. self.highlighted_domains[item.text()] = color
  572.  
  573. def mark_modified(self):
  574. self.btn_apply.setEnabled(True)
  575. # Force the worker to fetch and the UI to "re-paint" colors
  576. self.start_worker("list_domains")
  577.  
  578. # ---------------- Apply Changes -----------------
  579. def apply_changes(self):
  580. for name, data in self.staged_domains.items():
  581. self.store.save_domain(data)
  582. for name in self.deleted_domains:
  583. self.store.delete_domain(name)
  584.  
  585. # Clear staging dictionaries to reset highlights to normal
  586. self.staged_domains.clear()
  587. self.deleted_domains.clear()
  588.  
  589. self.btn_apply.setEnabled(False)
  590. self.start_worker("list_domains")
  591.  
  592. # ---------------- Simulator & Refresh -----------------
  593. def run_simulator(self):
  594. self.log_console.clear()
  595. user=self.user_sim.currentText()
  596. ip=self.ip_sim.text().strip() or "-"
  597. dom_path=self.domain_sim.text().strip()
  598. if not dom_path: self.log_console.setPlainText("Enter domain/path to simulate"); return
  599. result=match_policy(self.store,user,ip,dom_path)
  600. self.log_console.setPlainText(json.dumps(result,indent=2,ensure_ascii=False))
  601.  
  602. def setup_auto_refresh(self):
  603. self.timer = QTimer()
  604. self.timer.timeout.connect(self.reload_all)
  605. self.timer.start(5000)
  606.  
  607. def domain_item_changed(self, item):
  608. if self.updating_items: return
  609.  
  610. self.updating_items = True
  611. # Sync the highlight to the checkmark state
  612. item.setSelected(item.checkState() == Qt.CheckState.Checked)
  613. self.updating_items = False
  614.  
  615. def domain_selection_changed(self, selected, deselected):
  616. if self.updating_items: return
  617.  
  618. self.updating_items = True
  619. # Block signals so checking boxes doesn't trigger 'domain_item_changed'
  620. self.domain_list.blockSignals(True)
  621.  
  622. try:
  623. for idx in range(self.domain_list.count()):
  624. itm = self.domain_list.item(idx)
  625. # Sync the checkmark to the highlight state
  626. itm.setCheckState(Qt.CheckState.Checked if itm.isSelected() else Qt.CheckState.Unchecked)
  627. finally:
  628. self.domain_list.blockSignals(False)
  629. self.updating_items = False
  630.  
  631. # ---------------- Reload Domains -----------------
  632. def reload_all(self):
  633. if self.domain_list.selectedItems() or self.staged_domains or self.deleted_domains or self.highlighted_domains:
  634. return # pause refresh when highlighted/staged/deleted items exist
  635. self.start_worker("list_domains")
  636.  
  637. def domains_loaded(self, domains):
  638. self.domain_list.clear()
  639.  
  640. # Combine everything: Redis items + Staged items + Deleted items
  641. all_view = sorted(list(set(domains) | set(self.staged_domains.keys()) | self.deleted_domains))
  642.  
  643. for d in all_view:
  644. item = QListWidgetItem(d)
  645. item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
  646. item.setCheckState(Qt.Unchecked)
  647.  
  648. # Priority 1: If it's in deleted_domains, paint it RED
  649. if d in self.deleted_domains:
  650. item.setBackground(QColor("red"))
  651. # Priority 2: If it's in staged_domains, paint it YELLOW
  652. elif d in self.staged_domains:
  653. item.setBackground(QColor("yellow"))
  654.  
  655. self.domain_list.addItem(item)
  656.  
  657. # ---------------- Import/Export -----------------
  658. def export_json(self):
  659. file_path, _ = QFileDialog.getSaveFileName(self, "Export Domains", "", "JSON Files (*.json)")
  660. if not file_path: return
  661.  
  662. # --- Your Original Data Preparation Logic ---
  663. export_data = {}
  664. all_names = self.store.list_domains()
  665. for name in all_names:
  666. if name in self.deleted_domains: continue
  667. export_data[name] = self.staged_domains.get(name, self.store.load_domain(name))
  668.  
  669. for name, data in self.staged_domains.items():
  670. if name not in export_data: export_data[name] = data
  671.  
  672. # --- Progress Bar Setup ---
  673. self.export_pd = QProgressDialog("Exporting...", "Cancel", 0, 100, self)
  674. self.export_pd.setMinimumDuration(0)
  675. self.export_pd.setValue(50) # Set to 50% immediately since preparation is done
  676.  
  677. self.ex_worker = JsonWorker('export', file_path, export_data)
  678. self.ex_worker.finished.connect(lambda: (self.export_pd.close(),
  679. QMessageBox.information(self, "Success", "Exported successfully.")))
  680. self.ex_worker.error.connect(lambda e: (self.export_pd.close(),
  681. QMessageBox.critical(self, "Error", e)))
  682. self.ex_worker.start()
  683.  
  684. def import_json(self):
  685. file_path, _ = QFileDialog.getOpenFileName(self, "Import Domains", "", "JSON Files (*.json)")
  686. if not file_path: return
  687.  
  688. self.import_pd = QProgressDialog("Reading File...", "Cancel", 0, 100, self)
  689. self.import_pd.setMinimumDuration(0)
  690. self.import_pd.setValue(10)
  691.  
  692. self.im_worker = JsonWorker('import', file_path)
  693.  
  694. def finalize_import(new_data):
  695. self.import_pd.setValue(80)
  696. if not isinstance(new_data, dict):
  697. self.import_pd.close()
  698. QMessageBox.critical(self, "Error", "Invalid JSON format.")
  699. return
  700.  
  701. # --- Your Original Merge Logic ---
  702. count = 0
  703. for domain_name, domain_content in new_data.items():
  704. self.staged_domains[domain_name] = domain_content
  705. if domain_name in self.deleted_domains:
  706. self.deleted_domains.remove(domain_name)
  707. count += 1
  708.  
  709. self.import_pd.setValue(100)
  710. self.import_pd.close()
  711. self.mark_modified() # Triggers Painter
  712. QMessageBox.information(self, "Success", f"Imported {count} domains.")
  713.  
  714. self.im_worker.finished.connect(finalize_import)
  715. self.im_worker.error.connect(lambda e: (self.import_pd.close(), QMessageBox.critical(self, "Error", e)))
  716. self.im_worker.start()
  717.  
  718. # ---------------- Run -----------------
  719. if __name__=="__main__":
  720. app=QApplication(sys.argv)
  721. win=PolicyGUI()
  722. win.show()
  723. sys.exit(app.exec())
  724.  
Advertisement
Add Comment
Please, Sign In to add comment