diff --git a/src/components/HostsSections/AllowedCreateKeytab.tsx b/src/components/HostsSections/AllowedCreateKeytab.tsx index 25d74e9e..3e595dad 100644 --- a/src/components/HostsSections/AllowedCreateKeytab.tsx +++ b/src/components/HostsSections/AllowedCreateKeytab.tsx @@ -7,10 +7,11 @@ import CreateKeytabUserGroupsTable from "../tables/HostsSettings/CreateKeytabUse import CreateKeytabHostsTable from "../tables/HostsSettings/CreateKeytabHostsTable"; import CreateKeytabHostGroupsTable from "../tables/HostsSettings/CreateKeytabHostGroupsTable"; // Data types -import { Host } from "src/utils/datatypes/globalDataTypes"; +import { Host } from "../../utils/datatypes/globalDataTypes"; interface PropsToAllowCreateKeytab { host: Partial; + onRefresh: () => void; } const AllowedCreateKeytab = (props: PropsToAllowCreateKeytab) => { @@ -18,10 +19,16 @@ const AllowedCreateKeytab = (props: PropsToAllowCreateKeytab) => { if (props.host.fqdn !== undefined) { fqdn = props.host.fqdn; } + return ( - + diff --git a/src/components/ServicesSections/AllowedCreateKeytab.tsx b/src/components/ServicesSections/AllowedCreateKeytab.tsx index f2eee213..d09c09bb 100644 --- a/src/components/ServicesSections/AllowedCreateKeytab.tsx +++ b/src/components/ServicesSections/AllowedCreateKeytab.tsx @@ -11,13 +11,19 @@ import { Service } from "../../utils/datatypes/globalDataTypes"; interface PropsToAllowCreateKeytab { service: Service; + onRefresh: () => void; } const AllowedCreateKeytab = (props: PropsToAllowCreateKeytab) => { return ( - + diff --git a/src/components/modals/HostsSettings/CreateKeytabElementsAddModal.tsx b/src/components/modals/HostsSettings/CreateKeytabElementsAddModal.tsx index 874876df..86b3e520 100644 --- a/src/components/modals/HostsSettings/CreateKeytabElementsAddModal.tsx +++ b/src/components/modals/HostsSettings/CreateKeytabElementsAddModal.tsx @@ -4,6 +4,12 @@ import { Button, DualListSelector } from "@patternfly/react-core"; // Layout import SecondaryButton from "src/components/layouts/SecondaryButton"; import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; +import SearchInputLayout from "../../layouts/SearchInputLayout"; +//Icons +import InfoCircleIcon from "@patternfly/react-icons/dist/esm/icons/info-circle-icon"; +import ExclamationTriangleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon"; +// RPC client +import { useGetIDListMutation, GenericPayload } from "../../../services/rpc"; interface PropsToAddModal { host: string; @@ -20,32 +26,131 @@ interface PropsToAddModal { } const CreateKeytabElementsAddModal = (props: PropsToAddModal) => { - // Dual list data - const data = props.availableData; - // Dual list selector - const [availableOptions, setAvailableOptions] = useState( - data.sort() + const initialList = ( +
+ Enter + a value in the search field +
); - const [chosenOptions, setChosenOptions] = useState([]); + + const [availableOptions, setAvailableOptions] = useState< + string[] | ReactNode[] + >([initialList]); + const [chosenOptions, setChosenOptions] = useState([]); + const [searchValue, setSearchValue] = React.useState(""); + const [searchDisabled, setSearchIsDisabled] = useState(false); + + const updateAvailableOptions = (newList: string[]) => { + if (newList.length === 0) { + const emptyList = ( +
+ {" "} + No matching results +
+ ); + setAvailableOptions([emptyList]); + return; + } + // Filter out options already in the table + const filterUsersData = newList.filter((item) => { + return !props.tableElementsList.some((itm) => { + return item === itm; + }); + }); + + // Filter out options that have already been chosen + const cleanList = filterUsersData.filter((item) => { + return !chosenOptions.some((itm) => { + return item === itm; + }); + }); + setAvailableOptions(cleanList); + }; + + // Issue a search using a specific search value + const [retrieveIDs] = useGetIDListMutation({}); + const submitSearchValue = () => { + setSearchIsDisabled(true); + retrieveIDs({ + searchValue: searchValue, + sizeLimit: 200, + startIdx: 0, + stopIdx: 200, + entryType: props.elementType, + } as GenericPayload).then((result) => { + if ("data" in result) { + updateAvailableOptions(result.data.list); + } + setSearchIsDisabled(false); + }); + }; + + const updateSearchValue = (value: string) => { + setSearchValue(value); + }; + + const searchValueData = { + searchValue: searchValue, + updateSearchValue: updateSearchValue, + submitSearchValue: submitSearchValue, + }; const listChange = ( newAvailableOptions: ReactNode[], newChosenOptions: ReactNode[] ) => { - setAvailableOptions(newAvailableOptions.sort()); - setChosenOptions(newChosenOptions.sort()); + // Only "message" options are actually react nodes, + // revise the lists as needed. + for (let idx = 0; idx < newChosenOptions.length; idx++) { + // if not typeof string, remove from list + if (typeof newChosenOptions[idx] !== "string") { + return; + } + } + const newAvailOptions: string[] = []; + for (let idx = 0; idx < newAvailableOptions.length; idx++) { + // Revise avail list to only includue valid string options + if (typeof newAvailableOptions[idx] === "string") { + const option = newAvailableOptions[idx] as string; + newAvailOptions.push(option); + } + } + + setAvailableOptions(newAvailOptions.sort() as string[]); + setChosenOptions(newChosenOptions.sort() as string[]); // The recently added entries are removed from the available data options - props.updateAvailableData(newAvailableOptions.sort()); + props.updateAvailableData(newAvailOptions.sort()); }; + let availOptions; + if (availableOptions.length === 0) { + // No option, should display some info about this + if (searchValue === "") { + availOptions = [initialList]; + } + } else { + availOptions = availableOptions; + } + const fields = [ + { + id: "dual-list-search", + pfComponent: ( + + ), + }, { id: "dual-list-selector", pfComponent: ( { // When clean data, set to original values const cleanData = () => { - setAvailableOptions(data); + setAvailableOptions([]); setChosenOptions([]); props.updateSelectedElements([]); }; @@ -84,14 +189,12 @@ const CreateKeytabElementsAddModal = (props: PropsToAddModal) => { // Add element to the list const addElementToList = () => { const itemsToAdd: string[] = []; - const newTableElementsList = props.tableElementsList; chosenOptions.map((opt) => { if (opt !== undefined && opt !== null) { itemsToAdd.push(opt.toString()); - newTableElementsList.push(opt.toString()); } }); - props.updateTableElementsList(newTableElementsList); + props.updateTableElementsList(itemsToAdd); props.updateSelectedElements([]); cleanAndCloseModal(); }; @@ -125,7 +228,7 @@ const CreateKeytabElementsAddModal = (props: PropsToAddModal) => { props.elementType + "s to " + props.operationType + - " keytab of " + + " keytab for " + props.host } formId={ diff --git a/src/components/tables/HostsSettings/CreateKeytabUsersTable.tsx b/src/components/tables/HostsSettings/CreateKeytabUsersTable.tsx index ccf08754..9493f4a7 100644 --- a/src/components/tables/HostsSettings/CreateKeytabUsersTable.tsx +++ b/src/components/tables/HostsSettings/CreateKeytabUsersTable.tsx @@ -2,43 +2,154 @@ import React, { useEffect, useState } from "react"; // PatternFly import { Td, Th, Tr } from "@patternfly/react-table"; // Layout -import TableWithButtonsLayout from "src/components/layouts/TableWithButtonsLayout"; +import TableWithButtonsLayout from "../../../components/layouts/TableWithButtonsLayout"; // Modals -import CreateKeytabElementsAddModal from "src/components/modals/HostsSettings/CreateKeytabElementsAddModal"; -import CreateKeytabElementsDeleteModal from "src/components/modals/HostsSettings/CreateKeytabElementsDeleteModal"; -// Redux -import { useAppSelector } from "src/store/hooks"; +import CreateKeytabElementsAddModal from "../../../components/modals/HostsSettings/CreateKeytabElementsAddModal"; +import CreateKeytabElementsDeleteModal from "../../../components/modals/HostsSettings/CreateKeytabElementsDeleteModal"; +// Hooks +import { useAlerts } from "../../../hooks/useAlerts"; +// Data types +import { Host, Service, User } from "../../../utils/datatypes/globalDataTypes"; +// React Router DOM +import { Link } from "react-router-dom"; +// Utils +import { API_VERSION_BACKUP } from "../../../utils/utils"; +// Navigation +import { URL_PREFIX } from "../../../navigation/NavRoutes"; + +import { + ErrorResult, + KeyTabPayload, + GetEntriesPayload, + useUpdateKeyTabMutation, + useGetEntriesMutation, +} from "../../../services/rpc"; interface PropsToTable { - host: string; + from: "host" | "service"; + id: string; + entry: Partial | Partial; + onRefresh: () => void; } const CreateKeytabUsersTable = (props: PropsToTable) => { - // Full users list -> Initial data - const fullUsersList = useAppSelector((state) => state.activeUsers.usersList); - const fullUserIdsList = fullUsersList.map((user) => user.uid); + const attr = "ipaallowedtoperform_write_keys_user"; + let users: string[] = []; + if (props.entry[attr] !== undefined) { + users = props.entry[attr]; + } - // Users list on the table - const [tableUsersList, setTableUsersList] = useState([]); + // Alerts to show in the UI + const alerts = useAlerts(); - const updateTableUsersList = (newTableUsersList: string[]) => { - setTableUsersList(newTableUsersList); - }; + // Users list on the table + const [tableUsersList, setTableUsersList] = useState(users); + const [fullUser, setFullUsers] = useState([]); - // Filter function to compare the available data with the data that - // is in the table already. This is done to prevent duplicates - // (e.g: adding the same element twice). - const filterUsersData = () => { - return fullUserIdsList.filter((item) => { - return !tableUsersList.some((itm) => { - return item === itm; + // Gather User objects + const [getEntries] = useGetEntriesMutation({}); + useEffect(() => { + if (tableUsersList.length > 0) { + getEntries({ + idList: tableUsersList, + entryType: "user", + apiVersion: API_VERSION_BACKUP, + } as GetEntriesPayload).then((result) => { + if ("data" in result) { + const usersListResult = result.data.result.results; + const usersListSize = result.data.result.count; + const usersList: User[] = []; + for (let i = 0; i < usersListSize; i++) { + usersList.push(usersListResult[i].result); + } + setFullUsers(usersList); + } }); + } + }, [tableUsersList]); + + const [executeUpdate] = useUpdateKeyTabMutation({}); + let add_method = ""; + let remove_method = ""; + if (props.from === "host") { + add_method = "host_allow_create_keytab"; + remove_method = "host_disallow_create_keytab"; + } else { + // Service + add_method = "service_allow_create_keytab"; + remove_method = "service_disallow_create_keytab"; + } + + const addUserList = (newUsers: string[]) => { + executeUpdate({ + id: props.id, + entryType: "user", + entries: newUsers, + method: add_method, + } as KeyTabPayload).then((response) => { + if ("data" in response) { + if (response.data.result) { + alerts.addAlert( + "add-users-allow-keytab", + "Successfully added users that are allowed to create keytabs", + "success" + ); + // Update table + const users = [...tableUsersList, ...newUsers].sort(); + setTableUsersList(users); + setShowAddModal(false); + props.onRefresh(); + } else if (response.data.error) { + // Set alert: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "add-users-allow-keytab", + "Failed to add users that are allowed to create keytabs: " + + errorMessage.message, + "danger" + ); + } + } }); }; - // const usersFilteredData = filterUsersData(); - const [usersFilteredData, setUsersFilteredData] = useState(filterUsersData()); + const removeUserList = () => { + executeUpdate({ + id: props.id, + entryType: "user", + entries: selectedUsers, + method: remove_method, + } as KeyTabPayload).then((response) => { + if ("data" in response) { + if (response.data.result) { + alerts.addAlert( + "remove-users-allow-create-keytab", + "Removed users that are allowed to create keytabs", + "success" + ); + // Filter out removed users + const users = tableUsersList.filter(function (user) { + return selectedUsers.indexOf(user) < 0; + }); + // Update table + setTableUsersList(users); + setShowAddModal(false); + props.onRefresh(); + } else if (response.data.error) { + // Set alert: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "remove-users-allow-create-keytab", + "Failed to remove users that are allowed to create keytabs: " + + errorMessage.message, + "danger" + ); + } + } + }); + }; + const [usersFilteredData, setUsersFilteredData] = useState([]); const updateUsersFilteredData = (newFilteredUsers: unknown[]) => { setUsersFilteredData(newFilteredUsers as string[]); }; @@ -168,7 +279,14 @@ const CreateKeytabUsersTable = (props: PropsToTable) => { isDisabled: false, }} /> - {user} + + + {user} + + )); @@ -204,6 +322,7 @@ const CreateKeytabUsersTable = (props: PropsToTable) => { return ( <> + { /> {showAddModal && ( { updateAvailableData={updateUsersFilteredData} updateSelectedElements={updateSelectedUsers} tableElementsList={tableUsersList} - updateTableElementsList={updateTableUsersList} + updateTableElementsList={addUserList} /> )} {showDeleteModal && ( { updateIsDeleteButtonDisabled={updateIsDeleteButtonDisabled} updateSelectedElements={updateSelectedUsers} tableElementsList={tableUsersList} - updateTableElementsList={updateTableUsersList} + updateTableElementsList={removeUserList} availableData={usersFilteredData} updateAvailableData={updateUsersFilteredData} /> diff --git a/src/pages/Hosts/HostsSettings.tsx b/src/pages/Hosts/HostsSettings.tsx index 036434a2..c83bb816 100644 --- a/src/pages/Hosts/HostsSettings.tsx +++ b/src/pages/Hosts/HostsSettings.tsx @@ -256,7 +256,10 @@ const HostsSettings = (props: PropsToHostsSettings) => { id="allow-create-keytab" text="Allow to create keytab" /> - +
diff --git a/src/pages/Services/ServicesSettings.tsx b/src/pages/Services/ServicesSettings.tsx index 8c6d12d4..bd97d9fd 100644 --- a/src/pages/Services/ServicesSettings.tsx +++ b/src/pages/Services/ServicesSettings.tsx @@ -31,6 +31,7 @@ import AllowedCreateKeytab from "src/components/ServicesSections/AllowedCreateKe interface PropsToServicesSettings { service: Service; + onRefresh: () => void; } const ServicesSettings = (props: PropsToServicesSettings) => { @@ -172,7 +173,10 @@ const ServicesSettings = (props: PropsToServicesSettings) => { id="allowed-create-keytab" text="Allowed to create keytab" /> - +
diff --git a/src/pages/Services/ServicesTabs.tsx b/src/pages/Services/ServicesTabs.tsx index 19ebc14e..6f02050e 100644 --- a/src/pages/Services/ServicesTabs.tsx +++ b/src/pages/Services/ServicesTabs.tsx @@ -21,11 +21,22 @@ import BreadcrumbLayout from "src/components/layouts/BreadcrumbLayout"; import TitleLayout from "src/components/layouts/TitleLayout"; // Data types import { Service } from "src/utils/datatypes/globalDataTypes"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query"; +import { SerializedError } from "@reduxjs/toolkit"; +// Utils +import { API_VERSION_BACKUP } from "../../utils/utils"; +// Hooks +import { useAlerts } from "../../hooks/useAlerts"; +// RPC client +import { useSearchEntriesMutation, GenericPayload } from "../../services/rpc"; const ServicesTabs = () => { // Get location (React Router DOM) and get state data const location = useLocation(); - const serviceData: Service = location.state as Service; + let serviceData: Service = location.state as Service; + + // Alerts to show in the UI + const alerts = useAlerts(); // Tab const [activeTabKey, setActiveTabKey] = useState(0); @@ -46,8 +57,48 @@ const ServicesTabs = () => { }, ]; + // Handle refresh of a service + const [retrieveService] = useSearchEntriesMutation({}); + const onRefresh = () => { + retrieveService({ + searchValue: serviceData.krbcanonicalname, + sizeLimit: 0, + apiVersion: API_VERSION_BACKUP, + startIdx: 0, + stopIdx: 1, + entryType: "service", + } as GenericPayload).then((result) => { + // Manage new response here + if ("data" in result) { + const searchError = result.data.error as + | FetchBaseQueryError + | SerializedError; + + if (searchError) { + // Error + let error: string | undefined = ""; + if ("error" in searchError) { + error = searchError.error; + } else if ("message" in searchError) { + error = searchError.message; + } + alerts.addAlert( + "refresh service", + error || "Error when searching for services", + "danger" + ); + } else { + // Success + const serviceListResult = result.data.result.results; + serviceData = serviceListResult[0]; + } + } + }); + }; + return ( + { title={Settings} > - + { const payloadWithParams = { @@ -876,6 +894,127 @@ export const api = createApi({ }; }, }), + // Take a list of ID's and get the full entries + getEntries: build.mutation({ + async queryFn(payloadData, _queryApi, _extraOptions, fetchWithBQ) { + const { idList, apiVersion, entryType } = payloadData; + + if (apiVersion === undefined) { + return { + error: { + status: "CUSTOM_ERROR", + data: "", + error: "API version not available", + } as FetchBaseQueryError, + }; + } + + let show_method = ""; + if (entryType === "user") { + show_method = "user_show"; + } else if (entryType === "host") { + show_method = "host_show"; + } else if (entryType === "hostgroup") { + show_method = "hostgroup_show"; + } else { + // user group + show_method = "group_show"; + } + + const payloadDataBatch: Command[] = idList.map((id) => ({ + method: show_method, + params: [[id], { no_members: true }], + })); + + // Make call using 'fetchWithBQ' + const partialInfoResult = await fetchWithBQ( + getBatchCommand(payloadDataBatch as Command[], apiVersion) + ); + + const response = partialInfoResult.data as BatchRPCResponse; + if (response) { + response.result.totalCount = idList.length; + } + + // Return results + return response + ? { data: response } + : { + error: partialInfoResult.error as unknown as FetchBaseQueryError, + }; + }, + }), + getIDList: build.mutation({ + async queryFn(payloadData, _queryApi, _extraOptions, fetchWithBQ) { + const { searchValue, sizeLimit, startIdx, stopIdx, entryType } = + payloadData; + + // Prepare search parameters + const params = { + pkey_only: true, + sizelimit: sizeLimit, + version: API_VERSION_BACKUP, + all: true, + }; + + let method = ""; + if (entryType === "user") { + method = "user_find"; + } else if (entryType === "stage") { + method = "stageuser_find"; + } else if (entryType === "preserved") { + method = "user_find"; + params["preserved"] = true; + } else if (entryType === "host") { + method = "host_find"; + } else if (entryType === "service") { + method = "service_find"; + } + + // Prepare payload + const payloadDataIds: Command = { + method: method, + params: [[searchValue], params], + }; + + // Make call using 'fetchWithBQ' + const getGroupIDsResult = await fetchWithBQ(getCommand(payloadDataIds)); + // Return possible errors + if (getGroupIDsResult.error) { + return { + error: { + status: "CUSTOM_ERROR", + data: "", + error: "Failed to search for entries", + } as FetchBaseQueryError, + }; + } + // If no error: cast and assign 'ids' + const responseData = getGroupIDsResult.data as FindRPCResponse; + + const ids: string[] = []; + const itemsCount = responseData.result.result.length as number; + for (let i = startIdx; i < itemsCount && i < stopIdx; i++) { + if (entryType === "user") { + const userId = responseData.result.result[i] as UIDType; + const { uid } = userId; + ids.push(uid[0] as string); + } else if (entryType === "host") { + const hostId = responseData.result.result[i] as fqdnType; + const { fqdn } = hostId; + ids.push(fqdn[0] as string); + } else if (entryType === "service") { + const serviceId = responseData.result.result[i] as servicesType; + const { krbprincipalname } = serviceId; + ids.push(krbprincipalname[0] as string); + } + } + + const result = { list: ids, count: itemsCount } as ListResponse; + + return { data: result }; + }, + }), // Autommeber Users autoMemberRebuildUsers: build.mutation({ query: (users) => { @@ -1097,7 +1236,7 @@ export const api = createApi({ }, }), getGenericList: build.query({ - query(objName) { + query: (objName) => { return getCommand({ method: objName + "_find", params: [[], { version: API_VERSION_BACKUP }], @@ -1114,10 +1253,32 @@ export const api = createApi({ transformResponse: (response: FindRPCResponse): User => apiToUser(response.result.result), }), + updateKeyTab: build.mutation({ + query: (payload: KeyTabPayload) => { + const params = { version: API_VERSION_BACKUP }; + if (payload.entryType === "user") { + params["user"] = payload.entries; + } else if (payload.entryType === "host") { + params["host"] = payload.entries; + } else if (payload.entryType === "usergroup") { + params["group"] = payload.entries; + } else { + // hostgroup + params["hostgroup"] = payload.entries; + } + + return getCommand({ + method: payload.method, + params: [[payload.id], params], + }); + }, + }), }), }); +// // Wrappers +// export const useGetDNSZonesQuery = () => { return useGetGenericListQuery("dnszone"); }; @@ -1183,6 +1344,9 @@ export const useGetStageUsersFullQuery = (userId: string) => { }; return useGetGenericUsersFullDataQuery(query_args); }; +// +// End of wrappers +// export const { useSimpleCommandQuery, @@ -1230,4 +1394,7 @@ export const { useRemoveServicesMutation, useSearchEntriesMutation, useGetUserByUidQuery, + useGetIDListMutation, + useUpdateKeyTabMutation, + useGetEntriesMutation, } = api;