Commit ca504d48 authored by lgarron@chromium.org's avatar lgarron@chromium.org

Add origin views to the Security panel.

BUG=502118, 503170

Review URL: https://codereview.chromium.org/1301833003

git-svn-id: svn://svn.chromium.org/blink/trunk@201187 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent 181cabbc
......@@ -534,7 +534,9 @@
],
'devtools_security_js_files': [
'front_end/security/lockIcon.css',
'front_end/security/originView.css',
'front_end/security/securityPanel.css',
'front_end/security/sidebar.css',
'front_end/security/SecurityModel.js',
'front_end/security/SecurityPanel.js',
],
......@@ -767,6 +769,9 @@
'front_end/Images/responsiveDesign_2x.png',
'front_end/Images/searchNext.png',
'front_end/Images/searchPrev.png',
'front_end/Images/securityPropertyInsecure.svg',
'front_end/Images/securityPropertySecure.svg',
'front_end/Images/securityPropertyWarning.svg',
'front_end/Images/securityStateInsecure_2x.png',
'front_end/Images/securityStateInsecure.png',
'front_end/Images/securityStateNeutral_2x.png',
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48px"
height="48px"
viewBox="0 0 48 48"
version="1.1"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="securityPropertyInsecure.svg">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10000"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1173"
inkscape:window-height="1491"
id="namedview11"
showgrid="true"
inkscape:snap-global="true"
inkscape:zoom="6.9532167"
inkscape:cx="24"
inkscape:cy="21.966102"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid4143" />
</sodipodi:namedview>
<defs
id="defs4" />
<path
style="fill:#d8463c;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m 10,33 9,-9 -9,-9 5,-5 9,9 9,-9 5,5 -9,9 9,9 -5,5 -9,-9 -9,9 z"
id="path4145"
inkscape:connector-curvature="0" />
<g
id="Page-1"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd" />
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
fill="#000000"
height="24"
viewBox="0 0 24 24"
width="24"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="securityPropertySecure.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1643"
inkscape:window-height="853"
id="namedview8"
showgrid="true"
inkscape:zoom="17.13151"
inkscape:cx="-1.5254237"
inkscape:cy="12"
inkscape:window-x="1245"
inkscape:window-y="786"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid4140" />
</sodipodi:namedview>
<circle
style="fill:#1ac222;fill-opacity:1"
id="path4142"
cx="12"
cy="12"
r="7" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path4" />
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48px"
height="48px"
viewBox="0 0 48 48"
version="1.1"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="securityPropertyWarning.svg">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1487"
inkscape:window-height="840"
id="namedview12"
showgrid="true"
inkscape:zoom="7.4692757"
inkscape:cx="21.565556"
inkscape:cy="24"
inkscape:window-x="1232"
inkscape:window-y="751"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid4142" />
</sodipodi:namedview>
<defs
id="defs4" />
<g
id="Page-1"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd">
<g
id="Material"
transform="translate(-64.000000, 0.000000)">
<g
id="ic_change_history_black_48px"
transform="translate(64.000000, 0.000000)">
<path
d="m 24,11.818633 -15.2267073,24.362734 30.4534143,0 L 24,11.818633 Z"
id="Shape"
inkscape:connector-curvature="0"
style="fill:#ffb003"
sodipodi:nodetypes="ccccc" />
<path
d="M0,0 L48,0 L48,48 L0,48 L0,0 L0,0 Z"
id="path10" />
</g>
</g>
</g>
</svg>
......@@ -7,7 +7,8 @@
* @extends {WebInspector.PanelWithSidebar}
* @implements {WebInspector.TargetManager.Observer}
*/
WebInspector.SecurityPanel = function() {
WebInspector.SecurityPanel = function()
{
WebInspector.PanelWithSidebar.call(this, "security");
this.registerRequiredCSS("security/securityPanel.css");
this.registerRequiredCSS("security/lockIcon.css");
......@@ -15,24 +16,39 @@ WebInspector.SecurityPanel = function() {
var sidebarTree = new TreeOutlineInShadow();
sidebarTree.element.classList.add("sidebar-tree");
this.panelSidebarElement().appendChild(sidebarTree.element);
sidebarTree.registerRequiredCSS("security/sidebar.css");
sidebarTree.registerRequiredCSS("security/lockIcon.css");
this.setDefaultFocusedElement(sidebarTree.element);
this._sidebarMainViewElement = new WebInspector.SecurityMainViewSidebarTreeElement(this);
sidebarTree.appendChild(this._sidebarMainViewElement);
// TODO(lgarron): Add a section for the main origin. (https://crbug.com/523586)
this._sidebarOriginSection = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("Origins"));
this._sidebarOriginSection.listItemElement.classList.add("security-sidebar-origins");
sidebarTree.appendChild(this._sidebarOriginSection);
this._mainView = new WebInspector.SecurityMainView();
this.showMainView();
/** @type {!Map<string, !{securityState: !SecurityAgent.SecurityState, securityDetails: ?NetworkAgent.SecurityDetails}>} */
/** @type {!Map<!WebInspector.SecurityPanel.Origin, !WebInspector.SecurityPanel.OriginState>} */
this._origins = new Map();
WebInspector.targetManager.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._clear, this);
WebInspector.targetManager.addEventListener(WebInspector.ResourceTreeModel.EventTypes.WillReloadPage, this._clear, this);
WebInspector.targetManager.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._clear, this);
// TODO(lgarron): add event listeners to call _clear() once we figure out how to clear the panel properly (https://crbug.com/522762).
WebInspector.targetManager.observeTargets(this);
}
/** @typedef {string} */
WebInspector.SecurityPanel.Origin;
/**
* @typedef {Object}
* @property {!SecurityAgent.SecurityState} securityState - Current security state of the origin.
* @property {?NetworkAgent.SecurityDetails} securityDetails - Security details of the origin, if available.
* @property {?NetworkAgent.CertificateDetails} securityDetails.certificateDetails - Certificate details of the origin (attached to security details), if available.
* @property {?WebInspector.SecurityOriginView} originView - Current SecurityOriginView corresponding to origin.
*/
WebInspector.SecurityPanel.OriginState;
WebInspector.SecurityPanel.prototype = {
/**
......@@ -60,6 +76,25 @@ WebInspector.SecurityPanel.prototype = {
this._setVisibleView(this._mainView);
},
/**
* @param {!WebInspector.SecurityPanel.Origin} origin
*/
showOrigin: function(origin)
{
var originState = this._origins.get(origin);
if (!originState.originView)
originState.originView = new WebInspector.SecurityOriginView(this, origin, originState.securityState, originState.securityDetails);
this._setVisibleView(originState.originView);
},
wasShown: function()
{
WebInspector.Panel.prototype.wasShown.call(this);
if (!this._visibleView)
this._sidebarMainViewElement.select();
},
/**
* @param {!WebInspector.VBox} view
*/
......@@ -87,16 +122,28 @@ WebInspector.SecurityPanel.prototype = {
var securityState = /** @type {!SecurityAgent.SecurityState} */ (data.securityState);
if (this._origins.has(origin)) {
var originData = this._origins.get(origin);
originData.securityState = this._securityStateMin(originData.securityState, securityState);
var originState = this._origins.get(origin);
var oldSecurityState = originState.securityState;
originState.securityState = this._securityStateMin(oldSecurityState, securityState);
if (oldSecurityState != originState.securityState) {
originState.sidebarElement.setSecurityState(securityState);
if (originState.originView)
originState.originView.setSecurityState(securityState);
}
} else {
// TODO(lgarron): Store a (deduplicated) list of different security details we have seen.
var originData = {};
originData.securityState = securityState;
// TODO(lgarron): Store a (deduplicated) list of different security details we have seen. https://crbug.com/503170
var originState = {};
originState.securityState = securityState;
if (data.securityDetails)
originData.securityDetails = data.securityDetails;
originState.securityDetails = data.securityDetails;
this._origins.set(origin, originState);
this._origins.set(origin, originData);
originState.sidebarElement = new WebInspector.SecurityOriginViewSidebarTreeElement(this, origin);
this._sidebarOriginSection.appendChild(originState.sidebarElement);
originState.sidebarElement.setSecurityState(securityState);
// Don't construct the origin view yet (let it happen lazily).
}
},
......@@ -148,6 +195,8 @@ WebInspector.SecurityPanel.prototype = {
_clear: function()
{
this._updateSecurityState(SecurityAgent.SecurityState.Unknown, []);
this._sidebarMainViewElement.select();
this._sidebarOriginSection.removeChildren();
this._origins.clear();
},
......@@ -172,8 +221,7 @@ WebInspector.SecurityPanel._instance = function()
WebInspector.SecurityMainViewSidebarTreeElement = function(panel)
{
this._panel = panel;
this.small = true;
WebInspector.SidebarTreeElement.call(this, "security-sidebar-tree-item", WebInspector.UIString("Overview"));
WebInspector.SidebarTreeElement.call(this, "security-main-view-sidebar-tree-item", WebInspector.UIString("Overview"));
this.iconElement.classList.add("lock-icon");
}
......@@ -188,9 +236,10 @@ WebInspector.SecurityMainViewSidebarTreeElement.prototype = {
*/
setSecurityState: function(newSecurityState)
{
for (var className of this.iconElement.classList)
if (className.indexOf("lock-icon-") === 0)
for (var className of Array.prototype.slice.call(this.iconElement.classList)) {
if (className.startsWith("lock-icon-"))
this.iconElement.classList.remove(className);
}
this.iconElement.classList.add("lock-icon-" + newSecurityState);
},
......@@ -208,6 +257,48 @@ WebInspector.SecurityMainViewSidebarTreeElement.prototype = {
__proto__: WebInspector.SidebarTreeElement.prototype
}
/**
* @constructor
* @extends {WebInspector.SidebarTreeElement}
* @param {!WebInspector.SecurityPanel} panel
* @param {!WebInspector.SecurityPanel.Origin} origin
*/
WebInspector.SecurityOriginViewSidebarTreeElement = function(panel, origin)
{
this._panel = panel;
this._origin = origin;
this.small = true;
WebInspector.SidebarTreeElement.call(this, "security-sidebar-tree-item", origin);
this.iconElement.classList.add("security-property");
}
WebInspector.SecurityOriginViewSidebarTreeElement.prototype = {
/**
* @override
* @return {boolean}
*/
onselect: function()
{
this._panel.showOrigin(this._origin);
return true;
},
/**
* @param {!SecurityAgent.SecurityState} newSecurityState
*/
setSecurityState: function(newSecurityState)
{
for (var className of Array.prototype.slice.call(this.iconElement.classList)) {
if (className.startsWith("security-property-"))
this.iconElement.classList.remove(className);
}
this.iconElement.classList.add("security-property-" + newSecurityState);
},
__proto__: WebInspector.SidebarTreeElement.prototype
}
/**
* @constructor
* @implements {WebInspector.PanelFactory}
......@@ -257,8 +348,8 @@ WebInspector.SecurityMainView.prototype = {
var explanationLockIcon = explanationDiv.createChild("div", "lock-icon");
explanationLockIcon.classList.add("lock-icon-" + explanation.securityState);
explanationDiv.createChild("div", "explanation-title").textContent = explanation.summary;
explanationDiv.createChild("div", "explanation-text").textContent = explanation.description;
explanationDiv.createChild("div", "explanation-title").textContent = WebInspector.UIString(explanation.summary);
explanationDiv.createChild("div", "explanation-text").textContent = WebInspector.UIString(explanation.description);
},
/**
......@@ -283,3 +374,148 @@ WebInspector.SecurityMainView.prototype = {
__proto__: WebInspector.VBox.prototype
}
/**
* @constructor
* @extends {WebInspector.VBox}
* @param {!WebInspector.SecurityPanel} panel
* @param {!WebInspector.SecurityPanel.Origin} origin
* @param {!SecurityAgent.SecurityState} securityState
* @param {?NetworkAgent.SecurityDetails} securityDetails
*/
WebInspector.SecurityOriginView = function(panel, origin, securityState, securityDetails)
{
this._panel = panel;
WebInspector.VBox.call(this);
this.setMinimumSize(200, 100);
this.element.classList.add("security-origin-view");
this.registerRequiredCSS("security/originView.css");
this.registerRequiredCSS("security/lockIcon.css");
var titleSection = this.element.createChild("div", "origin-view-section title-section");
titleSection.createChild("div", "origin-view-title").textContent = WebInspector.UIString("Origin");
var originDisplay = titleSection.createChild("div", "origin-display");
this._originLockIcon = originDisplay.createChild("span", "security-property");
this._originLockIcon.classList.add("security-property-" + securityState);
// TODO(lgarron): Highlight the origin scheme. https://crbug.com/523589
originDisplay.createChild("span", "origin").textContent = origin;
if (securityDetails && securityDetails.certificateDetails) {
var connectionSection = this.element.createChild("div", "origin-view-section");
connectionSection.createChild("div", "origin-view-section-title").textContent = WebInspector.UIString("Connection");
var table = new WebInspector.SecurityDetailsTable();
connectionSection.appendChild(table.element());
table.addRow("Protocol", securityDetails.protocol);
table.addRow("Key Exchange", securityDetails.keyExchange);
table.addRow("Cipher Suite", securityDetails.cipher + (securityDetails.mac ? " with " + securityDetails.mac : ""));
}
if (securityDetails) {
var certificateSection = this.element.createChild("div", "origin-view-section");
certificateSection.createChild("div", "origin-view-section-title").textContent = WebInspector.UIString("Certificate");
var sanDiv = this._createSanDiv(securityDetails);
var validFromString = new Date(1000 * securityDetails.certificateDetails.validFrom).toUTCString();
var validUntilString = new Date(1000 * securityDetails.certificateDetails.validTo).toUTCString();
var table = new WebInspector.SecurityDetailsTable();
certificateSection.appendChild(table.element());
table.addRow("Subject", securityDetails.certificateDetails.subject.name);
table.addRow("SAN", sanDiv);
table.addRow("Valid From", validFromString);
table.addRow("Valid Until", validUntilString);
table.addRow("Issuer", securityDetails.certificateDetails.issuer);
// TODO(lgarron): Make SCT status available in certificate details and show it here.
// TODO(lgarron): Implement a link to get certificateDetails. https://crbug.com/506468
var noteSection = this.element.createChild("div", "origin-view-section");
noteSection.createChild("div", "origin-view-section-title").textContent = WebInspector.UIString("Development Note");
// TODO(lgarron): Fix the issue and then remove this section. See comment in _onResponseReceivedSecurityDetails
noteSection.createChild("div").textContent = WebInspector.UIString("At the moment, this view only shows security details from the first connection made to %s", origin);
}
if (!securityDetails) {
var notSecureSection = this.element.createChild("div", "origin-view-section");
notSecureSection.createChild("div", "origin-view-section-title").textContent = WebInspector.UIString("Not Secure");
notSecureSection.createChild("div").textContent = WebInspector.UIString("Your connection to this origin is not secure.");
}
}
WebInspector.SecurityOriginView.prototype = {
/**
* @param {!NetworkAgent.SecurityDetails} securityDetails
* *return {!Element}
*/
_createSanDiv: function(securityDetails)
{
// TODO(lgarron): Truncate the display of SAN entries and add a button to toggle the full list. https://crbug.com/523591
var sanDiv = createElement("div");
var sanList = securityDetails.certificateDetails.subject.sanDnsNames.concat(securityDetails.certificateDetails.subject.sanIpAddresses);
if (sanList.length === 0) {
sanDiv.textContent = WebInspector.UIString("(N/A)");
} else {
for (var sanEntry of sanList) {
var span = sanDiv.createChild("span", "san-entry");
span.textContent = WebInspector.UIString(sanEntry);
}
}
return sanDiv;
},
/**
* @param {!SecurityAgent.SecurityState} newSecurityState
*/
setSecurityState: function(newSecurityState)
{
for (var className of Array.prototype.slice.call(this._originLockIcon.classList)) {
if (className.startsWith("security-property-"))
this._originLockIcon.classList.remove(className);
}
this._originLockIcon.classList.add("security-property-" + newSecurityState);
},
__proto__: WebInspector.VBox.prototype
}
/**
* @constructor
*/
WebInspector.SecurityDetailsTable = function()
{
this._element = createElement("table");
this._element.classList.add("details-table");
}
WebInspector.SecurityDetailsTable.prototype = {
/**
* @return: {!Element}
*/
element: function()
{
return this._element;
},
/**
* @param {string} key
* @param {string|!HTMLDivElement} value
*/
addRow: function(key, value)
{
var row = this._element.createChild("div", "details-table-row");
row.createChild("div").textContent = WebInspector.UIString(key);
var valueDiv = row.createChild("div");
if (value instanceof HTMLDivElement) {
valueDiv.appendChild(value);
} else {
valueDiv.textContent = value;
}
}
}
......@@ -3,7 +3,8 @@
* found in the LICENSE file.
*/
.lock-icon {
.lock-icon,
.security-property {
background-size: cover;
height: 16px;
width: 16px;
......@@ -52,7 +53,18 @@
}
}
.sidebar-tree-item .lock-icon {
float: left;
margin-right: 2px;
.security-property-insecure {
background-image: url(Images/securityPropertyInsecure.svg);
}
.security-property-neutral {
background-image: url(Images/securityPropertyWarning.svg);
}
.security-property-warning {
background-image: url(Images/securityPropertyWarning.svg);
}
.security-property-secure {
background-image: url(Images/securityPropertySecure.svg);
}
......@@ -16,6 +16,8 @@
],
"resources": [
"lockIcon.css",
"securityPanel.css"
"originView.css",
"securityPanel.css",
"sidebar.css"
]
}
/* Copyright (c) 2015 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
.security-origin-view {
overflow-x: hidden;
overflow-y: scroll;
display: block;
-webkit-user-select: text;
}
.security-origin-view .origin-view-section {
padding: 0.5em 1.5em 1.5em;
border-bottom: 1px solid rgb(230, 230, 230);
}
.security-origin-view .title-section {
padding-bottom: 1.5em;
}
.security-origin-view .origin-display .security-property {
margin: -1px 2px 0px 0px;
display: inline-block;
vertical-align: middle;
}
.security-origin-view .origin-view-title {
font-size: 1.25em;
margin-top: 0.5em;
margin-bottom: 0.25em;
}
.security-origin-view .origin-view-section-title {
font-weight: bold;
font-size: 1em;
margin-top: 0.5em;
margin-bottom: 0.25em;
}
.security-origin-view .details-table-row {
display: flex;
white-space: nowrap;
overflow: hidden;
margin-top: 6px;
}
.security-origin-view .details-table-row > div {
align-items: flex-start;
}
.security-origin-view .details-table-row > div:first-child {
color: rgb(140, 140, 140);
width: 7em;
margin-right: 1em;
flex: none;
display: flex;
justify-content: flex-end;
}
.security-origin-view .details-table-row > div:nth-child(2) {
flex: auto;
white-space: normal;
}
.security-origin-view .details-table .san-entry {
display: block;
}
/* Copyright (c) 2015 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
.tree-outline {
padding: 0;
}
.tree-outline .security-main-view-sidebar-tree-item {
border-bottom: 1px solid rgb(230, 230, 230);
padding-top: 0;
}
.tree-outline .security-main-view-sidebar-tree-item .icon,
.tree-outline .security-main-view-sidebar-tree-item .titles {
margin-top: 1.5em;
margin-bottom: 1.5em;
}
.tree-outline .security-sidebar-origins {
padding: 1px 8px 6px 8px;
margin-top: 1em;
margin-bottom: 0.5em;
color: rgb(90, 90, 90);
}
.tree-outline ol {
padding-left: 0;
}
.tree-outline li::before {
content: none;
}
.tree-outline .security-main-view-sidebar-tree-item,
.tree-outline .security-sidebar-origins,
.tree-outline .sidebar-tree-section + .children > .sidebar-tree-item {
padding-left: 16px;
}
.tree-outline .sidebar-tree-item .lock-icon,
.tree-outline .sidebar-tree-item .security-property {
float: left;
margin-right: 2px;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment