diff --git a/msp/msp/doctype/x509_certificate/__init__.py b/msp/msp/doctype/ssl_certificate/__init__.py similarity index 100% rename from msp/msp/doctype/x509_certificate/__init__.py rename to msp/msp/doctype/ssl_certificate/__init__.py diff --git a/msp/msp/doctype/ssl_certificate/ssl_certificate.js b/msp/msp/doctype/ssl_certificate/ssl_certificate.js new file mode 100644 index 0000000..191fa15 --- /dev/null +++ b/msp/msp/doctype/ssl_certificate/ssl_certificate.js @@ -0,0 +1,112 @@ +function copy_to_clipboard(text) { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } + // Fallback for non-HTTPS contexts + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + return Promise.resolve(); +} + +frappe.ui.form.on("SSL Certificate", { + refresh(frm) { + if (!frm.is_new() && frm.doc.certificate_data) { + frm.trigger("render_downloads"); + } + }, + + render_downloads(frm) { + const cn = frm.doc.common_name || frm.doc.name; + const has_key = !!frm.doc.private_key; + const has_ca = !!frm.doc.ca_chain; + const has_csr = !!frm.doc.csr_data; + + const btn = (label, icon, format_type, disabled) => { + const cls = disabled ? "btn-default disabled" : "btn-primary-light"; + const title = disabled ? 'title="Nicht vorhanden"' : ""; + return ``; + }; + + const copy_btn = (label, format_type, disabled) => { + const cls = disabled ? "btn-default disabled" : "btn-default"; + const title = disabled ? 'title="Nicht vorhanden"' : ""; + return ``; + }; + + const html = ` +
+
+ Download + ${btn("Certificate", "certificate", "cert", false)} + ${btn("Private Key", "key", "key", !has_key)} + ${btn("Fullchain", "link", "fullchain", false)} + ${btn("Cert + Key", "files-o", "cert_key", !has_key)} + ${btn("CA Chain", "sitemap", "ca", !has_ca)} + ${btn("CSR", "file-text-o", "csr", !has_csr)} +
+
+ Copy to Clipboard + ${copy_btn("Certificate", "cert", false)} + ${copy_btn("Private Key", "key", !has_key)} + ${copy_btn("Fullchain", "fullchain", false)} + ${copy_btn("CA Chain", "ca", !has_ca)} + ${copy_btn("CSR", "csr", !has_csr)} +
+
`; + + frm.fields_dict.downloads_html.$wrapper.html(html); + + // Download handlers + frm.fields_dict.downloads_html.$wrapper.find("button[data-format]:not(.copy-btn):not(:disabled)").on("click", function () { + const format_type = $(this).data("format"); + frappe.call({ + method: "msp.msp.doctype.ssl_certificate.ssl_certificate.get_certificate_pem", + args: { name: frm.doc.name, format_type }, + callback(r) { + if (r.message) { + const blob = new Blob([r.message.content], { type: "application/x-pem-file" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = r.message.filename; + a.click(); + URL.revokeObjectURL(url); + frappe.show_alert({ message: `${r.message.filename} heruntergeladen`, indicator: "green" }); + } + }, + }); + }); + + // Copy handlers + frm.fields_dict.downloads_html.$wrapper.find(".copy-btn:not(:disabled)").on("click", function () { + const format_type = $(this).data("format"); + const btn_el = $(this); + frappe.call({ + method: "msp.msp.doctype.ssl_certificate.ssl_certificate.get_certificate_pem", + args: { name: frm.doc.name, format_type }, + callback(r) { + if (r.message) { + copy_to_clipboard(r.message.content).then(() => { + const orig = btn_el.html(); + btn_el.html(' Copied!'); + setTimeout(() => btn_el.html(orig), 2000); + frappe.show_alert({ message: "In Zwischenablage kopiert", indicator: "green" }); + }); + } + }, + }); + }); + }, +}); diff --git a/msp/msp/doctype/ssl_certificate/ssl_certificate.json b/msp/msp/doctype/ssl_certificate/ssl_certificate.json new file mode 100644 index 0000000..628c1ea --- /dev/null +++ b/msp/msp/doctype/ssl_certificate/ssl_certificate.json @@ -0,0 +1,330 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-06-04 14:19:04.168850", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "certificate_name", + "customer", + "it_landscape", + "column_break_meta", + "common_name", + "issuer", + "certificate_type", + "is_wildcard", + "status", + "validity_section", + "not_valid_before", + "column_break_validity", + "not_valid_after", + "renewal_section", + "auto_renew", + "renewal_method", + "import_section", + "attach_zip", + "import_html", + "certificate_pem_section", + "certificate_data", + "ca_chain", + "column_break_pem", + "private_key", + "csr_data", + "verification_section", + "private_key_verification", + "column_break_verify", + "ca_verification", + "certificate_info_section", + "certificate_information", + "column_break_info", + "ca_information", + "downloads_section", + "downloads_html", + "domains_section", + "domains", + "installations_section", + "installations" + ], + "fields": [ + { + "fieldname": "certificate_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Certificate Name", + "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Customer", + "options": "Customer" + }, + { + "fieldname": "it_landscape", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "IT Landscape", + "options": "IT Landscape" + }, + { + "fieldname": "column_break_meta", + "fieldtype": "Column Break" + }, + { + "fieldname": "common_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Common Name (CN)", + "read_only": 1 + }, + { + "fieldname": "issuer", + "fieldtype": "Data", + "label": "Issuer", + "read_only": 1 + }, + { + "fieldname": "certificate_type", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Certificate Type", + "options": "\nDV\nOV\nEV\nSelf-Signed\nInternal CA" + }, + { + "default": "0", + "fieldname": "is_wildcard", + "fieldtype": "Check", + "label": "Is Wildcard", + "read_only": 1 + }, + { + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Active\nExpired\nRevoked\nPending" + }, + { + "fieldname": "validity_section", + "fieldtype": "Section Break", + "label": "Validity" + }, + { + "fieldname": "not_valid_before", + "fieldtype": "Datetime", + "label": "Not Valid Before", + "read_only": 1 + }, + { + "fieldname": "column_break_validity", + "fieldtype": "Column Break" + }, + { + "fieldname": "not_valid_after", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Not Valid After", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "renewal_section", + "fieldtype": "Section Break", + "label": "Renewal" + }, + { + "default": "0", + "fieldname": "auto_renew", + "fieldtype": "Check", + "label": "Auto Renew" + }, + { + "fieldname": "renewal_method", + "fieldtype": "Select", + "label": "Renewal Method", + "options": "\nACME\nManual\nManaged" + }, + { + "fieldname": "import_section", + "fieldtype": "Section Break", + "label": "Import" + }, + { + "description": "ZIP-Datei mit .crt.pem, .key.pem, .ca.pem, .csr.pem, .fullchain.pem hochladen", + "fieldname": "attach_zip", + "fieldtype": "Attach", + "label": "Certificate ZIP" + }, + { + "fieldname": "import_html", + "fieldtype": "HTML" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:!doc.certificate_data", + "fieldname": "certificate_pem_section", + "fieldtype": "Section Break", + "label": "PEM Data" + }, + { + "fieldname": "certificate_data", + "fieldtype": "Code", + "label": "Certificate (PEM)", + "options": "PEM" + }, + { + "fieldname": "ca_chain", + "fieldtype": "Code", + "label": "CA Chain (PEM)", + "options": "PEM" + }, + { + "fieldname": "column_break_pem", + "fieldtype": "Column Break" + }, + { + "fieldname": "private_key", + "fieldtype": "Password", + "label": "Private Key (PEM)" + }, + { + "fieldname": "csr_data", + "fieldtype": "Code", + "label": "CSR (PEM)", + "options": "PEM" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.certificate_data", + "fieldname": "verification_section", + "fieldtype": "Section Break", + "label": "Verification" + }, + { + "fieldname": "private_key_verification", + "fieldtype": "Data", + "label": "Private Key Match", + "read_only": 1 + }, + { + "fieldname": "column_break_verify", + "fieldtype": "Column Break" + }, + { + "fieldname": "ca_verification", + "fieldtype": "Data", + "label": "CA Chain Valid", + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:doc.certificate_data", + "fieldname": "certificate_info_section", + "fieldtype": "Section Break", + "label": "Certificate Details" + }, + { + "fieldname": "certificate_information", + "fieldtype": "HTML", + "label": "Certificate Information", + "read_only": 1 + }, + { + "fieldname": "column_break_info", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.ca_chain", + "fieldname": "ca_information", + "fieldtype": "HTML", + "label": "CA Information", + "read_only": 1 + }, + { + "depends_on": "eval:doc.certificate_data", + "fieldname": "downloads_section", + "fieldtype": "Section Break", + "label": "Downloads" + }, + { + "fieldname": "downloads_html", + "fieldtype": "HTML", + "label": "Downloads" + }, + { + "fieldname": "domains_section", + "fieldtype": "Section Break", + "label": "SAN Domains" + }, + { + "fieldname": "domains", + "fieldtype": "Table", + "label": "Domains", + "options": "SSL Certificate Domain" + }, + { + "fieldname": "installations_section", + "fieldtype": "Section Break", + "label": "Installations" + }, + { + "fieldname": "installations", + "fieldtype": "Table", + "label": "Installations", + "options": "SSL Certificate Installation" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-04-10 00:00:00.000000", + "modified_by": "Administrator", + "module": "MSP", + "name": "SSL Certificate", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "MSP User", + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "MSP Admin", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "common_name", + "show_title_field_in_link": 1 +} diff --git a/msp/msp/doctype/ssl_certificate/ssl_certificate.py b/msp/msp/doctype/ssl_certificate/ssl_certificate.py new file mode 100644 index 0000000..cc2e964 --- /dev/null +++ b/msp/msp/doctype/ssl_certificate/ssl_certificate.py @@ -0,0 +1,238 @@ +import frappe +import zipfile +import os +import datetime +from frappe.model.document import Document +from frappe.utils import get_files_path +from cryptography import x509 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 + + +class SSLCertificate(Document): + def before_save(self): + if self.attach_zip and self.has_value_changed("attach_zip"): + self._import_from_zip() + + if self.certificate_data: + self._parse_certificate() + + if self.ca_chain: + self._parse_ca() + + if self.certificate_data and self.get_password("private_key"): + self._verify_private_key() + + def after_insert(self): + if self.common_name and not self.certificate_name: + self._set_auto_name() + + def _import_from_zip(self): + zip_path = self._resolve_file_path(self.attach_zip) + if not zip_path or not os.path.exists(zip_path): + frappe.throw("ZIP-Datei nicht gefunden") + + with zipfile.ZipFile(zip_path, "r") as zf: + for info in zf.infolist(): + name = info.filename + content = zf.read(name).decode("utf-8").strip() + + if name.endswith(".crt.pem") and not name.endswith(".fullchain.pem"): + self.certificate_data = content + elif name.endswith(".key.pem"): + self.private_key = content + elif name.endswith(".ca.pem"): + self.ca_chain = content + elif name.endswith(".csr.pem"): + self.csr_data = content + + def _parse_certificate(self): + try: + cert = x509.load_pem_x509_certificate(self.certificate_data.encode()) + except Exception as e: + frappe.throw(f"Zertifikat konnte nicht gelesen werden: {e}") + + self.common_name = self._get_attr(cert.subject, x509.NameOID.COMMON_NAME) + self.issuer = self._get_attr(cert.issuer, x509.NameOID.COMMON_NAME) + self.not_valid_before = cert.not_valid_before_utc.strftime("%Y-%m-%d %H:%M:%S") + self.not_valid_after = cert.not_valid_after_utc.strftime("%Y-%m-%d %H:%M:%S") + self.is_wildcard = 1 if self.common_name and self.common_name.startswith("*.") else 0 + + if not self.certificate_name: + exp = cert.not_valid_after_utc.strftime("%Y-%m") + self.certificate_name = f"{self.common_name} ({exp})" + + # Auto-set status based on expiry + now_utc = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + if cert.not_valid_before_utc.replace(tzinfo=None) > now_utc: + self.status = "Pending" + elif cert.not_valid_after_utc.replace(tzinfo=None) < now_utc: + self.status = "Expired" + + # Auto-detect certificate type from issuer + if not self.certificate_type: + issuer_org = self._get_attr(cert.issuer, x509.NameOID.ORGANIZATION_NAME) or "" + if self.common_name and self._get_attr(cert.issuer, x509.NameOID.COMMON_NAME) == self.common_name: + self.certificate_type = "Self-Signed" + elif "Let's Encrypt" in issuer_org or "ZeroSSL" in issuer_org or "Sectigo" in issuer_org: + self.certificate_type = "DV" + + # Populate SAN domains + self._populate_san_domains(cert) + + # Build certificate info HTML + self._build_cert_info_html(cert) + + def _populate_san_domains(self, cert): + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) + san_names = ext.value.get_values_for_type(x509.DNSName) + except x509.ExtensionNotFound: + san_names = [] + + if self.common_name and self.common_name not in san_names: + san_names.insert(0, self.common_name) + + existing = {row.domain_name for row in self.domains} + for domain_name in san_names: + if domain_name not in existing: + # Try to find matching IT Domain + it_domain = frappe.db.exists("IT Domain", domain_name.lstrip("*.")) + self.append("domains", { + "domain": it_domain or None, + "domain_name": domain_name, + }) + + def _build_cert_info_html(self, cert): + subject = cert.subject + cn = self._get_attr(subject, x509.NameOID.COMMON_NAME) + org = self._get_attr(subject, x509.NameOID.ORGANIZATION_NAME) + country = self._get_attr(subject, x509.NameOID.COUNTRY_NAME) + state = self._get_attr(subject, x509.NameOID.STATE_OR_PROVINCE_NAME) + + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) + sans = ext.value.get_values_for_type(x509.DNSName) + except x509.ExtensionNotFound: + sans = [] + + serial_hex = format(cert.serial_number, "X") + serial_formatted = ":".join(serial_hex[i:i+2] for i in range(0, len(serial_hex), 2)) + + self.certificate_information = f""" + + + + + + + + +
Common Name{cn or 'N/A'}
Organization{org or 'N/A'}
Country / State{country or 'N/A'} / {state or 'N/A'}
Issuer{self._get_attr(cert.issuer, x509.NameOID.COMMON_NAME) or 'N/A'}
SANs{', '.join(sans) if sans else 'None'}
Serial{serial_formatted}
Signature{cert.signature_hash_algorithm.name if cert.signature_hash_algorithm else 'N/A'}
""" + + def _parse_ca(self): + try: + ca_cert = x509.load_pem_x509_certificate(self.ca_chain.encode()) + except Exception: + self.ca_verification = "CA Chain nicht lesbar" + return + + ca_cn = self._get_attr(ca_cert.subject, x509.NameOID.COMMON_NAME) + ca_org = self._get_attr(ca_cert.issuer, x509.NameOID.ORGANIZATION_NAME) + + # Verify CA signed the certificate + if self.certificate_data: + cert = x509.load_pem_x509_certificate(self.certificate_data.encode()) + cert_issuer_cn = self._get_attr(cert.issuer, x509.NameOID.COMMON_NAME) + ca_subject_cn = self._get_attr(ca_cert.subject, x509.NameOID.COMMON_NAME) + if cert_issuer_cn == ca_subject_cn: + self.ca_verification = "CA Chain valid" + else: + self.ca_verification = f"Mismatch: Cert Issuer={cert_issuer_cn}, CA CN={ca_subject_cn}" + + self.ca_information = f""" + + + + +
CA Common Name{ca_cn or 'N/A'}
CA Issuer{ca_org or 'N/A'}
Valid Until{ca_cert.not_valid_after_utc}
""" + + def _verify_private_key(self): + try: + cert = x509.load_pem_x509_certificate(self.certificate_data.encode()) + key_pem = self.get_password("private_key") + private_key = serialization.load_pem_private_key(key_pem.encode(), password=None) + + message = b"x509_verify" + signature = private_key.sign(message, PKCS1v15(), hashes.SHA256()) + cert.public_key().verify(signature, message, PKCS1v15(), hashes.SHA256()) + self.private_key_verification = "Key matches certificate" + except Exception as e: + self.private_key_verification = f"Key mismatch: {e}" + + def _set_auto_name(self): + if self.not_valid_after: + dt = frappe.utils.get_datetime(self.not_valid_after) + new_name = f"{self.common_name}_{dt.strftime('%Y-%m-%d')}" + else: + new_name = self.common_name + + if new_name != self.name: + frappe.rename_doc("SSL Certificate", self.name, new_name, force=True) + + @staticmethod + def _get_attr(name_obj, oid): + try: + return name_obj.get_attributes_for_oid(oid)[0].value + except (IndexError, Exception): + return None + + @staticmethod + def _resolve_file_path(file_url): + if not file_url: + return None + if file_url.startswith("/private/files/"): + return get_files_path(*file_url.split("/private/files/", 1)[1].split("/"), is_private=1) + elif file_url.startswith("/files/"): + return get_files_path(*file_url.split("/files/", 1)[1].split("/")) + return None + + +@frappe.whitelist() +def get_certificate_pem(name, format_type): + """Return PEM data for download. format_type: cert, key, ca, csr, fullchain, cert_key""" + doc = frappe.get_doc("SSL Certificate", name) + doc.check_permission("read") + + if format_type == "cert": + return {"content": doc.certificate_data, "filename": f"{doc.common_name or name}.crt.pem"} + + elif format_type == "key": + key = doc.get_password("private_key") + if not key: + frappe.throw("Kein Private Key vorhanden") + return {"content": key, "filename": f"{doc.common_name or name}.key.pem"} + + elif format_type == "ca": + if not doc.ca_chain: + frappe.throw("Keine CA Chain vorhanden") + return {"content": doc.ca_chain, "filename": f"{doc.common_name or name}.ca.pem"} + + elif format_type == "csr": + if not doc.csr_data: + frappe.throw("Kein CSR vorhanden") + return {"content": doc.csr_data, "filename": f"{doc.common_name or name}.csr.pem"} + + elif format_type == "fullchain": + parts = [doc.certificate_data] + if doc.ca_chain: + parts.append(doc.ca_chain) + return {"content": "\n".join(parts), "filename": f"{doc.common_name or name}.fullchain.pem"} + + elif format_type == "cert_key": + key = doc.get_password("private_key") + if not key: + frappe.throw("Kein Private Key vorhanden") + return {"content": doc.certificate_data + "\n" + key, "filename": f"{doc.common_name or name}.combined.pem"} + + frappe.throw(f"Unbekanntes Format: {format_type}") diff --git a/msp/msp/doctype/x509_certificate/test_x509_certificate.py b/msp/msp/doctype/ssl_certificate/test_ssl_certificate.py similarity index 100% rename from msp/msp/doctype/x509_certificate/test_x509_certificate.py rename to msp/msp/doctype/ssl_certificate/test_ssl_certificate.py diff --git a/msp/msp/doctype/ssl_certificate_domain/__init__.py b/msp/msp/doctype/ssl_certificate_domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msp/msp/doctype/ssl_certificate_domain/ssl_certificate_domain.json b/msp/msp/doctype/ssl_certificate_domain/ssl_certificate_domain.json new file mode 100644 index 0000000..8287c87 --- /dev/null +++ b/msp/msp/doctype/ssl_certificate_domain/ssl_certificate_domain.json @@ -0,0 +1,36 @@ +{ + "actions": [], + "creation": "2026-04-09 00:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "domain", + "domain_name" + ], + "fields": [ + { + "fieldname": "domain", + "fieldtype": "Link", + "in_list_view": 1, + "label": "IT Domain", + "options": "IT Domain" + }, + { + "description": "Fallback fuer externe Domains ohne IT Domain-Eintrag", + "fieldname": "domain_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Domain Name" + } + ], + "istable": 1, + "links": [], + "modified": "2026-04-09 00:00:00.000000", + "modified_by": "Administrator", + "module": "MSP", + "name": "SSL Certificate Domain", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} diff --git a/msp/msp/doctype/x509_certificate/x509_certificate.py b/msp/msp/doctype/ssl_certificate_domain/ssl_certificate_domain.py similarity index 63% rename from msp/msp/doctype/x509_certificate/x509_certificate.py rename to msp/msp/doctype/ssl_certificate_domain/ssl_certificate_domain.py index 6d67881..69bb92c 100644 --- a/msp/msp/doctype/x509_certificate/x509_certificate.py +++ b/msp/msp/doctype/ssl_certificate_domain/ssl_certificate_domain.py @@ -1,5 +1,6 @@ import frappe from frappe.model.document import Document -class x509_certificate(Document): + +class SSLCertificateDomain(Document): pass diff --git a/msp/msp/doctype/ssl_certificate_installation/__init__.py b/msp/msp/doctype/ssl_certificate_installation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msp/msp/doctype/ssl_certificate_installation/ssl_certificate_installation.json b/msp/msp/doctype/ssl_certificate_installation/ssl_certificate_installation.json new file mode 100644 index 0000000..bf32e1e --- /dev/null +++ b/msp/msp/doctype/ssl_certificate_installation/ssl_certificate_installation.json @@ -0,0 +1,42 @@ +{ + "actions": [], + "creation": "2026-04-09 00:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "it_object", + "service", + "path" + ], + "fields": [ + { + "fieldname": "it_object", + "fieldtype": "Link", + "in_list_view": 1, + "label": "IT Object", + "options": "IT Object" + }, + { + "fieldname": "service", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Service" + }, + { + "fieldname": "path", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Path" + } + ], + "istable": 1, + "links": [], + "modified": "2026-04-09 00:00:00.000000", + "modified_by": "Administrator", + "module": "MSP", + "name": "SSL Certificate Installation", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} diff --git a/msp/msp/doctype/ssl_certificate_installation/ssl_certificate_installation.py b/msp/msp/doctype/ssl_certificate_installation/ssl_certificate_installation.py new file mode 100644 index 0000000..8ab22ed --- /dev/null +++ b/msp/msp/doctype/ssl_certificate_installation/ssl_certificate_installation.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class SSLCertificateInstallation(Document): + pass diff --git a/msp/msp/doctype/x509_certificate/x509_certificate.js b/msp/msp/doctype/x509_certificate/x509_certificate.js deleted file mode 100644 index 70fa247..0000000 --- a/msp/msp/doctype/x509_certificate/x509_certificate.js +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2024, itsdave GmbH and Contributors -// For license information, please see license.txt - - -//------------------------------------------------------------------------------------------------------------------------------- -frappe.ui.form.on('x509_certificate', { - after_save: function(frm) { - //loadCertificateInformation(frm); - } -}); - -// Function to load certificate information -function loadCertificateInformation(frm) { - // Extract and attach ZIP files first - frappe.call({ - method: 'msp.msp.doctype.x509_certificate.x509_certificate.extract_and_read_zip_file', - args: { - docname: frm.doc.name - }, - callback: function(response) { - if (response.message && response.message.success) { - // Refresh the certificate_data and private_key fields - frm.set_value('certificate_data'); - frm.set_value('private_key'); - - // Refresh the form to show updated values - frm.refresh_field('certificate_data'); - frm.refresh_field('private_key'); - - } - - // Proceed to load certificate information - frappe.call({ - method: 'msp.msp.doctype.x509_certificate.x509_certificate.read_cert_data', - args: { - certificate_name: frm.doc.name, - doc: frm.doc - }, - callback: function(response) { - if (response.message) { - if (response.message.error) { - if (response.message.error === 'Invalid certificate format') { - frappe.msgprint("Invalid Certificate Data"); - } else { - frappe.msgprint(response.message.error); - } - return; - } - - var parsedSubject = response.message.subject; - var parsedIssuer = parseIssuerInfo(response.message.issuer); - - var certificateInfo = "Subject:
" + - "- Common Name: " + (parsedSubject.common_name || 'N/A') + "
" + - "- Country: " + (parsedSubject.country || 'N/A') + "
" + - "- State/Province: " + (parsedSubject.state || 'N/A') + "
" + - "- City: " + (parsedSubject.city || 'N/A') + "
" + - "- Organization: " + (parsedSubject.organization || 'N/A') + "
" + - "- Organizational Unit: " + (parsedSubject.organization_unit || 'N/A') + "
" + - "Issuer: " + (parsedIssuer.commonName || 'N/A') + "
" + - "Subject Alternative Name: " + (response.message.subject_alt_names || 'N/A') + "
" + - "Validity Period:
" + - "- Not Before: " + response.message.not_valid_before + "
" + - "- Not After: " + response.message.not_valid_after + "
" + - "Serial Number: " + response.message.serial_number; - - frappe.msgprint("Setting information field with: " + frm.doc.name + "
" + certificateInfo); - frm.set_value('certificate_information', certificateInfo); - - // Check if private key is empty - if (!frm.doc.private_key) { - frappe.msgprint("Private Key is empty"); - } else { - // Check if private key is valid - frappe.msgprint(response.message.private_key_valid ? "Private Key is correct" : "Private Key is not valid"); - } - - // Set the create_date and expiry_date fields - frm.set_value('not_valid_before', response.message.not_valid_before); - frm.set_value('not_valid_after', response.message.not_valid_after); - - // Refresh the form to show updated values - frm.refresh_field('not_valid_before'); - frm.refresh_field('not_valid_after'); - frm.refresh_field('certificate_information'); - frm.refresh_field('certificate_data'); - frm.refresh_field('private_key'); - } else { - frappe.msgprint("Can't fetch certificate information."); - } - } - }); - } - }); -} - -function parseSubjectInfo(subjectInfo) { - if (subjectInfo) { - // Regular expression to extract Common Name (CN) from subject - var parenthesesPattern = /CN=([^,]+)/; - var cnMatch = subjectInfo.match(parenthesesPattern); - var commonName = cnMatch ? cnMatch[1] : null; - return { - commonName: commonName - }; - } - return null; - -} - -function parseIssuerInfo(issuerInfo) { - if (issuerInfo) { - var parenthesesPattern = /CN=([^,]+)/; - var cnMatch = issuerInfo.match(parenthesesPattern); - var commonName = cnMatch ? cnMatch[1] : null; - return { - commonName: commonName - }; - } - return null; -} - -// Function to format Subject Alternative Name -function formatSubjectAltName(subjectAltName) { - if (subjectAltName) { - return subjectAltName !== 'No more' ? subjectAltName : 'No hay nombres alternativos'; - } - return 'No hay nombres alternativos'; -} diff --git a/msp/msp/doctype/x509_certificate/x509_certificate.json b/msp/msp/doctype/x509_certificate/x509_certificate.json deleted file mode 100644 index 0fa0cd0..0000000 --- a/msp/msp/doctype/x509_certificate/x509_certificate.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2024-06-04 14:19:04.168850", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "certificate_name", - "attach_zip", - "attach_certificate_copy", - "attach_key", - "certificate_data", - "private_key", - "private_key_verification", - "ca_label_content", - "ca_verification", - "not_valid_before", - "not_valid_after", - "certificate_information", - "ca_information" - ], - "fields": [ - { - "fieldname": "certificate_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Certificate Name" - }, - { - "fieldname": "certificate_data", - "fieldtype": "Long Text", - "in_list_view": 1, - "label": "Certificate Data", - "read_only_depends_on": "eval:doc.attach_certificate_copy" - }, - { - "fieldname": "private_key", - "fieldtype": "Long Text", - "in_list_view": 1, - "label": "Private Key", - "read_only_depends_on": "eval:doc.attach_key" - }, - { - "fieldname": "not_valid_before", - "fieldtype": "Datetime", - "label": "Not Valid Before", - "read_only": 1 - }, - { - "fieldname": "not_valid_after", - "fieldtype": "Datetime", - "label": "Not Valid After", - "read_only": 1 - }, - { - "fieldname": "certificate_information", - "fieldtype": "Long Text", - "label": "Certificate Information", - "read_only": 1 - }, - { - "description": "Upload Key for PEM format Certificate", - "fieldname": "attach_key", - "fieldtype": "Attach", - "label": "Attach Key", - "read_only_depends_on": "eval:doc.attach_zip" - }, - { - "description": "Upload Certificate in PEM or P12 Format", - "fieldname": "attach_certificate_copy", - "fieldtype": "Attach", - "label": "Attach Certificate Copy", - "read_only_depends_on": "eval:doc.attach_zip" - }, - { - "fieldname": "attach_zip", - "fieldtype": "Attach", - "label": "zip" - }, - { - "fieldname": "private_key_verification", - "fieldtype": "Data", - "label": "Private Key Verification", - "read_only": 1 - }, - { - "depends_on": "eval:doc.ca_label_content", - "fieldname": "ca_information", - "fieldtype": "Long Text", - "label": "CA Information", - "read_only": 1 - }, - { - "depends_on": "eval:doc.attach_zip", - "fieldname": "ca_label_content", - "fieldtype": "Long Text", - "label": "Certificate Authority", - "read_only_depends_on": "eval:doc.attach_zip" - }, - { - "depends_on": "eval:doc.ca_label_content", - "fieldname": "ca_verification", - "fieldtype": "Data", - "label": "CA Verification", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-06-14 10:50:44.471107", - "modified_by": "Administrator", - "module": "MSP", - "name": "x509_certificate", - "naming_rule": "Expression (old style)", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file