#!/usr/bin/env python3 # role-info v1.2 — Analyze 1C role rights # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os import re import sys from collections import OrderedDict from lxml import etree sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") # --- Argument parsing --- parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False) parser.add_argument("-RightsPath", "-Path", required=True, help="Path to Rights.xml") parser.add_argument("-ShowDenied", action="store_true", default=False, help="Show denied rights") parser.add_argument("-Limit", type=int, default=150, help="Max lines to show") parser.add_argument("-Offset", type=int, default=0, help="Lines to skip") parser.add_argument("-OutFile", default="", help="Write output to file") args = parser.parse_args() # --- Output helper (collect all, paginate at the end) --- lines_buf = [] def out(text=""): lines_buf.append(text) # --- Resolve paths --- rights_path = args.RightsPath if not os.path.isabs(rights_path): rights_path = os.path.join(os.getcwd(), rights_path) if not os.path.isfile(rights_path): print(f"[ERROR] File not found: {rights_path}", file=sys.stderr) sys.exit(1) # --- Try to find metadata file for role name/synonym --- role_name = "" role_synonym = "" ext_dir = os.path.dirname(rights_path) # .../Ext role_dir = os.path.dirname(ext_dir) # .../RoleName roles_dir = os.path.dirname(role_dir) # .../Roles role_folder_name = os.path.basename(role_dir) meta_path = os.path.join(roles_dir, f"{role_folder_name}.xml") if os.path.isfile(meta_path): try: meta_tree = etree.parse(meta_path, etree.XMLParser(remove_blank_text=False)) meta_root = meta_tree.getroot() meta_ns = { "md": "http://v8.1c.ru/8.3/MDClasses", "v8": "http://v8.1c.ru/8.1/data/core", } name_node = meta_root.find(".//md:Role/md:Properties/md:Name", meta_ns) if name_node is not None and name_node.text: role_name = name_node.text syn_node = meta_root.find( ".//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", meta_ns ) if syn_node is not None and syn_node.text: role_synonym = syn_node.text except Exception: pass if not role_name: role_name = role_folder_name # --- Parse Rights.xml --- tree = etree.parse(rights_path, etree.XMLParser(remove_blank_text=False)) root = tree.getroot() rights_ns = "http://v8.1c.ru/8.2/roles" NSMAP = {"r": rights_ns} # Global flags set_for_new = root.get("setForNewObjects", "") set_for_attrs = root.get("setForAttributesByDefault", "") independent_child = root.get("independentRightsOfChildObjects", "") # --- Collect objects --- allowed = OrderedDict() # type -> OrderedDict { shortName -> [rights] } denied = OrderedDict() rls_objects = [] total_allowed = 0 total_denied = 0 for obj in root.findall("r:object", NSMAP): obj_name = "" rights = [] for child in obj: local = etree.QName(child.tag).localname if local == "name" and child.tag == f"{{{rights_ns}}}name": obj_name = child.text or "" if local == "right" and child.tag == f"{{{rights_ns}}}right": r_name = "" r_value = "" has_rls = False for rc in child: rc_local = etree.QName(rc.tag).localname if rc_local == "name": r_name = rc.text or "" if rc_local == "value": r_value = rc.text or "" if rc_local == "restrictionByCondition": has_rls = True if r_name and r_value: rights.append({"name": r_name, "value": r_value, "rls": has_rls}) if not obj_name or len(rights) == 0: continue dot_idx = obj_name.find(".") if dot_idx < 0: continue type_prefix = obj_name[:dot_idx] short_name = obj_name[dot_idx + 1:] for r in rights: if r["value"] == "true": total_allowed += 1 if type_prefix not in allowed: allowed[type_prefix] = OrderedDict() if short_name not in allowed[type_prefix]: allowed[type_prefix][short_name] = [] suffix = r["name"] if r["rls"]: suffix += " [RLS]" rls_objects.append(f"{type_prefix}.{short_name} ({r['name']})") allowed[type_prefix][short_name].append(suffix) else: total_denied += 1 if type_prefix not in denied: denied[type_prefix] = OrderedDict() if short_name not in denied[type_prefix]: denied[type_prefix][short_name] = [] denied[type_prefix][short_name].append(r["name"]) # --- Restriction templates --- templates = [] for tpl in root.findall("r:restrictionTemplate", NSMAP): for child in tpl: if etree.QName(child.tag).localname == "name": t_name = child.text or "" paren_idx = t_name.find("(") if paren_idx > 0: t_name = t_name[:paren_idx] templates.append(t_name) def get_support_status_for_path(target_path): try: def root_uuid(xml_path): if not os.path.isfile(xml_path): return None try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str) and child.get("uuid"): return child.get("uuid") except Exception: pass return None def is_external_root(xml_path): if not os.path.isfile(xml_path): return False try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str): return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport") except Exception: return False return False rp = os.path.abspath(target_path) # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). elem_uuid = root_uuid(rp) if is_external_root(rp): return None bin_path = None d = os.path.dirname(rp) for _ in range(12): if not d: break if is_external_root(d + ".xml"): return None if not elem_uuid: elem_uuid = root_uuid(d + ".xml") if not bin_path: cand = os.path.join(d, "Ext", "ParentConfigurations.bin") if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")): bin_path = cand if elem_uuid and bin_path: break parent = os.path.dirname(d) if parent == d: break d = parent if not bin_path or not os.path.exists(bin_path): return "не на поддержке" data = open(bin_path, "rb").read() if len(data) <= 32: return "снято с поддержки (правки свободны)" if data[:3] == b"\xef\xbb\xbf": data = data[3:] text = data.decode("utf-8", "replace") h = re.match(r"\{6,(\d+),(\d+),", text) if not h: return "не на поддержке" g = int(h.group(1)) k = int(h.group(2)) if k == 0: return "снято с поддержки (правки свободны)" if g == 1: return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" if not elem_uuid: return "не на поддержке" best = None for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text): f1 = int(m.group(1)) if best is None or f1 < best: best = f1 if best is None: return "не на поддержке" return { 0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта", 1: "редактируется с сохранением поддержки", 2: "снято с поддержки (правки свободны)", }.get(best, "не на поддержке") except Exception: return "не на поддержке" # --- Output --- header = f"=== Role: {role_name}" if role_synonym: header += f' --- "{role_synonym}"' header += " ===" out(header) _support = get_support_status_for_path(rights_path) if _support is not None: out(f"Поддержка: {_support}") out() out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}") out() # Helper: output group def out_group(obj_map, is_denied=False): for short_name, rights_list in obj_map.items(): if is_denied: rights_str = ", ".join(f"-{r}" for r in rights_list) else: rights_str = ", ".join(rights_list) out(f" {short_name}: {rights_str}") # Allowed rights grouped by type if len(allowed) > 0: out("Allowed rights:") out() for type_prefix, obj_map in allowed.items(): out(f" {type_prefix} ({len(obj_map)}):") out_group(obj_map) out() else: out("(no allowed rights)") out() # Denied rights if args.ShowDenied and len(denied) > 0: out("Denied rights:") out() for type_prefix, obj_map in denied.items(): out(f" {type_prefix} ({len(obj_map)}):") out_group(obj_map, is_denied=True) out() elif total_denied > 0: out(f"Denied: {total_denied} rights (use -ShowDenied to list)") out() # RLS summary if len(rls_objects) > 0: out(f"RLS: {len(rls_objects)} restrictions") # Templates if len(templates) > 0: out(f"Templates: {', '.join(templates)}") out() out("---") out(f"Total: {total_allowed} allowed, {total_denied} denied") # --- Pagination and output --- total_lines = len(lines_buf) out_lines = lines_buf[:] if args.Offset > 0: if args.Offset >= total_lines: print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.") sys.exit(0) out_lines = out_lines[args.Offset:] if args.Limit > 0 and len(out_lines) > args.Limit: shown = out_lines[:args.Limit] remaining = total_lines - args.Offset - args.Limit shown.append("") shown.append(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.") out_lines = shown if args.OutFile: out_file = args.OutFile if not os.path.isabs(out_file): out_file = os.path.join(os.getcwd(), out_file) with open(out_file, "w", encoding="utf-8-sig") as f: f.write("\n".join(out_lines)) print(f"Output written to {out_file}") else: for line in out_lines: print(line)