Avoid overwriting services and domain sections of system with an existing sssd.conf (IPA client for example). Copy the services section used in sssd.conf, and append 'proxy' to existing domain section.
652 lines
25 KiB
JavaScript
652 lines
25 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 conf_syntax_object = {
|
|
parse: ini.parse,
|
|
};
|
|
|
|
this.sssdconf = cockpit.file("/etc/sssd/sssd.conf", {
|
|
syntax: conf_syntax_object,
|
|
superuser: true,
|
|
});
|
|
|
|
const promise = this.file.read();
|
|
const sssdconfpromise = this.sssdconf.read();
|
|
|
|
promise.fail(function(error) {
|
|
console.log(error);
|
|
});
|
|
|
|
/* It is not an error when the file does not exist, then() callback will
|
|
* be called with a null value for content and tag is "-" */
|
|
sssdconfpromise
|
|
.then((content, tag) => {
|
|
if (content !== null) {
|
|
this.existingServices = content.sssd.services;
|
|
this.existingDomains = content.sssd.domains;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.log("Error: " + error);
|
|
});
|
|
}
|
|
|
|
handleSubmit(e) {
|
|
const obj = {};
|
|
/* SSSD section */
|
|
obj.sssd = {};
|
|
/* Avoid overwriting services and domain sections of existing sssd.conf
|
|
* Copy the services section used in sssd.conf, and append 'proxy' to
|
|
* existing domain section */
|
|
if (this.existingServices) {
|
|
obj.sssd.services = this.existingServices;
|
|
} else {
|
|
obj.sssd.services = "nss, pam";
|
|
}
|
|
|
|
if (this.existingDomains) {
|
|
obj.sssd.domains = this.existingDomains + ", nssfiles";
|
|
} else {
|
|
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>
|
|
);
|
|
}
|