diff --git a/Dockerfile.tomcat b/Dockerfile.tomcat index 27221b39dc..de861b4f9b 100644 --- a/Dockerfile.tomcat +++ b/Dockerfile.tomcat @@ -10,8 +10,8 @@ LABEL name="Entando App" \ summary="Entando Application" \ description="This Entando app engine application provides APIs and composition for Entando applications" -#COPY target/generated-resources/licenses /licenses -#COPY target/generated-resources/licenses.xml / +COPY target/generated-resources/licenses /licenses +COPY target/generated-resources/licenses.xml / COPY --chown=185:0 webapp/target/*.war /usr/local/tomcat/webapps/ # Copy CookieProcessor JAR if it exists diff --git a/README.md b/README.md index cfb8547e45..fc67935f80 100644 --- a/README.md +++ b/README.md @@ -66,4 +66,6 @@ The general log level is controlled by the variable `ROOT_LOG_LEVEL`, that in te || ENTANDO_BUNDLE_CLI_ETC | ${ENTANDO_BUNDLE_CLI_ETC}/hub/credentials | Credentials/parameters saved within JSON files under this path for ent bundle add hub command || ENTANDO_APP_ENGINE_HEALTH_CHECK_TYPE | db.migration.strategy | [auto], skip, disabled, generate_sql | Liquibase strategy || LOG_CONFIG_FILE_PATH | | to use the logback composable feature | -|| ENTANDO_DOCKER_REGISTRY_OVERRIDE | | Deprecated-for v1 bundles, to propagate to CM for plugins | +|| ENTANDO_DOCKER_REGISTRY_OVERRIDE | | Deprecated-for v1 bundles, to propagate to CM for plugins | +| Feature Flags | ENTANDO_FEATURE_FLAGS | comma-separated list of tags | Enable experimental features. Example: `CACHE_PIPELINE` | +|| ENTANDO_FF_DEEP_DEBUG | comma-separated list of tags | Enable deep debug logging for specific components. Example: `service-reload` | diff --git a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp index 424d599f72..687cf8557b 100644 --- a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp +++ b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/common/template/subMenu.jsp @@ -46,6 +46,17 @@ + + + + + + + + + + +
  • jacmsContentManager"> diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java index 6bdf5080bf..79b3020a26 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentJobs.java @@ -21,47 +21,47 @@ */ package org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content; + +import com.agiletec.aps.system.ApsSystemUtils; +import com.agiletec.aps.system.common.entity.model.AttributeFieldError; +import com.agiletec.aps.system.common.entity.model.AttributeTracer; +import com.agiletec.aps.system.common.entity.model.attribute.AttributeInterface; +import com.agiletec.aps.system.services.category.Category; +import com.agiletec.aps.system.services.category.ICategoryManager; +import com.agiletec.aps.system.services.lang.ILangManager; +import com.agiletec.aps.system.services.page.IPage; +import com.agiletec.aps.system.services.page.IPageManager; +import com.agiletec.aps.system.services.page.Widget; +import com.agiletec.aps.util.ApsProperties; +import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; +import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; +import com.agiletec.plugins.jacms.aps.system.services.contentmodel.ContentModel; +import com.agiletec.plugins.jacms.aps.system.services.contentmodel.IContentModelManager; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; - -import com.agiletec.aps.system.services.lang.ILangManager; +import org.entando.entando.ent.exception.EntException; +import org.entando.entando.ent.util.EntLogging.EntLogger; +import org.entando.entando.ent.util.EntLogging.EntLogFactory; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.ContentThreadConstants; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentState; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentSuspendMove; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.util.Utils; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.scheduling.quartz.QuartzJobBean; -import com.agiletec.aps.system.ApsSystemUtils; -import com.agiletec.aps.system.common.entity.model.AttributeFieldError; -import com.agiletec.aps.system.common.entity.model.AttributeTracer; -import com.agiletec.aps.system.common.entity.model.attribute.AttributeInterface; -import com.agiletec.aps.system.exception.ApsSystemException; -import com.agiletec.aps.system.services.category.Category; -import com.agiletec.aps.system.services.category.ICategoryManager; -import com.agiletec.aps.system.services.page.IPage; -import com.agiletec.aps.system.services.page.IPageManager; -import com.agiletec.aps.system.services.page.Widget; -import com.agiletec.aps.util.ApsProperties; -import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; -import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; -import com.agiletec.plugins.jacms.aps.system.services.contentmodel.ContentModel; -import com.agiletec.plugins.jacms.aps.system.services.contentmodel.IContentModelManager; + public class ContentJobs extends QuartzJobBean implements ApplicationContextAware { - private static final Logger _logger = LoggerFactory.getLogger(ContentJobs.class); + private static final EntLogger _logger = EntLogFactory.getSanitizedLogger(ContentJobs.class); private static final String APPLICATION_CONTEXT_KEY = "applicationContext"; @@ -70,12 +70,10 @@ public class ContentJobs extends QuartzJobBean implements ApplicationContextAwar private ICategoryManager _categoryManager; private IPageManager _pageManager; private IContentModelManager _contentModelManager; + private ILangManager _langManager; private ApplicationContext _ctx; - @Autowired - private ILangManager langManager; - @Override public void setApplicationContext(ApplicationContext ac) throws BeansException { this._ctx = ac; @@ -97,6 +95,7 @@ private void initBeans(ApplicationContext appCtx) { this.setContentModelManager((IContentModelManager) appCtx.getBean("jacmsContentModelManager")); this.setCategoryManager((ICategoryManager) appCtx.getBean("CategoryManager")); this.setPageManager((IPageManager) appCtx.getBean("PageManager")); + this.setLangManager((ILangManager) appCtx.getBean("LangManager")); } @Override @@ -116,7 +115,7 @@ public void executeJob(ApplicationContext appCtx) throws JobExecutionException { if (this.getContentSchedulerManager().getConfig() .isActive()/* && isCurrentSiteAllowed() */) { Date startJobDate = new Date(); - _logger.info(ContentThreadConstants.START_TIME_LOG + Utils.printTimeStamp(startJobDate)); + _logger.info(ContentThreadConstants.START_TIME_LOG + "{}", Utils.printTimeStamp(startJobDate)); List removedContents = new ArrayList(); List publishedContents = new ArrayList(); List moveContents = new ArrayList(); @@ -130,11 +129,11 @@ public void executeJob(ApplicationContext appCtx) throws JobExecutionException { Collections.sort(removedContents); Collections.sort(moveContents); Date endJobDate = new Date(); - _logger.info(ContentThreadConstants.END_TIME_LOG + Utils.printTimeStamp(endJobDate)); + _logger.info(ContentThreadConstants.END_TIME_LOG + "{}", Utils.printTimeStamp(endJobDate)); this.getContentSchedulerManager().sendMailWithResults(publishedContents, removedContents, moveContents, startJobDate, endJobDate); } catch (Throwable t) { - throw new ApsSystemException(ContentThreadConstants.ERROR_ON_MAIL, t); + throw new EntException(ContentThreadConstants.ERROR_ON_MAIL, t); } } catch (Throwable t) { ApsSystemUtils.logThrowable(t, this, t.getMessage()); @@ -147,7 +146,7 @@ public void executeJob(ApplicationContext appCtx) throws JobExecutionException { } } - private void publishContentsJob(List publishedContents) throws ApsSystemException { + private void publishContentsJob(List publishedContents) throws EntException { try { // Restituisce gli id dei contenuti che hanno un attributo con nome // key Data_inizio e valore la data corrente @@ -164,24 +163,21 @@ private void publishContentsJob(List publishedContents) throws Aps if (null == contentToPublish) { publishedContents.add(new ContentState(contentId, "null", "null", ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.NULL_CONTENT)); - _logger.info("Pubblicazione automatica non riuscita: " + contentId + " - " - + ContentThreadConstants.NULL_CONTENT); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.NULL_CONTENT, contentId); continue; } if (contentToPublish.isOnLine()) { publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.ISALREADYONLINE)); - _logger.info("Pubblicazione automatica non riuscita: " + contentToPublish.getId() + " - " - + ContentThreadConstants.ISALREADYONLINE); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.ISALREADYONLINE, contentToPublish.getId()); continue; } if (!Content.STATUS_READY.equals(contentToPublish.getStatus())) { publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.NOTREADYSTATUS)); - _logger.info("Pubblicazione automatica non riuscita: " + contentToPublish.getId() + " - " - + ContentThreadConstants.NOTREADYSTATUS); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.NOTREADYSTATUS, contentToPublish.getId()); continue; } boolean validation = this.scanEntity(contentToPublish); @@ -189,14 +185,13 @@ private void publishContentsJob(List publishedContents) throws Aps publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.CONTENTWITHERRORS)); - _logger.info("Pubblicazione automatica non riuscita: " + contentToPublish.getId() + " - " - + ContentThreadConstants.CONTENTWITHERRORS); + _logger.info("Pubblicazione automatica non riuscita: {} - " + ContentThreadConstants.CONTENTWITHERRORS, contentToPublish.getId()); continue; } // pubblicazione on line del contenuto e modifica data di // ultima modifica this.getContentManager().insertOnLineContent(contentToPublish); - _logger.info("Pubblicato automaticamente contenuto " + contentToPublish.getId()); + _logger.info("Pubblicato automaticamente contenuto {}", contentToPublish.getId()); publishedContents.add(new ContentState(contentToPublish.getId(), contentToPublish.getTypeCode(), contentToPublish.getDescription(), ContentThreadConstants.PUBLISH_ACTION, ContentThreadConstants.ACTION_SUCCESS)); @@ -207,7 +202,7 @@ private void publishContentsJob(List publishedContents) throws Aps } } } catch (Throwable t) { - throw new ApsSystemException(ContentThreadConstants.ERROR_ON_PUBLISH, t); + throw new EntException(ContentThreadConstants.ERROR_ON_PUBLISH, t); } } @@ -217,7 +212,7 @@ public boolean scanEntity(Content currentContent) { for (int i = 0; i < attributes.size(); i++) { AttributeInterface entityAttribute = attributes.get(i); if (entityAttribute.isActive()) { - List errors = entityAttribute.validate(new AttributeTracer(), langManager); + List errors = entityAttribute.validate(new AttributeTracer(), this.getLangManager()); if (null != errors && errors.size() > 0) { return false; } @@ -232,7 +227,7 @@ public boolean scanEntity(Content currentContent) { @SuppressWarnings("unchecked") private void suspendOrMoveContentsJob(List removedContents, List moveContents, - ApplicationContext appCtx) throws ApsSystemException { + ApplicationContext appCtx) throws EntException { try { // Restituisce gli id dei contenuti che hanno un attributo con nome // key Data_fine e valore la data corrente @@ -767,4 +762,9 @@ public void setContentModelManager(IContentModelManager contentModelManager) { this._contentModelManager = contentModelManager; } + public ILangManager getLangManager() { return _langManager; } + + public void setLangManager(ILangManager langManager) { this._langManager = langManager; } + + } diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java index 59bf16505e..22cb694b1c 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/ContentSchedulerManager.java @@ -51,6 +51,8 @@ import org.entando.entando.aps.system.services.tenants.RefreshableBeanTenantAware; import org.entando.entando.aps.system.services.userprofile.model.UserProfile; import org.entando.entando.ent.exception.EntException; +import org.entando.entando.ent.util.EntLogging.EntLogger; +import org.entando.entando.ent.util.EntLogging.EntLogFactory; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.ContentThreadConstants; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentState; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentSuspendMove; @@ -58,8 +60,6 @@ import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentTypeElem; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.parse.ContentThreadConfigDOM; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.util.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; @@ -70,7 +70,7 @@ public class ContentSchedulerManager extends AbstractService implements IContentSchedulerManager, RefreshableBeanTenantAware { - private static final Logger _logger = LoggerFactory.getLogger(ContentSchedulerManager.class); + private static final EntLogger _logger = EntLogFactory.getSanitizedLogger(ContentSchedulerManager.class); private static final long serialVersionUID = 6880576602469119814L; private IContentSearcherDAO _workContentSearcherDAO; @@ -213,7 +213,7 @@ public List getContentAttrDataFine() throws EntException { */ @Override public void sendMailWithResults(List publishedContents, List suspendedContents, List movedContents, Date startJobDate, - Date endJobDate) throws EntException, ApsSystemException { + Date endJobDate) throws EntException { // TODO send to groups // sendToGroups(publishedContents, suspendedContents); sendToUsers(publishedContents, suspendedContents, movedContents, startJobDate, endJobDate); @@ -229,11 +229,10 @@ public void sendMailWithResults(List publishedContents, List publishedContents, List suspendedContents, List moveContents, Date startJobDate, Date endJobDate) - throws EntException, ApsSystemException { + throws EntException { Map> mapUsers = this.getConfig().getUsersContentType(); Set keys = mapUsers.keySet(); - for (Iterator i = keys.iterator(); i.hasNext();) { - String key = i.next(); + for (String key : keys) { List typesList = mapUsers.get(key); List contentPList = contentOfTypes(publishedContents, typesList); List contentSList = contentOfTypes(suspendedContents, typesList); @@ -241,7 +240,7 @@ private void sendToUsers(List publishedContents, List 0) || (contentSList != null && contentSList.size() > 0) || (contentMList != null && contentMList.size() > 0)) { UserDetails user = this.getUserManager().getUser(key); if (user == null) { - ApsSystemUtils.getLogger().error(ContentThreadConstants.USER_IS_NULL + key); + _logger.error(ContentThreadConstants.USER_IS_NULL + "{}", key); continue; } else { UserProfile profile = (UserProfile) user.getProfile(); @@ -251,28 +250,22 @@ private void sendToUsers(List publishedContents, List 0) { email[0] = mailAttribute.getText(); String simpleText = Utils.prepareMailText(contentPList, contentSList, contentMList, this.getConfig(), startJobDate, endJobDate); + boolean issent = false; if (this.getConfig().isAlsoHtml()) { String applBaseUrl = this.getConfigManager().getParam(SystemConstants.PAR_APPL_BASE_URL); String htmlText = Utils.prepareMailHtml(contentPList, contentSList, contentMList, this.getConfig(), startJobDate, endJobDate, applBaseUrl); - boolean issent = this.getMailManager().sendMixedMail(simpleText, htmlText, config.getSubject(), null, email, null, null, config.getSenderCode()); - // System.out.println("***MAIL html"); - if (issent) { - ApsSystemUtils.getLogger().info(ContentThreadConstants.MAIL_SENT + key); - } else { - ApsSystemUtils.getLogger().error(ContentThreadConstants.SEND_ERROR + key); - } + issent = this.getMailManager().sendMixedMail(simpleText, htmlText, config.getSubject(), null, email, null, null, config.getSenderCode()); } else { - // System.out.println("***MAIL simple"); - boolean issent = this.getMailManager().sendMail(simpleText, config.getSubject(), email, null, null, config.getSenderCode()); - if (issent) { - ApsSystemUtils.getLogger().info(ContentThreadConstants.MAIL_SENT + key); - } else { - ApsSystemUtils.getLogger().error(ContentThreadConstants.SEND_ERROR + key); - } + issent = this.getMailManager().sendMail(simpleText, config.getSubject(), email, null, null, config.getSenderCode()); + } + if (issent) { + _logger.info(ContentThreadConstants.MAIL_SENT + "{}", key); + } else { + _logger.error(ContentThreadConstants.SEND_ERROR + "{}", key); } } } else { - ApsSystemUtils.getLogger().error(ContentThreadConstants.PROFILE_IS_NULL + key); + _logger.error(ContentThreadConstants.PROFILE_IS_NULL + "{}", key); } } } @@ -358,8 +351,6 @@ public String getSystemParam(String paramName) { Map systemParams = SystemParamsUtils.getParams(xmlParams); param = systemParams.get(paramName); } catch (Throwable t) { - // _logger.error("error getting the system parameter " + paramName, - // t); ApsSystemUtils.logThrowable(t, this, "error getting the system parameter " + paramName); } return param; diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java index 4a8dadf043..dd19f1db28 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/IContentSchedulerManager.java @@ -21,7 +21,6 @@ */ package org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content; -import com.agiletec.aps.system.exception.ApsSystemException; import java.util.Date; import java.util.List; @@ -65,7 +64,7 @@ public interface IContentSchedulerManager { public void updateConfig(ContentThreadConfig config) throws EntException; public void sendMailWithResults(List publishedContents, List suspendedContents, List moveContents, Date startJobDate, Date endJobDate) - throws EntException, ApsSystemException; + throws EntException; /** * Return the desired system parameter diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java index ab9f480dfd..01b09b2f8e 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/parse/ContentThreadConfigDOM.java @@ -32,6 +32,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.entando.entando.ent.exception.EntException; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentThreadConfig; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentTypeElem; import org.jdom2.CDATA; @@ -42,7 +43,6 @@ import org.jdom2.output.XMLOutputter; import com.agiletec.aps.system.ApsSystemUtils; -import com.agiletec.aps.system.exception.ApsSystemException; /** * Classe DOM delegata alle operazioni di lettura/scrittura della configurazione @@ -56,10 +56,10 @@ public class ContentThreadConfigDOM { * @param xml * The xml containing the configuration. * @return The contentthread configuration. - * @throws ApsSystemException + * @throws EntException * In case of parsing errors. */ - public ContentThreadConfig extractConfig(String xml) throws ApsSystemException { + public ContentThreadConfig extractConfig(String xml) throws EntException { ContentThreadConfig config = new ContentThreadConfig(); config.setGroupsContentType(new HashMap<>()); config.setUsersContentType(new HashMap<>()); @@ -80,10 +80,10 @@ public ContentThreadConfig extractConfig(String xml) throws ApsSystemException { * @param config * The contentThread configuration. * @return The xml containing the configuration. - * @throws ApsSystemException + * @throws EntException * In case of errors. */ - public String createConfigXml(ContentThreadConfig config) throws ApsSystemException { + public String createConfigXml(ContentThreadConfig config) throws EntException { Element root = this.createConfigElement(config); Document doc = new Document(root); String xml = new XMLOutputter().outputString(doc); @@ -440,10 +440,10 @@ private Element createMailElement(ContentThreadConfig config) { * @param xmlText * The text containing an Xml. * @return The Xml element from a given text. - * @throws ApsSystemException + * @throws EntException * In case of parsing exceptions. */ - private Element getRootElement(String xmlText) throws ApsSystemException { + private Element getRootElement(String xmlText) throws EntException { SAXBuilder builder = new SAXBuilder(); builder.setValidation(false); StringReader reader = new StringReader(xmlText); @@ -453,10 +453,10 @@ private Element getRootElement(String xmlText) throws ApsSystemException { root = doc.getRootElement(); } catch (JDOMException t) { ApsSystemUtils.getLogger().error("Error parsing xml: " + t.getMessage()); - throw new ApsSystemException("Error parsing xml", t); + throw new EntException("Error parsing xml", t); } catch (IOException t) { ApsSystemUtils.getLogger().error("Error parsing xml: " + t.getMessage()); - throw new ApsSystemException("Error parsing xml", t); + throw new EntException("Error parsing xml", t); } return root; } diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java index 62c2f2ab30..3e7a075929 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/aps/system/services/content/util/Utils.java @@ -29,13 +29,13 @@ import java.util.List; import java.util.Map; +import org.entando.entando.ent.exception.EntException; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.ContentThreadConstants; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentState; import org.entando.entando.plugins.jpcontentscheduler.aps.system.services.content.model.ContentThreadConfig; import org.springframework.context.ApplicationContext; import com.agiletec.aps.system.ApsSystemUtils; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.plugins.jacms.aps.system.services.content.ContentUtilizer; import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; @@ -59,7 +59,7 @@ public static String printTimeStamp(Date date) { @SuppressWarnings("unchecked") public static Map getReferencingObjects(Content content, ApplicationContext appCtx) - throws ApsSystemException { + throws EntException { Map references = new HashMap(); try { // String[] defNames = @@ -82,7 +82,7 @@ public static Map getReferencingObjects(Content content, Applicati } } } catch (Throwable t) { - throw new ApsSystemException("Errore in hasReferencingObject", t); + throw new EntException("Errore in hasReferencingObject", t); } return references; } diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java index b7f14424ed..4e4959b9d9 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigContentTypesAction.java @@ -25,7 +25,6 @@ import java.util.List; import com.agiletec.aps.system.common.tree.ITreeNode; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.apsadmin.system.ITreeNodeBaseActionHelper; import com.agiletec.apsadmin.system.TreeNodeWrapper; import org.apache.commons.lang3.StringUtils; @@ -37,11 +36,9 @@ import com.agiletec.aps.system.common.entity.model.SmallEntityType; import com.agiletec.aps.system.services.baseconfig.ConfigInterface; -import com.agiletec.aps.system.services.category.Category; import com.agiletec.aps.system.services.category.ICategoryManager; import com.agiletec.apsadmin.system.AbstractTreeAction; -import static com.agiletec.apsadmin.system.BaseAction.FAILURE; import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; import org.apache.struts2.action.Action; diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java index adaf2dbb59..8a54e82ebf 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/ContentThreadConfigUsersAction.java @@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory; import com.agiletec.aps.system.common.entity.model.SmallEntityType; -import com.agiletec.aps.system.exception.ApsSystemException; import com.agiletec.aps.system.services.baseconfig.ConfigInterface; import com.agiletec.aps.system.services.user.IUserManager; import com.agiletec.apsadmin.system.BaseAction; @@ -59,8 +58,10 @@ public class ContentThreadConfigUsersAction extends BaseAction { */ public String viewUsers() { try { - this.setConfigItemOnSession(); - + // Only initialize session if not already set, to preserve any pending changes + if (this.getRequest().getSession().getAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE) == null) { + this.setConfigItemOnSession(); + } } catch (Throwable t) { _logger.error("Error in viewUsers", t); return FAILURE; @@ -216,7 +217,7 @@ private boolean validateAdd() throws EntException { return true; } - private boolean validateRemoveContentType() throws ApsSystemException { + private boolean validateRemoveContentType() throws EntException { if (StringUtils.isBlank(this.getUsername())) { this.addFieldError("username", this.getText("requiredstringByArg", this.getText("username"))); return false; @@ -229,7 +230,7 @@ private boolean validateRemoveContentType() throws ApsSystemException { return true; } - private boolean validateRemoveUser() throws ApsSystemException { + private boolean validateRemoveUser() throws EntException { if (StringUtils.isBlank(this.getUsername())) { this.addFieldError("username", this.getText("requiredstringByArg", this.getText("username"))); return false; @@ -255,8 +256,13 @@ public String saveUsersItem() { private Map> setConfigItemOnSession() { Map> usersContentType = this.getContentSchedulerManager().getConfig().getUsersContentType(); - this.getRequest().getSession().setAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE, usersContentType); - return usersContentType; + // Create mutable copies to allow add/remove operations + Map> mutableCopy = new java.util.HashMap<>(); + if (usersContentType != null) { + usersContentType.forEach((key, value) -> mutableCopy.put(key, new ArrayList<>(value))); + } + this.getRequest().getSession().setAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE, mutableCopy); + return mutableCopy; } private void setConfigItemOnSession(Map> config) { @@ -264,8 +270,11 @@ private void setConfigItemOnSession(Map> config) { } public Map> getUsersContentType() { - return (Map>) this.getRequest().getSession().getAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE); - } + Map> config = (Map>) this.getRequest().getSession().getAttribute(THREAD_CONFIG_SESSION_PARAM_USERS_CONTENT_TYPE); + if (config == null) { + config = this.setConfigItemOnSession(); + } + return config; } public List getContentTypes() { List smallContentTypes = this.getContentManager().getSmallEntityTypes(); diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties index 3c916a2c12..182b183669 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_en.properties @@ -53,6 +53,8 @@ requiredstringByArg=the file {0} is required legend.User=Users jpcontentscheduler.label.username=Username jpcontentscheduler.label.contentTypes=Content Types +jpcontentscheduler.label.contentTypes.all=All Content Types + jpcontentscheduler.label.contentTypes.contentType=Content type diff --git a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties index e19f15397f..8924375e75 100644 --- a/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties +++ b/contentscheduler-plugin/src/main/java/org/entando/entando/plugins/jpcontentscheduler/apsadmin/config/package_it.properties @@ -55,7 +55,7 @@ requiredstringByArg=Il file {0} é richiesto legend.User=Utenti jpcontentscheduler.label.username=Username jpcontentscheduler.label.contentTypes=Tipi di contenuto - +jpcontentscheduler.label.contentTypes.all=Tutti i Tipi di Contenuto jpcontentscheduler.label.contentTypes.contentType=Tipo di contenuto jpcontentscheduler.label.contentTypes.startDate=Data pubblicazione diff --git a/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml b/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml index 606a30be5b..a32ddc94bb 100644 --- a/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml +++ b/contentscheduler-plugin/src/main/resources/spring/plugins/jpcontentscheduler/aps/contentThreadConfig.xml @@ -29,6 +29,7 @@ + diff --git a/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java b/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java index 0c9cd2dad8..0d34504880 100644 --- a/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java +++ b/engine/src/main/java/com/agiletec/aps/system/common/AbstractService.java @@ -13,14 +13,14 @@ */ package com.agiletec.aps.system.common; +import com.agiletec.aps.system.common.notify.ApsEvent; +import com.agiletec.aps.system.common.notify.INotifyManager; +import com.agiletec.aps.util.ApsTenantApplicationUtils; import jakarta.annotation.PreDestroy; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; - -import com.agiletec.aps.system.common.notify.ApsEvent; -import com.agiletec.aps.system.common.notify.INotifyManager; import org.springframework.context.ApplicationEvent; /** @@ -82,6 +82,12 @@ public void setBeanName(String name) { public String getName() { return _name; } + + + protected String getTenantCode() { + return ApsTenantApplicationUtils.getTenant() + .orElse(""); + } protected INotifyManager getNotifyManager() { return _notifyManager; diff --git a/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java b/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java index f03062b9b6..8416116dc8 100644 --- a/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java +++ b/engine/src/main/java/org/entando/entando/web/swagger/OpenApiConfig.java @@ -11,6 +11,7 @@ import io.swagger.v3.oas.models.servers.Server; import jakarta.servlet.ServletContext; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @@ -18,7 +19,8 @@ @Configuration public class OpenApiConfig { - private static final String SECURITY_SCHEME_NAME = "entando"; + private static final String OAUTH2_SECURITY_SCHEME_NAME = "oauth2"; + private static final String BASIC_AUTH_SECURITY_SCHEME_NAME = "basicAuth"; @Autowired ServletContext servletContext; // package-private for testing @@ -26,6 +28,9 @@ public class OpenApiConfig { @Autowired Environment environment; // package-private for testing + @Value("${keycloak.enabled:false}") + boolean keycloakEnabled; // package-private for testing + private String getAuthServer() { String baseAuthServer = environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL); String kcRealm = environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_REALM); @@ -50,7 +55,18 @@ public OpenAPI customOpenAPI() { openAPI.addServersItem(new Server().url(contextPath)); } - // Add OAuth2 security scheme if auth server is configured + if (keycloakEnabled) { + // Add OAuth2 security scheme when Keycloak is enabled + configureOAuth2Security(openAPI); + } else { + // Add Basic Auth security scheme when Keycloak is disabled + configureBasicAuthSecurity(openAPI); + } + + return openAPI; + } + + private void configureOAuth2Security(OpenAPI openAPI) { String authServer = getAuthServer(); if (authServer != null && !authServer.isEmpty()) { OAuthFlow authorizationCodeFlow = new OAuthFlow() @@ -62,15 +78,24 @@ public OpenAPI customOpenAPI() { authorizationCodeFlow.addExtension("x-logout-url", authServer + "/logout"); openAPI.components(new Components() - .addSecuritySchemes(SECURITY_SCHEME_NAME, new SecurityScheme() + .addSecuritySchemes(OAUTH2_SECURITY_SCHEME_NAME, new SecurityScheme() .type(SecurityScheme.Type.OAUTH2) .flows(new OAuthFlows() .authorizationCode(authorizationCodeFlow)))); // Apply security globally - openAPI.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)); + openAPI.addSecurityItem(new SecurityRequirement().addList(OAUTH2_SECURITY_SCHEME_NAME)); } + } - return openAPI; + private void configureBasicAuthSecurity(OpenAPI openAPI) { + openAPI.components(new Components() + .addSecuritySchemes(BASIC_AUTH_SECURITY_SCHEME_NAME, new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("basic") + .description("Basic Authentication - Use your Entando username and password"))); + + // Apply security globally + openAPI.addSecurityItem(new SecurityRequirement().addList(BASIC_AUTH_SECURITY_SCHEME_NAME)); } } \ No newline at end of file diff --git a/engine/src/main/resources/base.xml b/engine/src/main/resources/base.xml index 4ba04cb7b8..cd1f42ecb0 100644 --- a/engine/src/main/resources/base.xml +++ b/engine/src/main/resources/base.xml @@ -87,4 +87,10 @@ + + + + + + \ No newline at end of file diff --git a/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java b/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java index 260fccaf10..3c73d2fff5 100644 --- a/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java +++ b/engine/src/test/java/org/entando/entando/web/swagger/OpenApiConfigTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.agiletec.aps.system.SystemConstants; @@ -35,26 +34,39 @@ class OpenApiConfigTest { @BeforeEach public void setup() { - when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL)).thenReturn(authUrl); - when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_REALM)).thenReturn(realm); + Mockito.lenient().when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL)).thenReturn(authUrl); + Mockito.lenient().when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_REALM)).thenReturn(realm); Mockito.lenient().when(servletContext.getContextPath()).thenReturn("/entando-de-app"); } @Test - void createOpenApiConfig() { + void createOpenApiConfig_keycloakEnabled() { OpenApiConfig config = new OpenApiConfig(); config.environment = environment; config.servletContext = servletContext; + config.keycloakEnabled = true; OpenAPI openAPI = config.customOpenAPI(); assertNotNull(openAPI); } @Test - void customOpenAPITest() { + void createOpenApiConfig_keycloakDisabled() { OpenApiConfig config = new OpenApiConfig(); config.environment = environment; config.servletContext = servletContext; + config.keycloakEnabled = false; + + OpenAPI openAPI = config.customOpenAPI(); + assertNotNull(openAPI); + } + + @Test + void customOpenAPI_keycloakEnabled_shouldConfigureOAuth2() { + OpenApiConfig config = new OpenApiConfig(); + config.environment = environment; + config.servletContext = servletContext; + config.keycloakEnabled = true; OpenAPI openAPI = config.customOpenAPI(); @@ -70,11 +82,11 @@ void customOpenAPITest() { assertEquals(1, openAPI.getServers().size()); assertEquals("/entando-de-app", openAPI.getServers().get(0).getUrl()); - // Check security configuration with Keycloak enabled + // Check OAuth2 security configuration when Keycloak is enabled assertNotNull(openAPI.getComponents()); assertNotNull(openAPI.getComponents().getSecuritySchemes()); - SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("entando"); - assertNotNull(securityScheme); + SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("oauth2"); + assertNotNull(securityScheme, "OAuth2 security scheme should be configured when Keycloak is enabled"); assertEquals(SecurityScheme.Type.OAUTH2, securityScheme.getType()); assertNotNull(securityScheme.getFlows()); assertNotNull(securityScheme.getFlows().getAuthorizationCode()); @@ -87,23 +99,59 @@ void customOpenAPITest() { assertNotNull(openAPI.getSecurity()); assertEquals(1, openAPI.getSecurity().size()); SecurityRequirement securityRequirement = openAPI.getSecurity().get(0); - assertTrue(securityRequirement.containsKey("entando")); + assertTrue(securityRequirement.containsKey("oauth2")); + } + + @Test + void customOpenAPI_keycloakDisabled_shouldConfigureBasicAuth() { + OpenApiConfig config = new OpenApiConfig(); + config.environment = environment; + config.servletContext = servletContext; + config.keycloakEnabled = false; + + OpenAPI openAPI = config.customOpenAPI(); + + assertNotNull(openAPI); + Info info = openAPI.getInfo(); + assertNotNull(info); + assertEquals("Entando API", info.getTitle()); + + // Check server configuration + assertNotNull(openAPI.getServers()); + assertEquals(1, openAPI.getServers().size()); + assertEquals("/entando-de-app", openAPI.getServers().get(0).getUrl()); + + // Check Basic Auth security configuration when Keycloak is disabled + assertNotNull(openAPI.getComponents()); + assertNotNull(openAPI.getComponents().getSecuritySchemes()); + SecurityScheme securityScheme = openAPI.getComponents().getSecuritySchemes().get("basicAuth"); + assertNotNull(securityScheme, "Basic Auth security scheme should be configured when Keycloak is disabled"); + assertEquals(SecurityScheme.Type.HTTP, securityScheme.getType()); + assertEquals("basic", securityScheme.getScheme()); + assertNotNull(securityScheme.getDescription()); + + // Check security requirement + assertNotNull(openAPI.getSecurity()); + assertEquals(1, openAPI.getSecurity().size()); + SecurityRequirement securityRequirement = openAPI.getSecurity().get(0); + assertTrue(securityRequirement.containsKey("basicAuth")); } @Test - void customOpenAPIWithoutAuthServer() { + void customOpenAPI_keycloakEnabled_withoutAuthServer_shouldNotConfigureSecurity() { when(environment.getProperty(SystemConstants.SYSTEM_PROP_KEYCLOAK_AUTH_URL)).thenReturn(null); when(servletContext.getContextPath()).thenReturn("/entando-de-app"); OpenApiConfig config = new OpenApiConfig(); config.environment = environment; config.servletContext = servletContext; + config.keycloakEnabled = true; OpenAPI openAPI = config.customOpenAPI(); assertNotNull(openAPI); - // Security should not be configured when auth server is null + // Security should not be configured when auth server is null (even if Keycloak is enabled) if (openAPI.getComponents() != null) { assertNull(openAPI.getComponents().getSecuritySchemes()); } @@ -117,6 +165,7 @@ void customOpenAPIWithEmptyContextPath() { OpenApiConfig config = new OpenApiConfig(); config.environment = environment; config.servletContext = servletContext; + config.keycloakEnabled = true; OpenAPI openAPI = config.customOpenAPI(); diff --git a/jakarta-ee10-compatibility-matrix.md b/jakarta-ee10-compatibility-matrix.md deleted file mode 100644 index fe28bdc02a..0000000000 --- a/jakarta-ee10-compatibility-matrix.md +++ /dev/null @@ -1,207 +0,0 @@ -# Jakarta EE 10 Compatibility Matrix for Entando App Engine - -**Generated**: September 22, 2025 -**Project**: Entando App Engine v7.5.0 -**Current State**: Java EE 8 (javax.* namespace) -**Target**: Jakarta EE 10 (jakarta.* namespace) - -## Executive Summary - -- **Total Modules**: 14 (1 parent + 13 sub-modules) -- **Test Files**: 578 test files with 3,346 test annotations -- **Critical Dependencies**: 23 major libraries requiring updates -- **Risk Level**: HIGH (Major framework upgrade required) - -## Core Framework Dependencies - -### Spring Ecosystem (CRITICAL - Major Version Jump Required) - -| Component | Current Version | Target Version | Compatibility | Risk Level | Notes | -|-----------|----------------|----------------|---------------|------------|-------| -| Spring Framework | 5.3.39 | 6.2.1 | ❌ Breaking | CRITICAL | Major version jump, requires Jakarta EE namespace | -| Spring Security | 5.8.16 | 6.3.1 | ❌ Breaking | CRITICAL | Depends on Spring 6.x | -| Spring Security OAuth2 | 2.5.2.RELEASE | 6.3.1 | ❌ Breaking | CRITICAL | Major architectural changes | -| Spring Data Redis | 2.5.12 | 3.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x | -| Spring Data Commons | 2.5.3 | 3.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x | -| Spring Data REST | 3.5.6 | 4.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x | -| Spring Session Data Redis | 2.7.4 | 3.2.1 | ❌ Breaking | HIGH | Requires Spring 6.x | -| Spring HATEOAS | 1.5.6 | 2.2.1 | ❌ Breaking | MEDIUM | API changes expected | - -### Web & Servlet Stack - -| Component | Current Version | Target Version | Compatibility | Risk Level | Notes | -|-----------|----------------|----------------|---------------|------------|-------| -| Servlet API | javax.servlet-api 4.0.1 | jakarta.servlet-api 6.0.0 | ❌ Breaking | CRITICAL | Namespace change required | -| JSP API | jsp-api 2.2 | jakarta.servlet.jsp-api 3.1.1 | ❌ Breaking | HIGH | Namespace + version jump | -| EL API | javax.el-api 3.0.0 | jakarta.el-api 5.0.1 | ❌ Breaking | HIGH | Namespace + version jump | -| Annotation API | javax.annotation-api 1.3.2 | jakarta.annotation-api 2.1.1 | ❌ Breaking | HIGH | Namespace change | -| Tomcat Servlet API | 10.0.8 | 10.1.33 | ⚠️ Partial | MEDIUM | Already Jakarta EE 9, needs 10 | - -### Struts2 & MVC - -| Component | Current Version | Target Version | Compatibility | Risk Level | Notes | -|-----------|----------------|----------------|---------------|------------|-------| -| Struts2 Core | 6.7.4 | 6.7.4+ | ✅ Compatible | LOW | Should work with Jakarta EE 10 | -| Struts2 Spring Plugin | 6.7.4 | 6.7.4+ | ⚠️ Depends | MEDIUM | Requires Spring 6 compatibility testing | -| Struts2 Tiles Plugin | 6.7.4 | 6.7.4+ | ✅ Compatible | LOW | No direct Jakarta EE dependency | -| Struts2 JSON Plugin | 6.7.4 | 6.7.4+ | ✅ Compatible | LOW | No direct Jakarta EE dependency | - -### Validation & Data Binding - -| Component | Current Version | Target Version | Compatibility | Risk Level | Notes | -|-----------|----------------|----------------|---------------|------------|-------| -| Hibernate Validator | 6.0.20.Final | 8.0.1.Final | ❌ Breaking | HIGH | Major version jump for Jakarta EE 10 | -| Validation API | javax.validation 2.0.1.Final | jakarta.validation-api 3.0.2 | ❌ Breaking | HIGH | Namespace change | -| Jackson Core | 2.16.2 | 2.18.1 | ✅ Compatible | LOW | Should work with Jakarta EE 10 | -| Jackson Databind | 2.16.2 | 2.18.1 | ✅ Compatible | LOW | Should work with Jakarta EE 10 | - -### API & Documentation - -| Component | Current Version | Target Version | Compatibility | Risk Level | Notes | -|-----------|----------------|----------------|---------------|------------|-------| -| JAX-RS API | jakarta.ws.rs-api 2.1.6 | jakarta.ws.rs-api 3.1.0 | ⚠️ Minor | MEDIUM | Already Jakarta, needs version update | -| JAXB API | javax.xml.bind 2.3.1 | jakarta.xml.bind-api 4.0.0 | ❌ Breaking | HIGH | Namespace + major version | -| SpringFox Swagger2 | 2.10.5 | SpringDoc 2.x | ❌ Breaking | HIGH | SpringFox deprecated, migrate to SpringDoc | - -## Module-Specific Analysis - -### Core Modules - -| Module | Java EE Dependencies | Jakarta Ready | Risk Level | Migration Effort | -|--------|---------------------|---------------|------------|------------------| -| **engine** | Heavy (servlet, annotation, validation) | ❌ No | CRITICAL | HIGH - Core module with extensive Spring usage | -| **webapp** | Heavy (servlet, JSP, EL) | ❌ No | HIGH | HIGH - Main assembly with all integrations | -| **admin-console** | Medium (servlet, JSP) | ❌ No | HIGH | MEDIUM - Admin interface dependencies | -| **portal-ui** | Medium (servlet, filters) | ❌ No | HIGH | MEDIUM - UI components and filters | - -### Plugin Modules - -| Plugin | Primary Dependencies | Jakarta Ready | Risk Level | Migration Effort | -|--------|---------------------|---------------|------------|------------------| -| **keycloak-plugin** | Spring Security, Servlet | ❌ No | CRITICAL | HIGH - OAuth/Security integration | -| **cms-plugin** | JAX-RS, Validation, Servlet | ❌ No | HIGH | HIGH - Content management APIs | -| **mail-plugin** | Spring, Validation | ❌ No | MEDIUM | MEDIUM - Email functionality | -| **redis-plugin** | Spring Data Redis, Session | ❌ No | HIGH | MEDIUM - Session management | -| **seo-plugin** | Servlet, Validation | ❌ No | MEDIUM | MEDIUM - SEO features | -| **versioning-plugin** | Spring, Servlet | ❌ No | MEDIUM | MEDIUM - Content versioning | -| **cds-plugin** | Servlet, JAX-RS | ❌ No | MEDIUM | MEDIUM - Content delivery | -| **solr-plugin** | Spring, Servlet | ❌ No | MEDIUM | MEDIUM - Search functionality | -| **contentscheduler-plugin** | Spring, Quartz | ❌ No | MEDIUM | MEDIUM - Job scheduling | - -## Third-Party Library Impact - -### High Risk Libraries (Require Major Updates) -- **Spring Framework**: Complete ecosystem migration needed -- **Hibernate Validator**: Major version jump (6.x → 8.x) -- **SpringFox**: Migration to SpringDoc required -- **Spring Security OAuth2**: Architecture changes - -### Medium Risk Libraries (Version Updates) -- **JAX-RS**: Already Jakarta, needs version update -- **JAXB**: Namespace change required -- **Tomcat**: Minor version update for Jakarta EE 10 - -### Low Risk Libraries (Should be Compatible) -- **Jackson**: Modern versions support Jakarta EE -- **Struts2**: Framework is Jakarta EE 10 compatible -- **Apache Commons**: No direct Jakarta EE dependencies -- **Logging (Logback/SLF4J)**: No direct Jakarta EE dependencies - -## Testing Impact Analysis - -### Current Test Coverage -- **Total Test Files**: 578 -- **Test Annotations**: 3,346 (@Test, @IntegrationTest, @SpringBootTest) -- **Test Structure**: Standard Maven test structure (src/test/java) - -### Testing Migration Requirements - -| Test Category | Count (Estimated) | Migration Effort | Risk Level | -|---------------|------------------|------------------|------------| -| Unit Tests | ~2,500 | LOW | Tests should mostly work after namespace migration | -| Integration Tests | ~600 | HIGH | Spring context configuration changes required | -| Web Tests | ~200 | HIGH | Servlet/MockMvc configuration updates needed | -| Security Tests | ~46 | CRITICAL | Spring Security 6 has major changes | - -### Test Dependencies Requiring Updates -- **Spring Test**: 5.3.39 → 6.2.1 -- **Spring Boot Test**: If used, requires 3.x -- **MockMvc**: Spring 6 changes -- **Security Test**: Spring Security 6 changes - -## Migration Timeline Estimates - -### Phase 1: Core Framework (4 weeks) -- Spring Framework 6.2 migration -- Basic compilation fixes -- Core module updates - -### Phase 2: Namespace Migration (4 weeks) -- Automated javax → jakarta conversion -- Manual verification and fixes -- Compilation error resolution - -### Phase 3: Plugin Updates (4 weeks) -- Individual plugin migrations -- Plugin integration testing -- Cross-plugin compatibility verification - -### Phase 4: Testing & Validation (4 weeks) -- Test framework updates -- Integration test fixes -- End-to-end validation - -## Risk Mitigation Strategies - -### Critical Risks -1. **Spring Framework Breaking Changes** - - Create compatibility layer where possible - - Extensive integration testing - - Phased rollout approach - -2. **Test Suite Failures** - - Update test frameworks in parallel - - Maintain test coverage metrics - - Create new tests for Jakarta EE 10 features - -3. **Plugin Incompatibilities** - - Test plugins independently - - Maintain plugin isolation - - Create fallback mechanisms - -### Recommended Approach -1. **Parallel Development**: Maintain current branch while developing migration -2. **Incremental Testing**: Test each module independently -3. **Rollback Plan**: Maintain ability to revert at any phase -4. **Documentation**: Document all breaking changes and workarounds - -## Success Metrics - -### Technical Metrics -- ✅ All 578 test files pass -- ✅ All 14 modules compile successfully -- ✅ All 9 plugins function correctly -- ✅ Performance regression < 5% -- ✅ Memory usage regression < 10% - -### Functional Metrics -- ✅ All REST APIs maintain compatibility -- ✅ Web interface functions correctly -- ✅ Authentication/authorization works -- ✅ Plugin activation/deactivation works -- ✅ Database operations function correctly - ---- - -**Legend**: -- ✅ Compatible: No migration required -- ⚠️ Partial: Minor updates required -- ❌ Breaking: Major migration required - -**Risk Levels**: -- **CRITICAL**: Project-blocking issues -- **HIGH**: Significant development effort required -- **MEDIUM**: Moderate effort with some risk -- **LOW**: Minimal effort, low risk - -**Next Steps**: Begin with Phase 1 (Core Framework Migration) after stakeholder approval. \ No newline at end of file diff --git a/keycloak-plugin/README.md b/keycloak-plugin/README.md index b43b7c72a4..9a23ba8e22 100644 --- a/keycloak-plugin/README.md +++ b/keycloak-plugin/README.md @@ -16,6 +16,7 @@ This plugin doesn't come with Role and Group management, because Entando Core ro ## Properties >- `keycloak.enabled`: Enables this plugin. (The default is `false`) +> > ⚠️ **WARNING:** When `keycloak.enabled=false`, Basic Authentication is used instead. This is **UNSAFE FOR PRODUCTION** and should only be used for local development. >- `keycloak.auth.url`: It's the Keycloak auth url. Example: `https://is.yourdomain.com/auth`. (The default is `http://localhost:8081/auth`) >- `keycloak.realm`: The keycloak realm. See https://www.keycloak.org/docs/3.2/server_admin/topics/overview/concepts.html . (The default is `entando`) >- `keycloak.client.id`: The keycloak confidential client id. (The default is `entando-app`) diff --git a/keycloak-plugin/keycloak/docker-compose.yml b/keycloak-plugin/keycloak/docker-compose.yml index fb605544ec..e984aee710 100644 --- a/keycloak-plugin/keycloak/docker-compose.yml +++ b/keycloak-plugin/keycloak/docker-compose.yml @@ -1,40 +1,21 @@ -version: '2.1' +version: '3.8' services: - database: - container_name: database - hostname: database - image: "mysql:5.7" + keycloak-dev: + image: entando/entando-keycloak:v7.3.1-fix.3 + container_name: dev-docker-keycloak24 ports: - - "3307:3306" + - "127.0.0.1:9080:8080" + - "127.0.0.1:9443:9443" + - "127.0.0.1:10990:10990" environment: - - MYSQL_USER=keycloak - - MYSQL_PASSWORD=password - - MYSQL_ROOT_PASSWORD=password - healthcheck: - test: "/usr/bin/mysql --user=root --password=password --execute \"CREATE DATABASE IF NOT EXISTS keycloak; GRANT ALL ON keycloak.* TO 'keycloak'@'%';\"" - interval: 3s - timeout: 1s - retries: 10 + - KEYCLOAK_USER=admin + - KEYCLOAK_PASSWORD=admin + - DB_VENDOR=dev-file + entrypoint: ["/bin/bash", "-c", "source /opt/convert-vars.sh && mkdir -p /opt/keycloak/data/import && cp /tmp/realm-export.json /opt/keycloak/data/import/ && /opt/keycloak/bin/kc.sh start --import-realm"] + volumes: + - ./realm-export.json:/tmp/realm-export.json:ro + - keycloak-data:/opt/keycloak/data - keycloak: -# image: entando/keycloak - image: jboss/keycloak - hostname: keycloak - ports: - - "8081:8080" - environment: - KEYCLOAK_USER: entando-admin - KEYCLOAK_PASSWORD: qwe123 - DB_ADDR: database - DB_PORT: 3306 - DB_DATABASE: keycloak - DB_PASSWORD: password - DB_USER: keycloak - DB_VENDOR: mysql - PROXY_ADDRESS_FORWARDING: "true" - depends_on: - database: - condition: service_healthy - links: - - database:database \ No newline at end of file +volumes: + keycloak-data: \ No newline at end of file diff --git a/keycloak-plugin/keycloak/realm-export.json b/keycloak-plugin/keycloak/realm-export.json index 57006507ce..90f6d67f39 100644 --- a/keycloak-plugin/keycloak/realm-export.json +++ b/keycloak-plugin/keycloak/realm-export.json @@ -415,6 +415,39 @@ } }, "groups": [], + "users": [ + { + "id": "admin-user-id", + "username": "admin", + "enabled": true, + "emailVerified": true, + "firstName": "Admin", + "lastName": "User", + "email": "admin@entando.local", + "credentials": [ + { + "type": "password", + "value": "admin", + "temporary": false + } + ], + "realmRoles": ["offline_access", "uma_authorization"], + "clientRoles": { + "entando-core": ["superuser"], + "account": ["manage-account", "view-profile"] + } + }, + { + "id": "service-account-entando-core-id", + "username": "service-account-entando-core", + "enabled": true, + "serviceAccountClientId": "entando-core", + "realmRoles": ["offline_access", "uma_authorization"], + "clientRoles": { + "realm-management": ["realm-admin"] + } + } + ], "defaultRoles": [ "uma_authorization", "offline_access" @@ -627,7 +660,7 @@ "redirectUris": [ "http://localhost:8080/*" ], - "webOrigins": [], + "webOrigins": ["http://localhost:8080"], "notBefore": 0, "bearerOnly": false, "consentRequired": false, diff --git a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/BasicAuthFilter.java b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/BasicAuthFilter.java new file mode 100644 index 0000000000..a91a115680 --- /dev/null +++ b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/BasicAuthFilter.java @@ -0,0 +1,224 @@ +/* + * Copyright 2022-Present Entando S.r.l. (http://www.entando.com) All rights reserved. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + */ +package org.entando.entando.aps.servlet.security; + +import com.agiletec.aps.system.SystemConstants; +import com.agiletec.aps.system.services.user.IAuthenticationProviderManager; +import com.agiletec.aps.system.services.user.IUserManager; +import com.agiletec.aps.system.services.user.UserDetails; +import com.agiletec.aps.util.ApsTenantApplicationUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.entando.entando.ent.exception.EntException; +import org.entando.entando.ent.util.EntLogging; +import org.entando.entando.web.common.model.RestError; +import org.entando.entando.web.common.model.RestResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.entando.entando.aps.servlet.security.KeycloakSecurityConfig.API_PATH; + +/** + * Authentication filter that validates Basic Auth credentials against the database. + * Used when Keycloak is disabled (keycloak.enabled=false). + */ +public class BasicAuthFilter extends AbstractAuthenticationProcessingFilter implements AuthenticationFailureHandler { + + private static final EntLogging.EntLogger log = EntLogging.EntLogFactory.getSanitizedLogger(BasicAuthFilter.class); + + private final ObjectMapper objectMapper; + private final IUserManager userManager; + private final IAuthenticationProviderManager authenticationProviderManager; + + public BasicAuthFilter(final IUserManager userManager, + final IAuthenticationProviderManager authenticationProviderManager) { + super("/api/**"); + this.objectMapper = new ObjectMapper(); + this.userManager = userManager; + this.authenticationProviderManager = authenticationProviderManager; + this.setAuthenticationManager(authenticationProviderManager); + } + + @Override + public Authentication attemptAuthentication(final HttpServletRequest request, + final HttpServletResponse response) throws AuthenticationException { + ApsTenantApplicationUtils.extractCurrentTenantCode(request) + .filter(StringUtils::isNotBlank) + .ifPresentOrElse(ApsTenantApplicationUtils::setTenant, ApsTenantApplicationUtils::removeTenant); + log.warn("Keycloak disabled, handling Basic Auth"); + final String authorization = request.getHeader("Authorization"); + + // No Authorization header - return guest + if (authorization == null || authorization.isBlank()) { + return createGuestAuthentication(request); + } + + // Handle Basic Auth + if (authorization.toLowerCase().startsWith("basic ")) { + return handleBasicAuth(request, authorization); + } + + // Handle Bearer token (validate against DB-stored tokens) + if (authorization.toLowerCase().startsWith("bearer ")) { + return handleBearerToken(request, authorization); + } + + // Unknown auth scheme - return guest + return createGuestAuthentication(request); + } + + private Authentication handleBasicAuth(HttpServletRequest request, String authorization) { + try { + String base64Credentials = authorization.substring("Basic ".length()).trim(); + byte[] decodedBytes = Base64.getDecoder().decode(base64Credentials); + String credentials = new String(decodedBytes, StandardCharsets.UTF_8); + + int colonIndex = credentials.indexOf(':'); + if (colonIndex == -1) { + throw new BadCredentialsException("Invalid Basic Auth format"); + } + + String username = credentials.substring(0, colonIndex); + String password = credentials.substring(colonIndex + 1); + + if (username.isBlank() || password.isBlank()) { + throw new BadCredentialsException("Username and password are required"); + } + + // Validate credentials against the database + UserDetails user = authenticationProviderManager.getUser(username, password); + + if (user == null) { + throw new BadCredentialsException("Invalid username or password"); + } + + if (user.isDisabled()) { + throw new BadCredentialsException("User account is disabled"); + } + + if (!user.isAccountNotExpired()) { + throw new BadCredentialsException("User account has expired"); + } + + log.warn("Successfully authenticated user '{}' via Basic Auth", username); + + UserAuthentication userAuthentication = new UserAuthentication(user); + setUserOnContext(request, user, userAuthentication); + return userAuthentication; + + } catch (EntException e) { + log.error("Error during Basic Auth authentication", e); + throw new BadCredentialsException("Authentication error", e); + } catch (IllegalArgumentException e) { + log.warn("Invalid Base64 in Basic Auth header"); + throw new BadCredentialsException("Invalid Basic Auth encoding"); + } + } + + private Authentication handleBearerToken(HttpServletRequest request, String authorization) { + try { + String token = authorization.substring("Bearer ".length()).trim(); + + // Try to find user by access token in the database + UserDetails user = findUserByAccessToken(token); + + if (user == null) { + log.debug("No valid user found for Bearer token"); + throw new BadCredentialsException("Invalid or expired token"); + } + + log.debug("Successfully authenticated user '{}' via Bearer token", user.getUsername()); + + UserAuthentication userAuthentication = new UserAuthentication(user); + setUserOnContext(request, user, userAuthentication); + return userAuthentication; + + } catch (EntException e) { + log.error("Error during Bearer token authentication", e); + throw new BadCredentialsException("Authentication error", e); + } + } + + private UserDetails findUserByAccessToken(String token) throws EntException { + // The token was generated by AuthenticationProviderManager.extractUser() + // which stores it in the OAuth2 token manager + // We need to look up the user by their stored access token + // For now, we'll try to get the user from the token manager + // This requires the token to be valid and stored in the database + + // Note: This is a simplified implementation. In a production scenario, + // you might want to validate the token through the OAuth2 token manager + // and extract the username from there. + return null; // Bearer tokens require Keycloak validation - return null to reject + } + + private Authentication createGuestAuthentication(HttpServletRequest request) { + UserDetails guestUser = userManager.getGuestUser(); + GuestAuthentication guestAuthentication = new GuestAuthentication(guestUser); + setUserOnContext(request, guestUser, guestAuthentication); + return guestAuthentication; + } + + private void setUserOnContext(HttpServletRequest request, UserDetails user, Authentication authentication) { + SecurityContextHolder.getContext().setAuthentication(authentication); + if (API_PATH.equals(request.getServletPath())) { + request.setAttribute("user", user); + } else { + request.getSession().setAttribute("user", user); + request.getSession().setAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER, user); + } + } + + @Override + protected void successfulAuthentication(final HttpServletRequest request, + final HttpServletResponse response, + final FilterChain chain, + final Authentication authResult) throws IOException, ServletException { + chain.doFilter(request, response); + } + + @Override + protected void unsuccessfulAuthentication(final HttpServletRequest request, + final HttpServletResponse response, + final AuthenticationException failed) throws IOException { + this.onAuthenticationFailure(request, response, failed); + } + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException { + final RestResponse restResponse = new RestResponse<>(null, null); + restResponse.addError(new RestError(HttpStatus.UNAUTHORIZED.toString(), exception.getMessage())); + + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.addHeader("Content-Type", "application/json"); + response.getOutputStream().println(objectMapper.writeValueAsString(restResponse)); + } +} \ No newline at end of file diff --git a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java index d10057b3ea..ee73d768f5 100644 --- a/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java +++ b/keycloak-plugin/src/main/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfig.java @@ -1,14 +1,14 @@ package org.entando.entando.aps.servlet.security; +import com.agiletec.aps.system.services.user.IAuthenticationProviderManager; +import com.agiletec.aps.system.services.user.IUserManager; import org.apache.commons.lang3.StringUtils; -import org.entando.entando.aps.system.services.userprofile.api.ApiMyUserProfileInterface; import org.entando.entando.ent.util.EntLogging; import org.entando.entando.keycloak.services.KeycloakConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -29,12 +29,18 @@ public class KeycloakSecurityConfig extends AuthorizationServerConfiguration { private final KeycloakAuthenticationFilter keycloakAuthenticationFilter; private final KeycloakConfiguration configuration; + private final IUserManager userManager; + private final IAuthenticationProviderManager authenticationProviderManager; @Autowired public KeycloakSecurityConfig(final KeycloakAuthenticationFilter keycloakAuthenticationFilter, - final KeycloakConfiguration configuration) { + final KeycloakConfiguration configuration, + final IUserManager userManager, + final IAuthenticationProviderManager authenticationProviderManager) { this.keycloakAuthenticationFilter = keycloakAuthenticationFilter; this.configuration = configuration; + this.userManager = userManager; + this.authenticationProviderManager = authenticationProviderManager; } @Bean @@ -74,7 +80,33 @@ public SecurityFilterChain keycloakSecurityFilterChain(HttpSecurity http) throws .cors(cors -> cors.configurationSource(corsConfigurationSource())); return http.build(); } else { - return super.filterChain(http); + // Keycloak disabled - use BasicAuthFilter for DB authentication + _logger.warn("▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒"); + _logger.warn("▒▒▒ [SECURITY] Keycloak disabled; using Basic Auth. UNSAFE FOR PRODUCTION. ▒▒▒"); + _logger.warn("▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒"); + + BasicAuthFilter basicAuthFilter = new BasicAuthFilter(userManager, authenticationProviderManager); + + http.authorizeHttpRequests(authorize -> { + authorize + .requestMatchers(new AntPathRequestMatcher("/api/health")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/v3/api-docs")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/v3/api-docs/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/swagger-ui/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/swagger-ui.html")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/webjars/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/**")).authenticated() + .anyRequest().permitAll(); + }); + + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + .addFilterBefore(basicAuthFilter, BasicAuthenticationFilter.class) + .anonymous(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) //NOSONAR + .cors(cors -> cors.configurationSource(corsConfigurationSource())); + + return http.build(); } } diff --git a/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/BasicAuthFilterTest.java b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/BasicAuthFilterTest.java new file mode 100644 index 0000000000..ae2dcb8f70 --- /dev/null +++ b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/BasicAuthFilterTest.java @@ -0,0 +1,438 @@ +/* + * Copyright 2022-Present Entando S.r.l. (http://www.entando.com) All rights reserved. + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + */ +package org.entando.entando.aps.servlet.security; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import com.agiletec.aps.system.SystemConstants; +import com.agiletec.aps.system.services.user.IAuthenticationProviderManager; +import com.agiletec.aps.system.services.user.IUserManager; +import com.agiletec.aps.system.services.user.User; +import com.agiletec.aps.system.services.user.UserDetails; +import com.google.common.net.HttpHeaders; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.entando.entando.aps.system.services.tenants.ITenantManager; +import org.entando.entando.aps.util.UrlUtils; +import org.entando.entando.ent.exception.EntException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@ExtendWith(MockitoExtension.class) +class BasicAuthFilterTest { + + private static final String USERNAME = "testuser"; + private static final String PASSWORD = "testpassword"; + + @Mock + private IUserManager userManager; + @Mock + private IAuthenticationProviderManager authenticationProviderManager; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private HttpSession session; + @Mock + private ServletContext servletContext; + @Mock + private WebApplicationContext webApplicationContext; + @Mock + private ITenantManager tenantManager; + @Mock + private FilterChain filterChain; + @Mock + private ServletOutputStream outputStream; + + private BasicAuthFilter basicAuthFilter; + + @BeforeEach + void setUp() { + basicAuthFilter = new BasicAuthFilter(userManager, authenticationProviderManager); + + Mockito.lenient().when(request.getSession()).thenReturn(session); + Mockito.lenient().when(request.getHeader(UrlUtils.ENTANDO_TENANT_CODE_CUSTOM_HEADER)).thenReturn(null); + Mockito.lenient().when(request.getHeader(HttpHeaders.X_FORWARDED_HOST)).thenReturn(null); + Mockito.lenient().when(request.getHeader(HttpHeaders.HOST)).thenReturn("localhost"); + Mockito.lenient().when(request.getServerName()).thenReturn("localhost"); + Mockito.lenient().when(session.getServletContext()).thenReturn(servletContext); + Mockito.lenient().when(webApplicationContext.getBean(ITenantManager.class)).thenReturn(tenantManager); + } + + @Test + void attemptAuthentication_noAuthorizationHeader_shouldReturnGuestAuthentication() { + when(request.getHeader("Authorization")).thenReturn(null); + when(request.getServletPath()).thenReturn("/api"); + + User guestUser = createGuestUser(); + when(userManager.getGuestUser()).thenReturn(guestUser); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + Authentication auth = basicAuthFilter.attemptAuthentication(request, response); + + assertNotNull(auth); + assertInstanceOf(GuestAuthentication.class, auth); + assertFalse(auth.isAuthenticated()); + verify(request).setAttribute(eq("user"), any()); + } + } + + @Test + void attemptAuthentication_emptyAuthorizationHeader_shouldReturnGuestAuthentication() { + when(request.getHeader("Authorization")).thenReturn(""); + when(request.getServletPath()).thenReturn("/api"); + + User guestUser = createGuestUser(); + when(userManager.getGuestUser()).thenReturn(guestUser); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + Authentication auth = basicAuthFilter.attemptAuthentication(request, response); + + assertNotNull(auth); + assertInstanceOf(GuestAuthentication.class, auth); + } + } + + @Test + void attemptAuthentication_validBasicAuth_shouldReturnUserAuthentication() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + when(request.getServletPath()).thenReturn("/api"); + + User user = createActiveUser(); + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + Authentication auth = basicAuthFilter.attemptAuthentication(request, response); + + assertNotNull(auth); + assertInstanceOf(UserAuthentication.class, auth); + assertTrue(auth.isAuthenticated()); + assertEquals(user, auth.getPrincipal()); + verify(request).setAttribute(eq("user"), eq(user)); + } + } + + @Test + void attemptAuthentication_validBasicAuthLowercase_shouldReturnUserAuthentication() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("basic " + encodedCredentials); + when(request.getServletPath()).thenReturn("/api"); + + User user = createActiveUser(); + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + Authentication auth = basicAuthFilter.attemptAuthentication(request, response); + + assertNotNull(auth); + assertInstanceOf(UserAuthentication.class, auth); + assertTrue(auth.isAuthenticated()); + } + } + + @Test + void attemptAuthentication_invalidCredentials_shouldThrowBadCredentialsException() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(null); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Invalid username or password", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_disabledUser_shouldThrowBadCredentialsException() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + User user = createDisabledUser(); + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("User account is disabled", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_expiredAccount_shouldThrowBadCredentialsException() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + // Use a mock to control isAccountNotExpired() return value + UserDetails user = mock(UserDetails.class); + Mockito.lenient().when(user.getUsername()).thenReturn(USERNAME); + when(user.isDisabled()).thenReturn(false); + when(user.isAccountNotExpired()).thenReturn(false); // Account is expired + + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("User account has expired", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_invalidBase64Encoding_shouldThrowBadCredentialsException() { + when(request.getHeader("Authorization")).thenReturn("Basic not-valid-base64!!!"); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Invalid Basic Auth encoding", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_noColonInCredentials_shouldThrowBadCredentialsException() { + String invalidCredentials = "usernameWithoutPassword"; + String encodedCredentials = Base64.getEncoder().encodeToString(invalidCredentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Invalid Basic Auth format", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_emptyUsername_shouldThrowBadCredentialsException() { + String credentials = ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Username and password are required", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_emptyPassword_shouldThrowBadCredentialsException() { + String credentials = USERNAME + ":"; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Username and password are required", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_bearerToken_shouldThrowBadCredentialsException() { + when(request.getHeader("Authorization")).thenReturn("Bearer some-jwt-token"); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Invalid or expired token", exception.getMessage()); + } + } + + @Test + void attemptAuthentication_unknownAuthScheme_shouldReturnGuestAuthentication() { + when(request.getHeader("Authorization")).thenReturn("Digest somevalue"); + when(request.getServletPath()).thenReturn("/api"); + + User guestUser = createGuestUser(); + when(userManager.getGuestUser()).thenReturn(guestUser); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + Authentication auth = basicAuthFilter.attemptAuthentication(request, response); + + assertNotNull(auth); + assertInstanceOf(GuestAuthentication.class, auth); + } + } + + @Test + void attemptAuthentication_nonApiPath_shouldSetSessionAttributes() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + when(request.getServletPath()).thenReturn("/do/something"); + + User user = createActiveUser(); + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenReturn(user); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + basicAuthFilter.attemptAuthentication(request, response); + + verify(session).setAttribute(eq("user"), eq(user)); + verify(session).setAttribute(eq(SystemConstants.SESSIONPARAM_CURRENT_USER), eq(user)); + } + } + + @Test + void attemptAuthentication_authenticationProviderThrowsException_shouldThrowBadCredentialsException() throws Exception { + String credentials = USERNAME + ":" + PASSWORD; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + + when(authenticationProviderManager.getUser(USERNAME, PASSWORD)).thenThrow(new EntException("DB error")); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + BadCredentialsException exception = assertThrows(BadCredentialsException.class, + () -> basicAuthFilter.attemptAuthentication(request, response)); + + assertEquals("Authentication error", exception.getMessage()); + } + } + + @Test + void onAuthenticationFailure_shouldReturnUnauthorizedResponse() throws Exception { + when(response.getOutputStream()).thenReturn(outputStream); + + BadCredentialsException exception = new BadCredentialsException("Test error message"); + + basicAuthFilter.onAuthenticationFailure(request, response, exception); + + verify(response).setStatus(HttpStatus.UNAUTHORIZED.value()); + verify(response).addHeader("Content-Type", "application/json"); + verify(outputStream).println(anyString()); + } + + @Test + void successfulAuthentication_shouldContinueFilterChain() throws Exception { + Authentication auth = mock(Authentication.class); + + basicAuthFilter.successfulAuthentication(request, response, filterChain, auth); + + verify(filterChain).doFilter(request, response); + } + + @Test + void attemptAuthentication_passwordWithColons_shouldParseCorrectly() throws Exception { + String passwordWithColons = "pass:word:with:colons"; + String credentials = USERNAME + ":" + passwordWithColons; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + when(request.getHeader("Authorization")).thenReturn("Basic " + encodedCredentials); + when(request.getServletPath()).thenReturn("/api"); + + User user = createActiveUser(); + when(authenticationProviderManager.getUser(USERNAME, passwordWithColons)).thenReturn(user); + + try (MockedStatic wacUtil = Mockito.mockStatic(WebApplicationContextUtils.class)) { + wacUtil.when(() -> WebApplicationContextUtils.getWebApplicationContext(servletContext)).thenReturn(webApplicationContext); + + Authentication auth = basicAuthFilter.attemptAuthentication(request, response); + + assertNotNull(auth); + assertInstanceOf(UserAuthentication.class, auth); + verify(authenticationProviderManager).getUser(USERNAME, passwordWithColons); + } + } + + // Helper methods to create test users + + private User createGuestUser() { + User user = new User(); + user.setUsername("guest"); + return user; + } + + private User createActiveUser() { + User user = new User(); + user.setUsername(USERNAME); + user.setDisabled(false); + return user; + } + + private User createDisabledUser() { + User user = new User(); + user.setUsername(USERNAME); + user.setDisabled(true); + return user; + } + +} \ No newline at end of file diff --git a/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java index 30d97248be..e8f971ae86 100644 --- a/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java +++ b/keycloak-plugin/src/test/java/org/entando/entando/aps/servlet/security/KeycloakSecurityConfigTest.java @@ -13,19 +13,31 @@ */ package org.entando.entando.aps.servlet.security; +import com.agiletec.aps.system.services.user.IAuthenticationProviderManager; +import com.agiletec.aps.system.services.user.IUserManager; +import jakarta.servlet.Filter; import org.entando.entando.keycloak.services.KeycloakConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.SecurityFilterChain; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -//TODO verificare queste modifiche con l'as-is javaxEE8 @ExtendWith(MockitoExtension.class) class KeycloakSecurityConfigTest { @@ -34,12 +46,18 @@ class KeycloakSecurityConfigTest { @Mock private KeycloakConfiguration configuration; + + @Mock + private IUserManager userManager; + + @Mock + private IAuthenticationProviderManager authenticationProviderManager; private KeycloakSecurityConfig securityConfig; @BeforeEach public void setUp() { - this.securityConfig = new KeycloakSecurityConfig(keycloakAuthenticationFilter, configuration); + this.securityConfig = new KeycloakSecurityConfig(keycloakAuthenticationFilter, configuration, userManager, authenticationProviderManager); } @Test @@ -95,5 +113,105 @@ void shouldExecuteSessionSettings() { // which includes the SessionCreationPolicy.ALWAYS setting in the lambda // This functionally verifies the same behavior as the original test } - + + + @Test + void keycloakEnabled_shouldAddKeycloakAuthenticationFilter() throws Exception { + // Setup + when(configuration.isEnabled()).thenReturn(true); + Mockito.lenient().when(configuration.getSecureUris()).thenReturn(""); + + HttpSecurity httpSecurity = mock(HttpSecurity.class, Mockito.RETURNS_DEEP_STUBS); + DefaultSecurityFilterChain filterChain = mock(DefaultSecurityFilterChain.class); + + when(httpSecurity.authorizeHttpRequests(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.sessionManagement(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.headers(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.addFilterBefore(any(Filter.class), any(Class.class))).thenReturn(httpSecurity); + when(httpSecurity.anonymous(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.csrf(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.cors(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.build()).thenReturn(filterChain); + + // Execute + SecurityFilterChain result = securityConfig.keycloakSecurityFilterChain(httpSecurity); + + // Verify + assertNotNull(result); + + // Capture the filter added to verify it's the KeycloakAuthenticationFilter + ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + verify(httpSecurity).addFilterBefore(filterCaptor.capture(), any(Class.class)); + + Filter addedFilter = filterCaptor.getValue(); + assertInstanceOf(KeycloakAuthenticationFilter.class, addedFilter, + "When Keycloak is enabled, KeycloakAuthenticationFilter should be added"); + } + + @Test + void keycloakDisabled_shouldAddBasicAuthFilter() throws Exception { + // Setup + when(configuration.isEnabled()).thenReturn(false); + + HttpSecurity httpSecurity = mock(HttpSecurity.class, Mockito.RETURNS_DEEP_STUBS); + DefaultSecurityFilterChain filterChain = mock(DefaultSecurityFilterChain.class); + + when(httpSecurity.authorizeHttpRequests(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.sessionManagement(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.headers(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.addFilterBefore(any(Filter.class), any(Class.class))).thenReturn(httpSecurity); + when(httpSecurity.anonymous(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.csrf(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.cors(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.build()).thenReturn(filterChain); + + // Execute + SecurityFilterChain result = securityConfig.keycloakSecurityFilterChain(httpSecurity); + + // Verify + assertNotNull(result); + + // Capture the filter added to verify it's the BasicAuthFilter + ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + verify(httpSecurity).addFilterBefore(filterCaptor.capture(), any(Class.class)); + + Filter addedFilter = filterCaptor.getValue(); + assertInstanceOf(BasicAuthFilter.class, addedFilter, + "When Keycloak is disabled, BasicAuthFilter should be added"); + } + + @Test + void keycloakEnabled_shouldConfigureHttpSecurityCorrectly() throws Exception { + // Setup + when(configuration.isEnabled()).thenReturn(true); + Mockito.lenient().when(configuration.getSecureUris()).thenReturn("/api/secure1,/api/secure2"); + + HttpSecurity httpSecurity = mock(HttpSecurity.class, Mockito.RETURNS_DEEP_STUBS); + DefaultSecurityFilterChain filterChain = mock(DefaultSecurityFilterChain.class); + + when(httpSecurity.authorizeHttpRequests(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.sessionManagement(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.headers(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.addFilterBefore(any(Filter.class), any(Class.class))).thenReturn(httpSecurity); + when(httpSecurity.anonymous(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.csrf(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.cors(any(Customizer.class))).thenReturn(httpSecurity); + when(httpSecurity.build()).thenReturn(filterChain); + + // Execute + SecurityFilterChain result = securityConfig.keycloakSecurityFilterChain(httpSecurity); + + // Verify filter chain was built successfully + assertNotNull(result); + + // Verify all HttpSecurity configuration methods were called + verify(httpSecurity).authorizeHttpRequests(any(Customizer.class)); + verify(httpSecurity).sessionManagement(any(Customizer.class)); + verify(httpSecurity).headers(any(Customizer.class)); + verify(httpSecurity).anonymous(any(Customizer.class)); + verify(httpSecurity).csrf(any(Customizer.class)); + verify(httpSecurity).cors(any(Customizer.class)); + verify(httpSecurity).build(); + } + } diff --git a/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java b/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java index 83049b1649..ffe8043b5b 100644 --- a/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java +++ b/mail-plugin/src/main/java/com/agiletec/plugins/jpmail/aps/services/mail/MailManager.java @@ -39,6 +39,8 @@ import jakarta.mail.internet.*; import java.util.Date; import java.util.Iterator; +import java.util.HashMap; +import java.util.Map; import java.util.Map.Entry; import java.util.Properties; @@ -83,7 +85,7 @@ private void loadConfigs() throws EntException { @Override public MailConfig getMailConfig() throws EntException { try { - return (MailConfig) this._config.clone(); + return (MailConfig) this.getConfig().clone(); } catch (Throwable t) { throw new EntException("Error loading mail service configuration", t); } @@ -250,7 +252,7 @@ protected Session prepareSession(MailConfig config) { switch (config.getSmtpProtocol()) { case JpmailSystemConstants.PROTO_SSL: props.put("mail.smtp.socketFactory.port", port); - props.put("mail.smtp.socketFactory.class", "jakarta.net.ssl.SSLSocketFactory"); + props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); props.put("mail.smtp.ssl.checkserveridentity", String.valueOf(config.isCheckServerIdentity())); props.put("mail.transport.protocol", "smtps"); break; @@ -367,7 +369,7 @@ protected void closeTransport(Transport transport) throws EntException { * @return The mail service configuration. */ protected MailConfig getConfig() { - return _config; + return this.tenantConfigs.get(this.getTenantCode()); } /** @@ -375,7 +377,8 @@ protected MailConfig getConfig() { * @param config The mail service configuration. */ protected void setConfig(MailConfig config) { - this._config = config; + String tenantCode = this.getTenantCode(); + this.tenantConfigs.put(tenantCode, config); } protected Boolean isActive() { @@ -406,7 +409,7 @@ public void setConfigManager(ConfigInterface configManager) { } private Boolean _active; - private MailConfig _config; + private Map tenantConfigs = new HashMap<>(); private ConfigInterface _configManager; /* diff --git a/migrate-engine-namespaces.sh b/migrate-engine-namespaces.sh deleted file mode 100755 index ac53e6efdc..0000000000 --- a/migrate-engine-namespaces.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -echo "=== Jakarta EE 10 Namespace Migration for Engine Module ===" -echo "Starting migration of javax.* imports to jakarta.* in engine module..." - -cd engine/ - -# Backup original files -echo "Creating backup..." -find . -name "*.java" -exec cp {} {}.pre-jakarta \; 2>/dev/null - -# Count files before migration -echo "Files to migrate:" -echo " - Servlet API: $(find . -name "*.java" -exec grep -l "import javax\.servlet\." {} \; | wc -l) files" -echo " - Annotation API: $(find . -name "*.java" -exec grep -l "import javax\.annotation\." {} \; | wc -l) files" -echo " - Validation API: $(find . -name "*.java" -exec grep -l "import javax\.validation\." {} \; | wc -l) files" -echo " - EL API: $(find . -name "*.java" -exec grep -l "import javax\.el\." {} \; | wc -l) files" -echo " - JAX-RS API: $(find . -name "*.java" -exec grep -l "import javax\.ws\.rs\." {} \; | wc -l) files" -echo " - JAXB API: $(find . -name "*.java" -exec grep -l "import javax\.xml\.bind\." {} \; | wc -l) files" - -echo "" -echo "Performing namespace migration..." - -# Servlet API migration -echo " → Migrating javax.servlet.* to jakarta.servlet.*" -find . -name "*.java" -exec sed -i 's/import javax\.servlet\./import jakarta.servlet./g' {} \; -find . -name "*.java" -exec sed -i 's/javax\.servlet\./jakarta.servlet./g' {} \; - -# Annotation API migration -echo " → Migrating javax.annotation.* to jakarta.annotation.*" -find . -name "*.java" -exec sed -i 's/import javax\.annotation\./import jakarta.annotation./g' {} \; -find . -name "*.java" -exec sed -i 's/javax\.annotation\./jakarta.annotation./g' {} \; - -# Validation API migration -echo " → Migrating javax.validation.* to jakarta.validation.*" -find . -name "*.java" -exec sed -i 's/import javax\.validation\./import jakarta.validation./g' {} \; -find . -name "*.java" -exec sed -i 's/javax\.validation\./jakarta.validation./g' {} \; - -# EL API migration -echo " → Migrating javax.el.* to jakarta.el.*" -find . -name "*.java" -exec sed -i 's/import javax\.el\./import jakarta.el./g' {} \; -find . -name "*.java" -exec sed -i 's/javax\.el\./jakarta.el./g' {} \; - -# JAX-RS API migration -echo " → Migrating javax.ws.rs.* to jakarta.ws.rs.*" -find . -name "*.java" -exec sed -i 's/import javax\.ws\.rs\./import jakarta.ws.rs./g' {} \; -find . -name "*.java" -exec sed -i 's/javax\.ws\.rs\./jakarta.ws.rs./g' {} \; - -# JAXB API migration -echo " → Migrating javax.xml.bind.* to jakarta.xml.bind.*" -find . -name "*.java" -exec sed -i 's/import javax\.xml\.bind\./import jakarta.xml.bind./g' {} \; -find . -name "*.java" -exec sed -i 's/javax\.xml\.bind\./jakarta.xml.bind./g' {} \; - -# Count files after migration -echo "" -echo "Migration completed! Files affected:" -echo " - Total Java files processed: $(find . -name "*.java" | wc -l)" -echo " - Files with jakarta.servlet imports: $(find . -name "*.java" -exec grep -l "import jakarta\.servlet\." {} \; | wc -l)" -echo " - Files with jakarta.annotation imports: $(find . -name "*.java" -exec grep -l "import jakarta\.annotation\." {} \; | wc -l)" -echo " - Files with jakarta.validation imports: $(find . -name "*.java" -exec grep -l "import jakarta\.validation\." {} \; | wc -l)" - -echo "" -echo "=== Migration Summary ===" -echo "Namespace migration completed for engine module" -echo "Backup files created with .pre-jakarta extension" -echo "Ready for compilation testing" \ No newline at end of file diff --git a/migrate-spring-security-oauth2.sh b/migrate-spring-security-oauth2.sh deleted file mode 100755 index 95d7c469eb..0000000000 --- a/migrate-spring-security-oauth2.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -echo "Migrating Java source imports from javax to jakarta..." - -# Update servlet imports -find . -name "*.java" -type f -exec sed -i 's/import javax\.servlet\./import jakarta.servlet./g' {} + -find . -name "*.java" -type f -exec sed -i 's/import javax\.servlet\.http\./import jakarta.servlet.http./g' {} + -find . -name "*.java" -type f -exec sed -i 's/import javax\.servlet\.jsp\./import jakarta.servlet.jsp./g' {} + - -# Update validation imports -find . -name "*.java" -type f -exec sed -i 's/import javax\.validation\./import jakarta.validation./g' {} + - -# Update xml.bind imports -find . -name "*.java" -type f -exec sed -i 's/import javax\.xml\.bind\./import jakarta.xml.bind./g' {} + - -# Update ws.rs imports -find . -name "*.java" -type f -exec sed -i 's/import javax\.ws\.rs\./import jakarta.ws.rs./g' {} + - -# Update annotation imports -find . -name "*.java" -type f -exec sed -i 's/import javax\.annotation\./import jakarta.annotation./g' {} + - -echo "Java source import migration completed." diff --git a/pom.xml.backup-pre-jakarta-migration b/pom.xml.backup-pre-jakarta-migration deleted file mode 100644 index 9b7499d2f4..0000000000 --- a/pom.xml.backup-pre-jakarta-migration +++ /dev/null @@ -1,1651 +0,0 @@ - - - 4.0.0 - - org.entando - entando-maven-root - 7.4.0-ENG-5316-PR-21 - - org.entando - - app-engine - 7.5.0 - pom - - - engine - admin-console - portal-ui - keycloak-plugin - cms-plugin - mail-plugin - redis-plugin - cds-plugin - seo-plugin - versioning-plugin - contentscheduler-plugin - solr-plugin - webapp - - - Entando App Engine Parent POM - Entando App Engine Parent POM - 2019 - https://central.entando.com - - - GNU LESSER GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 - https://www.gnu.org/licenses/lgpl-3.0.txt - repo - - - - Entando Inc. - https://www.entando.com/ - - - - nexus-jx - https://nexus-jx.apps.serv.run/repository/maven-releases/ - - true - - - false - - - - - scm:git:git@github.com:${github.organization}/${project.artifactId}.git - scm:git:git@github.com:${github.organization}/${project.artifactId}.git - - https://github.com/${github.organization}/${project.artifactId}/ - HEAD - - - 17 - 17 - true - - - - **/*.js - - org.apache.derby.jdbc.EmbeddedDriver - localhost - 1527 - agile - agile - jdbc:derby:memory:testPort;create=true - jdbc:derby:memory:testServ;create=true - - 5.3.39 - 5.8.16 - 2.5.2.RELEASE - 6.7.4 - 2.16.2 - 2.16.2 - 6.0.20.Final - 8.11.4 - 2.9.0 - 2.10.5 - 1.11.6 - 33.4.8-jre - 3.5.6 - 1.11.0 - 1.2 - 1.12.0 - 1.16.1 - 3.2.2 - 2.9.0 - 2.3.32 - 1.9.0 - 1.2.5 - 3.20.2 - 2.1 - 1.5.18 - 0.1.5 - 0.1.5 - 1.9.7 - 4.40 - 4.8.0 - 2.0.17 - 2.20.0 - 1.5.4 - 4.5.14 - 3.0.1 - 3.0.1 - 10.15.2.0 - 2.0.1.Final - 2.2.6 - 3.0.0 - 10.0.8 - 1.4.0 - 1.0 - 2.10.10 - 2.2 - 1.3 - 3.11.2 - 5.11.4 - 1.17.6 - 2.1.3 - 4.0.1 - 1.3.2 - 1.1.3 - - 2.0.6.1 - 3.28.0-GA - 2.4.1 - 3.3.5 - 2.19.0 - 4.8.172 - 3.18.0 - 1.3.5 - 1.1 - 1.6 - 1.4.01 - 2.0.8 - 1.3.7 - 2.7.0.0 - 3.2.2 - 0.9.5.5 - 0.8 - 2.18.0 - 4.5.0-M2 - 1.18.38 - 3.3.0 - 3.5.2 - 2.5 - 2.2.1 - 5.8.0 - 9.8 - 1.6.7 - 3.1.7 - 2.0.24 - 2.0.1 - 4.2.2 - 2.3.1 - 1.1.1 - 1.2.2 - 1.28.0 - 2.5.12 - 2.7.4 - 6.1.5.RELEASE - 1.5.20 - 2.9.0 - 2.5.2 - 2.4.0 - 2.1.6 - 6.0.20.Final - 2.3.3 - 1.2.3 - 1.2.3 - 4.4.16 - 2.20.0 - 1.3.4 - 2.5.3 - 2.2 - 2.3.4 - 1.3.5 - 3.25.5 - 2.12.2 - 7.4.1 - 4.1.125.Final - 1.6.0 - - 9.8.0 - - 1.5.6 - 7.1.0 - 1.1.10.7 - 3.2.1 - 2.39.0 - 3.9.3 - - - Github - https://github.com/${github.organization}/${project.artifactId}/issues/ - - - - - - - org.owasp - dependency-check-maven - 12.1.0 - - 7 - false - - HTML - JSON - - - - - verify - - check - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - ${maven-dependency-plugin.version} - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - ${argLine} -Dliquibase.supportPropertyEscaping=true -Dlogback.statusListenerClass=ch.qos.logback.core.status.NopStatusListener - 1 - exit - methods - 8 - alphabetical - - **/*BaseTestCase* - **/Abstract* - - - WARN - - 1 - - - - - org.apache.maven.plugins - maven-assembly-plugin - ${maven-assembly-plugin.version} - - - package-classes - package - - single - - - - ${project.basedir}/src/main/assembly/classes.xml - - - - - - - - - - - org.apache.maven.plugins - maven-resources-plugin - ${maven-resources-plugin.version} - - - copy-resources-test - - generate-test-resources - - copy-resources - - - ${project.build.directory}/test - UTF-8 - - - ${project.basedir}/src/test/config - true - - - - - - copy-resources-file-test- - - generate-test-resources - - copy-resources - - - ${project.build.directory}/test/resources/ - UTF-8 - - - ${project.basedir}/src/test/config - true - - - - - - - - - - - - - org.apache.lucene - lucene-core - ${lucene-core.version} - jar - - - org.apache.lucene - lucene-analyzers-common - ${lucene-core.version} - jar - - - - com.jayway.jsonpath - json-path-assert - ${json-path-assert.version} - test - - - - - org.apache.solr - solr-solrj - ${solr-solrj.version} - - - - org.eclipse.jetty - * - - - org.eclipse.jetty.http2 - * - - - io.netty - netty-transport-native-epoll - - - - - - io.springfox - springfox-swagger2 - ${springfox-swagger2.version} - - - com.google.guava - guava - - - - - io.springfox - springfox-swagger-ui - ${springfox-swagger2.version} - - - com.google.guava - guava - - - net.bytebuddy - byte-buddy - - - - - io.springfox - springfox-spring-webmvc - ${springfox-swagger2.version} - - - net.java.dev.jna - jna - ${jna.version} - - - net.bytebuddy - byte-buddy - ${byte-buddy.version} - - - - com.google.guava - guava - ${guava.version} - - - org.springframework.data - spring-data-rest-webmvc - ${spring-data-rest-webmvc.version} - - - org.springframework - spring-tx - - - - - org.springframework.data - spring-data-redis - ${spring-data-redis.version} - - - org.springframework.session - spring-session-data-redis - ${spring-session-data-redis.version} - - - commons-beanutils - commons-beanutils - ${commons-beanutils.version} - jar - - - - commons-chain - commons-chain - ${commons-chain.version} - jar - - - - org.apache.commons - commons-text - ${commons-text.version} - jar - - - - commons-codec - commons-codec - ${commons-codec.version} - jar - - - - commons-collections - commons-collections - ${commons-collections.version} - jar - - - - org.apache.commons - commons-dbcp2 - ${commons-dbcp2.version} - jar - - - - commons-digester - commons-digester - ${commons-digester.version} - jar - - - - commons-io - commons-io - ${commons-io.version} - jar - - - io.github.classgraph - classgraph - ${classgraph.version} - - - - - - - - - org.apache.commons - commons-lang3 - ${commons-lang3.version} - jar - - - - commons-logging - commons-logging - ${commons-logging.version} - jar - - - - commons-logging - commons-logging-api - ${commons-logging-api.version} - jar - - - - commons-pool - commons-pool - ${commons-pool.version} - jar - - - - commons-validator - commons-validator - ${commons-validator.version} - jar - - - - javax.validation - validation-api - ${javax-validator.version} - jar - - - - org.freemarker - freemarker - ${freemarker.version} - jar - - - - org.apache.taglibs - taglibs-standard-impl - ${taglibs-standard-impl.version} - - - - ch.qos.logback - logback-core - ${logback.version} - - - - ch.qos.logback - logback-classic - ${logback.version} - - - - ch.qos.logback.contrib - logback-json-classic - ${logback-json.version} - - - ch.qos.logback.contrib - logback-jackson - ${logback-jackson.version} - - - org.springframework - spring-expression - ${spring.version} - jar - - - org.springframework - spring-aop - ${spring.version} - jar - - - org.apache.tika - tika-core - ${tika-version} - - - org.apache.tika - tika-parsers-standard-package - ${tika-version} - - - org.apache.cxf - cxf-core - - - org.apache.cxf - cxf-rt-frontend-jaxrs - - - joda-time - joda-time - - - edu.usc.ir - sentiment-analysis-parser - - - c3p0 - c3p0 - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - org.gagravarr - vorbis-java-tika - - - org.apache.commons - commons-compress - - - org.quartz-scheduler - quartz - - - - - - com.mchange - c3p0 - ${c3p0.version} - - - org.ow2.asm - asm - ${asm.version} - - - org.jvnet.staxex - stax-ex - ${stax-ex.version} - - - org.codehaus.woodstox - stax2-api - ${stax2-api.version} - - - org.gagravarr - vorbis-java-tika - ${vorbis-java-tika.version} - - - org.apache.tika - tika-parsers - - - org.apache.tika - tika-core - - - - - com.drewnoakes - metadata-extractor - ${drewnoakes-metadata-extractor.version} - - - org.apache.commons - commons-collections4 - ${commons-collections4.version} - jar - - - org.projectlombok - lombok - ${lombok.version} - - - - org.springframework - spring-beans - ${spring.version} - jar - - - - org.springframework - spring-context - ${spring.version} - jar - - - - org.springframework - spring-context-support - ${spring.version} - jar - - - - org.springframework - spring-core - ${spring.version} - jar - - - org.springframework - spring-web - ${spring.version} - jar - - - org.springframework - spring-webmvc - ${spring.version} - jar - - - org.springframework - spring-tx - ${spring.version} - jar - - - org.springframework - spring-oxm - ${spring.version} - jar - - - org.springframework - spring-jcl - ${spring.version} - jar - - - org.springframework.security - spring-security-web - ${springsecurity.version} - - - org.springframework - spring-context - - - org.springframework - spring-beans - - - org.springframework - spring-core - - - org.springframework - spring-expression - - - org.springframework - spring-aop - - - org.springframework - spring-web - - - - - org.springframework.security - spring-security-config - ${springsecurity.version} - - - org.springframework - spring-context - - - org.springframework - spring-beans - - - org.springframework - spring-core - - - org.springframework - spring-aop - - - org.springframework - spring-web - - - - - org.springframework.security.oauth - spring-security-oauth2 - ${springsecurityoauth2.version} - - - org.springframework - spring-core - - - org.springframework - spring-context - - - org.springframework - spring-web - - - org.springframework - spring-webmvc - - - org.springframework - spring-beans - - - org.springframework - spring-aop - - - org.springframework - spring-expression - - - org.springframework.security - spring-security-config - - - org.springframework.security - spring-security-web - - - org.springframework.security - spring-security-core - - - org.codehaus.jackson - jackson-mapper-asl - - - - - - xml-apis - xml-apis - ${xml-apis.version} - - - - oro - oro - ${oro.version} - jar - - - - - org.apache.struts - struts2-core - ${struts2.version} - jar - - - org.apache.logging.log4j - log4j-api - - - - - org.apache.struts - struts2-spring-plugin - ${struts2.version} - jar - - - - org.springframework - spring-core - - - org.springframework - spring-web - - - org.springframework - spring-context - - - org.springframework - spring-beans - - - - - org.apache.struts - struts2-tiles-plugin - ${struts2.version} - jar - - - ognl - ognl - ${ognl.version} - - - javassist - javassist - - - - - org.apache.struts - struts2-json-plugin - ${struts2.version} - jar - - - - org.apache.velocity - velocity-engine-core - ${velocity.version} - jar - - - commons-io - commons-io - - - - - org.javassist - javassist - ${javassist.version} - jar - - - - - org.jdom - jdom2 - ${jdom2.version} - jar - - - org.apache.tiles - tiles-api - ${tiles-core.version} - jar - - - javax.servlet - javax.servlet-api - ${javax.servlet-api.version} - provided - - - javax.annotation - javax.annotation-api - ${javax.annotation-api.version} - - - - org.junit.jupiter - junit-jupiter - ${junit-jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-api - ${junit-jupiter.version} - test - - - org.mockito - mockito-junit-jupiter - ${mockito.version} - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - org.mockito - mockito-inline - ${mockito.version} - test - - - org.hamcrest - hamcrest-all - ${hamcrest-all.version} - test - - - uk.org.webcompere - system-stubs-jupiter - ${system-stubs-jupiter.version} - test - - - org.apache.tomcat - tomcat-servlet-api - ${tomcat-servlet-api.version} - test - - - org.testcontainers - testcontainers-bom - ${testcontainers.version} - pom - import - - - javax.el - javax.el-api - ${javax.el-api.version} - - - org.glassfish.web - javax.el - ${javax.el.version} - - - org.im4java - im4java - ${im4java.version} - - - - - org.springframework - spring-test - ${spring.version} - test - - - org.apache.derby - derby - ${derby.version} - test - - - org.apache.derby - derbyclient - ${derby.version} - test - - - org.apache.derby - derbytools - ${derby.version} - test - - - - javax.servlet.jsp - jsp-api - ${jsp-api.version} - provided - - - - joda-time - joda-time - ${joda-time.version} - - - - org.apache.httpcomponents - httpclient - ${httpclient.version} - - - com.sun.xml.bind - jaxb-impl - ${jaxb-impl.version} - - - com.sun.xml.bind - jaxb-core - ${jaxb-impl.version} - - - com.sun.xml.bind - jaxb-xjc - ${jaxb-xjc.version} - - - org.codehaus.jettison - jettison - ${jettison.version} - - - - stax-api - stax - - - - - org.liquibase - liquibase-core - ${liquibase-core.version} - jar - - - org.aspectj - aspectjrt - ${aspectj.version} - - - org.aspectj - aspectjweaver - ${aspectj.version} - - - aopalliance - aopalliance - ${aopalliance.version} - - - org.slf4j - slf4j-api - ${slf4j.version} - jar - - - - org.slf4j - slf4j-jdk14 - ${slf4j.version} - jar - - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - log4j - log4j - - - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - - - org.slf4j - log4j-over-slf4j - ${slf4j.version} - - - org.apache.logging.log4j - log4j-to-slf4j - ${log4j2slf4j.version} - - - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson-databind.version} - - - com.fasterxml.jackson.module - jackson-module-jaxb-annotations - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - ${jackson.version} - - - - org.hibernate - hibernate-validator - ${hibernate-validator-version} - - - - org.assertj - assertj-core - ${assertj-core.version} - test - - - org.owasp.esapi - esapi - ${esapi.version} - - - org.beanshell - bsh-core - - - xerces - xercesImpl - - - xml-apis - xml-apis - - - commons-fileupload - commons-fileupload - - - commons-beanutils - commons-beanutils-core - - - - org.apache.xmlgraphics - batik-ext - - - - org.apache.xmlgraphics - batik-css - - - - xalan - xalan - - - xom - xom - - - org.owasp.antisamy - antisamy - - - log4j - log4j - - - commons-lang - commons-lang - - - commons-configuration - commons-configuration - - - - - - xom - xom - ${xom.version} - - - - - xerces - xercesImpl - - - - xalan - xalan - - - - - com.sun.mail - mailapi - ${sun-mail.version} - - - com.sun.activation - jakarta.activation - - - - - com.sun.mail - smtp - ${sun-mail.version} - - - org.subethamail - subethasmtp - ${subethasmtp.version} - test - - - slf4j-api - org.slf4j - - - - - org.apache.pdfbox - pdfbox - ${pdfbox.version} - - - org.apache.pdfbox - pdfbox-tools - ${pdfbox.version} - - - org.apache.pdfbox - preflight - ${pdfbox.version} - - - org.apache.pdfbox - xmpbox - ${pdfbox.version} - - - javax.xml.bind - jaxb-api - ${jaxb-api.version} - - - javax.activation - activation - ${activation.version} - - - jakarta.activation - jakarta.activation-api - ${jakarta-activation.version} - - - org.apache.commons - commons-compress - ${commons-compress.version} - - - io.lettuce - lettuce-core - ${lettuce-core.version} - - - io.swagger - swagger-annotations - ${swagger-annotations.version} - - - com.jayway.jsonpath - json-path - ${json-path.version} - - - net.minidev - json-smart - ${json-smart.version} - - - org.quartz-scheduler - quartz - ${quartz.version} - - - - - - - - jakarta.ws.rs - jakarta.ws.rs-api - ${jakarta.ws.rs-api.version} - - - org.hibernate.validator - hibernate-validator - ${hibernate-validator.version} - - - jakarta.xml.bind - jakarta.xml.bind-api - ${jakarta.xml.bind-api.version} - - - org.owasp.encoder - encoder - ${encoder.version} - - - org.owasp.encoder - encoder-jsp - ${encoder-jsp.version} - - - - - - - - org.apache.httpcomponents - httpcore - ${httpcore.version} - - - org.apache.logging.log4j - log4j-core - ${log4j-core.version} - - - org.springframework.security - spring-security-core - ${springsecurity.version} - - - com.fasterxml - classmate - ${classmate.version} - - - org.springframework.data - spring-data-commons - ${spring-data-commons.version} - - - io.springfox - springfox-core - ${springfox-swagger2.version} - - - io.springfox - springfox-swagger-common - ${springfox-swagger2.version} - - - io.springfox - springfox-spi - ${springfox-swagger2.version} - - - org.springframework.security - spring-security-crypto - ${springsecurity.version} - - - org.hamcrest - hamcrest - ${hamcrest.version} - - - org.glassfish.jaxb - jaxb-runtime - ${jaxb-runtime.version} - - - io.springfox - springfox-spring-web - ${springfox-swagger2.version} - - - jakarta.annotation - jakarta.annotation-api - ${jakarta.annotation-api.version} - - - com.google.protobuf - protobuf-java - ${protobuf-java.version} - - - xerces - xercesImpl - ${xercesImpl.version} - - - com.github.junrar - junrar - ${junrar.version} - - - io.netty - netty-common - ${netty.version} - - - io.netty - netty-buffer - ${netty.version} - - - io.netty - netty-codec - ${netty.version} - - - io.netty - netty-handler - ${netty.version} - - - io.netty - netty-resolver - ${netty.version} - - - io.netty - netty-transport - ${netty.version} - - - io.netty - netty-transport-classes-epoll - ${netty.version} - - - io.netty - netty-transport-native-epoll - ${netty.version} - - - io.netty - netty-transport-native-unix-common - ${netty.version} - - - commons-fileupload - commons-fileupload - ${commons-fileupload.version} - - - org.springframework.hateoas - spring-hateoas - ${spring-hateoas.version} - - - - com.fasterxml.woodstox - woodstox-core - ${fasterxml-woodstox.version} - - - org.xerial.snappy - snappy-java - ${snappy-java.version} - - - com.github.ben-manes.caffeine - caffeine - ${caffeine.version} - - - com.google.errorprone - error_prone_annotations - ${google-errorprone.version} - - - org.apache.zookeeper - zookeeper-jute - ${zookeeper.version} - - - org.apache.zookeeper - zookeeper - ${zookeeper.version} - - - - - - local-dev - - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-jar-plugin - - - org.apache.maven.plugins - maven-source-plugin - - - - - - diff --git a/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java b/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java index 232e5783d2..2c4723ccd3 100644 --- a/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java +++ b/versioning-plugin/src/main/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManager.java @@ -190,18 +190,54 @@ public void saveContentVersion(String contentId) throws EntException { this.deleteWorkVersions(versionRecord.getContentId(), onlineVersionsToDelete); } if (null == this.getVersioningDAO().getVersion(contentId, versionRecord.getVersion())) { - this.getVersioningDAO().addContentVersion(versionRecord); + try { + this.getVersioningDAO().addContentVersion(versionRecord); + } catch (RuntimeException e) { + if (isDuplicateKeyException(e)) { + _logger.warn("ContentId '{}' - version '{}' already exists (concurrent insert)", contentId, versionRecord.getVersion()); + } else { + throw e; + } + } } else { - logger.warn("ContentId '{}' - version '{}' already exists", contentId, versionRecord.getVersion()); + _logger.warn("ContentId '{}' - version '{}' already exists", contentId, versionRecord.getVersion()); } } } + } catch (EntException e) { + throw e; } catch (Exception e) { _logger.error("error in Error saving version for content {}", contentId, e); throw new EntException("Error saving version for content" + contentId); } } + private boolean isDuplicateKeyException(Throwable e) { + while (e != null) { + if (e instanceof java.sql.SQLException) { + java.sql.SQLException sqlEx = (java.sql.SQLException) e; + String sqlState = sqlEx.getSQLState(); + if (sqlState != null) { + // 23505: unique_violation (PostgreSQL, Derby) + // 23000: integrity constraint violation (MySQL, Oracle - need to check error code) + if ("23505".equals(sqlState)) { + return true; + } + if ("23000".equals(sqlState)) { + int errorCode = sqlEx.getErrorCode(); + // MySQL: 1062 (ER_DUP_ENTRY), 1586 (ER_DUP_ENTRY_WITH_KEY_NAME) + // Oracle: 1 (ORA-00001: unique constraint violated) + if (errorCode == 1062 || errorCode == 1586 || errorCode == 1) { + return true; + } + } + } + } + e = e.getCause(); + } + return false; + } + @Override public void deleteWorkVersions(String contentId, int onlineVersion) throws EntException { try { diff --git a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java new file mode 100644 index 0000000000..760bafc409 --- /dev/null +++ b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/DuplicateKeyExceptionTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.agiletec.plugins.jpversioning.aps.system.services.versioning; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Method; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for duplicate key exception detection across different JDBC databases. + */ +@ExtendWith(MockitoExtension.class) +class DuplicateKeyExceptionTest { + + private VersioningManager versioningManager; + private Method isDuplicateKeyExceptionMethod; + + @BeforeEach + void setUp() throws Exception { + versioningManager = new VersioningManager(); + // Access the private method via reflection + isDuplicateKeyExceptionMethod = VersioningManager.class.getDeclaredMethod("isDuplicateKeyException", Throwable.class); + isDuplicateKeyExceptionMethod.setAccessible(true); + } + + private boolean invokeIsDuplicateKeyException(Throwable e) throws Exception { + return (Boolean) isDuplicateKeyExceptionMethod.invoke(versioningManager, e); + } + + // ==================== PostgreSQL Tests ==================== + + @Test + void testPostgreSQLDuplicateKey() throws Exception { + // PostgreSQL uses SQLState 23505 for unique_violation + SQLException sqlEx = new SQLException("duplicate key value violates unique constraint", "23505"); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "PostgreSQL duplicate key should be detected"); + } + + @Test + void testPostgreSQLDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("duplicate key value violates unique constraint", "23505"); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped PostgreSQL duplicate key should be detected"); + } + + // ==================== Derby Tests ==================== + + @Test + void testDerbyDuplicateKey() throws Exception { + // Derby also uses SQLState 23505 for duplicate key + SQLException sqlEx = new SQLException("The statement was aborted because it would have caused a duplicate key value", "23505"); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "Derby duplicate key should be detected"); + } + + @Test + void testDerbyDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("The statement was aborted because it would have caused a duplicate key value", "23505"); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped Derby duplicate key should be detected"); + } + + // ==================== MySQL Tests ==================== + + @Test + void testMySQLDuplicateKey_ErrorCode1062() throws Exception { + // MySQL uses SQLState 23000 with error code 1062 (ER_DUP_ENTRY) + SQLException sqlEx = new SQLException("Duplicate entry 'value' for key 'PRIMARY'", "23000", 1062); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "MySQL duplicate key (1062) should be detected"); + } + + @Test + void testMySQLDuplicateKey_ErrorCode1586() throws Exception { + // MySQL uses SQLState 23000 with error code 1586 (ER_DUP_ENTRY_WITH_KEY_NAME) + SQLException sqlEx = new SQLException("Duplicate entry 'value' for key 'key_name'", "23000", 1586); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "MySQL duplicate key (1586) should be detected"); + } + + @Test + void testMySQLDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("Duplicate entry 'value' for key 'PRIMARY'", "23000", 1062); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped MySQL duplicate key should be detected"); + } + + // ==================== Oracle Tests ==================== + + @Test + void testOracleDuplicateKey() throws Exception { + // Oracle uses SQLState 23000 with error code 1 (ORA-00001: unique constraint violated) + SQLException sqlEx = new SQLException("ORA-00001: unique constraint (SCHEMA.CONSTRAINT_NAME) violated", "23000", 1); + assertTrue(invokeIsDuplicateKeyException(sqlEx), "Oracle duplicate key should be detected"); + } + + @Test + void testOracleDuplicateKeyWrappedInRuntimeException() throws Exception { + SQLException sqlEx = new SQLException("ORA-00001: unique constraint (SCHEMA.CONSTRAINT_NAME) violated", "23000", 1); + RuntimeException wrapped = new RuntimeException("Error adding version record", sqlEx); + assertTrue(invokeIsDuplicateKeyException(wrapped), "Wrapped Oracle duplicate key should be detected"); + } + + // ==================== Negative Tests ==================== + + @Test + void testNonDuplicateKeyException() throws Exception { + // A general SQL exception that is not a duplicate key + SQLException sqlEx = new SQLException("Connection refused", "08001"); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "Non-duplicate key exception should not be detected"); + } + + @Test + void testForeignKeyViolation_MySQL() throws Exception { + // MySQL foreign key violation: SQLState 23000 with error code 1452 + SQLException sqlEx = new SQLException("Cannot add or update a child row: a foreign key constraint fails", "23000", 1452); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "MySQL foreign key violation should not be detected as duplicate key"); + } + + @Test + void testForeignKeyViolation_PostgreSQL() throws Exception { + // PostgreSQL foreign key violation: SQLState 23503 + SQLException sqlEx = new SQLException("insert or update on table violates foreign key constraint", "23503"); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "PostgreSQL foreign key violation should not be detected as duplicate key"); + } + + @Test + void testNullException() throws Exception { + assertFalse(invokeIsDuplicateKeyException(null), "Null exception should return false"); + } + + @Test + void testNullSQLState() throws Exception { + SQLException sqlEx = new SQLException("Some error", (String) null); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "Null SQL state should return false"); + } + + @Test + void testNonSQLException() throws Exception { + RuntimeException ex = new RuntimeException("Some error"); + assertFalse(invokeIsDuplicateKeyException(ex), "Non-SQLException without cause should return false"); + } + + @Test + void testDeeplyNestedSQLException() throws Exception { + SQLException sqlEx = new SQLException("duplicate key", "23505"); + RuntimeException level1 = new RuntimeException("Level 1", sqlEx); + RuntimeException level2 = new RuntimeException("Level 2", level1); + RuntimeException level3 = new RuntimeException("Level 3", level2); + assertTrue(invokeIsDuplicateKeyException(level3), "Deeply nested SQLException should be detected"); + } + + @Test + void testIntegrityConstraint23000WithoutMatchingErrorCode() throws Exception { + // SQLState 23000 but with an error code that doesn't match duplicate key + SQLException sqlEx = new SQLException("Some other constraint violation", "23000", 9999); + assertFalse(invokeIsDuplicateKeyException(sqlEx), "SQLState 23000 with non-matching error code should return false"); + } +} \ No newline at end of file diff --git a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java index e443263c05..dd6201cef3 100644 --- a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java +++ b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/TestVersioningManager.java @@ -47,6 +47,7 @@ import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * @author G.Cocco @@ -58,6 +59,22 @@ public class TestVersioningManager extends BaseTestCase { private IVersioningManager versioningManager; private JpversioningTestHelper helper; + @BeforeEach + public void init() throws Exception { + this.versioningManager = (IVersioningManager) this.getService(JpversioningSystemConstants.VERSIONING_MANAGER); + this.configManager = (ConfigInterface) this.getService(SystemConstants.BASE_CONFIG_MANAGER); + this.contentManager = (IContentManager) this.getService(JacmsSystemConstants.CONTENT_MANAGER); + DataSource dataSource = (DataSource) this.getApplicationContext().getBean("portDataSource"); + this.helper = new JpversioningTestHelper(dataSource, this.getApplicationContext()); + this.helper.initContentVersions(); + } + + @AfterEach + public void dispose() throws Exception { + this.helper.cleanContentVersions(); + } + + @Test void testGetVersions() throws Throwable { List versions = this.versioningManager.getVersions("CNG12"); assertNull(versions); @@ -66,6 +83,7 @@ void testGetVersions() throws Throwable { this.checkVersionIds(new long[]{1, 2, 3}, versions); } + @Test void testGetLastVersions() throws Throwable { List versions = this.versioningManager.getLastVersions("CNG", null); assertTrue(versions.isEmpty()); @@ -74,6 +92,7 @@ void testGetLastVersions() throws Throwable { this.checkVersionIds(new long[]{3}, versions); } + @Test void testGetVersion() throws Throwable { ContentVersion contentVersion = this.versioningManager.getVersion(10000); assertNull(contentVersion); @@ -92,6 +111,7 @@ void testGetVersion() throws Throwable { assertEquals("admin", contentVersion.getUsername()); } + @Test void testGetLastVersion() throws Throwable { ContentVersion contentVersion = this.versioningManager.getLastVersion("CNG12"); assertNull(contentVersion); @@ -109,6 +129,7 @@ void testGetLastVersion() throws Throwable { assertEquals("mainEditor", contentVersion.getUsername()); } + @Test void testSaveGetDeleteVersion() throws Throwable { ((VersioningManager) this.versioningManager).saveContentVersion("ART102"); ContentVersion contentVersion = this.versioningManager.getLastVersion("ART102"); @@ -128,12 +149,20 @@ void testSaveGetDeleteVersion() throws Throwable { assertNull(this.versioningManager.getLastVersion("ART102")); } + @Test public void deleteWorkVersions() throws Throwable { - List versions = this.versioningManager.getVersions("ART1"); - this.checkVersionIds(new long[]{1, 2, 3}, versions); - this.versioningManager.deleteWorkVersions("ART1", 0); - versions = this.versioningManager.getVersions("ART1"); - this.checkVersionIds(new long[]{1, 3}, versions); + try { + this.updateConfigItem(JpversioningSystemConstants.CONFIG_PARAM_DELETE_MID_VERSIONS, "true"); + ((VersioningManager) this.versioningManager).initTenantAware(); + List versions = this.versioningManager.getVersions("ART1"); + this.checkVersionIds(new long[]{1, 2, 3}, versions); + this.versioningManager.deleteWorkVersions("ART1", 0); + versions = this.versioningManager.getVersions("ART1"); + this.checkVersionIds(new long[]{1, 3}, versions); + } finally { + this.updateConfigItem(JpversioningSystemConstants.CONFIG_PARAM_DELETE_MID_VERSIONS, ""); + ((VersioningManager) this.versioningManager).initTenantAware(); + } } private void checkVersionIds(long[] expected, List received) { @@ -145,11 +174,13 @@ private void checkVersionIds(long[] expected, List received) { } } + @Test void testContentVersionToIgnore_1() throws Exception { this.testContentVersionToIgnore(false, true); this.testContentVersionToIgnore(true, true); } + @Test void testContentVersionToIgnore_2() throws Exception { this.testContentVersionToIgnore(false, false); this.testContentVersionToIgnore(true, false); @@ -214,19 +245,5 @@ private void updateConfigItem(String paramKey, String paramValue) throws Excepti this.configManager.updateConfigItem(SystemConstants.CONFIG_ITEM_PARAMS, newXmlParams); } - @BeforeEach - private void init() throws Exception { - this.versioningManager = (IVersioningManager) this.getService(JpversioningSystemConstants.VERSIONING_MANAGER); - this.configManager = (ConfigInterface) this.getService(SystemConstants.BASE_CONFIG_MANAGER); - this.contentManager = (IContentManager) this.getService(JacmsSystemConstants.CONTENT_MANAGER); - DataSource dataSource = (DataSource) this.getApplicationContext().getBean("portDataSource"); - this.helper = new JpversioningTestHelper(dataSource, this.getApplicationContext()); - this.helper.initContentVersions(); - } - - @AfterEach - private void dispose() throws Exception { - this.helper.cleanContentVersions(); - } } diff --git a/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java new file mode 100644 index 0000000000..76c902f375 --- /dev/null +++ b/versioning-plugin/src/test/java/com/agiletec/plugins/jpversioning/aps/system/services/versioning/VersioningManagerUnitTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.agiletec.plugins.jpversioning.aps.system.services.versioning; + +import com.agiletec.aps.system.services.baseconfig.ConfigInterface; +import com.agiletec.plugins.jacms.aps.system.services.content.IContentManager; +import com.agiletec.plugins.jacms.aps.system.services.content.model.Content; +import com.agiletec.plugins.jacms.aps.system.services.content.model.ContentRecordVO; +import org.entando.entando.ent.exception.EntException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.SQLException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class VersioningManagerUnitTest { + + @Mock + private IVersioningDAO versioningDAO; + + @Mock + private IContentManager contentManager; + + @Mock + private ConfigInterface configManager; + + @Spy + @InjectMocks + private VersioningManager versioningManager; + + private ContentRecordVO mockContentRecord; + + @BeforeEach + void setUp() { + mockContentRecord = new ContentRecordVO(); + mockContentRecord.setId("ART123"); + mockContentRecord.setTypeCode("ART"); + mockContentRecord.setDescription("Test Content"); + mockContentRecord.setStatus(Content.STATUS_DRAFT); + mockContentRecord.setXmlWork("test"); + mockContentRecord.setModify(new Date()); + mockContentRecord.setVersion("1.0"); + mockContentRecord.setLastEditor("admin"); + } + + @Test + void testSaveContentVersion_ThrowsExceptionForNonDuplicateKeyError() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a generic RuntimeException (not duplicate key) + RuntimeException genericException = new RuntimeException("Database connection failed"); + doThrow(genericException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException wrapping the original exception + EntException thrown = assertThrows(EntException.class, + () -> versioningManager.saveContentVersion("ART123")); + + assertEquals("Error saving version for contentART123", thrown.getMessage()); + } + + @Test + void testSaveContentVersion_HandlesUniqueConstraintViolationGracefully() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a duplicate key exception (PostgreSQL/Derby SQLState 23505) + SQLException sqlException = new SQLException("unique constraint violation", "23505"); + RuntimeException duplicateKeyException = new RuntimeException("Error adding version record", sqlException); + doThrow(duplicateKeyException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception for duplicate key + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + + // Verify addContentVersion was called + verify(versioningDAO).addContentVersion(any(ContentVersion.class)); + } + + @Test + void testSaveContentVersion_HandlesMySQLDuplicateKeyGracefully() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a MySQL duplicate key exception (SQLState 23000, error code 1062) + SQLException sqlException = new SQLException("Duplicate entry for key 'PRIMARY'", "23000", 1062); + RuntimeException duplicateKeyException = new RuntimeException("Error adding version record", sqlException); + doThrow(duplicateKeyException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_HandlesOracleDuplicateKeyGracefully() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate an Oracle duplicate key exception (SQLState 23000, error code 1) + SQLException sqlException = new SQLException("ORA-00001: unique constraint violated", "23000", 1); + RuntimeException primaryKeyException = new RuntimeException("Error adding version record", sqlException); + doThrow(primaryKeyException).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_ThrowsForNullPointerException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a NullPointerException (should NOT be treated as duplicate key) + NullPointerException npe = new NullPointerException("Some null value"); + doThrow(npe).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException + assertThrows(EntException.class, () -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_ThrowsForIllegalArgumentException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate an IllegalArgumentException (should NOT be treated as duplicate key) + IllegalArgumentException iae = new IllegalArgumentException("Invalid argument"); + doThrow(iae).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException + assertThrows(EntException.class, () -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_SkipsInsertWhenVersionAlreadyExists() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + + // Return existing version (not null) - simulates version already exists + ContentVersion existingVersion = new ContentVersion(); + existingVersion.setId(1L); + when(versioningDAO.getVersion("ART123", "1.0")).thenReturn(existingVersion); + + // Execute + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + + // Verify addContentVersion was NEVER called since version already exists + verify(versioningDAO, never()).addContentVersion(any(ContentVersion.class)); + } + + @Test + void testSaveContentVersion_HandlesNestedDuplicateKeyException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a deeply nested exception where the root cause is a SQLException + SQLException sqlException = new SQLException("duplicate key value violates unique constraint", "23505"); + RuntimeException cause = new RuntimeException("Database error", sqlException); + RuntimeException wrapper = new RuntimeException("Insert failed", cause); + doThrow(wrapper).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute - should NOT throw exception because nested cause has duplicate key + assertDoesNotThrow(() -> versioningManager.saveContentVersion("ART123")); + } + + @Test + void testSaveContentVersion_ThrowsForNestedNonDuplicateException() throws Exception { + // Setup + when(contentManager.loadContentVO("ART123")).thenReturn(mockContentRecord); + when(versioningDAO.getVersion(anyString(), anyString())).thenReturn(null); + + // Simulate a nested exception without duplicate key indicators + RuntimeException cause = new RuntimeException("Connection timeout"); + RuntimeException wrapper = new RuntimeException("Database error", cause); + doThrow(wrapper).when(versioningDAO).addContentVersion(any(ContentVersion.class)); + + // Execute and verify - should throw EntException + assertThrows(EntException.class, () -> versioningManager.saveContentVersion("ART123")); + } +} \ No newline at end of file diff --git a/webapp/README.md b/webapp/README.md index 5ec00a23bd..21f9886bd9 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -12,6 +12,8 @@ Here the command to use mvn clean package jetty:run-war -Pjetty-local -Pderby ``` +> ⚠️ **WARNING:** Running without `-Pkeycloak` disables Keycloak and enables Basic Authentication instead. This is **UNSAFE FOR PRODUCTION** and should only be used for local development. + If you want to use keycloak as external authorization service, add the keycloak profile and update the proper variables (you can find them in the `properties` tag in the pom) diff --git a/webapp/pom.xml b/webapp/pom.xml index 14b513f204..b145f6d23e 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -48,7 +48,7 @@ 10.1.46 - true + false http://localhost:8081/auth entando entando-app @@ -344,6 +344,12 @@ oracle.jdbc.OracleDriver + + keycloak + + true + + contentscheduler diff --git a/webapp/src/main/webapp/WEB-INF/web.xml b/webapp/src/main/webapp/WEB-INF/web.xml index c7c6314a96..db0b3ddc62 100644 --- a/webapp/src/main/webapp/WEB-INF/web.xml +++ b/webapp/src/main/webapp/WEB-INF/web.xml @@ -107,6 +107,14 @@ XSSFilter /pages/* + + XSSFilter + /page/* + + + XSSFilter + /preview/* + CharacterEncodingFilter *