From b9d1d4057ca9f2a62cb9979fc8475f40167e18fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Wed, 6 Sep 2023 05:24:34 +0200 Subject: [PATCH] [SYNCOPE-1764] Managing connectors' Spring beans (un)registration in case HA is set up (#512) --- .../resources/ConnectorDetailsPanel.java | 6 +- .../syncope/core/logic/ConnectorLogic.java | 21 +-- .../syncope/core/logic/ResourceLogic.java | 74 ++++----- .../api/dao/ExternalResourceDAO.java | 2 + .../persistence/jpa/DomainConfFactory.java | 7 +- .../core/persistence/jpa/MasterDomain.java | 3 + .../jpa/dao/JPAExternalResourceDAO.java | 12 ++ .../jpa/entity/JPAConnInstance.java | 5 +- .../ConnectorManagerRemoteCommitListener.java | 151 ++++++++++++++++++ .../DomainEntityManagerFactoryBean.java | 21 +++ .../jpa/DummyConnectorManager.java | 5 +- .../persistence/jpa/inner/ResourceTest.java | 7 + .../provisioning/api/ConnectorManager.java | 4 +- .../java/DefaultConnectorManager.java | 11 +- pom.xml | 2 +- src/main/asciidoc/getting-started/obtain.adoc | 2 +- .../reference-guide/architecture/core.adoc | 2 +- .../configuration/highavailability.adoc | 8 +- .../reference-guide/usage/customization.adoc | 2 +- 19 files changed, 262 insertions(+), 83 deletions(-) create mode 100644 core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/openjpa/ConnectorManagerRemoteCommitListener.java diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java index de283c09c0..8feec773a7 100644 --- a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java +++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java @@ -90,7 +90,7 @@ protected Iterator getChoices(final String input) { "bundleName", "bundleName", new PropertyModel<>(connInstanceTO, "bundleName"), false); - bundleName.setEnabled(connInstanceTO.getKey() == null); + bundleName.setEnabled(connInstanceTO.getKey() == null || connInstanceTO.isErrored()); bundleName.setChoices(bundles.stream().map(ConnIdBundle::getBundleName). distinct().sorted().collect(Collectors.toList())); bundleName.getField().setOutputMarkupId(true); @@ -100,14 +100,14 @@ protected Iterator getChoices(final String input) { "connectorName", "connectorName", new PropertyModel<>(connInstanceTO, "connectorName"), false); - connectorName.setEnabled(connInstanceTO.getBundleName() == null); + connectorName.setEnabled(connInstanceTO.getBundleName() == null || connInstanceTO.isErrored()); Optional.ofNullable(connInstanceTO.getConnectorName()).ifPresent(v -> connectorName.setChoices(List.of(v))); connectorName.getField().setOutputMarkupId(true); add(connectorName.addRequiredLabel().setOutputMarkupId(true)); AjaxDropDownChoicePanel version = new AjaxDropDownChoicePanel<>( "version", "version", new PropertyModel<>(connInstanceTO, "version"), false); - version.setEnabled(connInstanceTO.getConnectorName() == null); + version.setEnabled(connInstanceTO.getConnectorName() == null || connInstanceTO.isErrored()); Optional.ofNullable(connInstanceTO.getVersion()).ifPresent(v -> version.setChoices(List.of(v))); version.getField().setOutputMarkupId(true); add(version.addRequiredLabel().setOutputMarkupId(true)); diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ConnectorLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ConnectorLogic.java index 03c22af783..415b404401 100644 --- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ConnectorLogic.java +++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ConnectorLogic.java @@ -82,8 +82,7 @@ public ConnectorLogic( } protected void securityChecks(final Set effectiveRealms, final String realm, final String key) { - boolean authorized = effectiveRealms.stream().anyMatch(realm::startsWith); - if (!authorized) { + if (!effectiveRealms.stream().anyMatch(realm::startsWith)) { throw new DelegatedAdministrationException(realm, ConnInstance.class.getSimpleName(), key); } } @@ -132,10 +131,8 @@ public ConnInstanceTO update(final ConnInstanceTO connInstanceTO) { @PreAuthorize("hasRole('" + IdMEntitlement.CONNECTOR_DELETE + "')") public ConnInstanceTO delete(final String key) { - ConnInstance connInstance = connInstanceDAO.authFind(key); - if (connInstance == null) { - throw new NotFoundException("Connector '" + key + '\''); - } + ConnInstance connInstance = Optional.ofNullable(connInstanceDAO.authFind(key)). + orElseThrow(() -> new NotFoundException("Connector '" + key + '\'')); Set effectiveRealms = RealmUtils.getEffective( AuthContextUtils.getAuthorizations().get(IdMEntitlement.CONNECTOR_DELETE), @@ -151,7 +148,6 @@ public ConnInstanceTO delete(final String key) { ConnInstanceTO deleted = binder.getConnInstanceTO(connInstance); connInstanceDAO.delete(key); - connectorManager.unregisterConnector(key); return deleted; } @@ -210,7 +206,6 @@ public List getBundles(final String lang) { } @PreAuthorize("hasRole('" + IdMEntitlement.CONNECTOR_READ + "')") - public List buildObjectClassInfo( final ConnInstanceTO connInstanceTO, final boolean includeSpecial) { @@ -261,12 +256,10 @@ public void check(final ConnInstanceTO connInstanceTO) { public ConnInstanceTO readByResource(final String resourceName, final String lang) { CurrentLocale.set(StringUtils.isBlank(lang) ? Locale.ENGLISH : new Locale(lang)); - ExternalResource resource = resourceDAO.find(resourceName); - if (resource == null) { - throw new NotFoundException("Resource '" + resourceName + '\''); - } - ConnInstanceTO connInstance = binder. - getConnInstanceTO(connectorManager.getConnector(resource).getConnInstance()); + ExternalResource resource = Optional.ofNullable(resourceDAO.find(resourceName)). + orElseThrow(() -> new NotFoundException("Resource '" + resourceName + '\'')); + ConnInstanceTO connInstance = binder.getConnInstanceTO( + connectorManager.getConnector(resource).getConnInstance()); connInstance.setKey(resource.getConnector().getKey()); return connInstance; } diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java index 1fd59b9b63..68ed12ff3f 100644 --- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java +++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java @@ -167,10 +167,8 @@ public ResourceTO create(final ResourceTO resourceTO) { @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_UPDATE + "')") public ResourceTO update(final ResourceTO resourceTO) { - ExternalResource resource = resourceDAO.authFind(resourceTO.getKey()); - if (resource == null) { - throw new NotFoundException("Resource '" + resourceTO.getKey() + '\''); - } + ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(resourceTO.getKey())). + orElseThrow(() -> new NotFoundException("Resource '" + resourceTO.getKey() + '\'')); Set effectiveRealms = RealmUtils.getEffective( AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_UPDATE), @@ -202,10 +200,8 @@ public void setLatestSyncToken(final String key, final String anyTypeKey) { resource.getOrgUnit().setSyncToken(ConnObjectUtils.toString( connector.getLatestSyncToken(new ObjectClass(resource.getOrgUnit().getObjectClass())))); } else { - AnyType anyType = anyTypeDAO.find(anyTypeKey); - if (anyType == null) { - throw new NotFoundException("AnyType '" + anyTypeKey + '\''); - } + AnyType anyType = Optional.ofNullable(anyTypeDAO.find(anyTypeKey)). + orElseThrow(() -> new NotFoundException("AnyType '" + anyTypeKey + '\'')); Provision provision = resource.getProvisionByAnyType(anyType.getKey()). orElseThrow(() -> new NotFoundException( "Provision for AnyType '" + anyTypeKey + "' in Resource '" + key + '\'')); @@ -224,10 +220,8 @@ public void setLatestSyncToken(final String key, final String anyTypeKey) { @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_UPDATE + "')") public void removeSyncToken(final String key, final String anyTypeKey) { - ExternalResource resource = resourceDAO.authFind(key); - if (resource == null) { - throw new NotFoundException("Resource '" + key + '\''); - } + ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)). + orElseThrow(() -> new NotFoundException("Resource '" + key + '\'')); if (SyncopeConstants.REALM_ANYTYPE.equals(anyTypeKey)) { if (resource.getOrgUnit() == null) { throw new NotFoundException("Realm provision not enabled for Resource '" + key + '\''); @@ -235,10 +229,8 @@ public void removeSyncToken(final String key, final String anyTypeKey) { resource.getOrgUnit().setSyncToken(null); } else { - AnyType anyType = anyTypeDAO.find(anyTypeKey); - if (anyType == null) { - throw new NotFoundException("AnyType '" + anyTypeKey + '\''); - } + AnyType anyType = Optional.ofNullable(anyTypeDAO.find(anyTypeKey)). + orElseThrow(() -> new NotFoundException("AnyType '" + anyTypeKey + '\'')); Provision provision = resource.getProvisionByAnyType(anyType.getKey()). orElseThrow(() -> new NotFoundException( "Provision for AnyType '" + anyTypeKey + "' in Resource '" + key + '\'')); @@ -256,30 +248,26 @@ public void removeSyncToken(final String key, final String anyTypeKey) { @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_DELETE + "')") public ResourceTO delete(final String key) { - ExternalResource resource = resourceDAO.authFind(key); - if (resource == null) { - throw new NotFoundException("Resource '" + key + '\''); - } + ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)). + orElseThrow(() -> new NotFoundException("Resource '" + key + '\'')); Set effectiveRealms = RealmUtils.getEffective( AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_DELETE), resource.getConnector().getAdminRealm().getFullPath()); securityChecks(effectiveRealms, resource.getConnector().getAdminRealm().getFullPath(), resource.getKey()); - ResourceTO resourceToDelete = binder.getResourceTO(resource); + connectorManager.unregisterConnector(resource); + ResourceTO deleted = binder.getResourceTO(resource); resourceDAO.delete(key); - - return resourceToDelete; + return deleted; } @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_READ + "')") @Transactional(readOnly = true) public ResourceTO read(final String key) { - ExternalResource resource = resourceDAO.authFind(key); - if (resource == null) { - throw new NotFoundException("Resource '" + key + '\''); - } + ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)). + orElseThrow(() -> new NotFoundException("Resource '" + key + '\'')); return binder.getResourceTO(resource); } @@ -293,15 +281,11 @@ public List list() { protected Triple getProvision( final String anyTypeKey, final String resourceKey) { - AnyType anyType = anyTypeDAO.find(anyTypeKey); - if (anyType == null) { - throw new NotFoundException("AnyType '" + anyTypeKey + "'"); - } + AnyType anyType = Optional.ofNullable(anyTypeDAO.find(anyTypeKey)). + orElseThrow(() -> new NotFoundException("AnyType '" + anyTypeKey + '\'')); - ExternalResource resource = resourceDAO.find(resourceKey); - if (resource == null) { - throw new NotFoundException("Resource '" + resourceKey + "'"); - } + ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(resourceKey)). + orElseThrow(() -> new NotFoundException("Resource '" + resourceKey + '\'')); Provision provision = resource.getProvisionByAnyType(anyType.getKey()). orElseThrow(() -> new NotFoundException( "Provision for " + anyType + " on Resource '" + resourceKey + "'")); @@ -322,10 +306,9 @@ public String getConnObjectKeyValue( Triple triple = getProvision(anyTypeKey, key); // 1. find any - Any any = anyUtilsFactory.getInstance(triple.getLeft().getKind()).dao().authFind(anyKey); - if (any == null) { - throw new NotFoundException(triple.getLeft() + " " + anyKey); - } + Any any = Optional.ofNullable(anyUtilsFactory.getInstance(triple.getLeft().getKind()). + dao().authFind(anyKey)). + orElseThrow(() -> new NotFoundException(triple.getLeft() + " " + anyKey)); // 2.get ConnObjectKey value return mappingManager.getConnObjectKeyValue(any, triple.getMiddle(), triple.getRight()). @@ -343,10 +326,9 @@ public ConnObject readConnObjectByAnyKey( Triple triple = getProvision(anyTypeKey, key); // 1. find any - Any any = anyUtilsFactory.getInstance(triple.getLeft().getKind()).dao().authFind(anyKey); - if (any == null) { - throw new NotFoundException(triple.getLeft() + " " + anyKey); - } + Any any = Optional.ofNullable(anyUtilsFactory.getInstance(triple.getLeft().getKind()). + dao().authFind(anyKey)). + orElseThrow(() -> new NotFoundException(triple.getLeft() + " " + anyKey)); // 2. find on resource List connObjs = outboundMatcher.match( @@ -473,10 +455,8 @@ public void handleResult(final SearchResult sr) { @PreAuthorize("hasRole('" + IdMEntitlement.CONNECTOR_READ + "')") @Transactional(readOnly = true) public void check(final ResourceTO resourceTO) { - ConnInstance connInstance = connInstanceDAO.find(resourceTO.getConnector()); - if (connInstance == null) { - throw new NotFoundException("Connector '" + resourceTO.getConnector() + '\''); - } + ConnInstance connInstance = Optional.ofNullable(connInstanceDAO.find(resourceTO.getConnector())). + orElseThrow(() -> new NotFoundException("Connector '" + resourceTO.getConnector() + '\'')); connectorManager.createConnector( connectorManager.buildConnInstanceOverride( diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ExternalResourceDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ExternalResourceDAO.java index bc3ec6e880..dcf59bd798 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ExternalResourceDAO.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ExternalResourceDAO.java @@ -37,6 +37,8 @@ public interface ExternalResourceDAO extends DAO { boolean anyItemHaving(Implementation transformer); + List findByConnInstance(String connInstance); + List findByProvisionSorter(Implementation provisionSorter); List findByPropagationActions(Implementation propagationActions); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/DomainConfFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/DomainConfFactory.java index b5069b798a..0941445343 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/DomainConfFactory.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/DomainConfFactory.java @@ -28,6 +28,7 @@ import javax.sql.DataSource; import org.apache.syncope.common.keymaster.client.api.model.Domain; import org.apache.syncope.core.persistence.api.DomainRegistry; +import org.apache.syncope.core.persistence.jpa.openjpa.ConnectorManagerRemoteCommitListener; import org.apache.syncope.core.persistence.jpa.spring.DomainEntityManagerFactoryBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -124,12 +125,16 @@ public void register(final Domain domain) { vendorAdapter.setGenerateDdl(true); vendorAdapter.setDatabasePlatform(domain.getDatabasePlatform()); + ConnectorManagerRemoteCommitListener connectorManagerRemoteCommitListener = + new ConnectorManagerRemoteCommitListener(domain.getKey()); + BeanDefinitionBuilder emf = BeanDefinitionBuilder.rootBeanDefinition(DomainEntityManagerFactoryBean.class). addPropertyValue("mappingResources", domain.getOrm()). addPropertyValue("persistenceUnitName", domain.getKey()). addPropertyReference("dataSource", domain.getKey() + "DataSource"). addPropertyValue("jpaVendorAdapter", vendorAdapter). - addPropertyReference("commonEntityManagerFactoryConf", "commonEMFConf"); + addPropertyReference("commonEntityManagerFactoryConf", "commonEMFConf"). + addPropertyValue("connectorManagerRemoteCommitListener", connectorManagerRemoteCommitListener); if (ctx.getEnvironment().containsProperty("openjpaMetaDataFactory")) { emf.addPropertyValue("jpaPropertyMap", Map.of( "openjpa.MetaDataFactory", diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MasterDomain.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MasterDomain.java index 5b05547240..eef3e7635e 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MasterDomain.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MasterDomain.java @@ -26,6 +26,7 @@ import java.util.Objects; import javax.sql.DataSource; import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.core.persistence.jpa.openjpa.ConnectorManagerRemoteCommitListener; import org.apache.syncope.core.persistence.jpa.spring.CommonEntityManagerFactoryConf; import org.apache.syncope.core.persistence.jpa.spring.DomainEntityManagerFactoryBean; import org.springframework.beans.factory.annotation.Qualifier; @@ -106,6 +107,8 @@ public DomainEntityManagerFactoryBean masterEntityManagerFactory( masterEntityManagerFactory.setDataSource(Objects.requireNonNull((DataSource) masterDataSource.getObject())); masterEntityManagerFactory.setJpaVendorAdapter(vendorAdapter); masterEntityManagerFactory.setCommonEntityManagerFactoryConf(commonEMFConf); + masterEntityManagerFactory.setConnectorManagerRemoteCommitListener( + new ConnectorManagerRemoteCommitListener(SyncopeConstants.MASTER_DOMAIN)); if (props.getMetaDataFactory() != null) { masterEntityManagerFactory.setJpaPropertyMap(Map.of( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAExternalResourceDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAExternalResourceDAO.java index 2af367a5a5..8b55e9caf0 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAExternalResourceDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAExternalResourceDAO.java @@ -92,6 +92,7 @@ public int count() { return ((Number) query.getSingleResult()).intValue(); } + @Transactional(readOnly = true) @Override public ExternalResource find(final String name) { return entityManager().find(JPAExternalResource.class, name); @@ -135,6 +136,17 @@ public boolean anyItemHaving(final Implementation transformer) { count() > 0; } + @Transactional(readOnly = true) + @Override + public List findByConnInstance(final String connInstance) { + TypedQuery query = entityManager().createQuery( + "SELECT e FROM " + JPAExternalResource.class.getSimpleName() + " e " + + "WHERE e.connector.id=:connInstance", ExternalResource.class); + query.setParameter("connInstance", connInstance); + + return query.getResultList(); + } + @Override public List findByPropagationActions(final Implementation propagationActions) { TypedQuery query = entityManager().createQuery( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java index 5307b8616e..195c55d497 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java @@ -206,14 +206,13 @@ public void setDisplayName(final String displayName) { @Override public List getResources() { - return this.resources; + return resources; } @Override public boolean add(final ExternalResource resource) { checkType(resource, JPAExternalResource.class); - return this.resources.contains((JPAExternalResource) resource) - || this.resources.add((JPAExternalResource) resource); + return resources.contains((JPAExternalResource) resource) || resources.add((JPAExternalResource) resource); } @Override diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/openjpa/ConnectorManagerRemoteCommitListener.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/openjpa/ConnectorManagerRemoteCommitListener.java new file mode 100644 index 0000000000..96c9016fb7 --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/openjpa/ConnectorManagerRemoteCommitListener.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.persistence.jpa.openjpa; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import org.apache.openjpa.event.RemoteCommitEvent; +import org.apache.openjpa.event.RemoteCommitListener; +import org.apache.openjpa.util.StringId; +import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; +import org.apache.syncope.core.persistence.api.entity.ExternalResource; +import org.apache.syncope.core.persistence.jpa.entity.JPAConnInstance; +import org.apache.syncope.core.persistence.jpa.entity.JPAExternalResource; +import org.apache.syncope.core.provisioning.api.ConnectorManager; +import org.apache.syncope.core.spring.ApplicationContextProvider; +import org.apache.syncope.core.spring.security.AuthContextUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Takes care of connectors' Spring beans (un)registration in case HA is set up and the actual change is performed by + * another node in the OpenJPA cluster. + */ +public class ConnectorManagerRemoteCommitListener implements RemoteCommitListener, Serializable { + + private static final long serialVersionUID = 5260753255454140460L; + + protected static final Logger LOG = LoggerFactory.getLogger(ConnectorManagerRemoteCommitListener.class); + + protected final String domain; + + public ConnectorManagerRemoteCommitListener(final String domain) { + this.domain = domain; + } + + protected void registerForExternalResource(final String resourceKey) { + AuthContextUtils.callAsAdmin(domain, () -> { + ExternalResource resource = ApplicationContextProvider.getApplicationContext(). + getBean(ExternalResourceDAO.class).find(resourceKey); + if (resource == null) { + LOG.debug("No resource found for '{}', ignoring", resourceKey); + } + + try { + ApplicationContextProvider.getApplicationContext(). + getBean(ConnectorManager.class).registerConnector(resource); + } catch (Exception e) { + LOG.error("While registering connector for resource {}", resourceKey, e); + } + + return null; + }); + } + + protected void registerForConnInstance(final String connInstanceKey) { + AuthContextUtils.callAsAdmin(domain, () -> { + List resources = ApplicationContextProvider.getApplicationContext(). + getBean(ExternalResourceDAO.class).findByConnInstance(connInstanceKey); + if (resources.isEmpty()) { + LOG.debug("No resources found for connInstance '{}', ignoring", connInstanceKey); + } + + resources.forEach(resource -> { + try { + ApplicationContextProvider.getApplicationContext(). + getBean(ConnectorManager.class).registerConnector(resource); + } catch (Exception e) { + LOG.error("While registering connector {} for resource {}", connInstanceKey, resource, e); + } + }); + + return null; + }); + } + + protected void unregister(final String resourceKey) { + AuthContextUtils.callAsAdmin(domain, () -> { + ExternalResource resource = ApplicationContextProvider.getApplicationContext(). + getBean(ExternalResourceDAO.class).find(resourceKey); + if (resource == null) { + LOG.debug("No resource found for '{}', ignoring", resourceKey); + } + + try { + ApplicationContextProvider.getApplicationContext(). + getBean(ConnectorManager.class).unregisterConnector(resource); + } catch (Exception e) { + LOG.error("While unregistering connector for resource {}", resourceKey, e); + } + + return null; + }); + } + + @SuppressWarnings("unchecked") + @Override + public void afterCommit(final RemoteCommitEvent event) { + ((Collection) event.getPersistedObjectIds()).stream(). + filter(StringId.class::isInstance). + map(StringId.class::cast). + forEach(id -> { + if (JPAExternalResource.class.isAssignableFrom(id.getType())) { + registerForExternalResource(id.getId()); + } else if (JPAConnInstance.class.isAssignableFrom(id.getType())) { + registerForConnInstance(id.getId()); + } + }); + + ((Collection) event.getUpdatedObjectIds()).stream(). + filter(StringId.class::isInstance). + map(StringId.class::cast). + forEach(id -> { + if (JPAExternalResource.class.isAssignableFrom(id.getType())) { + registerForExternalResource(id.getId()); + } else if (JPAConnInstance.class.isAssignableFrom(id.getType())) { + registerForConnInstance(id.getId()); + } + }); + + ((Collection) event.getDeletedObjectIds()).stream(). + filter(StringId.class::isInstance). + map(StringId.class::cast). + forEach(id -> { + if (JPAExternalResource.class.isAssignableFrom(id.getType())) { + unregister(id.getId()); + } + }); + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainEntityManagerFactoryBean.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainEntityManagerFactoryBean.java index dc9623a7a6..a016a2a435 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainEntityManagerFactoryBean.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainEntityManagerFactoryBean.java @@ -18,6 +18,11 @@ */ package org.apache.syncope.core.persistence.jpa.spring; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.spi.PersistenceUnitInfo; +import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; +import org.apache.openjpa.persistence.OpenJPAPersistence; +import org.apache.syncope.core.persistence.jpa.openjpa.ConnectorManagerRemoteCommitListener; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; /** @@ -28,6 +33,8 @@ public class DomainEntityManagerFactoryBean extends LocalContainerEntityManagerF private static final long serialVersionUID = 49152547930966545L; + protected ConnectorManagerRemoteCommitListener connectorManagerRemoteCommitListener; + public void setCommonEntityManagerFactoryConf(final CommonEntityManagerFactoryConf commonEMFConf) { super.setJpaPropertyMap(commonEMFConf.getJpaPropertyMap()); @@ -43,4 +50,18 @@ public void setCommonEntityManagerFactoryConf(final CommonEntityManagerFactoryCo commonEMFConf.getDomains().put(this.getPersistenceUnitName(), this.getDataSource()); } + + public void setConnectorManagerRemoteCommitListener( + final ConnectorManagerRemoteCommitListener connectorManagerRemoteCommitListener) { + + this.connectorManagerRemoteCommitListener = connectorManagerRemoteCommitListener; + } + + @Override + protected void postProcessEntityManagerFactory(final EntityManagerFactory emf, final PersistenceUnitInfo pui) { + super.postProcessEntityManagerFactory(emf, pui); + + OpenJPAEntityManagerFactorySPI emfspi = (OpenJPAEntityManagerFactorySPI) OpenJPAPersistence.cast(emf); + emfspi.getConfiguration().getRemoteCommitEventManager().addListener(connectorManagerRemoteCommitListener); + } } diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/DummyConnectorManager.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/DummyConnectorManager.java index 7e5442518a..974333e2ac 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/DummyConnectorManager.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/DummyConnectorManager.java @@ -23,7 +23,6 @@ import org.apache.syncope.common.lib.to.ConnInstanceTO; import org.apache.syncope.common.lib.types.ConnConfProperty; import org.apache.syncope.common.lib.types.ConnectorCapability; -import org.apache.syncope.core.persistence.api.dao.NotFoundException; import org.apache.syncope.core.persistence.api.entity.ConnInstance; import org.apache.syncope.core.persistence.api.entity.ExternalResource; import org.apache.syncope.core.provisioning.api.Connector; @@ -32,11 +31,11 @@ public class DummyConnectorManager implements ConnectorManager { @Override - public void registerConnector(final ExternalResource resource) throws NotFoundException { + public void registerConnector(final ExternalResource resource) { } @Override - public void unregisterConnector(final String id) { + public void unregisterConnector(final ExternalResource resource) { } @Override diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ResourceTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ResourceTest.java index 0cd2b02fdb..0177040968 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ResourceTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ResourceTest.java @@ -82,6 +82,13 @@ public void findById() { } } + @Test + public void findByConnInstance() { + List resources = resourceDAO.findByConnInstance("88a7a819-dab5-46b4-9b90-0b9769eabdb8"); + assertEquals(6, resources.size()); + assertTrue(resources.contains(resourceDAO.find("ws-target-resource-1"))); + } + @Test public void findWithOrgUnit() { ExternalResource resource = resourceDAO.find("resource-ldap-orgunit"); diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/ConnectorManager.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/ConnectorManager.java index 080bb41afe..66b2a6eb08 100644 --- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/ConnectorManager.java +++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/ConnectorManager.java @@ -92,9 +92,9 @@ ConnInstance buildConnInstanceOverride( void registerConnector(ExternalResource resource); /** - * Removes the Spring bean for the given id from the context. + * Removes the Spring bean for the given resource from the context. * * @param id Spring bean id */ - void unregisterConnector(String id); + void unregisterConnector(ExternalResource resource); } diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultConnectorManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultConnectorManager.java index e1560cbb1a..1135a37708 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultConnectorManager.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultConnectorManager.java @@ -177,11 +177,18 @@ public void registerConnector(final ExternalResource resource) { LOG.debug("Successfully registered bean {}", beanName); } - @Override - public void unregisterConnector(final String id) { + protected void unregisterConnector(final String id) { ApplicationContextProvider.getBeanFactory().destroySingleton(id); } + @Override + public void unregisterConnector(final ExternalResource resource) { + String beanName = getBeanName(resource); + if (ApplicationContextProvider.getBeanFactory().containsSingleton(beanName)) { + unregisterConnector(beanName); + } + } + @Transactional(readOnly = true) @Override public void load() { diff --git a/pom.xml b/pom.xml index 125ce89845..bfadba0112 100644 --- a/pom.xml +++ b/pom.xml @@ -457,7 +457,7 @@ under the License. 7.0.0 4.0.0-M1 - 4.13.0 + 4.13.1 5.5.0 3.8.1 diff --git a/src/main/asciidoc/getting-started/obtain.adoc b/src/main/asciidoc/getting-started/obtain.adoc index 2648bde4fd..a941fb2fd0 100644 --- a/src/main/asciidoc/getting-started/obtain.adoc +++ b/src/main/asciidoc/getting-started/obtain.adoc @@ -137,7 +137,7 @@ Environment variables: * `DB_POOL_MIN`: internal storage connection pool: floor * `OPENJPA_REMOTE_COMMIT`: configure multiple instances, with high availability; valid values are the ones accepted by OpenJPA for -http://openjpa.apache.org/builds/3.1.2/apache-openjpa/docs/ref_guide_event.html[remote event notification^] including +https://openjpa.apache.org/builds/3.2.2/apache-openjpa/docs/ref_guide_event.html[remote event notification^] including `sjvm` (single instance) ===== Console diff --git a/src/main/asciidoc/reference-guide/architecture/core.adoc b/src/main/asciidoc/reference-guide/architecture/core.adoc index 0eecc3435c..e490ec31d3 100644 --- a/src/main/asciidoc/reference-guide/architecture/core.adoc +++ b/src/main/asciidoc/reference-guide/architecture/core.adoc @@ -81,7 +81,7 @@ https://camunda.org/[Camunda^] or http://jbpm.jboss.org/[jBPM^], can be written ==== Persistence All data (users, groups, attributes, resources, ...) is internally managed at a high level using a standard -https://en.wikipedia.org/wiki/Java_Persistence_API[JPA 2.2^] approach based on http://openjpa.apache.org[Apache OpenJPA^]. +https://en.wikipedia.org/wiki/Java_Persistence_API[JPA 2.2^] approach based on https://openjpa.apache.org[Apache OpenJPA^]. The data is persisted into an underlying database, referred to as *_Internal Storage_*. Consistency is ensured via the comprehensive https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/data-access.html#transaction[transaction management^] diff --git a/src/main/asciidoc/reference-guide/configuration/highavailability.adoc b/src/main/asciidoc/reference-guide/configuration/highavailability.adoc index 0d7a45e7cf..0186816a39 100644 --- a/src/main/asciidoc/reference-guide/configuration/highavailability.adoc +++ b/src/main/asciidoc/reference-guide/configuration/highavailability.adoc @@ -23,7 +23,7 @@ When deploying multiple Syncope <> instances with a single database or database cluster, it is of fundamental importance that the contained OpenJPA instances are correctly configured for -http://openjpa.apache.org/builds/3.1.2/apache-openjpa/docs/ref_guide_event.html[remote event notification^]. + +https://openjpa.apache.org/builds/3.2.2/apache-openjpa/docs/ref_guide_event.html[remote event notification^]. + Such configuration, in fact, allows the OpenJPA data cache to remain synchronized when deployed in multiple JVMs, thus enforcing data consistency across all Syncope Core instances. @@ -39,19 +39,19 @@ see the OpenJPA documentation for reference. [WARNING] ==== -The http://openjpa.apache.org/builds/3.1.2/apache-openjpa/docs/ref_guide_event.html[OpenJPA documentation^]'s XML +The https://openjpa.apache.org/builds/3.2.2/apache-openjpa/docs/ref_guide_event.html[OpenJPA documentation^]'s XML snippets refer to a different configuration style; for example, when used in `core.properties`, this: [source,xml] .... - + .... becomes: [source] .... -persistence.remoteCommitProvider=tcp(Addresses=10.0.1.10;10.0.1.11) +persistence.remoteCommitProvider=tcp(Addresses=10.0.1.10;10.0.1.11,TransmitPersistedObjectIds=true) .... ==== diff --git a/src/main/asciidoc/reference-guide/usage/customization.adoc b/src/main/asciidoc/reference-guide/usage/customization.adoc index c7d64feb14..fd7f735dcc 100644 --- a/src/main/asciidoc/reference-guide/usage/customization.adoc +++ b/src/main/asciidoc/reference-guide/usage/customization.adoc @@ -276,7 +276,7 @@ components: ===== Customize OpenJPA settings Apache OpenJPA is at the core of the <> layer; its configuration can be tweaked under several -aspects - including http://openjpa.apache.org/builds/3.1.2/apache-openjpa/docs/ref_guide_caching.html[caching^] for +aspects - including https://openjpa.apache.org/builds/3.2.2/apache-openjpa/docs/ref_guide_caching.html[caching^] for example, to best suit the various environments. The main configuration classes are: