diff --git a/java/code/src/com/redhat/rhn/common/security/acl/Access.java b/java/code/src/com/redhat/rhn/common/security/acl/Access.java index 9a847782c85e..1bd11cdfce61 100644 --- a/java/code/src/com/redhat/rhn/common/security/acl/Access.java +++ b/java/code/src/com/redhat/rhn/common/security/acl/Access.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 SUSE LLC * Copyright (c) 2009--2015 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, @@ -43,6 +44,7 @@ import com.redhat.rhn.manager.system.SystemManager; import com.redhat.rhn.manager.user.UserManager; +import com.suse.manager.model.hub.HubFactory; import com.suse.manager.webui.controllers.utils.ContactMethodUtil; import com.suse.manager.webui.utils.ViewHelper; @@ -676,4 +678,26 @@ public boolean aclSystemHasModularChannels(Map ctx, String[] par return server.getChannels().stream().anyMatch(Channel::isModular); } + + /** + * Checks if this server is a hub + * @param ctx the acl context + * @param params the parameters for the acl + * @return true if this server has peripheral registered + */ + public boolean aclIsHub(Map ctx, String[] params) { + HubFactory factory = new HubFactory(); + return factory.isISSHub(); + } + + /** + * Checks if this server is a peripheral + * @param ctx the acl context + * @param params the parameters for the acl + * @return true if this server is registered as a peripheral on a hub + */ + public boolean aclIsPeripheral(Map ctx, String[] params) { + HubFactory factory = new HubFactory(); + return factory.isISSPeripheral(); + } } diff --git a/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml b/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml index f0e8db02f4af..1d6ea5f441e0 100644 --- a/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml +++ b/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml @@ -7784,6 +7784,9 @@ Follow this url to see the full list of inactive systems: A record for Slave @@PRODUCT_NAME@@ {0} already exists + + Unable to invoke the remote server {0} + Security Patches @@ -9160,6 +9163,12 @@ Alternatively, you will want to download <strong>Incremental Channel Conte Unable to store the token. Please check the server logs. + + Cannot find a remote server with the specified id. + + + Unable to deregister: an unexpected error is occurred while contacting the remote server. Please check the server logs. + CVE Audit diff --git a/java/code/src/com/redhat/rhn/frontend/strings/nav/StringResource_en_US.xml b/java/code/src/com/redhat/rhn/frontend/strings/nav/StringResource_en_US.xml index 0722a1926ddc..d36e66aac45a 100644 --- a/java/code/src/com/redhat/rhn/frontend/strings/nav/StringResource_en_US.xml +++ b/java/code/src/com/redhat/rhn/frontend/strings/nav/StringResource_en_US.xml @@ -653,6 +653,30 @@ Navigation Menu + + Hub Configuration + + Navigation Menu + + + + Hub Details + + Navigation Menu + + + + Peripherals Configuration + + Navigation Menu + + + + Access Tokens + + Navigation Menu + + Remote Command diff --git a/java/code/src/com/redhat/rhn/frontend/xmlrpc/ServerInvocationException.java b/java/code/src/com/redhat/rhn/frontend/xmlrpc/ServerInvocationException.java new file mode 100644 index 000000000000..6f1f254f98c4 --- /dev/null +++ b/java/code/src/com/redhat/rhn/frontend/xmlrpc/ServerInvocationException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 SUSE LLC + * Copyright (c) 2013 Red Hat, Inc. + * + * 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.frontend.xmlrpc; + +import com.redhat.rhn.FaultException; +import com.redhat.rhn.common.localization.LocalizationService; + +/** + * ISS Slave we're trying to create already exists + * + */ +public class ServerInvocationException extends FaultException { + + private static final long serialVersionUID = -736057155929214695L; + + /** + * Tried to create a slave when one already exists w/a given name + * @param fqdn the fqdn of the problematic server + */ + public ServerInvocationException(String fqdn) { + super(3002, "serverInvocationException", LocalizationService.getInstance(). + getMessage("api.iss.serverinvocationexception", fqdn)); + } + + /** + * Tried to create a slave when one already exists w/a given name + * @param fqdn the fqdn of the problematic server + * @param cause what caused this exception + */ + public ServerInvocationException(String fqdn, Throwable cause) { + super(3002, "serverInvocationException", LocalizationService.getInstance(). + getMessage("api.iss.serverinvocationexception", fqdn), cause); + + + } +} diff --git a/java/code/src/com/suse/manager/hub/HubManager.java b/java/code/src/com/suse/manager/hub/HubManager.java index e56a184f225c..1b1e0970b636 100644 --- a/java/code/src/com/suse/manager/hub/HubManager.java +++ b/java/code/src/com/suse/manager/hub/HubManager.java @@ -206,6 +206,19 @@ public IssServer findServer(User user, String serverFqdn, IssRole role) { return lookupServerByFqdnAndRole(serverFqdn, role); } + /** + * Returns the ISS of the specified role, if present + * @param user the user performing the operation + * @param id the id of the server + * @param role the role of the server + * @return an {@link IssHub} or {@link IssPeripheral} depending on the specified role, null if the FQDN is unknown + */ + public IssServer findServer(User user, long id, IssRole role) { + ensureSatAdmin(user); + + return lookupServerByIdAndRole(id, role); + } + /** * Save the given remote server as hub or peripheral depending on the specified role * @param accessToken the access token granting access and identifying the caller @@ -222,17 +235,30 @@ public IssServer saveNewServer(IssAccessToken accessToken, IssRole role, String } /** - * Delete locally all ISS artifacts for the hub or peripheral server identified by the FQDN + * Deregister the server with the given FQDN. The de-registration can be optionally performed also on the + * remote server. * @param user the user * @param fqdn the FQDN + * @param onlyLocal specify if the de-registration has to be performed also on the remote server + * @throws CertificateException when it's not possible to use remote server certificate + * @throws IOException when the connection with the remote server fails */ - public void deleteIssServerLocal(User user, String fqdn) { + public void deregister(User user, String fqdn, boolean onlyLocal) throws CertificateException, IOException { ensureSatAdmin(user); - if (hubFactory.isISSPeripheral()) { - deleteHub(fqdn); + + IssRole remoteRole = hubFactory.isISSPeripheral() ? IssRole.HUB : IssRole.PERIPHERAL; + IssServer server = findServer(user, fqdn, remoteRole); + + if (!onlyLocal) { + IssAccessToken accessToken = hubFactory.lookupAccessTokenFor(server.getFqdn()); + var internalClient = clientFactory.newInternalClient(fqdn, accessToken.getToken(), server.getRootCa()); + internalClient.deregister(); } - else { - deletePeripheral(fqdn); + + switch (remoteRole) { + case HUB -> deleteHub(fqdn); + case PERIPHERAL -> deletePeripheral(fqdn); + default -> throw new IllegalStateException("Role should either be HUB or PERIPHERAL"); } } @@ -257,10 +283,15 @@ private void deletePeripheral(String peripheralFqdn) { LOG.info("Peripheral Server with name {} not found", peripheralFqdn); return; // no error as the state is already as wanted. } + IssPeripheral peripheral = issPeripheral.get(); + deletePeripheral(peripheral); + } + + private void deletePeripheral(IssPeripheral peripheral) { CredentialsFactory.removeCredentials(peripheral.getMirrorCredentials()); hubFactory.remove(peripheral); - hubFactory.removeAccessTokensFor(peripheralFqdn); + hubFactory.removeAccessTokensFor(peripheral.getFqdn()); } private void deleteHub(String hubFqdn) { @@ -270,9 +301,13 @@ private void deleteHub(String hubFqdn) { return; // no error as the state is already as wanted. } IssHub hub = issHub.get(); + deleteHub(hub); + } + + private void deleteHub(IssHub hub) { CredentialsFactory.removeCredentials(hub.getMirrorCredentials()); hubFactory.remove(hub); - hubFactory.removeAccessTokensFor(hubFqdn); + hubFactory.removeAccessTokensFor(hub.getFqdn()); } /** @@ -737,6 +772,13 @@ private IssServer lookupServerByFqdnAndRole(String serverFqdn, IssRole role) { }; } + private IssServer lookupServerByIdAndRole(long id, IssRole role) { + return switch (role) { + case HUB -> hubFactory.findHubById(id); + case PERIPHERAL -> hubFactory.findPeripheralById(id); + }; + } + private IssServer createServer(IssRole role, String serverFqdn, String rootCA, String gpgKey, User user) throws TaskomaticApiException { taskomaticApi.scheduleSingleRootCaCertUpdate(computeRootCaFileName(role, serverFqdn), rootCA); @@ -872,6 +914,27 @@ public List collectAllChannels(IssAccessToken accessToken) { return ChannelFactory.listAllChannels(); } + /** + * Count the registered peripherals on "this" hub + * @param user the SatAdmin + * @return the count of registered peripherals entities + */ + public Long countRegisteredPeripherals(User user) { + ensureSatAdmin(user); + return hubFactory.countPeripherals(); + } + + /** + * List the peripherals with pagination //TODO: and search + * @param user the SatAdmin + * @param pc the PageControl + * @return a List of Peripherals entities + */ + public List listRegisteredPeripherals(User user, PageControl pc) { + ensureSatAdmin(user); + return hubFactory.listPaginatedPeripherals(pc.getStart() - 1, pc.getPageSize()); + } + /** * add vendor channel to peripheral * @@ -933,7 +996,6 @@ public List addVendorChannels(IssAccessToken accessToken, List .toList(); } - /** * add custom channels to peripheral * diff --git a/java/code/src/com/suse/manager/hub/test/HubManagerTest.java b/java/code/src/com/suse/manager/hub/test/HubManagerTest.java index 547ec055e3f3..f8edd4f592c2 100644 --- a/java/code/src/com/suse/manager/hub/test/HubManagerTest.java +++ b/java/code/src/com/suse/manager/hub/test/HubManagerTest.java @@ -589,10 +589,30 @@ public void canStoreSCCCredentials() throws TaskomaticApiException { } @Test - public void canDeregisterHub() throws TokenBuildingException, TaskomaticApiException, TokenParsingException { + public void canDeregisterHub() throws Exception { String fqdn = LOCAL_SERVER_FQDN; - createHubRegistration(fqdn, null, null); - hubManager.deleteIssServerLocal(satAdmin, fqdn); + IssAccessToken token = createHubRegistration(fqdn, null, null); + HubInternalClient internalClient = mock(HubInternalClient.class); + + context().checking(new Expectations() {{ + allowing(clientFactoryMock).newInternalClient(fqdn, token.getToken(), null); + will(returnValue(internalClient)); + + allowing(internalClient).deregister(); + }}); + + hubManager.deregister(satAdmin, fqdn, false); + + assertNull(hubFactory.lookupAccessTokenFor(fqdn)); + assertNull(hubFactory.lookupIssuedToken(fqdn)); + assertTrue(hubFactory.lookupIssHub().isEmpty(), "Failed to remove Hub"); + assertEquals(0, CredentialsFactory.listSCCCredentials().size()); + } + + @Test + public void canDeregisterHubLocalOnly() throws Exception { + String fqdn = LOCAL_SERVER_FQDN; + hubManager.deregister(satAdmin, fqdn, true); assertNull(hubFactory.lookupAccessTokenFor(fqdn)); assertNull(hubFactory.lookupIssuedToken(fqdn)); @@ -601,10 +621,31 @@ public void canDeregisterHub() throws TokenBuildingException, TaskomaticApiExcep } @Test - public void canDeregisterPeripheral() throws TokenBuildingException, TaskomaticApiException, TokenParsingException { + public void canDeregisterPeripheral() throws Exception { String fqdn = LOCAL_SERVER_FQDN; - createPeripheralRegistration(fqdn, null); - hubManager.deleteIssServerLocal(satAdmin, fqdn); + IssAccessToken token = createPeripheralRegistration(fqdn, null); + HubInternalClient internalClient = mock(HubInternalClient.class); + + context().checking(new Expectations() {{ + allowing(clientFactoryMock).newInternalClient(fqdn, token.getToken(), null); + will(returnValue(internalClient)); + + allowing(internalClient).deregister(); + }}); + + hubManager.deregister(satAdmin, fqdn, false); + + assertNull(hubFactory.lookupAccessTokenFor(fqdn)); + assertNull(hubFactory.lookupIssuedToken(fqdn)); + assertTrue(hubFactory.lookupIssPeripheralByFqdn(fqdn).isEmpty(), "Failed to remove Peripheral"); + assertEquals(0, CredentialsFactory.listCredentialsByType(HubSCCCredentials.class).size()); + } + + @Test + public void canDeregisterPeripheralLocalOnly() throws Exception { + String fqdn = LOCAL_SERVER_FQDN; + + hubManager.deregister(satAdmin, fqdn, true); assertNull(hubFactory.lookupAccessTokenFor(fqdn)); assertNull(hubFactory.lookupIssuedToken(fqdn)); diff --git a/java/code/src/com/suse/manager/model/hub/HubFactory.java b/java/code/src/com/suse/manager/model/hub/HubFactory.java index 45c660643cbe..1d0d44f6e02b 100644 --- a/java/code/src/com/suse/manager/model/hub/HubFactory.java +++ b/java/code/src/com/suse/manager/model/hub/HubFactory.java @@ -74,6 +74,16 @@ public void remove(IssHub hubIn) { removeObject(hubIn); } + + /** + * Retrieves a {@link IssHub} by id + * @param id the id of the hub + * @return the hub object + */ + public IssHub findHubById(long id) { + return getSession().get(IssHub.class, id); + } + /** * Lookup {@link IssHub} object by its FQDN * @param fqdnIn the fqdn @@ -85,6 +95,15 @@ public Optional lookupIssHubByFqdn(String fqdnIn) { .uniqueResultOptional(); } + /** + * Retrieves a {@link IssPeripheral} by id + * @param id the id of the peripheral + * @return the peripheral object + */ + public IssPeripheral findPeripheralById(long id) { + return getSession().get(IssPeripheral.class, id); + } + /** * Lookup {@link IssHub} object. * A peripheral server should have not more than 1 Hub @@ -102,6 +121,44 @@ public boolean isISSPeripheral() { return lookupIssHub().isPresent(); } + /** + * @return return true, when this system is an Inter-Server-Sync Hub Server + */ + public boolean isISSHub() { + return countPeripherals() != 0; + } + + /** + * get the list of all the peripheral servers for a hub + * + * @return a list of paginated peripherals + */ + public List listPeripherals() { + return getSession().createQuery("FROM IssPeripheral", IssPeripheral.class).list(); + } + + /** + * get number of peripheral registered on this server + * + * @return a number of peripherals + */ + public long countPeripherals() { + return getSession().createQuery("SELECT count(*) FROM IssPeripheral", Long.class).uniqueResult(); + } + + /** + * get the list of all the peripheral servers for a hub + * @param offset the first item to retrieve + * @param pageSize the maximum number of items to retrieve + * @return a list of paginated peripherals + */ + public List listPaginatedPeripherals(int offset, int pageSize) { + return getSession().createQuery("FROM IssPeripheral", IssPeripheral.class) + .setFirstResult(offset) + .setMaxResults(pageSize) + .list(); + } + /** * Lookup {@link IssPeripheral} object by its FQDN * @param fqdnIn the fqdn diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/AdminViewsController.java b/java/code/src/com/suse/manager/webui/controllers/admin/AdminViewsController.java index 472abf221fd7..d9e3b9e06bfc 100644 --- a/java/code/src/com/suse/manager/webui/controllers/admin/AdminViewsController.java +++ b/java/code/src/com/suse/manager/webui/controllers/admin/AdminViewsController.java @@ -29,12 +29,15 @@ import com.suse.manager.admin.PaygAdminManager; import com.suse.manager.model.hub.HubFactory; import com.suse.manager.reactor.utils.OptionalTypeAdapterFactory; +import com.suse.manager.webui.controllers.ECMAScriptDateAdapter; +import com.suse.manager.webui.controllers.admin.beans.HubDetailsData; import com.suse.manager.webui.controllers.admin.mappers.PaygResponseMappers; import com.suse.manager.webui.utils.FlashScopeHelper; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -52,11 +55,13 @@ public class AdminViewsController { private static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(new OptionalTypeAdapterFactory()) + .registerTypeAdapter(Date.class, new ECMAScriptDateAdapter()) .serializeNulls() .create(); private static final PaygAdminManager PAYG_ADMIN_MANAGER = new PaygAdminManager(new TaskomaticApi()); + private static final HubFactory HUB_FACTORY = new HubFactory(); private AdminViewsController() { } @@ -66,7 +71,7 @@ private AdminViewsController() { } */ public static void initRoutes(JadeTemplateEngine jade) { get("/manager/admin/config/monitoring", - withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showMonitoring))), jade); + withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showMonitoring))), jade); get("/manager/admin/config/password-policy", withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showPasswordPolicy))), jade); get("/manager/admin/setup/payg", @@ -77,6 +82,11 @@ public static void initRoutes(JadeTemplateEngine jade) { withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showPayg))), jade); get("/manager/admin/setup/proxy", withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showProxy))), jade); + + get("/manager/admin/hub/hub-details", + withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showISSv3Hub))), jade); + get("/manager/admin/hub/peripherals", + withUserPreferences(withCsrfToken(withOrgAdmin(AdminViewsController::showISSv3Peripherals))), jade); get("/manager/admin/hub/peripherals/register", withUserPreferences(withCsrfToken(withProductAdmin(AdminViewsController::registerPeripheral))), jade); get("/manager/admin/hub/access-tokens", @@ -112,6 +122,30 @@ public static ModelAndView showPasswordPolicy(Request request, Response response return new ModelAndView(data, "controllers/admin/templates/password-policy.jade"); } + /** + * Show iss hub tab. + * @param request http request + * @param response http response + * @param user current user + * @return the view to show + */ + public static ModelAndView showISSv3Hub(Request request, Response response, User user) { + Map data = new HashMap<>(); + data.put("hub", GSON.toJson(HUB_FACTORY.lookupIssHub().map(HubDetailsData::new).orElse(null))); + return new ModelAndView(data, "controllers/admin/templates/hub_details.jade"); + } + + /** + * Show iss peripheral tab. + * @param request http request + * @param response http response + * @param user current user + * @return the view to show + */ + public static ModelAndView showISSv3Peripherals(Request request, Response response, User user) { + return new ModelAndView(new HashMap<>(), "controllers/admin/templates/list_peripherals.jade"); + } + /** * show list of saved payg ssh connection data * @param request @@ -120,12 +154,11 @@ public static ModelAndView showPasswordPolicy(Request request, Response response * @return list of payg ssh connection data */ public static ModelAndView listPayg(Request request, Response response, User user) { - HubFactory hubFactory = new HubFactory(); Map data = new HashMap<>(); List payg = PAYG_ADMIN_MANAGER.list(); data.put("flashMessage", FlashScopeHelper.flash(request)); data.put("contentPaygInstances", GSON.toJson(PaygResponseMappers.mapPaygPropertiesResumeFromDB(payg))); - data.put("isIssPeripheral", hubFactory.isISSPeripheral()); + data.put("isIssPeripheral", HUB_FACTORY.isISSPeripheral()); return new ModelAndView(data, "controllers/admin/templates/payg_list.jade"); } diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/beans/HubDetailsData.java b/java/code/src/com/suse/manager/webui/controllers/admin/beans/HubDetailsData.java new file mode 100644 index 000000000000..532f20f37c03 --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/admin/beans/HubDetailsData.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025 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. + */ + +package com.suse.manager.webui.controllers.admin.beans; + +import com.suse.manager.model.hub.IssHub; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.util.Date; + +public class HubDetailsData { + + private long id; + + private String fqdn; + + private String rootCA; + + private String gpgKey; + + private String sccUsername; + + private Date created; + + private Date modified; + + /** + * Create an instance from the hub entity. + * @param hub the hub + */ + public HubDetailsData(IssHub hub) { + this.id = hub.getId(); + this.fqdn = hub.getFqdn(); + this.rootCA = hub.getRootCa(); + this.gpgKey = hub.getGpgKey(); + this.sccUsername = hub.getMirrorCredentials().getUsername(); + this.created = hub.getCreated(); + this.modified = hub.getModified(); + } + + public long getId() { + return id; + } + + public void setId(long idIn) { + this.id = idIn; + } + + public String getFqdn() { + return fqdn; + } + + public void setFqdn(String fqdnIn) { + this.fqdn = fqdnIn; + } + + public String getRootCA() { + return rootCA; + } + + public void setRootCA(String rootCAIn) { + this.rootCA = rootCAIn; + } + + public String getGpgKey() { + return gpgKey; + } + + public void setGpgKey(String gpgKeyIn) { + this.gpgKey = gpgKeyIn; + } + + public String getSccUsername() { + return sccUsername; + } + + public void setSccUsername(String sccUsernameIn) { + this.sccUsername = sccUsernameIn; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date createdIn) { + this.created = createdIn; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modifiedIn) { + this.modified = modifiedIn; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof HubDetailsData that)) { + return false; + } + + return new EqualsBuilder() + .append(getFqdn(), that.getFqdn()) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getFqdn()) + .toHashCode(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("HubDetailsData{"); + sb.append("id=").append(id); + sb.append(", fqdn='").append(fqdn).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/beans/PeripheralResponse.java b/java/code/src/com/suse/manager/webui/controllers/admin/beans/PeripheralResponse.java new file mode 100644 index 000000000000..9d1197543ce5 --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/admin/beans/PeripheralResponse.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 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. + */ +package com.suse.manager.webui.controllers.admin.beans; + +import com.suse.manager.model.hub.IssPeripheral; +import com.suse.manager.model.hub.IssPeripheralChannels; + +public class PeripheralResponse { + private final long id; + private final String fqdn; + private final long nChannelsSync; + private final long nSyncOrgs; + private final String rootCA; + + /** + * Response for peripherals list + * @param idIn the id + * @param fqdnIn the fully qualified domain name + * @param nChannelsSyncIn the number of synced channels + * @param nSyncOrgsIn the number of synced organizations + * @param rootCAIn the root CA certificate, if present + */ + public PeripheralResponse( + long idIn, + String fqdnIn, + long nChannelsSyncIn, + long nSyncOrgsIn, + String rootCAIn + ) { + id = idIn; + fqdn = fqdnIn; + nChannelsSync = nChannelsSyncIn; + nSyncOrgs = nSyncOrgsIn; + rootCA = rootCAIn; + } + + public long getId() { + return id; + } + + public String getFqdn() { + return fqdn; + } + + public long getNChannelsSync() { + return nChannelsSync; + } + + public long getNSyncOrgs() { + return nSyncOrgs; + } + + public String getRootCA() { + return rootCA; + } + + /** + * Helper converter method from db entity + * @param peripheralEntity the entity representing the peripheral + * @return the response to provide to the fronted + */ + public static PeripheralResponse fromIssEntity(IssPeripheral peripheralEntity) { + return new PeripheralResponse( + peripheralEntity.getId(), + peripheralEntity.getFqdn(), + peripheralEntity.getPeripheralChannels().size(), + peripheralEntity.getPeripheralChannels().stream() + .map(IssPeripheralChannels::getPeripheralOrgId).distinct().count(), + peripheralEntity.getRootCa()); + } +} diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/beans/UpdateRootCARequest.java b/java/code/src/com/suse/manager/webui/controllers/admin/beans/UpdateRootCARequest.java new file mode 100644 index 000000000000..8d5e5c4f95fa --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/admin/beans/UpdateRootCARequest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 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. + */ + +package com.suse.manager.webui.controllers.admin.beans; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class UpdateRootCARequest { + + private String rootCA; + + public String getRootCA() { + return rootCA; + } + + public void setRootCA(String rootCAIn) { + this.rootCA = rootCAIn; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof UpdateRootCARequest that)) { + return false; + } + + return new EqualsBuilder() + .append(getRootCA(), that.getRootCA()) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getRootCA()) + .toHashCode(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("UpdateRootCARequest{"); + sb.append("rootCA='").append(rootCA).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/handlers/HubApiController.java b/java/code/src/com/suse/manager/webui/controllers/admin/handlers/HubApiController.java index 6a3240d7d827..e9cefb606fab 100644 --- a/java/code/src/com/suse/manager/webui/controllers/admin/handlers/HubApiController.java +++ b/java/code/src/com/suse/manager/webui/controllers/admin/handlers/HubApiController.java @@ -19,6 +19,7 @@ import static com.suse.manager.webui.utils.SparkApplicationHelper.withProductAdmin; import static spark.Spark.delete; import static spark.Spark.get; +import static spark.Spark.patch; import static spark.Spark.post; import com.redhat.rhn.common.localization.LocalizationService; @@ -36,6 +37,8 @@ import com.suse.manager.webui.controllers.ECMAScriptDateAdapter; import com.suse.manager.webui.controllers.admin.beans.CreateTokenRequest; import com.suse.manager.webui.controllers.admin.beans.HubRegisterRequest; +import com.suse.manager.webui.controllers.admin.beans.PeripheralResponse; +import com.suse.manager.webui.controllers.admin.beans.UpdateRootCARequest; import com.suse.manager.webui.controllers.admin.beans.ValidityRequest; import com.suse.manager.webui.utils.FlashScopeHelper; import com.suse.manager.webui.utils.PageControlHelper; @@ -44,6 +47,7 @@ import com.suse.manager.webui.utils.token.TokenBuildingException; import com.suse.manager.webui.utils.token.TokenException; import com.suse.manager.webui.utils.token.TokenParsingException; +import com.suse.utils.CertificateUtils; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -59,6 +63,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; import javax.net.ssl.SSLException; @@ -96,11 +101,49 @@ public HubApiController(HubManager hubManagerIn) { * initialize all the API Routes for the ISSv3 support */ public void initRoutes() { - post("/manager/api/admin/hub/peripherals", withProductAdmin(this::registerPeripheral)); + // Hub management get("/manager/api/admin/hub/access-tokens", withProductAdmin(this::listTokens)); post("/manager/api/admin/hub/access-tokens", withProductAdmin(this::createToken)); post("/manager/api/admin/hub/access-tokens/:id/validity", withProductAdmin(this::setAccessTokenValidity)); delete("/manager/api/admin/hub/access-tokens/:id", withProductAdmin(this::deleteAccessToken)); + get("/manager/api/admin/hub/peripherals", withProductAdmin(this::listPaginatedPeripherals)); + post("/manager/api/admin/hub/peripherals", withProductAdmin(this::registerPeripheral)); + get("/manager/api/admin/hub/peripherals/:id", withProductAdmin(this::pass)); + patch("/manager/api/admin/hub/peripherals/:id", withProductAdmin(this::pass)); + delete("/manager/api/admin/hub/peripherals/:id", withProductAdmin(this::deletePeripheral)); + delete("/manager/api/admin/hub/:id", withProductAdmin(this::deleteHub)); + post("/manager/api/admin/hub/:id/root-ca", withProductAdmin(this::updateHubRootCA)); + delete("/manager/api/admin/hub/:id/root-ca", withProductAdmin(this::removeHubRootCA)); + } + + private String deleteHub(Request request, Response response, User user) { + return deleteServer(request, response, user, IssRole.HUB); + } + + private String deletePeripheral(Request request, Response response, User user) { + return deleteServer(request, response, user, IssRole.PERIPHERAL); + } + + private String updateHubRootCA(Request request, Response response, User user) { + UpdateRootCARequest updateRequest; + try { + updateRequest = validateUpdateRootCARequest(GSON.fromJson(request.body(), UpdateRootCARequest.class)); + } + catch (CertificateException ex) { + LOGGER.error("Unable to parse the specified certificate", ex); + return badRequest(response, LOC.getMessage("hub.invalid_root_ca")); + } + catch (JsonSyntaxException ex) { + LOGGER.error("Unable to parse JSON request", ex); + return badRequest(response, LOC.getMessage("hub.invalid_request")); + } + + return updateServerRootCA(request, response, user, IssRole.HUB, updateRequest.getRootCA()); + } + + private String removeHubRootCA(Request request, Response response, User user) { + // Perform the update using null as value + return updateServerRootCA(request, response, user, IssRole.HUB, null); } private String registerPeripheral(Request request, Response response, User satAdmin) { @@ -170,6 +213,16 @@ private String registerPeripheral(Request request, Response response, User satAd } } + private String listPaginatedPeripherals(Request request, Response response, User satAdmin) { + PageControlHelper pageHelper = new PageControlHelper(request); + PageControl pc = pageHelper.getPageControl(); + long totalSize = hubManager.countRegisteredPeripherals(satAdmin); + List peripherals = hubManager.listRegisteredPeripherals(satAdmin, pc).stream() + .map(PeripheralResponse::fromIssEntity).toList(); + TypeToken> type = new TypeToken<>() { }; + return json(GSON, response, new PagedDataResultJson<>(peripherals, totalSize, Collections.emptySet()), type); + } + private String listTokens(Request request, Response response, User user) { PageControlHelper pageHelper = new PageControlHelper(request); PageControl pc = pageHelper.getPageControl(); @@ -253,6 +306,38 @@ private String deleteAccessToken(Request request, Response response, User user) return success(response); } + private String deleteServer(Request request, Response response, User user, IssRole issRole) { + long serverId = Long.parseLong(request.params("id")); + IssServer server = hubManager.findServer(user, serverId, issRole); + if (server == null) { + return badRequest(response, LOC.getMessage("hub.cannot_find_server")); + } + + try { + hubManager.deregister(user, server.getFqdn(), false); + } + catch (IOException | CertificateException ex) { + LOGGER.error("Unable to register: error to connect with the remote server {}", server.getFqdn(), ex); + internalServerError(response, LOC.getMessage("hub.unable_to_deregister")); + } + + return success(response); + } + + private String updateServerRootCA(Request request, Response response, User user, IssRole role, String rootCA) { + long serverId = Long.parseLong(request.params("id")); + IssServer server = hubManager.findServer(user, serverId, role); + if (server == null) { + return badRequest(response, LOC.getMessage("hub.cannot_find_server")); + } + + // Collections.singletonMap() is used in place of Map.of() because it allows null as value + Map dataMap = Collections.singletonMap("root_ca", rootCA); + hubManager.updateServerData(user, server.getFqdn(), role, dataMap); + + return success(response); + } + private static HubRegisterRequest validateRegisterRequest(HubRegisterRequest parsedRequest) { if (StringUtils.isEmpty(parsedRequest.getFqdn())) { throw new JsonSyntaxException("Missing required server FQDN in the request"); @@ -289,4 +374,19 @@ private static CreateTokenRequest validateCreationRequest(CreateTokenRequest req return request; } + + private UpdateRootCARequest validateUpdateRootCARequest(UpdateRootCARequest request) throws CertificateException { + if (request == null) { + throw new JsonSyntaxException("Request is empty"); + } + + CertificateUtils.parse(request.getRootCA()) + .orElseThrow(() -> new JsonSyntaxException("rootCA is empty")); + + return request; + } + + private String pass(Request request, Response response, User user) { + return success(response, ResultJson.success(request.requestMethod() + ": " + request.uri())); + } } diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/templates/hub_details.jade b/java/code/src/com/suse/manager/webui/controllers/admin/templates/hub_details.jade new file mode 100644 index 000000000000..e4a84c28c76f --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/admin/templates/hub_details.jade @@ -0,0 +1,12 @@ +include /templates/common.jade + +#hub-details + +script(type='text/javascript'). + window.csrfToken = "#{csrf_token}"; + +script(type='text/javascript'). + spaImportReactPage('admin/hub/hub-details') + .then(function (module) { + module.renderer("hub-details", !{hub}); + }); diff --git a/java/code/src/com/suse/manager/webui/controllers/admin/templates/list_peripherals.jade b/java/code/src/com/suse/manager/webui/controllers/admin/templates/list_peripherals.jade new file mode 100644 index 000000000000..21022dcfb858 --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/admin/templates/list_peripherals.jade @@ -0,0 +1,14 @@ +include /templates/common.jade + +#list-peripherals + +script(type='text/javascript'). + window.csrfToken = "#{csrf_token}"; + +script(type='text/javascript'). + spaImportReactPage('admin/hub/peripherals') + .then(function (module) { + module.renderer( + 'list-peripherals', !{peripherals} + ) + }); diff --git a/java/code/src/com/suse/manager/webui/menu/MenuTree.java b/java/code/src/com/suse/manager/webui/menu/MenuTree.java index 64b2b070f239..00b5c5591fc6 100644 --- a/java/code/src/com/suse/manager/webui/menu/MenuTree.java +++ b/java/code/src/com/suse/manager/webui/menu/MenuTree.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 SUSE LLC + * Copyright (c) 2017--2025 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 @@ -7,10 +7,6 @@ * 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.menu; @@ -452,7 +448,20 @@ private MenuItem getAdminNode(Map adminRoles) { .addChild(new MenuItem("Master Setup").withPrimaryUrl("/rhn/admin/iss/Master.do") .withVisibility(adminRoles.get("satellite"))) .addChild(new MenuItem("Slave Setup").withPrimaryUrl("/rhn/admin/iss/Slave.do") - .withVisibility(adminRoles.get("satellite")))) + .withVisibility(adminRoles.get("satellite"))) + ) + .addChild(new MenuItem("Hub Configuration") + .withVisibility(adminRoles.get("satellite")) + .withPrimaryUrl("/rhn/manager/admin/hub/peripherals") + .addChild(new MenuItem("Peripherals Configuration") + .withPrimaryUrl("/rhn/manager/admin/hub/peripherals") + .withVisibility(adminRoles.get("satellite"))) + .addChild(new MenuItem("Hub Details").withPrimaryUrl("/rhn/manager/admin/hub/hub-details") + .withVisibility(adminRoles.get("satellite"))) + .addChild(new MenuItem("Access Tokens") + .withPrimaryUrl("/rhn/manager/admin/hub/access-tokens") + .withVisibility(adminRoles.get("satellite"))) + ) .addChild(new MenuItem("Task Schedules") .withPrimaryUrl("/rhn/admin/SatSchedules.do") .withAltUrl("/rhn/admin/BunchDetail.do") @@ -553,6 +562,16 @@ public boolean checkAcl(User user, String aclMixin) { return acl.evalAcl(aclContext, aclMixin); } + /** + * Evaluate acl conditions for the current {@link User} + * + * @param aclMixin acls to evaluate + * @return the acl evaluated result + */ + public boolean checkAcl(String aclMixin) { + Acl acl = aclFactory.getAcl(Access.class.getName()); + return acl.evalAcl(new HashMap<>(), aclMixin); + } /** * Decode which is the active {@link MenuItem} from the current URL * based on the list of urls of the link diff --git a/java/code/src/com/suse/manager/xmlrpc/iss/HubHandler.java b/java/code/src/com/suse/manager/xmlrpc/iss/HubHandler.java index de7cbdaf681d..6a4a5d788ab0 100644 --- a/java/code/src/com/suse/manager/xmlrpc/iss/HubHandler.java +++ b/java/code/src/com/suse/manager/xmlrpc/iss/HubHandler.java @@ -15,6 +15,7 @@ import com.redhat.rhn.frontend.xmlrpc.BaseHandler; import com.redhat.rhn.frontend.xmlrpc.InvalidParameterException; import com.redhat.rhn.frontend.xmlrpc.InvalidTokenException; +import com.redhat.rhn.frontend.xmlrpc.ServerInvocationException; import com.redhat.rhn.frontend.xmlrpc.TokenAlreadyExistsException; import com.redhat.rhn.frontend.xmlrpc.TokenCreationException; import com.redhat.rhn.frontend.xmlrpc.TokenExchangeFailedException; @@ -334,6 +335,25 @@ public int registerPeripheralWithToken(User loggedInUser, String fqdn, String to * @apidoc.returntype #return_int_success() */ public int deregister(User loggedInUser, String fqdn) { + return deregister(loggedInUser, fqdn, true); + } + + /** + * De-register the server identified by the fqdn. + * @param loggedInUser the user + * @param fqdn the FQDN of the server to de-register + * @param onlyLocal true if the de-registration has to be performed only this server, false to instead fully + * deregister on both sides + * @return 1 on success, exception otherwise + * + * @apidoc.doc De-register the server identified by the fqdn. + * @apidoc.param #session_key() + * @apidoc.param #param_desc("string", "fqdn", "the FQDN of the remote server to de-register") + * @apidoc.param #param_desc("boolean", "onlyLocal", " true if the de - registration has to be performed only this + * server, false to instead fully deregister on both sides") + * @apidoc.returntype #return_int_success() + */ + public int deregister(User loggedInUser, String fqdn, boolean onlyLocal) { ensureSatAdmin(loggedInUser); if (StringUtils.isEmpty(fqdn)) { @@ -341,15 +361,18 @@ public int deregister(User loggedInUser, String fqdn) { } try { - hubManager.deleteIssServerLocal(loggedInUser, fqdn); + hubManager.deregister(loggedInUser, fqdn, onlyLocal); } - catch (Exception ex) { + catch (CertificateException ex) { LOGGER.error("De-registration failed for {} ", fqdn, ex); - throw ex; + throw new InvalidCertificateException(ex); } + catch (IOException ex) { + throw new ServerInvocationException(fqdn, ex); + } + return 1; } - /** * Set server details * diff --git a/web/html/src/components/buttons.tsx b/web/html/src/components/buttons.tsx index dfafd95e077e..45ffab174720 100644 --- a/web/html/src/components/buttons.tsx +++ b/web/html/src/components/buttons.tsx @@ -182,6 +182,9 @@ type LinkProps = BaseProps & { /** target of the link */ target?: string; + /** to treat the link URL as a download */ + download?: string; + /** Callback function to execute on button click. */ handler?: (...args: any[]) => any; }; @@ -208,6 +211,7 @@ export class LinkButton extends _ButtonBase { className={"btn " + this.props.className} href={this.props.href} onClick={this.props.handler} + download={this.props.download} {...targetProps} > {this.renderIcon()} diff --git a/web/html/src/components/hub/DeregisterServer.tsx b/web/html/src/components/hub/DeregisterServer.tsx new file mode 100644 index 000000000000..1476c4d2afa2 --- /dev/null +++ b/web/html/src/components/hub/DeregisterServer.tsx @@ -0,0 +1,95 @@ +import * as React from "react"; + +import { Button } from "components/buttons"; +import { DangerDialog } from "components/dialog/DangerDialog"; +import { IssRole } from "components/hub/types"; +import { showInfoToastr } from "components/toastr"; + +import Network from "utils/network"; + +type Props = { + /** Specify if the deregistration is for a hub or a peripheral */ + role: IssRole; + /** The unique identifier of the server */ + id: number; + /** The fully qualified domain name of the server */ + fqdn: string; + /** Callback invoked after the deregistration as been performed */ + onDeregistered?: () => void; +}; + +type State = { + confirmDeregistration: boolean; +}; + +export class DeregisterServer extends React.Component { + public constructor(props: Props) { + super(props); + + this.state = { confirmDeregistration: false }; + } + + public render(): React.ReactNode { + return ( + <> +