diff --git a/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java b/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java index 42a40271ea08..bfd422d66e85 100644 --- a/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java +++ b/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java @@ -65,6 +65,7 @@ import com.redhat.rhn.domain.recurringactions.state.RecurringConfigChannel; import com.redhat.rhn.domain.recurringactions.state.RecurringInternalState; import com.redhat.rhn.domain.recurringactions.type.RecurringHighstate; +import com.redhat.rhn.domain.recurringactions.type.RecurringScapPolicy; import com.redhat.rhn.domain.recurringactions.type.RecurringState; import com.redhat.rhn.domain.rhnpackage.PackageBreaks; import com.redhat.rhn.domain.rhnpackage.PackageConflicts; @@ -208,7 +209,8 @@ private AnnotationRegistry() { TokenChannelAppStream.class, PaygDimensionResult.class, TailoringFile.class, - ScapPolicy.class + ScapPolicy.class, + RecurringScapPolicy.class ); /** diff --git a/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringActionType.java b/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringActionType.java index 2d4bc8dc55a6..3b1ec9a24598 100644 --- a/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringActionType.java +++ b/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringActionType.java @@ -38,7 +38,8 @@ public abstract class RecurringActionType { public enum ActionType { HIGHSTATE("Highstate"), - CUSTOMSTATE("Custom state"); + CUSTOMSTATE("Custom state"), + SCAPPOLICY("Scap Policy"); private final String description; ActionType(String descriptionIn) { this.description = descriptionIn; diff --git a/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringScapPolicy.java b/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringScapPolicy.java new file mode 100644 index 000000000000..1db913691d11 --- /dev/null +++ b/java/code/src/com/redhat/rhn/domain/recurringactions/type/RecurringScapPolicy.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ + +package com.redhat.rhn.domain.recurringactions.type; + +import com.redhat.rhn.domain.audit.ScapPolicy; +import org.hibernate.annotations.Type; + +import javax.persistence.*; + +/** + * Recurring Action type for scap policy implementation + */ + +@Entity +@Table(name = "suseRecurringScapPolicy") +public class RecurringScapPolicy extends RecurringActionType { + + private boolean testMode; + private ScapPolicy scapPolicy; + + /** + * Standard constructor + */ + public RecurringScapPolicy() { + } + + /** + * Constructor + * + * @param testModeIn if action is in testMode + */ + public RecurringScapPolicy(boolean testModeIn) { + super(); + this.testMode = testModeIn; + } + + /** + * Constructor + * + * @param scapPolicyIn the scap policy + * @param testModeIn if action is in testMode + */ + public RecurringScapPolicy(ScapPolicy scapPolicyIn, boolean testModeIn) { + super(); + this.testMode = testModeIn; + this.scapPolicy = scapPolicyIn; + } + + @Override + @Transient + public ActionType getActionType() { + return ActionType.SCAPPOLICY; + } + + /** + * Gets if action is in testMode. + * + * @return testMode - if action is testMode + */ + @Column(name = "test_mode") + @Type(type = "yes_no") + public boolean isTestMode() { + return this.testMode; + } + + /** + * Sets testMode. + * + * @param testModeIn - testMode + */ + public void setTestMode(boolean testModeIn) { + this.testMode = testModeIn; + } + + /** + * Gets the related Scap policy + * + * @return the Scap Policy + */ + @ManyToOne + @JoinColumn(name = "scap_policy_id", nullable = false) + public ScapPolicy getScapPolicy() { + return this.scapPolicy; + } + + /** + * Set the related Scap Policy + * + * @param scapPolicyIn the scap policy + */ + public void setScapPolicy(ScapPolicy scapPolicyIn) { + this.scapPolicy = scapPolicyIn; + } +} diff --git a/java/code/src/com/redhat/rhn/manager/recurringactions/RecurringActionManager.java b/java/code/src/com/redhat/rhn/manager/recurringactions/RecurringActionManager.java index 67ccf310ea49..34a4d9021101 100644 --- a/java/code/src/com/redhat/rhn/manager/recurringactions/RecurringActionManager.java +++ b/java/code/src/com/redhat/rhn/manager/recurringactions/RecurringActionManager.java @@ -31,6 +31,7 @@ import com.redhat.rhn.domain.recurringactions.RecurringActionFactory; import com.redhat.rhn.domain.recurringactions.type.RecurringActionType; import com.redhat.rhn.domain.recurringactions.type.RecurringHighstate; +import com.redhat.rhn.domain.recurringactions.type.RecurringScapPolicy; import com.redhat.rhn.domain.recurringactions.type.RecurringState; import com.redhat.rhn.domain.role.RoleFactory; import com.redhat.rhn.domain.server.MinionServer; @@ -125,6 +126,8 @@ private static RecurringActionType createRecurringActionType(RecurringActionType return new RecurringHighstate(false); case CUSTOMSTATE: return new RecurringState(false); + case SCAPPOLICY: + return new RecurringScapPolicy(false); default: throw new UnsupportedOperationException("type not supported"); } diff --git a/java/code/src/com/suse/manager/webui/controllers/RecurringActionController.java b/java/code/src/com/suse/manager/webui/controllers/RecurringActionController.java index 491e69c10e23..8ac6b759beee 100644 --- a/java/code/src/com/suse/manager/webui/controllers/RecurringActionController.java +++ b/java/code/src/com/suse/manager/webui/controllers/RecurringActionController.java @@ -26,6 +26,7 @@ import static spark.Spark.get; import static spark.Spark.post; +import com.google.gson.JsonObject; import com.redhat.rhn.common.db.datasource.DataResult; import com.redhat.rhn.common.hibernate.HibernateFactory; import com.redhat.rhn.common.localization.LocalizationService; @@ -33,6 +34,8 @@ import com.redhat.rhn.common.validator.ValidatorException; import com.redhat.rhn.domain.action.Action; import com.redhat.rhn.domain.action.ActionFactory; +import com.redhat.rhn.domain.audit.ScapFactory; +import com.redhat.rhn.domain.audit.ScapPolicy; import com.redhat.rhn.domain.config.ConfigChannel; import com.redhat.rhn.domain.recurringactions.RecurringAction; import com.redhat.rhn.domain.recurringactions.RecurringAction.TargetType; @@ -40,11 +43,13 @@ import com.redhat.rhn.domain.recurringactions.state.RecurringStateConfig; import com.redhat.rhn.domain.recurringactions.type.RecurringActionType; import com.redhat.rhn.domain.recurringactions.type.RecurringHighstate; +import com.redhat.rhn.domain.recurringactions.type.RecurringScapPolicy; import com.redhat.rhn.domain.recurringactions.type.RecurringState; import com.redhat.rhn.domain.user.User; import com.redhat.rhn.frontend.listview.PageControl; import com.redhat.rhn.manager.action.ActionManager; import com.redhat.rhn.manager.configuration.ConfigurationManager; +import com.redhat.rhn.manager.org.OrgManager; import com.redhat.rhn.manager.recurringactions.RecurringActionManager; import com.redhat.rhn.manager.recurringactions.StateConfigFactory; import com.redhat.rhn.taskomatic.TaskomaticApi; @@ -79,6 +84,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import spark.ModelAndView; import spark.Request; @@ -111,6 +117,7 @@ public static void initRoutes(JadeTemplateEngine jade) { get("/manager/api/recurringactions/:id/details", asJson(withUser(RecurringActionController::getDetails))); get("/manager/api/recurringactions/:type/:id", asJson(withUser(RecurringActionController::listByEntity))); get("/manager/api/recurringactions/states", asJson(withUser(RecurringActionController::getStatesConfig))); + get("/manager/api/recurringactions/policies", asJson(withUser(RecurringActionController::listScapPolicies))); post("/manager/api/recurringactions/save", asJson(withUser(RecurringActionController::save))); post("/manager/api/recurringactions/custom/execute", asJson(withUser(RecurringActionController::executeCustom))); @@ -301,7 +308,33 @@ else if (RecurringActionType.ActionType.CUSTOMSTATE.equals(action.getActionType( } return dto; } - + /** + * Processes a GET request to get a list of all the scap policies + * + * @param req the request object + * @param res the response object + * @param user the authorized user + * @return the result JSON object + */ + public static String listScapPolicies(Request req, Response res, User user) { + Map data = new HashMap<>(); + List scapPolicies = ScapFactory.listScapPolicies(user.getOrg()); + List scapPoliciesJson = scapPolicies.stream() + .map(policy -> { + JsonObject json = new JsonObject(); + json.addProperty("id", policy.getId()); + json.addProperty("policyName", policy.getPolicyName()); + json.addProperty("dataStreamName", policy.getDataStreamName()); + json.addProperty("xccdfProfileId", policy.getXccdfProfileId()); + json.addProperty("tailoringFileName", policy.getTailoringFile().getName()); + json.addProperty("tailoringFileProfileId", policy.getTailoringProfileId()); + return json; + }) + .collect(Collectors.toList()); + //data.put("tailoringFiles", Json.GSON.toJson(tailoringFiles.)); + data.put("scapPolicies", scapPoliciesJson); + return json(res, scapPoliciesJson, new TypeToken<>() { }); + } /** * Creates a new Recurring Action Schedule @@ -319,7 +352,7 @@ public static String save(Request request, Response response, User user) { try { RecurringAction action = createOrGetAction(user, json); HibernateFactory.getSession().evict(action); // entity -> detached, prevent hibernate flushes - mapJsonToAction(json, action); + mapJsonToAction(json, action, user); RecurringActionManager.saveAndSchedule(action, user); } catch (ValidatorException e) { @@ -437,7 +470,7 @@ private static Set getStateConfigFromJson(Set newConfig = getStateConfigFromJson(details.getStates(), action.getCreator()); ((RecurringState) action.getRecurringActionType()).saveStateConfig(newConfig); } + } else if (action.getRecurringActionType() instanceof RecurringScapPolicy recurringScapPolicy) { + + details.getPolicies() + .stream() + .findFirst() + .flatMap(policyJson -> ScapFactory.lookupScapPolicyByIdAndOrg(policyJson.getId(), user.getOrg())) + .ifPresent(recurringScapPolicy::setScapPolicy); } String cron = json.getCron(); diff --git a/java/code/src/com/suse/manager/webui/templates/audit/create-scap-policy.jade b/java/code/src/com/suse/manager/webui/templates/audit/create-scap-policy.jade index 52748e7d0fc7..5a8ca2243b95 100644 --- a/java/code/src/com/suse/manager/webui/templates/audit/create-scap-policy.jade +++ b/java/code/src/com/suse/manager/webui/templates/audit/create-scap-policy.jade @@ -6,7 +6,6 @@ script(type='text/javascript'). window.csrfToken = "#{csrf_token}"; window.scapDataStreams = !{scapDataStreams}; window.tailoringFiles = !{tailoringFiles}; - window.selectedPolicy = !{selectedPolicy}; script(type='text/javascript'). spaImportReactPage('audit/create-scap-policy') diff --git a/java/code/src/com/suse/manager/webui/utils/ScapPolicyJson.java b/java/code/src/com/suse/manager/webui/utils/ScapPolicyJson.java index 904ac3f99261..872fffb738df 100644 --- a/java/code/src/com/suse/manager/webui/utils/ScapPolicyJson.java +++ b/java/code/src/com/suse/manager/webui/utils/ScapPolicyJson.java @@ -1,53 +1,49 @@ -/* - * Copyright (c) 2024 SUSE LLC - * - * This software is licensed to you under the GNU General Public License, - * version 2 (GPLv2). There is NO WARRANTY for this software, express or - * implied, including the implied warranties of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 - * along with this software; if not, see - * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. - * - * Red Hat trademarks are not licensed under GPLv2. No permission is - * granted to use or replicate Red Hat trademarks that are incorporated - * in this software or its documentation. - */ - package com.suse.manager.webui.utils; + import java.time.LocalDateTime; -import java.util.Optional; +import java.time.format.DateTimeFormatter; /** * Scap policy POST request object */ public class ScapPolicyJson { + /** Formatter for LocalDateTime */ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + /** The policy id */ + private Integer id; /** The policy Name */ private String policyName; - /** The earliest execution date */ - private Optional earliest = Optional.empty(); + /** The earliest execution date (serialized as a String) */ + private String earliest; // Changed to String for manual transformation /** The SCAP xccdf data stream name */ - private String dataStreamName; + private String dataStreamName; /** The XCCDF profile ID */ private String xccdfProfileId; /** The Tailoring file */ private String tailoringFile; - /** The profil ID from the selected tailoring file */ + + /** The profile ID from the selected tailoring file */ private String tailoringProfileId; + public String getDataStreamName() { return dataStreamName; } + public void setDataStreamName(String dataStreamNameIn) { this.dataStreamName = dataStreamNameIn; } + public String getXccdfProfileId() { return xccdfProfileId; } + public void setXccdfProfileId(String xccdfProfileIdIn) { this.xccdfProfileId = xccdfProfileIdIn; } @@ -55,12 +51,15 @@ public void setXccdfProfileId(String xccdfProfileIdIn) { public String getTailoringFile() { return tailoringFile; } + public void setTailoringFile(String tailoringFileIn) { this.tailoringFile = tailoringFileIn; } + public String getTailoringProfileId() { return tailoringProfileId; } + public void setTailoringProfileId(String tailoringProfileIdIn) { this.tailoringProfileId = tailoringProfileIdIn; } @@ -72,11 +71,37 @@ public String getPolicyName() { public void setPolicyName(String policyNameIn) { this.policyName = policyNameIn; } + public Integer getId() { + return id; + } + + public void setId(Integer idIn) { + this.id = idIn; + } + + /** + * Gets the earliest execution date as a LocalDateTime. + */ + public LocalDateTime getEarliest() { + return earliest != null ? LocalDateTime.parse(earliest, FORMATTER) : null; + } /** - * @return the date of earliest execution + * Sets the earliest execution date as a LocalDateTime. */ - public Optional getEarliest() { - return earliest; + public void setEarliest(LocalDateTime earliest) { + this.earliest = earliest != null ? earliest.format(FORMATTER) : null; + } + + @Override + public String toString() { + return "ScapPolicyJson{" + + "policyName='" + policyName + '\'' + + ", earliest='" + earliest + '\'' + + ", dataStreamName='" + dataStreamName + '\'' + + ", xccdfProfileId='" + xccdfProfileId + '\'' + + ", tailoringFile='" + tailoringFile + '\'' + + ", tailoringProfileId='" + tailoringProfileId + '\'' + + '}'; } -} +} \ No newline at end of file diff --git a/java/code/src/com/suse/manager/webui/utils/gson/RecurringActionDetailsDto.java b/java/code/src/com/suse/manager/webui/utils/gson/RecurringActionDetailsDto.java index d54e4a6c370d..0cd06fa793e7 100644 --- a/java/code/src/com/suse/manager/webui/utils/gson/RecurringActionDetailsDto.java +++ b/java/code/src/com/suse/manager/webui/utils/gson/RecurringActionDetailsDto.java @@ -14,6 +14,8 @@ */ package com.suse.manager.webui.utils.gson; +import com.suse.manager.webui.utils.ScapPolicyJson; + import java.util.Date; import java.util.Map; import java.util.Set; @@ -38,6 +40,9 @@ public class RecurringActionDetailsDto { /** The states assigned to a custom state schedule */ private Set states; + /** The policies assigned to a scap policy schedule */ + private Set policies; + /** * @return the Array containing Quartz information */ @@ -137,4 +142,19 @@ public Set getStates() { public void setStates(Set statesIn) { this.states = statesIn; } + /** + * @return the set of states + */ + public Set getPolicies() { + return this.policies; + } + + /** + * Sets the policies + * + * @param policiesIn the polices + */ + public void setPolicies(Set policiesIn) { + this.policies = policiesIn; + } } diff --git a/schema/spacewalk/common/tables/suseRecurringScapPolicy.sql b/schema/spacewalk/common/tables/suseRecurringScapPolicy.sql new file mode 100644 index 000000000000..c4017c602bf3 --- /dev/null +++ b/schema/spacewalk/common/tables/suseRecurringScapPolicy.sql @@ -0,0 +1,23 @@ +-- +-- Copyright (c) 2024 SUSE LLC +-- +-- This software is licensed to you under the GNU General Public License, +-- version 2 (GPLv2). There is NO WARRANTY for this software, express or +-- implied, including the implied warranties of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 +-- along with this software; if not, see +-- http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. +-- +CREATE TABLE suseRecurringScapPolicy +( + rec_id NUMERIC NOT NULL PRIMARY KEY, + test_mode CHAR(1) NOT NULL DEFAULT 'N', + + scap_policy_id INTEGER, + + -- Foreign Key references suseRecurringAction + FOREIGN KEY (rec_id) REFERENCES suseRecurringAction(id) ON DELETE CASCADE, + + -- Foreign Key references suseScapPolicy + FOREIGN KEY (scap_policy_id) REFERENCES suseScapPolicy(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/schema/spacewalk/common/tables/suseScapPolicy.sql b/schema/spacewalk/common/tables/suseScapPolicy.sql new file mode 100644 index 000000000000..22868533c55a --- /dev/null +++ b/schema/spacewalk/common/tables/suseScapPolicy.sql @@ -0,0 +1,49 @@ +-- +-- Copyright (c) 2021 SUSE +-- +-- This software is licensed to you under the GNU General Public License, +-- version 2 (GPLv2). There is NO WARRANTY for this software, express or +-- implied, including the implied warranties of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 +-- along with this software; if not, see +-- http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. +-- + +CREATE TABLE suseScapPolicy +( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + policy_name VARCHAR(255) NOT NULL , + data_stream_name TEXT NOT NULL , + xccdf_profile_id TEXT NOT NULL, + tailoring_file INTEGER + CONSTRAINT fk_tailoring_file + REFERENCES suse_scap_tailoring_file (id) + ON DELETE CASCADE, + + tailoring_profile_id TEXT, + + org_id INTEGER NOT NULL + CONSTRAINT fk_org_id + REFERENCES web_customer (id) + ON DELETE CASCADE, + created TIMESTAMPTZ DEFAULT current_timestamp NOT NULL, + modified TIMESTAMPTZ DEFAULT current_timestamp NOT NULL +); + +CREATE UNIQUE INDEX idx_org_id_policy_name + ON suse_scap_policy (org_id, policy_name); -- Ensures unique policy names within an organization + +-- Trigger to automatically update 'modified' column +CREATE OR REPLACE FUNCTION update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.modified = current_timestamp; -- Set the `modified` column to the current timestamp + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger that runs before any update +CREATE TRIGGER trg_update_modified +BEFORE UPDATE ON suse_scap_policy +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); diff --git a/susemanager-utils/susemanager-sls/scap/xccdf-profiles.xslt.in b/susemanager-utils/susemanager-sls/scap/xccdf-profiles.xslt.in new file mode 100644 index 000000000000..46216c519f21 --- /dev/null +++ b/susemanager-utils/susemanager-sls/scap/xccdf-profiles.xslt.in @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/html/src/manager/audit/scap/policy-picker.tsx b/web/html/src/manager/audit/scap/policy-picker.tsx new file mode 100644 index 000000000000..f24a95a5e368 --- /dev/null +++ b/web/html/src/manager/audit/scap/policy-picker.tsx @@ -0,0 +1,366 @@ +import * as React from "react"; + +import _partition from "lodash/partition"; +import _sortBy from "lodash/sortBy"; +import _unionBy from "lodash/unionBy"; + +import { inferEntityParams } from "manager/recurring/recurring-actions-utils"; + +import { pageSize } from "core/user-preferences"; + +import { DangerDialog } from "components/dialog/DangerDialog"; +import { SectionToolbar } from "components/section-toolbar/section-toolbar"; +import { Column } from "components/table/Column"; +import { TableFilter } from "components/table/TableFilter"; + +import { Utils } from "utils/functions"; + +import Network from "../../../utils/network" +import { AsyncButton } from "../../../components/buttons"; +import { TextField } from "../../../components/fields"; +import { Messages, MessageType } from "../../../components/messages/messages"; +import { Utils as MessagesUtils } from "../../../components/messages/messages"; +import { RankingTable } from "../../../components/ranking-table"; +import { SaltStatePopup } from "../../../components/salt-state-popup"; +import { Table } from "../../../components/table/Table"; + +function channelKey(channel) { + return channel.label; +} + +function channelIcon(channel) { + let iconClass, iconTitle, iconStyle; + if (channel.type === "state") { + iconClass = "fa spacewalk-icon-salt-add"; + iconTitle = t("State Configuration Channel"); + } else if (channel.type === "internal_state") { + iconClass = "fa spacewalk-icon-salt-add"; + iconTitle = t("Internal State"); + iconStyle = { border: "1px solid black" }; + } else { + iconClass = "fa spacewalk-icon-software-channels"; + iconTitle = t("Normal Configuration Channel"); + } + + return ; +} + +type PolicyPickerProps = { + type?: string; + matchUrl: (filter?: string) => any; + applyRequest?: (systems: any[]) => any; + saveRequest: (policies: any[]) => any; + messages?: (messages: MessageType[] | any) => any; +}; + +class PolicyPickerState { + filter = ""; + policies: any[] = []; + search = { + filter: null as string | null, + results: [] as any[], + }; + assigned: any[] = []; + changed = new Map(); + showSaltState?: any | null = undefined; + rank?: boolean = undefined; + messages: MessageType[] = []; +} + +class PolicyPicker extends React.Component { + policy = new PolicyPickerState(); + + constructor(props: PolicyPickerProps) { + super(props); + this.state = { + filter: "", + policies: [], + search: { filter: null, results: [] }, + assigned: [], + changed: new Map(), + showSaltState: null, + rank: undefined, + messages: [], + }; + this.init(); + } + + init = () => { + Network.get(this.props.matchUrl()).then((data) => { + data = this.getSortedList(data); + this.setState({ + policies: data, + search: { + filter: this.state.filter, + results: data, + }, + }); + }); + }; + + + + save = () => { + let messages: MessageType[] = []; + const policies = this.state.assigned; + if (this.props.type === "policy" && !policies.length) { + this.setMessages(MessagesUtils.error(t("Policy configuration must not be empty"))); + this.setState({ changed: new Map() }); + return; + } + const request = this.props.saveRequest(policies).then( + (data, textStatus, jqXHR) => { + const newSearchResults = this.state.search.results.map((policy) => { + const changed = this.state.changed.get(channelKey(policy)); + // We want to make sure the search results are updated with the changes. If there was a change + // pick the updated value from the response if not we keep the original. + if (changed !== undefined) { + return data.filter((c) => c.label === changed.value.label)[0] || changed.value; + } else { + return data.filter((c) => c.label === policy.policyName)[0] || policy; + } + }); + + messages = messages.concat(MessagesUtils.info(t("Policy assignments have been saved."))); + this.setState({ + changed: new Map(), // clear changed + // Update the policies with the new data + policies: _unionBy( + data, + this.state.policies.map((c) => Object.assign(c, { assigned: false, position: undefined })), + "name" + ), + search: { + filter: this.state.search.filter, + results: this.getSortedList(newSearchResults), + }, + }); + this.setMessages(messages); + }, + (jqXHR, textStatus, errorThrown) => { + this.setMessages(MessagesUtils.error(t("An error occurred on save."))); + } + ); + return request; + }; + + onSearchChange = (event) => { + this.setState({ + filter: event.target.value, + }); + }; + + getSortedList = (data) => { + const [assigned, unassigned] = _partition(data, (d) => d.assigned); + return _sortBy(assigned, "position").concat(_sortBy(unassigned, (n) => n.policyName.toLowerCase())); + }; + + search = () => { + return Promise.resolve().then(() => { + if (this.state.filter !== this.state.search.filter) { + // Since we don't commit our changes to the backend in case of state type we perform a local search + this.props.type === "state" + ? this.stateTypeSearch() + : Network.get(this.props.matchUrl(this.state.filter)).then((data) => { + this.setState({ + search: { + filter: this.state.filter, + results: this.getSortedList(data), + }, + }); + this.clearMessages(); + }); + } + }); + }; + + stateTypeSearch = () => { + this.setState({ + search: { + filter: this.state.filter, + results: this.state.policies.filter((c) => c.policyName.includes(this.state.filter)), + }, + }); + this.clearMessages(); + }; + + addChanged = (original, key, selected) => { + const currentChannel = this.state.changed.get(key); + if (selected === currentChannel?.original?.assigned) { + this.state.changed.delete(key); + } else { + this.state.changed.set(key, { + original: original, + value: Object.assign({}, original, { assigned: selected }), + }); + } + this.setState({ + changed: this.state.changed, + }); + }; + + handleSelectionChange = (original) => { + return (event) => { + const selectedPolicyName = event.target.value; + const updatedPolicies = this.state.search.results.map((policy) => ({ + ...policy, + assigned: policy.policyName === selectedPolicyName, // Assign true to the selected policy only + })); + + this.setState( + { + search: { + ...this.state.search, + results: updatedPolicies, // Update the search results with the updated policy assignments + }, + assigned: updatedPolicies.filter((policy) => policy.assigned), // Update assigned state + }, + () => { + this.save(); // Call save after state is updated + } + ); + }; + }; + + tableBody = () => { + const elements: React.ReactNode[] = []; + let rows: any[] = []; + rows = this.state.search.results.map((policy) => { + const changed = this.state.changed.get(channelKey(policy)); + if (changed !== undefined) { + return changed; + } else { + return { + original: policy, + }; + } + }); + + for (var row of rows) { + const changed = row.value; + const currentPolicy = changed === undefined ? row.original : changed; + + elements.push( + + + {channelIcon(currentPolicy)} + {currentPolicy.policyName} + + {currentPolicy.dataStreamName} + + + + +
+ +
+ + + ); + } + + return ( + + {elements.length > 0 ? ( + elements + ) : ( + + +
{t("No Policies Found")}
+ + + )} + + ); + }; + + + setMessages = (message) => { + this.setState({ + messages: message, + }); + if (this.props.messages) { + return this.props.messages(message); + } + }; + + clearMessages() { + this.setMessages([]); + } + + getCurrentAssignment = () => { + const unchanged = this.state.policies.filter((c) => !this.state.changed.has(channelKey(c))); + const changed = Array.from(this.state.changed.values()).map((c) => c.value); + + return unchanged.concat(changed).filter((c) => c.assigned); + }; + + render() { + const currentAssignment = this.getCurrentAssignment(); + + let buttons; + + return ( + + {!this.props.messages && this.state.messages ? : null} + +
+
{buttons}
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ + + + + + + + + + + {this.tableBody()} +
{t("Policy Name")}{t("Data Stream")}{t("Description")}{t("Assign")}
+
+ +
+
+
+ ); + } +} + + + +export { PolicyPicker }; diff --git a/web/html/src/manager/recurring/recurring-actions-edit.tsx b/web/html/src/manager/recurring/recurring-actions-edit.tsx index 0461822520f8..4b3698ece46b 100644 --- a/web/html/src/manager/recurring/recurring-actions-edit.tsx +++ b/web/html/src/manager/recurring/recurring-actions-edit.tsx @@ -14,6 +14,7 @@ import { Toggler } from "components/toggler"; import Network from "utils/network"; import { DisplayHighstate } from "../state/display-highstate"; +import { PolicyPicker } from "manager/audit/scap/policy-picker"; type Props = { schedule?: any; @@ -95,6 +96,9 @@ class RecurringActionsEdit extends React.Component { matchUrl = (target?: string) => { const id = this.state.recurringActionId; + if (target === "policy") { + return "/rhn/manager/api/recurringactions/policies?"; + } return "/rhn/manager/api/recurringactions/states?" + (id ? "id=" + id : "") + (target ? "&target=" + target : ""); }; @@ -184,6 +188,13 @@ class RecurringActionsEdit extends React.Component { return Promise.resolve(states); }; + onSavePolicies = (policies) => { + let { details } = this.state; + details.policies = policies; + this.setState({ details }); + return Promise.resolve(policies); + }; + toggleTestState = () => { let { details } = this.state; details.test = !this.state.details.test; @@ -234,7 +245,7 @@ class RecurringActionsEdit extends React.Component { name="actionTypeDescription" label={t("Action Type")} disabled={this.isEdit()} - options={["Highstate", "Custom state"]} + options={["Highstate", "Custom state", "Scap Policy"]} labelClass="col-sm-3" divClass="col-sm-6" /> @@ -268,6 +279,20 @@ class RecurringActionsEdit extends React.Component { /> )} + {this.state.actionTypeDescription === "Scap Policy" && ( + +

+ {t("Scap Policies")} +   +

+ this.matchUrl("policy")} + saveRequest={this.onSavePolicies} + applyRequest={this.onClickExecute} + /> +
+ )} ); }