From d6dfad9ca729e158d2faed42d7a76083996c4a98 Mon Sep 17 00:00:00 2001
From: Sebastian Wollner <wollner@edu-sharing.net>
Date: Mon, 25 Nov 2024 12:00:51 +0100
Subject: [PATCH 1/3] feat: ability to enforce Configuration Context
 #ITSJOINLTY-1715

---
 .../extension/custom-cache-context.xml        |  10 +-
 .../restservices/ApiAuthenticationFilter.java |  60 +++-----
 .../restservices/admin/v1/AdminApi.java       |  50 +++++-
 .../restservices/config/v1/ConfigApi.java     |   2 +-
 .../service/admin/AdminService.java           |  18 +--
 .../service/config/ConfigService.java         |   4 +-
 .../service/config/ConfigServiceFactory.java  |  48 +++++-
 .../service/config/ConfigServiceImpl.java     |  73 +++++++--
 .../rest/api/src/main/resources/openapi.json  | 144 ++++++++++++++++++
 9 files changed, 333 insertions(+), 76 deletions(-)

diff --git a/Backend/alfresco/module/src/main/amp/config/alfresco/extension/custom-cache-context.xml b/Backend/alfresco/module/src/main/amp/config/alfresco/extension/custom-cache-context.xml
index b4e235ab9..3d77059c7 100644
--- a/Backend/alfresco/module/src/main/amp/config/alfresco/extension/custom-cache-context.xml
+++ b/Backend/alfresco/module/src/main/amp/config/alfresco/extension/custom-cache-context.xml
@@ -54,9 +54,13 @@
         <constructor-arg value="cache.eduSharingConfigCache"/>
     </bean>
 
-  <bean name="eduSharingContextCache" factory-bean="cacheFactory" factory-method="createCache">
-    <constructor-arg value="cache.eduSharingContextCache"/>
-  </bean>
+    <bean name="eduSharingContextCacheByDomain" factory-bean="cacheFactory" factory-method="createCache">
+      <constructor-arg value="cache.eduSharingContextCacheByDomain"/>
+    </bean>
+
+    <bean name="eduSharingContextCacheById" factory-bean="cacheFactory" factory-method="createCache">
+      <constructor-arg value="cache.eduSharingContextCacheByDomain"/>
+    </bean>
 
     <bean name="eduSharingVersionCache" factory-bean="cacheFactory" factory-method="createCache">
         <constructor-arg value="cache.eduSharingVersionCache"/>
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/restservices/ApiAuthenticationFilter.java b/Backend/services/core/src/main/java/org/edu_sharing/restservices/ApiAuthenticationFilter.java
index e63257d06..e63c27655 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/restservices/ApiAuthenticationFilter.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/restservices/ApiAuthenticationFilter.java
@@ -1,29 +1,17 @@
 package org.edu_sharing.restservices;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-
 import com.typesafe.config.Config;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.FilterConfig;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
+import jakarta.servlet.*;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
-
-import org.alfresco.repo.security.authentication.AuthenticationComponent;
 import org.apache.log4j.Logger;
 import org.edu_sharing.alfresco.authentication.subsystems.SubsystemChainingAuthenticationService;
 import org.edu_sharing.alfresco.lightbend.LightbendConfigLoader;
-import org.edu_sharing.alfrescocontext.gate.AlfAppContextGate;
 import org.edu_sharing.repository.client.tools.CCConstants;
 import org.edu_sharing.repository.server.AuthenticationToolAPI;
 import org.edu_sharing.repository.server.authentication.AuthenticationFilter;
 import org.edu_sharing.repository.server.authentication.ContextManagementFilter;
-import org.edu_sharing.service.authentication.EduAuthentication;
 import org.edu_sharing.service.authentication.oauth2.TokenService;
 import org.edu_sharing.service.authentication.oauth2.TokenService.Token;
 import org.edu_sharing.service.authority.AuthorityServiceFactory;
