/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
var cockpit = require("cockpit");
var _ = cockpit.gettext;
var React = require("react");
var cockpitListing = require("cockpit-components-listing.jsx");
var OnOffSwitch = require("cockpit-components-onoff.jsx").OnOffSwitch;
/* Show details for an alert, including possible solutions
* Props correspond to an item in the setroubleshoot dataStore
*/
var SELinuxEventDetails = React.createClass({
getInitialState: function() {
var expanded;
// all details are collapsed by default
if (this.props.details)
expanded = this.props.details.pluginAnalysis.map(function() { return false; } );
return {
solutionExpanded: expanded, // show details for solution
};
},
handleSolutionDetailsClick: function(itmIdx, e) {
var solutionExpanded = this.state.solutionExpanded;
solutionExpanded[itmIdx] = !solutionExpanded[itmIdx];
this.setState( { solutionExpanded: solutionExpanded } );
e.stopPropagation();
e.preventDefault();
},
runFix: function(itmIdx) {
// make sure the details for the solution are collapsed, or they can hide the progress and result
var solutionExpanded = this.state.solutionExpanded;
if (solutionExpanded[itmIdx]) {
solutionExpanded[itmIdx] = false;
this.setState( { solutionExpanded: solutionExpanded } );
}
var localId = this.props.details.localId;
var analysisId = this.props.details.pluginAnalysis[itmIdx].analysisId;
this.props.runFix(localId, analysisId);
},
render: function() {
if (!this.props.details) {
// details should be requested by default, so we just need to wait for them
var waiting = (this.props.details === undefined);
return (
);
}
var self = this;
var fixEntries = this.props.details.pluginAnalysis.map(function(itm, itmIdx) {
var fixit = null;
var msg = null;
if (itm.fixable) {
if ((self.props.fix) && (self.props.fix.plugin == itm.analysisId)) {
if (self.props.fix.running) {
msg = (
{ _("Unable to apply this solution automatically") }
);
}
var detailsLink = { _("solution details") };
var doState;
var doElem;
var caret;
if (self.state.solutionExpanded[itmIdx]) {
caret = ;
doState =
{caret} {detailsLink}
;
doElem =
{itm.doText}
;
} else {
caret = ;
doState =
{caret} {detailsLink}
;
doElem = null;
}
return (
{itm.ifText}
{fixit}
{itm.thenText}
{doState}
{doElem}
{msg}
);
});
return (
{fixEntries}
);
}
});
/* Show the audit log events for an alert */
var SELinuxEventLog = React.createClass({
render: function() {
if (!this.props.details) {
// details should be requested by default, so we just need to wait for them
var waiting = (this.props.details === undefined);
return (
);
}
var self = this;
var logEntries = this.props.details.auditEvent.map(function(itm, idx) {
// use the alert id and index in the event log array as the data key for react
// if the log becomes dynamic, the entire log line might need to be considered as the key
return (
{itm}
);
});
return (
{logEntries}
);
}
});
/* Implements a subset of the PatternFly Empty State pattern
* https://www.patternfly.org/patterns/empty-state/
* Special values for icon property:
* - 'waiting' - display spinner
* - 'error' - display error icon
*/
var EmptyState = React.createClass({
render: function() {
var description = null;
if (this.props.description)
description =
{this.props.description}
;
var message = null;
if (this.props.message)
message =
{this.props.message}
;
var curtains = "curtains-ct";
if (this.props.relative)
curtains = "curtains-relative";
var icon = this.props.icon;
if (icon == 'waiting')
icon = ;
else if (icon == 'error')
icon = ;
return (
{icon}
{description}
{message}
);
}
});
/* Component to show a dismissable error, message as child text
* dismissError callback function triggered when the close button is pressed
*/
var DismissableError = React.createClass({
handleDismissError: function(e) {
// only consider primary mouse button
if (!e || e.button !== 0)
return;
if (this.props.dismissError)
this.props.dismissError();
e.stopPropagation();
},
render: function() {
return (
{this.props.children}
);
}
});
/* Component to show selinux status and offer an option to change it
* selinuxStatus status of selinux on the system, properties as defined in selinux-client.js
* selinuxStatusError error message from reading or setting selinux status/mode
* changeSelinuxMode function to use for changing the selinux enforcing mode
* dismissError function to dismiss the error message
*/
var SELinuxStatus = React.createClass({
render: function() {
var errorMessage;
if (this.props.selinuxStatusError) {
errorMessage = (
{this.props.selinuxStatusError}
);
}
if (this.props.selinuxStatus.enabled === undefined) {
// we don't know the current state
return (
{errorMessage}
{_("SELinux system status is unknown.")}
);
} else if (!this.props.selinuxStatus.enabled) {
// selinux is disabled on the system, not much we can do
return (
{errorMessage}
{_("SELinux is disabled on the system.")}
);
}
var note;
var configUnknown = (this.props.selinuxStatus.configEnforcing === undefined);
if (configUnknown)
note = {_("The configured state is unknown, it might change on the next boot.")};
else if (!configUnknown && this.props.selinuxStatus.enforcing !== this.props.selinuxStatus.configEnforcing)
note = {_("Setting deviates from the configured state and will revert on the next boot.")};
return (
{_("SELinux Policy")}
{errorMessage}
{note}
);
}
});
/* The listing only shows if we have a connection to the dbus API
* Otherwise we have blank slate: trying to connect, error
* Expected properties:
* connected true if the client is connected to setroubleshoot-server via dbus
* error error message to show (in EmptyState if not connected, as a dismissable alert otherwise
* dismissError callback, triggered for the dismissable error in connected state
* deleteAlert callback, triggered with an alert id as parameter to trigger deletion
* entries setroubleshoot entries
* - runFix function to run fix
* - details fix details as provided by the setroubleshoot client
* - description brief description of the error
* - count how many times (>= 1) this alert occurred
* selinuxStatus status of selinux on the system, properties as defined in selinux-client.js
* selinuxStatusError error message from reading or setting selinux status/mode
* changeSelinuxMode function to use for changing the selinux enforcing mode
* dismissStatusError function that is triggered to dismiss the selinux status error
*/
var SETroubleshootPage = React.createClass({
handleDeleteAlert: function(alertId, e) {
// only consider primary mouse button
if (!e || e.button !== 0)
return;
if (this.props.deleteAlert)
this.props.deleteAlert(alertId);
e.stopPropagation();
},
handleDismissError: function(e) {
// only consider primary mouse button
if (!e || e.button !== 0)
return;
if (this.props.dismissError)
this.props.dismissError();
e.stopPropagation();
},
render: function() {
// if selinux is disabled, we only show EmptyState
if (this.props.selinuxStatus.enabled === false) {
return (
}
description={ _("SELinux is disabled on the system") }
message={null}
relative={false}/>
);
}
var self = this;
var entries;
var troubleshooting;
var title = _("SELinux Access Control Errors");
var emptyCaption = _("No SELinux alerts.");
if (!this.props.connected) {
if (this.props.connecting) {
emptyCaption = (
{_("Connecting to SETroubleshoot daemon...")}
);
} else {
// if we don't have setroubleshoot-server, be more subtle about saying that
title = "";
emptyCaption = (
{_("Install setroubleshoot-server to troubleshoot SELinux events.")}
);
}
} else {
entries = this.props.entries.map(function(itm) {
itm.runFix = self.props.runFix;
var listingDetail;
if (itm.details && 'firstSeen' in itm.details) {
if (itm.details.reportCount >= 2) {
listingDetail = cockpit.format(_("Occurred between $0 and $1"),
itm.details.firstSeen.calendar(),
itm.details.lastSeen.calendar()
);
} else {
listingDetail = cockpit.format(_("Occurred $0"), itm.details.firstSeen.calendar());
}
}
var onDeleteClick;
if (itm.details)
onDeleteClick = self.handleDeleteAlert.bind(self, itm.details.localId);
var dismissAction = (
);
var tabRenderers = [
{
name: _("Solutions"),
renderer: SELinuxEventDetails,
data: itm,
},
{
name: _("Audit log"),
renderer: SELinuxEventLog,
data: itm,
},
];
// if the alert has level "red", it's critical
var criticalAlert = null;
if (itm.details && 'level' in itm.details && itm.details.level == "red")
criticalAlert = ;
var columns = [
criticalAlert,
{ name: itm.description, 'header': true }
];
var title;
if (itm.count > 1) {
title = cockpit.format(cockpit.ngettext("$0 occurrence", "$1 occurrences", itm.count),
itm.count);
columns.push({itm.count});
} else {
columns.push();
}
return (
);
});
}
troubleshooting = (
{entries}
);
var errorMessage;
if (this.props.error) {
errorMessage = (