mirror of
https://github.com/itsdave-de/msp.git
synced 2026-08-14 07:30:05 -03:00
feat: replace x509_certificate with SSL Certificate DocType
- Rename x509_certificate → SSL Certificate (DocType, table, files) - Rename child tables to SSL Certificate Domain/Installation - ZIP import: extracts .crt.pem, .key.pem, .ca.pem, .csr.pem - Auto-parse: CN, issuer, SANs, validity, wildcard, cert type - Private key stored encrypted (Password field) - Download buttons: cert, key, fullchain, cert+key, CA, CSR - Copy-to-clipboard with HTTP fallback - Certificate details as HTML table - Key verification (signature check) and CA chain validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7f5f7a5236
commit
549963affd
@@ -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 `<button class="btn btn-sm ${cls} mr-2 mb-2" ${title}
|
||||||
|
data-format="${format_type}" ${disabled ? "disabled" : ""}>
|
||||||
|
<i class="fa fa-${icon}"></i> ${label}
|
||||||
|
</button>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy_btn = (label, format_type, disabled) => {
|
||||||
|
const cls = disabled ? "btn-default disabled" : "btn-default";
|
||||||
|
const title = disabled ? 'title="Nicht vorhanden"' : "";
|
||||||
|
return `<button class="btn btn-sm ${cls} mr-2 mb-2 copy-btn" ${title}
|
||||||
|
data-format="${format_type}" ${disabled ? "disabled" : ""}>
|
||||||
|
<i class="fa fa-clipboard"></i> ${label}
|
||||||
|
</button>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong class="text-muted d-block mb-2">Download</strong>
|
||||||
|
${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)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="text-muted d-block mb-2">Copy to Clipboard</strong>
|
||||||
|
${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)}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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('<i class="fa fa-check"></i> Copied!');
|
||||||
|
setTimeout(() => btn_el.html(orig), 2000);
|
||||||
|
frappe.show_alert({ message: "In Zwischenablage kopiert", indicator: "green" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"""
|
||||||
|
<table class="table table-bordered table-sm" style="margin:0">
|
||||||
|
<tr><td style="width:180px"><strong>Common Name</strong></td><td>{cn or 'N/A'}</td></tr>
|
||||||
|
<tr><td><strong>Organization</strong></td><td>{org or 'N/A'}</td></tr>
|
||||||
|
<tr><td><strong>Country / State</strong></td><td>{country or 'N/A'} / {state or 'N/A'}</td></tr>
|
||||||
|
<tr><td><strong>Issuer</strong></td><td>{self._get_attr(cert.issuer, x509.NameOID.COMMON_NAME) or 'N/A'}</td></tr>
|
||||||
|
<tr><td><strong>SANs</strong></td><td>{', '.join(sans) if sans else 'None'}</td></tr>
|
||||||
|
<tr><td><strong>Serial</strong></td><td><code style="font-size:11px">{serial_formatted}</code></td></tr>
|
||||||
|
<tr><td><strong>Signature</strong></td><td>{cert.signature_hash_algorithm.name if cert.signature_hash_algorithm else 'N/A'}</td></tr>
|
||||||
|
</table>"""
|
||||||
|
|
||||||
|
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"""
|
||||||
|
<table class="table table-bordered table-sm" style="margin:0">
|
||||||
|
<tr><td style="width:180px"><strong>CA Common Name</strong></td><td>{ca_cn or 'N/A'}</td></tr>
|
||||||
|
<tr><td><strong>CA Issuer</strong></td><td>{ca_org or 'N/A'}</td></tr>
|
||||||
|
<tr><td><strong>Valid Until</strong></td><td>{ca_cert.not_valid_after_utc}</td></tr>
|
||||||
|
</table>"""
|
||||||
|
|
||||||
|
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}")
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class x509_certificate(Document):
|
|
||||||
|
class SSLCertificateDomain(Document):
|
||||||
pass
|
pass
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class SSLCertificateInstallation(Document):
|
||||||
|
pass
|
||||||
@@ -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 = "<strong>Subject:</strong><br>" +
|
|
||||||
"- <strong>Common Name:</strong> " + (parsedSubject.common_name || 'N/A') + "<br>" +
|
|
||||||
"- <strong>Country:</strong> " + (parsedSubject.country || 'N/A') + "<br>" +
|
|
||||||
"- <strong>State/Province:</strong> " + (parsedSubject.state || 'N/A') + "<br>" +
|
|
||||||
"- <strong>City:</strong> " + (parsedSubject.city || 'N/A') + "<br>" +
|
|
||||||
"- <strong>Organization:</strong> " + (parsedSubject.organization || 'N/A') + "<br>" +
|
|
||||||
"- <strong>Organizational Unit:</strong> " + (parsedSubject.organization_unit || 'N/A') + "<br>" +
|
|
||||||
"<strong>Issuer:</strong> " + (parsedIssuer.commonName || 'N/A') + "<br>" +
|
|
||||||
"<strong>Subject Alternative Name:</strong> " + (response.message.subject_alt_names || 'N/A') + "<br>" +
|
|
||||||
"<strong>Validity Period:</strong><br>" +
|
|
||||||
"- <strong>Not Before:</strong> " + response.message.not_valid_before + "<br>" +
|
|
||||||
"- <strong>Not After:</strong> " + response.message.not_valid_after + "<br>" +
|
|
||||||
"<strong>Serial Number:</strong> " + response.message.serial_number;
|
|
||||||
|
|
||||||
frappe.msgprint("Setting information field with: " + frm.doc.name + "<br>" + 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';
|
|
||||||
}
|
|
||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user