@@ -33,16 +21,19 @@
 import org.edu_sharing.spring.security.basic.CSRFConfig;
 import org.springframework.context.ApplicationContext;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
 public class ApiAuthenticationFilter implements jakarta.servlet.Filter {
 
     Logger logger = Logger.getLogger(ApiAuthenticationFilter.class);
 
     private TokenService tokenService;
 
-    @Override
-    public void destroy() {
-    }
-
     @Override
     public void doFilter(ServletRequest req, ServletResponse resp,
                          FilterChain chain) throws IOException, ServletException {
@@ -106,20 +97,18 @@ public void doFilter(ServletRequest req, ServletResponse resp,
                 }
             } else if (authHdr.length() > 10 && authHdr.substring(0, 10).equalsIgnoreCase(CCConstants.AUTH_HEADER_EDU_TICKET)) {
                 String ticket = authHdr.substring(10).trim();
-                if (ticket != null) {
-                    if (authTool.validateTicket(ticket)) {
-                        // Force a renew of all toolpermissions since they might have now changed!
-                        ToolPermissionServiceFactory.getInstance().getAllAvailableToolPermissions(true);
-                        //if its APIClient username is ignored and is figured out with authentication service
-                        authTool.storeAuthInfoInSession(authTool.getCurrentUser(), ticket, CCConstants.AUTH_TYPE_TICKET, httpReq.getSession());
-                        validatedAuth = authTool.validateAuthentication(session);
-                    }
+                if (authTool.validateTicket(ticket)) {
+                    // Force a renew of all toolpermissions since they might have now changed!
+                    ToolPermissionServiceFactory.getInstance().getAllAvailableToolPermissions(true);
+                    //if its APIClient username is ignored and is figured out with authentication service
+                    authTool.storeAuthInfoInSession(authTool.getCurrentUser(), ticket, CCConstants.AUTH_TYPE_TICKET, httpReq.getSession());
+                    validatedAuth = authTool.validateAuthentication(session);
                 }
             }
 
         }
         Config accessConfig = LightbendConfigLoader.get().getConfig("security.access");
-        List<String> AUTHLESS_ENDPOINTS = Arrays.asList(new String[]{"/authentication", "/_about", "/config", "/register", "/sharing",
+        List<String> AUTHLESS_ENDPOINTS = Arrays.asList("/authentication", "/_about", "/config", "/register", "/sharing",
                 "/lti/v13/oidc/login_initiations",
                 "/lti/v13/lti13",
                 "/lti/v13/registration/dynamic",
@@ -127,13 +116,13 @@ public void doFilter(ServletRequest req, ServletResponse resp,
                 "/lti/v13/details",
                 "/ltiplatform/v13/openid-configuration",
                 "/ltiplatform/v13/openid-registration",
-                "/ltiplatform/v13/content"});
-        List<String> ADMIN_ENDPOINTS = Arrays.asList(new String[]{"/admin", "/bulk", "/lti/v13/registration/static", "/lti/v13/registration/url"});
+                "/ltiplatform/v13/content");
+        List<String> ADMIN_ENDPOINTS = Arrays.asList("/admin", "/bulk", "/lti/v13/registration/static", "/lti/v13/registration/url");
         List<String> DISABLED_ENDPOINTS = new ArrayList<>();
 
         try {
-            if (!ConfigServiceFactory.getCurrentConfig(req).getValue("register.local", true)) {
-                if (ConfigServiceFactory.getCurrentConfig(req).getValue("register.recoverPassword", false)) {
+            if (!ConfigServiceFactory.getCurrentConfig(httpReq).getValue("register.local", true)) {
+                if (ConfigServiceFactory.getCurrentConfig(httpReq).getValue("register.recoverPassword", false)) {
                     DISABLED_ENDPOINTS.add("/register/v1/register");
                     DISABLED_ENDPOINTS.add("/register/v1/activate");
                 } else {
@@ -141,7 +130,7 @@ public void doFilter(ServletRequest req, ServletResponse resp,
                     DISABLED_ENDPOINTS.add("/register");
                 }
             }
-        } catch (Exception e) {
+        } catch (Exception ignored) {
         }
 
         boolean noAuthenticationNeeded = false;
@@ -203,13 +192,8 @@ public void doFilter(ServletRequest req, ServletResponse resp,
             return;
         }
 
-        /**
-         * allow authless calls with AUTH_SINGLE_USE_NODEID by appauth
-         */
-        boolean trustedAuth = false;
-        if (ContextManagementFilter.accessTool != null && ContextManagementFilter.accessTool.get() != null) {
-            trustedAuth = true;
-        }
+        // allow authless calls with AUTH_SINGLE_USE_NODEID by appauth
+        boolean trustedAuth = ContextManagementFilter.accessTool != null && ContextManagementFilter.accessTool.get() != null;
 
         // ignore the auth for the login
         if (validatedAuth == null && (!noAuthenticationNeeded && !trustedAuth)) {
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/restservices/admin/v1/AdminApi.java b/Backend/services/core/src/main/java/org/edu_sharing/restservices/admin/v1/AdminApi.java
index 0420b0f15..01a5961fb 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/restservices/admin/v1/AdminApi.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/restservices/admin/v1/AdminApi.java
@@ -11,6 +11,10 @@
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Response;
 import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
 import org.alfresco.service.ServiceRegistry;
 import org.alfresco.service.cmr.action.Action;
@@ -44,6 +48,7 @@
 import org.edu_sharing.service.admin.model.RepositoryConfig;
 import org.edu_sharing.service.admin.model.ServerUpdateInfo;
 import org.edu_sharing.service.admin.model.ToolPermission;
+import org.edu_sharing.service.config.ConfigServiceFactory;
 import org.edu_sharing.service.lifecycle.PersonDeleteOptions;
 import org.edu_sharing.service.lifecycle.PersonLifecycleService;
 import org.edu_sharing.service.lifecycle.PersonReport;
@@ -57,10 +62,6 @@
 import org.edu_sharing.service.version.RepositoryVersionInfo;
 import org.glassfish.jersey.media.multipart.FormDataParam;
 
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.ws.rs.*;
-import jakarta.ws.rs.core.Context;
-import jakarta.ws.rs.core.Response;
 import java.io.FileWriter;
 import java.io.InputStream;
 import java.io.Serializable;
@@ -1523,6 +1524,47 @@ public Response setConfig(@Context HttpServletRequest req,RepositoryConfig confi
 			return ErrorResponse.createResponse(t);
 		}
 	}
+
+	@PUT
+	@Path("/repositoryConfig/enforceContext/{contextId}")
+	@Operation(summary = "set/update the repository config object")
+	@ApiResponses(value = {
+			@ApiResponse(responseCode="200", description=RestConstants.HTTP_200, content = @Content(schema = @Schema(implementation = Void.class))),
+			@ApiResponse(responseCode="400", description=RestConstants.HTTP_400, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="401", description=RestConstants.HTTP_401, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="403", description=RestConstants.HTTP_403, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="404", description=RestConstants.HTTP_404, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="500", description=RestConstants.HTTP_500, content = @Content(schema = @Schema(implementation = ErrorResponse.class))) })
+	public Response enforceContext(@Context HttpServletRequest req, @PathParam("contextId") String contextId) {
+		try {
+			ConfigServiceFactory.enforceContext(contextId);
+			return Response.ok().build();
+		} catch (Throwable t) {
+			return ErrorResponse.createResponse(t);
+		}
+	}
+
+	@DELETE
+	@Path("/repositoryConfig/enforceContext")
+	@Operation(summary = "set/update the repository config object")
+	@ApiResponses(value = {
+			@ApiResponse(responseCode="200", description=RestConstants.HTTP_200, content = @Content(schema = @Schema(implementation = Void.class))),
+			@ApiResponse(responseCode="400", description=RestConstants.HTTP_400, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="401", description=RestConstants.HTTP_401, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="403", description=RestConstants.HTTP_403, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="404", description=RestConstants.HTTP_404, content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+			@ApiResponse(responseCode="500", description=RestConstants.HTTP_500, content = @Content(schema = @Schema(implementation = ErrorResponse.class))) })
+	public Response clearEnforcedContext(@Context HttpServletRequest req) {
+		try {
+			ConfigServiceFactory.clearEnforcedContext();
+			return Response.ok().build();
+		} catch (Throwable t) {
+			return ErrorResponse.createResponse(t);
+		}
+	}
+
+
+
 	@GET
 	@Path("/configFile")
 	@Operation(summary = "get a base system config file (e.g. edu-sharing.conf)")
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/restservices/config/v1/ConfigApi.java b/Backend/services/core/src/main/java/org/edu_sharing/restservices/config/v1/ConfigApi.java
index 49704fbb5..7498aea31 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/restservices/config/v1/ConfigApi.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/restservices/config/v1/ConfigApi.java
@@ -54,7 +54,7 @@ public Response getConfig() {
             ConfigService configService = ConfigServiceFactory.getConfigService();
             config.setGlobal(configService.getConfig().values);
             try {
-                Context context = configService.getContext(ConfigServiceFactory.getCurrentDomain());
+                Context context = configService.getContextByDomain(ConfigServiceFactory.getCurrentDomain());
                 if (context != null) {
                     config.setContextId(context.id);
                     config.setCurrent(configService.getConfigByContext(context).values);
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/service/admin/AdminService.java b/Backend/services/core/src/main/java/org/edu_sharing/service/admin/AdminService.java
index 18d6f95df..340a44b48 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/service/admin/AdminService.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/service/admin/AdminService.java
@@ -1,28 +1,28 @@
 package org.edu_sharing.service.admin;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-
 import org.alfresco.service.cmr.repository.NodeRef;
 import org.edu_sharing.repository.client.rpc.cache.CacheCluster;
 import org.edu_sharing.repository.client.rpc.cache.CacheInfo;
 import org.edu_sharing.repository.server.jobs.quartz.ImmediateJobListener;
 import org.edu_sharing.repository.server.jobs.quartz.JobDescription;
+import org.edu_sharing.repository.server.jobs.quartz.JobInfo;
 import org.edu_sharing.repository.server.tools.ApplicationInfo;
 import org.edu_sharing.repository.server.tools.PropertiesHelper;
 import org.edu_sharing.restservices.admin.v1.model.PluginStatus;
 import org.edu_sharing.service.admin.model.GlobalGroup;
-import org.edu_sharing.repository.server.jobs.quartz.JobInfo;
 import org.edu_sharing.service.admin.model.RepositoryConfig;
 import org.edu_sharing.service.admin.model.ServerUpdateInfo;
 import org.edu_sharing.service.admin.model.ToolPermission;
 import org.edu_sharing.service.version.RepositoryVersionInfo;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
 public interface AdminService {
 
     List<JobInfo> getJobs() throws Throwable;
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigService.java b/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigService.java
index 1b9e4d116..4a0264d96 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigService.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigService.java
@@ -17,9 +17,11 @@ public interface ConfigService {
      * @return
      * @throws Exception
      */
-    Context getContext(String domain) throws Exception;
+    Context getContextByDomain(String domain) throws Exception;
     List<Context> getAvailableContext() throws Exception;
 
+    Context getContextById(String id) throws Exception;
+
     Context createOrUpdateContext(Context context);
 
     Config getConfigByDomain(String domain) throws Exception;
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceFactory.java b/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceFactory.java
index 618d7fc7f..2cf0924f7 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceFactory.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceFactory.java
@@ -1,21 +1,23 @@
 package org.edu_sharing.service.config;
 
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.log4j.Logger;
-import org.edu_sharing.repository.server.AuthenticationToolAPI;
 import org.edu_sharing.alfresco.repository.server.authentication.Context;
 import org.edu_sharing.alfresco.service.config.model.Config;
 import org.edu_sharing.alfresco.service.config.model.KeyValuePair;
 import org.edu_sharing.alfresco.service.config.model.Language;
+import org.edu_sharing.repository.server.AuthenticationToolAPI;
 import org.edu_sharing.repository.server.RequestHelper;
-
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.http.HttpServletRequest;
 import org.edu_sharing.spring.ApplicationContextFactory;
 
+import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 
 public class ConfigServiceFactory {
-    private static final String[] DEFAULT_LANGUAGES = new String[]{"de", "en"};
+	private static final String[] DEFAULT_LANGUAGES = new String[]{"de", "en"};
+	public static final String ENFORCED_CONTEXT = "ENFORCED_CONTEXT";
 	static Logger logger = Logger.getLogger(ConfigServiceFactory.class);
 
 	public static ConfigService getConfigService(){
@@ -33,7 +35,7 @@ public static String getCurrentContextId(){
 	}
 	public static String getCurrentContextId(HttpServletRequest req){
 		try {
-			org.edu_sharing.alfresco.service.config.model.Context context = getConfigService().getContext(getCurrentDomain(req));
+			org.edu_sharing.alfresco.service.config.model.Context context = getConfigService().getContextByDomain(getCurrentDomain(req));
 			if(context == null) {
 				return null;
 			}
@@ -43,7 +45,7 @@ public static String getCurrentContextId(HttpServletRequest req){
 			return null;
 		}
 	}
-	public static Config getCurrentConfig(ServletRequest req) throws Exception {
+	public static Config getCurrentConfig(HttpServletRequest req) throws Exception {
 		try {
 			return getConfigService().getConfigByDomain(req==null ? getCurrentDomain() : getCurrentDomain(req));
 		}catch(Throwable t) {
@@ -54,12 +56,42 @@ public static Config getCurrentConfig(ServletRequest req) throws Exception {
 	public static String getCurrentDomain() {
 		return getCurrentDomain(Context.getCurrentInstance().getRequest());
 	}
-	public static String getCurrentDomain(ServletRequest req) {
+
+	public static String getCurrentDomain(HttpServletRequest req) {
+		Object enforcedContext = req.getSession().getAttribute(ENFORCED_CONTEXT);
+		if(StringUtils.isNotBlank((String)enforcedContext)){
+			return enforcedContext.toString();
+		}
+
 		String domain = new RequestHelper(req).getServerName();
 		logger.debug("current domain:" + domain);
 		return domain;
 	}
 
+	public static void enforceContext(String contextId){
+        try {
+			org.edu_sharing.alfresco.service.config.model.Context context = getConfigService().getContextById(contextId);
+			if(context == null){
+				throw new IllegalArgumentException(String.format("Context with contextId %s does not exists",contextId));
+			}
+
+			if(context.domain == null || context.domain.length == 0){
+				throw new IllegalArgumentException(String.format("Context %s doesn't has domains",contextId));
+			}
+
+			Optional<String> domain = Arrays.stream(context.domain).findFirst();
+			Context.getCurrentInstance().getRequest().getSession().setAttribute(ENFORCED_CONTEXT, domain.get());
+		} catch (RuntimeException e) {
+			throw e;
+		} catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+	}
+
+	public static void clearEnforcedContext(){
+		Context.getCurrentInstance().getRequest().getSession().removeAttribute(ENFORCED_CONTEXT);
+	}
+
 	public static List<KeyValuePair> getLanguageData(List<Language> languages,String language) {
 		if(languages!=null && languages.size()>0) {
 			for(org.edu_sharing.alfresco.service.config.model.Language entry : languages) {
diff --git a/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceImpl.java b/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceImpl.java
index 4fdec3579..86e5087d1 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceImpl.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/service/config/ConfigServiceImpl.java
@@ -45,7 +45,8 @@ public class ConfigServiceImpl implements ConfigService, ApplicationListener<Ref
     private static String CACHE_KEY = "CLIENT_CONFIG";
     // we use a non-serializable Config as value because this is a local cache and not distributed
     private static SimpleCache<String, Config> configCache = AlfAppContextGate.getApplicationContext().getBean("eduSharingConfigCache", SimpleCache.class);
-    private static SimpleCache<String, Context> contextCache = AlfAppContextGate.getApplicationContext().getBean("eduSharingContextCache", SimpleCache.class);
+    private static SimpleCache<String, Context> contextCacheByDomain = AlfAppContextGate.getApplicationContext().getBean("eduSharingContextCacheByDomain", SimpleCache.class);
+    private static SimpleCache<String, Context> contextCacheById = AlfAppContextGate.getApplicationContext().getBean("eduSharingContextCacheById", SimpleCache.class);
 
     private static final Unmarshaller jaxbUnmarshaller;
     private final ObjectMapper objectMapper = new ObjectMapper();
@@ -106,16 +107,26 @@ public void deleteContext(String id) throws Exception {
     }
 
     @Override
-    public Context getContext(String domain) throws Exception {
-        if(StringUtils.isBlank(domain)) {
+    public Context getContextByDomain(String domain) throws Exception {
+        if (StringUtils.isBlank(domain)) {
             return null;
         }
         buildContextCache();
-        return contextCache.get(domain);
+        return contextCacheByDomain.get(domain);
     }
 
     @Override
-    public List<Context> getAvailableContext() throws Exception {
+    public Context getContextById(String id) throws Exception {
+        if (StringUtils.isBlank(id)) {
+            return null;
+        }
+
+        buildContextCache();
+        return contextCacheById.get(id);
+    }
+
+    @Override
+    public List<Context> getAvailableContext() {
         return AuthenticationUtil.runAsSystem(() -> {
             String eduSharingSystemFolderContext = userEnvironmentTool.getEdu_SharingContextFolder();
             Map<String, Map<String, Object>> dynamicContextObjects = nodeService.getChildrenPropsByType(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, eduSharingSystemFolderContext, CCConstants.CCM_TYPE_CONTEXT);
@@ -130,9 +141,7 @@ public List<Context> getAvailableContext() throws Exception {
     }
 
     private void buildContextCache() throws Exception {
-        if (contextCache.getKeys().isEmpty()) {
-            // put an element so that next time, the cache is never empty!
-            contextCache.put("", null);
+        if (contextCacheByDomain.getKeys().isEmpty()) {
             Config config = getConfig();
             if (config.contexts != null && config.contexts.context != null) {
                 for (Context context : config.contexts.context) {
@@ -141,7 +150,7 @@ private void buildContextCache() throws Exception {
                     }
 
                     for (String dom : context.domain) {
-                        contextCache.put(dom, context);
+                        contextCacheByDomain.put(dom, context);
                     }
                 }
             }
@@ -155,13 +164,53 @@ private void buildContextCache() throws Exception {
                         .map(x -> x.get(CCConstants.CCM_PROP_CONTEXT_CONFIG).toString())
                         .map(CheckedFunction.wrap(x -> objectMapper.readValue(x, Context.class), null))
                         .filter(Objects::nonNull)
-                        .forEach(x -> Arrays.stream(x.domain).forEach(y -> contextCache.put(y, x)));
+                        .forEach(x -> Arrays.stream(x.domain).filter(Objects::nonNull).forEach(y -> contextCacheByDomain.put(y, x)));
+
+                return null;
+            });
+
+
+
+            if(contextCacheByDomain.getKeys().isEmpty()){
+                // put an element so that next time, the cache is never empty!
+                contextCacheByDomain.put("", null);
+            }
+        }
+
+        if(contextCacheById.getKeys().isEmpty()){
+            Config config = getConfig();
+            if (config.contexts != null && config.contexts.context != null) {
+                for (Context context : config.contexts.context) {
+                    if (StringUtils.isNotBlank(context.id)) {
+                        contextCacheById.put(context.id, context);
+                    }
+                }
+            }
 
+            AuthenticationUtil.runAsSystem(() -> {
+                String eduSharingSystemFolderContext = userEnvironmentTool.getEdu_SharingContextFolder();
+                Map<String, Map<String, Object>> dynamicContextObjects = nodeService.getChildrenPropsByType(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, eduSharingSystemFolderContext, CCConstants.CCM_TYPE_CONTEXT);
+                dynamicContextObjects
+                        .values()
+                        .stream()
+                        .map(x -> x.get(CCConstants.CCM_PROP_CONTEXT_CONFIG).toString())
+                        .map(CheckedFunction.wrap(x -> objectMapper.readValue(x, Context.class), null))
+                        .filter(Objects::nonNull)
+                        .filter(x -> StringUtils.isNotBlank(x.id))
+                        .forEach(x -> contextCacheById.put(x.id, x));
                 return null;
             });
+
+            if(contextCacheById.getKeys().isEmpty()){
+                // put an element so that next time, the cache is never empty!
+                contextCacheById.put("", null);
+            }
         }
     }
 
+
+
+
     @Override
     public Context createOrUpdateContext(Context context) {
         return AuthenticationUtil.runAsSystem(() -> {
@@ -183,7 +232,7 @@ public Context createOrUpdateContext(Context context) {
 
     @Override
     public Config getConfigByDomain(String domain) throws Exception {
-        Context context = getContext(domain);
+        Context context = getContextByDomain(domain);
         if (context == null) {
             throw new IllegalArgumentException("Context with domain " + domain + " does not exists");
         }
@@ -308,7 +357,7 @@ private void overrideValues(Values values, Values override) throws
 
     private void refresh() {
         configCache.clear();
-        contextCache.clear();
+        contextCacheByDomain.clear();
         try {
             getConfig();
         } catch (Exception e) {
diff --git a/Backend/services/rest/api/src/main/resources/openapi.json b/Backend/services/rest/api/src/main/resources/openapi.json
index 724e4ebc5..079285d0a 100644
--- a/Backend/services/rest/api/src/main/resources/openapi.json
+++ b/Backend/services/rest/api/src/main/resources/openapi.json
@@ -11165,6 +11165,150 @@
         ]
       }
     },
+    "/admin/v1/repositoryConfig/enforceContext": {
+      "delete": {
+        "operationId": "clearEnforcedContext",
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {}
+            },
+            "description": "OK."
+          },
+          "400": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Preconditions are not present."
+          },
+          "401": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Authorization failed."
+          },
+          "403": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Session user has insufficient rights to perform this operation."
+          },
+          "404": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Ressources are not found."
+          },
+          "500": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Fatal error occured."
+          }
+        },
+        "summary": "set/update the repository config object",
+        "tags": [
+          "ADMIN v1"
+        ]
+      }
+    },
+    "/admin/v1/repositoryConfig/enforceContext/{contextId}": {
+      "put": {
+        "operationId": "enforceContext",
+        "parameters": [
+          {
+            "in": "path",
+            "name": "contextId",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {}
+            },
+            "description": "OK."
+          },
+          "400": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Preconditions are not present."
+          },
+          "401": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Authorization failed."
+          },
+          "403": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Session user has insufficient rights to perform this operation."
+          },
+          "404": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Ressources are not found."
+          },
+          "500": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            },
+            "description": "Fatal error occured."
+          }
+        },
+        "summary": "set/update the repository config object",
+        "tags": [
+          "ADMIN v1"
+        ]
+      }
+    },
     "/admin/v1/serverUpdate/list": {
       "get": {
         "description": "list available update tasks",

From dad0e6859075a21400a808364dc00e47a27c9bd4 Mon Sep 17 00:00:00 2001
From: Torsten Simon <simon@edu-sharing.net>
Date: Wed, 16 Oct 2024 10:46:42 +0200
Subject: [PATCH 2/3] fix:default license version 4.0 if values are unset

---
 .../edu_sharing/service/license/LicenseService.java    |  6 +++---
 .../license-details/license-details.component.ts       | 10 +++++-----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/Backend/services/core/src/main/java/org/edu_sharing/service/license/LicenseService.java b/Backend/services/core/src/main/java/org/edu_sharing/service/license/LicenseService.java
index 59bfc681a..2812f5465 100644
--- a/Backend/services/core/src/main/java/org/edu_sharing/service/license/LicenseService.java
+++ b/Backend/services/core/src/main/java/org/edu_sharing/service/license/LicenseService.java
@@ -4,7 +4,7 @@
 import org.edu_sharing.repository.tools.URLHelper;
 
 public class LicenseService {
-
+	private static String DEFAULT_LICENSE_VERSION = "4.0";
 	public String getIconUrl(String license,boolean dynamic){
 		if(license==null || license.isEmpty())
 			license="none";
@@ -58,7 +58,7 @@ public String getLicenseUrl(String license, String locale, String version){
 		}
 		
 		if(result != null){
-			version = (version == null) ? "3.0" : version;
+			version = (version == null) ? DEFAULT_LICENSE_VERSION : version;
 			if(result.contains("${version}")){
 				result = result.replace("${version}", version);
 			}
@@ -71,5 +71,5 @@ public String getLicenseUrl(String license, String locale, String version){
 		
 		return result;
 	}
-		
+
 }
diff --git a/Frontend/src/app/features/mds/mds-editor/widgets/mds-editor-widget-license/license-details/license-details.component.ts b/Frontend/src/app/features/mds/mds-editor/widgets/mds-editor-widget-license/license-details/license-details.component.ts
index 9db094a36..ec679c049 100644
--- a/Frontend/src/app/features/mds/mds-editor/widgets/mds-editor-widget-license/license-details/license-details.component.ts
+++ b/Frontend/src/app/features/mds/mds-editor/widgets/mds-editor-widget-license/license-details/license-details.component.ts
@@ -57,11 +57,11 @@ export class LicenseDetailsComponent implements OnChanges {
             if (license.indexOf('ND') !== -1) this.ccShare = 'ND';
             if (license.indexOf('NC') !== -1) this.ccCommercial = 'NC';
 
-            this.ccVersion = this.getValueForAll(
-                RestConstants.CCM_PROP_LICENSE_CC_VERSION,
-                this.ccVersion,
-            );
-            this.ccCountry = this.getValueForAll(RestConstants.CCM_PROP_LICENSE_CC_LOCALE);
+            // also see @LicenseService
+            this.ccVersion =
+                this.getValueForAll(RestConstants.CCM_PROP_LICENSE_CC_VERSION, this.ccVersion) ||
+                '4.0';
+            this.ccCountry = this.getValueForAll(RestConstants.CCM_PROP_LICENSE_CC_LOCALE) || 'DE';
         }
         if (license === 'CC_0') {
             this.type = 'CC_0';

From 58f3b1a1ef638d8c86471911bc623bea7e7334c4 Mon Sep 17 00:00:00 2001
From: Frank Thomschke <thomschke@edu-sharing.net>
Date: Tue, 26 Nov 2024 14:41:59 +0100
Subject: [PATCH 3/3] Bump Redis to 7.4.1 due to hostname support

---
 deploy/docker/pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/deploy/docker/pom.xml b/deploy/docker/pom.xml
index 52980929b..25735f76a 100644
--- a/deploy/docker/pom.xml
+++ b/deploy/docker/pom.xml
@@ -87,7 +87,7 @@
     </docker.edu_sharing.community.common.redis.name>
 
     <docker.edu_sharing.community.common.redis.tag>
-      6.2.14
+      7.4.1
     </docker.edu_sharing.community.common.redis.tag>
 
     <docker.edu_sharing.community.common.varnish.name>