From 0eb74d9f60c74c13adbd30c5adf72ea9d2c1e3e6 Mon Sep 17 00:00:00 2001 From: vigneshan Date: Thu, 29 Jul 2021 10:45:38 +0530 Subject: [PATCH] Modify login cache implementatation with LRU cache --- .../mgt/core/config/ui/UIConfiguration.java | 10 ++ .../ui/request/interceptor/LoginHandler.java | 12 +- .../request/interceptor/SsoLoginHandler.java | 44 +++-- ...LoginCacheManager.java => LoginCache.java} | 34 ++-- .../interceptor/util/HandlerConstants.java | 2 +- .../request/interceptor/util/HandlerUtil.java | 159 +++++++++++++++++- .../src/main/resources/conf/mdm-ui-config.xml | 2 + 7 files changed, 213 insertions(+), 50 deletions(-) rename components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/{LoginCacheManager.java => LoginCache.java} (67%) diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/config/ui/UIConfiguration.java b/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/config/ui/UIConfiguration.java index 1c4a1e91cd..f494007330 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/config/ui/UIConfiguration.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.core/src/main/java/org/wso2/carbon/device/mgt/core/config/ui/UIConfiguration.java @@ -32,6 +32,7 @@ public class UIConfiguration { private List scopes; private boolean isSsoEnable; private int sessionTimeOut; + private int loginCacheCapacity; @XmlElement(name = "AppRegistration", required=true) public AppRegistration getAppRegistration() { @@ -69,4 +70,13 @@ public class UIConfiguration { public void setSessionTimeOut(int sessionTimeOut) { this.sessionTimeOut = sessionTimeOut; } + + @XmlElement(name = "LoginCacheCapacity") + public int getLoginCacheCapacity() { + return loginCacheCapacity; + } + + public void setLoginCacheCapacity(int loginCacheCapacity) { + this.loginCacheCapacity = loginCacheCapacity; + } } diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/LoginHandler.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/LoginHandler.java index 60dd31ab2d..ad6b90ee16 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/LoginHandler.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/LoginHandler.java @@ -24,7 +24,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import io.entgra.ui.request.interceptor.beans.AuthData; -import io.entgra.ui.request.interceptor.cache.LoginCacheManager; +import io.entgra.ui.request.interceptor.cache.LoginCache; import io.entgra.ui.request.interceptor.cache.OAuthApp; import io.entgra.ui.request.interceptor.cache.OAuthAppCacheKey; import io.entgra.ui.request.interceptor.exceptions.LoginException; @@ -39,7 +39,6 @@ import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.protocol.HTTP; import io.entgra.ui.request.interceptor.beans.ProxyResponse; -import org.json.JSONString; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; @@ -80,10 +79,9 @@ public class LoginHandler extends HttpServlet { httpSession.setMaxInactiveInterval(sessionTimeOut); // Check if OAuth app cache exists. If not create a new application. - LoginCacheManager loginCacheManager = new LoginCacheManager(); - loginCacheManager.initializeCacheManager(); + LoginCache loginCache = HandlerUtil.getLoginCache(httpSession); OAuthAppCacheKey oAuthAppCacheKey = new OAuthAppCacheKey(HandlerConstants.PUBLISHER_APPLICATION_NAME, username); - OAuthApp oAuthApp = loginCacheManager.getOAuthAppCache(oAuthAppCacheKey); + OAuthApp oAuthApp = loginCache.getOAuthAppCache(oAuthAppCacheKey); if (oAuthApp == null) { HttpPost apiRegEndpoint = new HttpPost(gatewayUrl + HandlerConstants.APP_REG_ENDPOINT); @@ -111,8 +109,6 @@ public class LoginHandler extends HttpServlet { clientSecret = jClientAppResultAsJsonObject.get("client_secret").getAsString(); encodedClientApp = Base64.getEncoder() .encodeToString((clientId + HandlerConstants.COLON + clientSecret).getBytes()); - - oAuthAppCacheKey = new OAuthAppCacheKey(HandlerConstants.PUBLISHER_APPLICATION_NAME, username); oAuthApp = new OAuthApp( HandlerConstants.PUBLISHER_APPLICATION_NAME, username, @@ -120,7 +116,7 @@ public class LoginHandler extends HttpServlet { clientSecret, encodedClientApp ); - loginCacheManager.addOAuthAppToCache(oAuthAppCacheKey, oAuthApp); + loginCache.addOAuthAppToCache(oAuthAppCacheKey, oAuthApp); } if (getTokenAndPersistInSession(req, resp, clientId, clientSecret, encodedClientApp, scopes)) { diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/SsoLoginHandler.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/SsoLoginHandler.java index fb4f93b0c9..10a488e294 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/SsoLoginHandler.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/SsoLoginHandler.java @@ -23,7 +23,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; -import io.entgra.ui.request.interceptor.cache.LoginCacheManager; +import io.entgra.ui.request.interceptor.cache.LoginCache; import io.entgra.ui.request.interceptor.cache.OAuthApp; import io.entgra.ui.request.interceptor.cache.OAuthAppCacheKey; import io.entgra.ui.request.interceptor.util.HandlerConstants; @@ -81,9 +81,9 @@ public class SsoLoginHandler extends HttpServlet { private JsonObject uiConfigJsonObject; private HttpSession httpSession; - - private LoginCacheManager loginCacheManager; + private LoginCache loginCache; private OAuthApp oAuthApp; + private OAuthAppCacheKey oAuthAppCacheKey; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { @@ -99,13 +99,23 @@ public class SsoLoginHandler extends HttpServlet { baseContextPath = req.getContextPath(); applicationName = baseContextPath.substring(1, baseContextPath.indexOf("-ui-request-handler")); - // Check if oauth app cache is available - loginCacheManager = new LoginCacheManager(); - loginCacheManager.initializeCacheManager(); - oAuthApp = loginCacheManager.getOAuthAppCache( - new OAuthAppCacheKey(applicationName, adminUsername) - ); + String iotsCorePort = System.getProperty(HandlerConstants.IOT_CORE_HTTPS_PORT_ENV_VAR); + if (HandlerConstants.HTTP_PROTOCOL.equals(req.getScheme())) { + iotsCorePort = System.getProperty(HandlerConstants.IOT_CORE_HTTP_PORT_ENV_VAR); + } + gatewayUrl = req.getScheme() + HandlerConstants.SCHEME_SEPARATOR + System.getProperty(HandlerConstants.IOT_GW_HOST_ENV_VAR) + + HandlerConstants.COLON + HandlerUtil.getGatewayPort(req.getScheme()); + iotsCoreUrl = req.getScheme() + HandlerConstants.SCHEME_SEPARATOR + System.getProperty(HandlerConstants.IOT_CORE_HOST_ENV_VAR) + + HandlerConstants.COLON + iotsCorePort; + + // Fetch ui config and persists in session + String uiConfigUrl = iotsCoreUrl + HandlerConstants.UI_CONFIG_ENDPOINT; + uiConfigJsonObject = HandlerUtil.getUIConfigAndPersistInSession(uiConfigUrl, gatewayUrl, httpSession, resp); + // Retrieving login cache and do a DCR if the cache is not available. + loginCache = HandlerUtil.getLoginCache(httpSession); + oAuthAppCacheKey = new OAuthAppCacheKey(applicationName, adminUsername); + oAuthApp = loginCache.getOAuthAppCache(oAuthAppCacheKey); if (oAuthApp == null) { dynamicClientRegistration(req, resp); } @@ -143,19 +153,6 @@ public class SsoLoginHandler extends HttpServlet { */ private void dynamicClientRegistration(HttpServletRequest req, HttpServletResponse resp) { try { - String iotsCorePort = System.getProperty(HandlerConstants.IOT_CORE_HTTPS_PORT_ENV_VAR); - - if (HandlerConstants.HTTP_PROTOCOL.equals(req.getScheme())) { - iotsCorePort = System.getProperty(HandlerConstants.IOT_CORE_HTTP_PORT_ENV_VAR); - } - - gatewayUrl = req.getScheme() + HandlerConstants.SCHEME_SEPARATOR + System.getProperty(HandlerConstants.IOT_GW_HOST_ENV_VAR) - + HandlerConstants.COLON + HandlerUtil.getGatewayPort(req.getScheme()); - iotsCoreUrl = req.getScheme() + HandlerConstants.SCHEME_SEPARATOR + System.getProperty(HandlerConstants.IOT_CORE_HOST_ENV_VAR) - + HandlerConstants.COLON + iotsCorePort; - String uiConfigUrl = iotsCoreUrl + HandlerConstants.UI_CONFIG_ENDPOINT; - - uiConfigJsonObject = HandlerUtil.getUIConfigAndPersistInSession(uiConfigUrl, gatewayUrl, httpSession, resp); JsonArray tags = uiConfigJsonObject.get("appRegistration").getAsJsonObject().get("tags").getAsJsonArray(); JsonArray scopes = uiConfigJsonObject.get("scopes").getAsJsonArray(); sessionTimeOut = Integer.parseInt(String.valueOf(uiConfigJsonObject.get("sessionTimeOut"))); @@ -191,9 +188,8 @@ public class SsoLoginHandler extends HttpServlet { } // cache the oauth app credentials - OAuthAppCacheKey oAuthAppCacheKey = new OAuthAppCacheKey(applicationName, adminUsername); oAuthApp = new OAuthApp(applicationName, adminUsername, clientId, clientSecret, encodedClientApp); - loginCacheManager.addOAuthAppToCache(oAuthAppCacheKey, oAuthApp); + loginCache.addOAuthAppToCache(oAuthAppCacheKey, oAuthApp); } // Get the details of the registered application diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/LoginCacheManager.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/LoginCache.java similarity index 67% rename from components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/LoginCacheManager.java rename to components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/LoginCache.java index 3ecd741350..06ca1cef6b 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/LoginCacheManager.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/cache/LoginCache.java @@ -18,25 +18,19 @@ package io.entgra.ui.request.interceptor.cache; -import io.entgra.ui.request.interceptor.util.HandlerConstants; - -import javax.cache.Cache; -import javax.cache.CacheManager; -import javax.cache.Caching; +import java.util.LinkedHashMap; /** * Contains necessary functions to manage oAuth app cache during login handling */ -public class LoginCacheManager { +public class LoginCache { - private CacheManager cacheManager = null; - private Cache cache = null; + private final LinkedHashMap cache; + private final int capacity; - /** - * Initialize the cache manager if it is not already initialized - */ - public void initializeCacheManager() { - cacheManager = Caching.getCacheManagerFactory().getCacheManager(HandlerConstants.LOGIN_CACHE); + public LoginCache(int capacity) { + this.capacity = capacity; + this.cache = new LinkedHashMap<>(capacity); } /** @@ -46,7 +40,9 @@ public class LoginCacheManager { * @param oAuthApp - The value of the cache which contains OAuth app data */ public void addOAuthAppToCache(OAuthAppCacheKey oAuthAppCacheKey, OAuthApp oAuthApp) { - cache = cacheManager.getCache(HandlerConstants.LOGIN_CACHE); + if (cache.size() == capacity) { + cache.remove(cache.entrySet().iterator().next().getKey()); + } cache.put(oAuthAppCacheKey, oAuthApp); } @@ -57,7 +53,13 @@ public class LoginCacheManager { * @return - Returns OAuthApp object */ public OAuthApp getOAuthAppCache(OAuthAppCacheKey oAuthAppCacheKey) { - cache = cacheManager.getCache(HandlerConstants.LOGIN_CACHE); - return cache.get(oAuthAppCacheKey); + OAuthApp oAuthApp = cache.get(oAuthAppCacheKey); + if (oAuthApp != null) { + if (cache.size() == capacity) { + cache.remove(oAuthAppCacheKey); + cache.put(oAuthAppCacheKey, oAuthApp); + } + } + return oAuthApp; } } diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java index acddc17ce9..feda440f3b 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java @@ -55,7 +55,7 @@ public class HandlerConstants { public static final String PASSWORD_GRANT_TYPE = "password"; public static final String JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; public static final String PRODUCTION_KEY = "PRODUCTION"; - public static final String LOGIN_CACHE = "LOGIN_CACHE"; + public static final String LOGIN_CACHE_CAPACITY_KEY = "loginCacheCapacity"; public static final String SCHEME_SEPARATOR = "://"; public static final String COLON = ":"; diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerUtil.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerUtil.java index 5acec832ce..ab702eb9ad 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerUtil.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerUtil.java @@ -23,13 +23,17 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import io.entgra.ui.request.interceptor.beans.AuthData; +import io.entgra.ui.request.interceptor.cache.LoginCache; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.Consts; +import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.ContentType; @@ -60,6 +64,9 @@ import java.io.StringWriter; public class HandlerUtil { private static final Log log = LogFactory.getLog(HandlerUtil.class); + private static LoginCache loginCache = null; + private static boolean isLoginCacheInitialized = false; + private static AuthData authData; /*** * @@ -166,9 +173,11 @@ public class HandlerUtil { /*** + * Handle error requests. * * @param resp {@link HttpServletResponse} - * Return Error Response. + * @param proxyResponse {@link ProxyResponse} + * @throws IOException If error occurred when trying to send the error response. */ public static void handleError(HttpServletResponse resp, ProxyResponse proxyResponse) throws IOException { Gson gson = new Gson(); @@ -188,6 +197,22 @@ public class HandlerUtil { } } + /** + * Handle error requests with custom error codes. + * + * @param resp {@link HttpServletResponse} + * @param errorCode HTTP error status code + * @throws IOException If error occurred when trying to send the error response. + */ + public static void handleError(HttpServletResponse resp, int errorCode) + throws IOException { + ProxyResponse proxyResponse = new ProxyResponse(); + proxyResponse.setCode(errorCode); + proxyResponse.setExecutorResponse( + HandlerConstants.EXECUTOR_EXCEPTION_PREFIX + HandlerUtil.getStatusKey(errorCode)); + HandlerUtil.handleError(resp, proxyResponse); + } + /*** * * @param resp {@link HttpServletResponse} @@ -400,4 +425,136 @@ public class HandlerUtil { return stringOutput; } + + /*** + * Search a key from a given json string object. + * + * @param jsonObjectString - json object in string format. + * @param key - the key to be searched. + * @return string value of the key value. + */ + private static String searchFromJsonObjectString(String jsonObjectString, String key) { + JsonParser jsonParser = new JsonParser(); + JsonElement jsonElement = jsonParser.parse(jsonObjectString); + JsonObject jsonObject = jsonElement.getAsJsonObject(); + return jsonObject.get(key).getAsString(); + } + + /*** + * Initializes the login cache. + * + * @param httpSession - current active HttpSession. + */ + private static void initializeLoginCache(HttpSession httpSession) { + String uiConfig = httpSession.getAttribute(HandlerConstants.UI_CONFIG_KEY).toString(); + int capacity = Integer.parseInt(searchFromJsonObjectString(uiConfig, HandlerConstants.LOGIN_CACHE_CAPACITY_KEY)); + loginCache = new LoginCache(capacity); + } + + /*** + * Retrieves login cache and initializes if its not done already. + * + * @param httpSession - current active HttpSession. + */ + public static LoginCache getLoginCache(HttpSession httpSession) { + if (!isLoginCacheInitialized || loginCache == null) { + isLoginCacheInitialized = true; + initializeLoginCache(httpSession); + } + return loginCache; + } + + /** + * Retry request again after refreshing the access token + * + * @param req incoming {@link HttpServletRequest} + * @param resp resp {@link HttpServletResponse} + * @param httpRequest subclass of {@link HttpRequestBase} related to the current request. + * @return {@link ProxyResponse} if successful and null if failed. + * @throws IOException If an error occurs when try to retry the request. + */ + public static ProxyResponse retryRequestWithRefreshedToken(HttpServletRequest req, HttpServletResponse resp, + HttpRequestBase httpRequest, String apiEndpoint) throws IOException { + if (refreshToken(req, resp, apiEndpoint)) { + HttpSession session = req.getSession(false); + if (session == null) { + log.error("Unauthorized, You are not logged in. Please log in to the portal"); + handleError(resp, HttpStatus.SC_UNAUTHORIZED); + return null; + } + httpRequest.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BEARER + authData.getAccessToken()); + ProxyResponse proxyResponse = HandlerUtil.execute(httpRequest); + if (proxyResponse.getExecutorResponse().contains(HandlerConstants.EXECUTOR_EXCEPTION_PREFIX)) { + log.error("Error occurred while invoking the API after refreshing the token."); + HandlerUtil.handleError(resp, proxyResponse); + return null; + } + return proxyResponse; + } + return null; + } + + /*** + * This method is responsible to get the refresh token + * + * @param req {@link HttpServletRequest} + * @param resp {@link HttpServletResponse} + * @return If successfully renew tokens, returns TRUE otherwise return FALSE + * @throws IOException If an error occurs while witting error response to client side or invoke token renewal API + */ + private static boolean refreshToken(HttpServletRequest req, HttpServletResponse resp, String gatewayUrl) + throws IOException { + if (log.isDebugEnabled()) { + log.debug("refreshing the token"); + } + HttpPost tokenEndpoint = new HttpPost( + gatewayUrl + HandlerConstants.TOKEN_ENDPOINT); + HttpSession session = req.getSession(false); + if (session == null) { + log.error("Couldn't find a session, hence it is required to login and proceed."); + handleError(resp, HttpStatus.SC_UNAUTHORIZED); + return false; + } + + authData = (AuthData) session.getAttribute(HandlerConstants.SESSION_AUTH_DATA_KEY); + StringEntity tokenEndpointPayload = new StringEntity( + "grant_type=refresh_token&refresh_token=" + authData.getRefreshToken() + "&scope=PRODUCTION", + ContentType.APPLICATION_FORM_URLENCODED); + + tokenEndpoint.setEntity(tokenEndpointPayload); + String encodedClientApp = authData.getEncodedClientApp(); + tokenEndpoint.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BASIC + + encodedClientApp); + tokenEndpoint.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.toString()); + + ProxyResponse tokenResultResponse = HandlerUtil.execute(tokenEndpoint); + if (tokenResultResponse.getExecutorResponse().contains(HandlerConstants.EXECUTOR_EXCEPTION_PREFIX)) { + log.error("Error occurred while refreshing access token."); + HandlerUtil.handleError(resp, tokenResultResponse); + return false; + } + + JsonParser jsonParser = new JsonParser(); + JsonElement jTokenResult = jsonParser.parse(tokenResultResponse.getData()); + + if (jTokenResult.isJsonObject()) { + JsonObject jTokenResultAsJsonObject = jTokenResult.getAsJsonObject(); + AuthData newAuthData = new AuthData(); + + newAuthData.setAccessToken(jTokenResultAsJsonObject.get("access_token").getAsString()); + newAuthData.setRefreshToken(jTokenResultAsJsonObject.get("refresh_token").getAsString()); + newAuthData.setScope(jTokenResultAsJsonObject.get("scope").getAsString()); + newAuthData.setClientId(authData.getClientId()); + newAuthData.setClientSecret(authData.getClientSecret()); + newAuthData.setEncodedClientApp(authData.getEncodedClientApp()); + newAuthData.setUsername(authData.getUsername()); + authData = newAuthData; + session.setAttribute(HandlerConstants.SESSION_AUTH_DATA_KEY, newAuthData); + return true; + } + + log.error("Error Occurred in token renewal process."); + handleError(resp, HttpStatus.SC_INTERNAL_SERVER_ERROR); + return false; + } } diff --git a/features/device-mgt/org.wso2.carbon.device.mgt.basics.feature/src/main/resources/conf/mdm-ui-config.xml b/features/device-mgt/org.wso2.carbon.device.mgt.basics.feature/src/main/resources/conf/mdm-ui-config.xml index 36fe025034..aa3f169a33 100644 --- a/features/device-mgt/org.wso2.carbon.device.mgt.basics.feature/src/main/resources/conf/mdm-ui-config.xml +++ b/features/device-mgt/org.wso2.carbon.device.mgt.basics.feature/src/main/resources/conf/mdm-ui-config.xml @@ -22,6 +22,8 @@ true 3600 + + 10000 application_management