starter-kit/src/config.jsx
2023-04-27 14:47:24 -04:00

617 lines
24 KiB
JavaScript

/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
*/
import React from "react";
import {
Breadcrumb, BreadcrumbItem,
Button,
Flex,
Form,
FormGroup,
FormSelect,
FormSelectOption,
TextInput,
ActionGroup,
Spinner,
Card,
CardTitle,
CardBody,
Checkbox,
Bullseye,
EmptyState,
EmptyStateIcon,
Title,
EmptyStateBody,
EmptyStateVariant,
Page, PageSection,
} from "@patternfly/react-core";
import { ExclamationCircleIcon } from "@patternfly/react-icons";
import { global_danger_color_200 } from "@patternfly/react-tokens";
import cockpit from 'cockpit';
const json = require('comment-json');
const ini = require('ini');
const _ = cockpit.gettext;
class GeneralConfig extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.setConfig = this.setConfig.bind(this);
this.fileReadFailed = this.fileReadFailed.bind(this);
this.readConfig = this.readConfig.bind(this);
this.file = null;
this.config = null;
this.state = {
config_loaded: false,
file_error: false,
submitting: false,
shell: "",
notice: "",
latency: "",
payload: "",
log_input: false,
log_output: true,
log_window: true,
limit_rate: "",
limit_burst: "",
limit_action: "",
file_path: "",
syslog_facility: "",
syslog_priority: "",
journal_augment: "",
journal_priority: "",
writer: "",
};
}
handleSubmit(event) {
this.setState({ submitting: true });
const config = {
shell: this.state.shell,
notice: this.state.notice,
latency: parseInt(this.state.latency),
payload: parseInt(this.state.payload),
log: {
input: this.state.log_input,
output: this.state.log_output,
window: this.state.log_window,
},
limit: {
rate: parseInt(this.state.limit_rate),
burst: parseInt(this.state.limit_burst),
action: this.state.limit_action,
},
file: {
path: this.state.file_path,
},
syslog: {
facility: this.state.syslog_facility,
priority: this.state.syslog_priority,
},
journal: {
priority: this.state.journal_priority,
augment: this.state.journal_augment
},
writer: this.state.writer
};
this.file.replace(config).done(() => {
this.setState({ submitting: false });
})
.fail((error) => {
console.log(error);
});
event.preventDefault();
}
setConfig(data) {
delete data.configuration;
delete data.args;
const flattenObject = function(ob) {
const toReturn = {};
for (const i in ob) {
if (!Object.prototype.hasOwnProperty.call(ob, i)) continue;
if ((typeof ob[i]) == 'object') {
const flatObject = flattenObject(ob[i]);
for (const x in flatObject) {
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) continue;
toReturn[i + '_' + x] = flatObject[x];
}
} else {
toReturn[i] = ob[i];
}
}
return toReturn;
};
const state = flattenObject(data);
state.config_loaded = true;
this.setState(state);
}
getConfig() {
const proc = cockpit.spawn(["tlog-rec-session", "--configuration"]);
proc.stream((data) => {
this.setConfig(json.parse(data, null, true));
proc.close();
});
proc.fail((fail) => {
console.log(fail);
this.readConfig();
});
}
readConfig() {
const parseFunc = function(data) {
return json.parse(data, null, true);
};
const stringifyFunc = function(data) {
return json.stringify(data, null, true);
};
// needed for cockpit.file usage
const syntax_object = {
parse: parseFunc,
stringify: stringifyFunc,
};
this.file = cockpit.file("/etc/tlog/tlog-rec-session.conf", {
syntax: syntax_object,
superuser: true,
});
}
fileReadFailed(reason) {
console.log(reason);
this.setState({ file_error: reason });
}
componentDidMount() {
this.getConfig();
this.readConfig();
}
render() {
const form =
(this.state.config_loaded === false && this.state.file_error === false)
? <Spinner />
: (this.state.config_loaded === true && this.state.file_error === false)
? (
<Form isHorizontal>
<FormGroup label={_("Shell")}>
<TextInput
id="shell"
value={this.state.shell}
onChange={shell => this.setState({ shell })}
/>
</FormGroup>
<FormGroup label={_("Notice")}>
<TextInput
id="notice"
value={this.state.notice}
onChange={notice => this.setState({ notice })}
/>
</FormGroup>
<FormGroup label={_("Latency")}>
<TextInput
id="latency"
type="number"
step="1"
value={this.state.latency}
onChange={latency => this.setState({ latency })}
/>
</FormGroup>
<FormGroup label={_("Payload Size, bytes")}>
<TextInput
id="payload"
type="number"
step="1"
value={this.state.payload}
onChange={payload => this.setState({ payload })}
/>
</FormGroup>
<FormGroup label={_("Logging")}>
<Checkbox
id="log_input"
isChecked={this.state.log_input}
onChange={log_input => this.setState({ log_input })}
label={_("User's Input")}
/>
<Checkbox
id="log_output"
isChecked={this.state.log_output}
onChange={log_output => this.setState({ log_output })}
label={_("User's Output")}
/>
<Checkbox
id="log_window"
isChecked={this.state.log_window}
onChange={log_window => this.setState({ log_window })}
label={_("Window Resize")}
/>
</FormGroup>
<FormGroup label={_("Limit Rate, bytes/sec")}>
<TextInput
id="limit_rate"
type="number"
step="1"
value={this.state.limit_rate}
onChange={limit_rate => this.setState({ limit_rate })}
/>
</FormGroup>
<FormGroup label={_("Burst, bytes")}>
<TextInput
id="limit_burst"
type="number"
step="1"
value={this.state.limit_burst}
onChange={limit_burst => this.setState({ limit_burst })}
/>
</FormGroup>
<FormGroup label={_("Logging Limit Action")}>
<FormSelect
id="limit_action"
value={this.state.limit_action}
onChange={limit_action => this.setState({ limit_action })}
>
{[
{ value: "", label: "" },
{ value: "pass", label: _("Pass") },
{ value: "delay", label: _("Delay") },
{ value: "drop", label: _("Drop") }
].map((option, index) =>
<FormSelectOption
key={index}
value={option.value}
label={option.label}
/>
)}
</FormSelect>
</FormGroup>
<FormGroup label={_("File Path")}>
<TextInput
id="file_path"
value={this.state.file_path}
onChange={file_path => this.setState({ file_path })}
/>
</FormGroup>
<FormGroup label={_("Syslog Facility")}>
<TextInput
id="syslog_facility"
value={this.state.syslog_facility}
onChange={syslog_facility =>
this.setState({ syslog_facility })}
/>
</FormGroup>
<FormGroup label={_("Syslog Priority")}>
<FormSelect
id="syslog_priority"
value={this.state.syslog_priority}
onChange={syslog_priority =>
this.setState({ syslog_priority })}
>
{[
{ value: "", label: "" },
{ value: "info", label: _("Info") },
].map((option, index) =>
<FormSelectOption
key={index}
value={option.value}
label={option.label}
/>
)}
</FormSelect>
</FormGroup>
<FormGroup label={_("Journal Priority")}>
<FormSelect
id="journal_priority"
value={this.state.journal_priority}
onChange={journal_priority =>
this.setState({ journal_priority })}
>
{[
{ value: "", label: "" },
{ value: "info", label: _("Info") },
].map((option, index) =>
<FormSelectOption
key={index}
value={option.value}
label={option.label}
/>
)}
</FormSelect>
</FormGroup>
<FormGroup>
<Checkbox
id="journal_augment"
isChecked={this.state.journal_augment}
onChange={journal_augment =>
this.setState({ journal_augment })}
label={_("Augment")}
/>
</FormGroup>
<FormGroup label={_("Writer")}>
<FormSelect
id="writer"
value={this.state.writer}
onChange={writer =>
this.setState({ writer })}
>
{[
{ value: "", label: "" },
{ value: "journal", label: _("Journal") },
{ value: "syslog", label: _("Syslog") },
{ value: "file", label: _("File") },
].map((option, index) =>
<FormSelectOption
key={index}
value={option.value}
label={option.label}
/>
)}
</FormSelect>
</FormGroup>
<ActionGroup>
<Button
id="btn-save-tlog-conf"
variant="primary"
onClick={this.handleSubmit}
>
{_("Save")}
</Button>
{this.state.submitting === true && <Spinner size="lg" />}
</ActionGroup>
</Form>
)
: (
<Bullseye>
<EmptyState variant={EmptyStateVariant.small}>
<EmptyStateIcon
icon={ExclamationCircleIcon}
color={global_danger_color_200.value}
/>
<Title headingLevel="h4" size="lg">
{_("There is no configuration file of tlog present in your system.")}
</Title>
<Title headingLevel="h4" size="lg">
{_("Please, check the /etc/tlog/tlog-rec-session.conf or if tlog is installed.")}
</Title>
<EmptyStateBody>
{this.state.file_error}
</EmptyStateBody>
</EmptyState>
</Bullseye>
);
return (
<Card>
<CardTitle>General Config</CardTitle>
<CardBody>{form}</CardBody>
</Card>
);
}
}
class SssdConfig extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.confSave = this.confSave.bind(this);
this.restartSSSD = this.restartSSSD.bind(this);
this.file = null;
this.state = {
scope: "",
users: "",
exclude_users: "",
exclude_groups: "",
groups: "",
submitting: false,
};
}
customIniUnparser(obj) {
return ini.stringify(obj, { platform: 'linux' }).replace('domainnssfiles', 'domain/nssfiles');
}
restartSSSD() {
const sssd_cmd = ["systemctl", "restart", "sssd"];
cockpit.spawn(sssd_cmd, { superuser: "require" });
this.setState({ submitting: false });
}
confSave(obj) {
const chmod_cmd = ["chmod", "600", "/etc/sssd/conf.d/sssd-session-recording.conf"];
/* Update nsswitch, this will fail on RHEL8/F34 and lower as 'with-files-domain' feature is not added there */
const authselect_cmd = ["authselect", "select", "sssd", "with-files-domain", "--force"];
this.setState({ submitting: true });
this.file.replace(obj)
.then(tag => {
cockpit.spawn(chmod_cmd, { superuser: "require" })
.then(() => {
cockpit.spawn(authselect_cmd, { superuser: "require" })
.then(this.restartSSSD)
.catch(this.restartSSSD);
});
})
.catch(error => {
console.error(error);
});
}
componentDidMount() {
const syntax_object = {
parse: ini.parse,
stringify: this.customIniUnparser,
};
this.file = cockpit.file("/etc/sssd/conf.d/sssd-session-recording.conf", {
syntax: syntax_object,
superuser: true,
});
const promise = this.file.read();
promise.fail(function(error) {
console.log(error);
});
}
handleSubmit(e) {
const obj = {};
/* SSSD section */
obj.sssd = {};
obj.sssd.services = "nss";
obj.sssd.domains = "nssfiles";
/* Proxy provider */
obj.domainnssfiles = {}; /* Unparser converts this into domain/nssfiles */
obj.domainnssfiles.id_provider = "proxy";
obj.domainnssfiles.proxy_lib_name = "files";
obj.domainnssfiles.proxy_pam_target = "sssd-shadowutils";
/* Session recording */
obj.session_recording = {};
obj.session_recording.scope = this.state.scope;
switch (this.state.scope) {
case "all":
obj.session_recording.exclude_users = this.state.exclude_users;
obj.session_recording.exclude_groups = this.state.exclude_groups;
break;
case "none":
break;
case "some":
obj.session_recording.users = this.state.users;
obj.session_recording.groups = this.state.groups;
break;
default:
break;
}
this.confSave(obj);
e.preventDefault();
}
render() {
const form = (
<Form isHorizontal>
<FormGroup label="Scope">
<FormSelect
id="scope"
value={this.state.scope}
onChange={scope => this.setState({ scope })}
>
{[
{ value: "none", label: _("None") },
{ value: "some", label: _("Some") },
{ value: "all", label: _("All") }
].map((option, index) =>
<FormSelectOption
key={index}
value={option.value}
label={option.label}
/>
)}
</FormSelect>
</FormGroup>
{this.state.scope === "some" &&
<>
<FormGroup label={_("Users")}>
<TextInput
id="users"
value={this.state.users}
onChange={users => this.setState({ users })}
/>
</FormGroup>
<FormGroup label={_("Groups")}>
<TextInput
id="groups"
value={this.state.groups}
onChange={groups => this.setState({ groups })}
/>
</FormGroup>
</>}
{this.state.scope === "all" &&
<>
<FormGroup label={_("Exclude Users")}>
<TextInput
id="exclude_users"
value={this.state.exclude_users}
onChange={exclude_users => this.setState({ exclude_users })}
/>
</FormGroup>
<FormGroup label={_("Exclude Groups")}>
<TextInput
id="exclude_groups"
value={this.state.exclude_groups}
onChange={exclude_groups => this.setState({ exclude_groups })}
/>
</FormGroup>
</>}
<ActionGroup>
<Button
id="btn-save-sssd-conf"
variant="primary"
onClick={this.handleSubmit}
>
{_("Save")}
</Button>
{this.state.submitting === true && <Spinner size="lg" />}
</ActionGroup>
</Form>
);
return (
<Card>
<CardTitle>SSSD Config</CardTitle>
<CardBody>{form}</CardBody>
</Card>
);
}
}
export function Config () {
const goBack = () => {
cockpit.location.go("/");
};
return (
<Page
groupProps={{ sticky: 'top' }}
isBreadcrumbGrouped
breadcrumb={
<Breadcrumb className='machines-listing-breadcrumb'>
<BreadcrumbItem to='#' onClick={goBack}>
{_("Session Recording")}
</BreadcrumbItem>
<BreadcrumbItem isActive>
{_("Settings")}
</BreadcrumbItem>
</Breadcrumb>
}
>
<PageSection>
<Flex className="config-container">
<GeneralConfig />
<SssdConfig />
</Flex>
</PageSection>
</Page>
);
}