Line data Source code
1 : /*
2 : * Copyright (C) 2021 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 11 : import React, { useState } from 'react';
6 : import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
7 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
8 : import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
9 : import { PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
10 :
11 : import { SimpleSelect } from "cockpit-components-simple-select";
12 :
13 11 : import cockpit from 'cockpit';
14 : import './cockpit-components-firewalld-request.scss';
15 :
16 11 : const _ = cockpit.gettext;
17 11 : const firewalld = cockpit.dbus('org.fedoraproject.FirewallD1', { superuser: "try" });
18 :
19 1 : function debug() {
20 0 : if (window.debugging == "all" || window.debugging?.includes("firewall"))
21 0 : console.debug.apply(console, arguments);
22 1 : }
23 :
24 : /* React component for an info alert to enable some new service in firewalld.
25 : * Use this when enabling some network-facing service. The alert will only be shown
26 : * if firewalld is running, has at least one active zone, and the service is not enabled
27 : * in any zone yet. It will allow the user to enable the service in any active zone,
28 : * or go to the firewall page for more fine-grained configuration.
29 : *
30 : * Properties:
31 : * - service (string, required): firewalld service name
32 : * - title (string, required): Human readable/translated alert title
33 : * - pageSection (bool, optional, default false): Render the alert inside a <PageSection>
34 : */
35 1 : export const FirewalldRequest = ({ service, title, pageSection }) => {
36 1 : const [zones, setZones] = useState(null);
37 1 : const [selectedZone, setSelectedZone] = useState(null);
38 1 : const [enabledAnywhere, setEnabledAnywhere] = useState(null);
39 1 : const [enableError, setEnableError] = useState(null);
40 1 : debug("FirewalldRequest", service, "zones", JSON.stringify(zones), "selected zone", selectedZone, "enabledAnywhere", enabledAnywhere);
41 :
42 1 : if (!service)
43 0 : return null;
44 :
45 : // query zones on component initialization
46 1 : if (zones === null) {
47 1 : firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "getActiveZones")
48 1 : .then(([info]) => {
49 1 : const names = Object.keys(info);
50 1 : Promise.all(names.map(name => firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "getZoneSettings2", [name])))
51 1 : .then(zoneInfos => {
52 1 : setEnabledAnywhere(!!zoneInfos.find(zoneInfo => ((zoneInfo[0].services || {}).v || []).indexOf(service) >= 0));
53 1 : setZones(names);
54 1 : })
55 0 : .catch(ex => {
56 0 : console.warn("FirewalldRequest: getZoneSettings failed:", JSON.stringify(ex));
57 0 : setZones([]);
58 0 : });
59 :
60 1 : firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1", "getDefaultZone")
61 1 : .then(([zone]) => setSelectedZone(zone))
62 0 : .catch(ex => console.warn("FirewalldRequest: getDefaultZone failed:", JSON.stringify(ex)));
63 1 : })
64 1 : .catch(ex => {
65 : // firewalld not running
66 1 : debug("FirewalldRequest: getActiveZones failed, considering firewall inactive:", JSON.stringify(ex));
67 1 : setZones([]);
68 1 : });
69 1 : }
70 :
71 1 : const onAddService = () => {
72 1 : firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "addService",
73 1 : [selectedZone, service, 0])
74 : // permanent config
75 1 : .then(() => firewalld.call("/org/fedoraproject/FirewallD1/config",
76 1 : "org.fedoraproject.FirewallD1.config",
77 1 : "getZoneByName", [selectedZone]))
78 1 : .then(([path]) => firewalld.call(path, "org.fedoraproject.FirewallD1.config.zone", "addService", [service]))
79 : // all successful, hide alert
80 1 : .then(() => setEnabledAnywhere(true))
81 1 : .catch(ex => {
82 : // may already be enabled in permanent config, that's ok
83 1 : if (ex.message && ex.message.indexOf("ALREADY_ENABLED") >= 0) {
84 1 : setEnabledAnywhere(true);
85 1 : return;
86 1 : }
87 :
88 1 : setEnableError(ex.toString());
89 1 : setEnabledAnywhere(true);
90 1 : console.error("Failed to enable", service, "in firewalld:", JSON.stringify(ex));
91 1 : });
92 1 : };
93 :
94 1 : let alert;
95 :
96 1 : if (enableError) {
97 1 : alert = (
98 1 : <Alert isInline variant="warning"
99 1 : title={ cockpit.format(_("Failed to enable $0 in firewalld"), service) }
100 1 : actionClose={ <AlertActionCloseButton onClose={ () => setEnableError(null) } /> }
101 1 : actionLinks={
102 0 : <AlertActionLink onClick={() => cockpit.jump("/network/firewall")}>
103 1 : { _("Visit firewall") }
104 1 : </AlertActionLink>
105 : }>
106 1 : {enableError}
107 1 : </Alert>
108 : );
109 : // don't show anything if firewalld is not active, or service is already enabled somewhere
110 1 : } else if (!zones || zones.length === 0 || !selectedZone || enabledAnywhere) {
111 1 : return null;
112 1 : } else {
113 1 : alert = (
114 1 : <Alert id="firewalld-request-alert" isInline variant="info" title={title} className="pf-v6-u-box-shadow-sm">
115 1 : <Toolbar>
116 1 : <ToolbarContent>
117 1 : <ToolbarGroup>
118 1 : <ToolbarItem variant="label">{ _("Zone") }</ToolbarItem>
119 1 : <ToolbarItem>
120 1 : <SimpleSelect
121 1 : onSelect={setSelectedZone}
122 1 : selected={selectedZone}
123 1 : options={zones.map(zone => ({ value: zone, content: zone }))}
124 1 : toggleProps={{
125 1 : id: "firewalld-request-" + service,
126 1 : "aria-label": _("Zone"),
127 1 : }} />
128 1 : </ToolbarItem>
129 :
130 1 : <ToolbarItem>
131 1 : <Button variant="primary" onClick={onAddService}>{ cockpit.format(_("Add $0"), service) }</Button>
132 1 : </ToolbarItem>
133 1 : </ToolbarGroup>
134 :
135 1 : <ToolbarItem variant="separator" />
136 :
137 1 : <ToolbarItem>
138 1 : <Button variant="link" onClick={() => cockpit.jump("/network/firewall")}>
139 1 : { _("Visit firewall") }
140 1 : </Button>
141 1 : </ToolbarItem>
142 1 : </ToolbarContent>
143 1 : </Toolbar>
144 1 : </Alert>
145 : );
146 1 : }
147 :
148 1 : if (pageSection)
149 0 : return <PageSection hasBodyWrapper={false} className="ct-no-bottom-padding">{alert}</PageSection>;
150 : else
151 0 : return alert;
152 1 : };
|