keycloak-aplcache

admin console

1/27/2018 4:05:02 PM

Changes

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 4cfea1d..c55013e 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -296,17 +296,48 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return getUserSessions(realm, client, firstResult, maxResults, false);
     }
 
+    @Override
+    public List<UserSessionModel> getOfflineUserSessions(RealmModel realm) {
+        return getOfflineUserSessions(realm, -1, -1);
+    }
+
+    @Override
+    public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, int first, int max) {
+        return getUserSessions(realm, first, max, true);
+    }
+
     protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
+        final String clientUuid = client.getId();
+        UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(clientUuid);
+
+        return getUserSessionModels(realm, firstResult, maxResults, offline, predicate);
+    }
+
+    @Override
+    public List<UserSessionModel> getUserSessions(RealmModel realm) {
+        return getUserSessions(realm, -1, -1);
+    }
+
+    @Override
+    public List<UserSessionModel> getUserSessions(RealmModel realm, int firstResult, int maxResults) {
+        return getUserSessions(realm, firstResult, maxResults, false);
+    }
+
+    protected List<UserSessionModel> getUserSessions(final RealmModel realm, int firstResult, int maxResults, final boolean offline) {
+        UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId());
+
+        return getUserSessionModels(realm, firstResult, maxResults, offline, predicate);
+    }
+
+    protected List<UserSessionModel> getUserSessionModels(RealmModel realm, int firstResult, int maxResults, boolean offline, UserSessionPredicate predicate) {
         Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
         cache = CacheDecorators.skipCacheLoaders(cache);
 
         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = getClientSessionCache(offline);
         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache);
 
-        final String clientUuid = client.getId();
-
         Stream<UserSessionEntity> stream = cache.entrySet().stream()
-                .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid))
+                .filter(predicate)
                 .map(Mappers.userSessionEntity())
                 .sorted(Comparators.userSessionLastSessionRefresh());
 
@@ -330,7 +361,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return sessions;
     }
 
-
     @Override
     public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate<UserSessionModel> predicate) {
         UserSessionModel userSession = getUserSession(realm, id, offline);
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
index b999485..29dc415 100644
--- a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
@@ -27,12 +27,12 @@ import java.util.Set;
  * @version $Revision: 1 $
  */
 public interface ClientProvider extends ClientLookupProvider, Provider {
+    List<ClientModel> getClients(RealmModel realm);
+
     ClientModel addClient(RealmModel realm, String clientId);
 
     ClientModel addClient(RealmModel realm, String id, String clientId);
 
-    List<ClientModel> getClients(RealmModel realm);
-
     RoleModel addClientRole(RealmModel realm, ClientModel client, String name);
 
     RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name);
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index fc67d4e..00827e1 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -35,9 +35,11 @@ public interface UserSessionProvider extends Provider {
     UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
     UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
     UserSessionModel getUserSession(RealmModel realm, String id);
+    List<UserSessionModel> getUserSessions(RealmModel realm);
     List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
     List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);
     List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults);
+    List<UserSessionModel> getUserSessions(RealmModel realm, int firstResult, int maxResults);
     List<UserSessionModel> getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId);
     UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId);
 
@@ -75,9 +77,11 @@ public interface UserSessionProvider extends Provider {
     /** Will automatically attach newly created offline client session to the offlineUserSession **/
     AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession);
     List<UserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user);
+    List<UserSessionModel> getOfflineUserSessions(RealmModel realm);
 
     long getOfflineSessionsCount(RealmModel realm, ClientModel client);
     List<UserSessionModel> getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max);
+    List<UserSessionModel> getOfflineUserSessions(RealmModel realm, int first, int max);
 
     /** Triggered by persister during pre-load. It optionally imports authenticatedClientSessions too if requested. Otherwise the imported UserSession will have empty list of AuthenticationSessionModel **/
     UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline, boolean importAuthenticatedClientSessions);
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
index 6cf80d9..c0773a6 100644
--- a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
@@ -25,6 +25,9 @@ import org.keycloak.storage.client.ClientLookupProvider;
 /**
  * Base interface for components that want to provide an alternative storage mechanism for clients
  *
+ * This is currently a private incomplete SPI.  Please discuss on dev list if you want us to complete it or want to do the work yourself.
+ * This work is described in KEYCLOAK-6408 JIRA issue.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index c0ea7df..580a0bc 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
@@ -98,7 +98,7 @@ public class ClientsResource {
     public List<ClientRepresentation> getClients(@QueryParam("clientId") String clientId, @QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly) {
         List<ClientRepresentation> rep = new ArrayList<>();
 
-        if (clientId == null) {
+        if (clientId == null || clientId.trim().equals("")) {
             List<ClientModel> clientModels = realm.getClients();
             auth.clients().requireList();
             boolean view = auth.clients().canView();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java
new file mode 100644
index 0000000..6c8561c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.managers.UserStorageSyncManager;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.user.SynchronizationResult;
+
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @resource User Storage Provider
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientStorageProviderResource {
+    private static final Logger logger = Logger.getLogger(ClientStorageProviderResource.class);
+
+    protected RealmModel realm;
+
+    protected AdminPermissionEvaluator auth;
+
+    protected AdminEventBuilder adminEvent;
+
+    @Context
+    protected ClientConnection clientConnection;
+
+    @Context
+    protected UriInfo uriInfo;
+
+    @Context
+    protected KeycloakSession session;
+
+    @Context
+    protected HttpHeaders headers;
+
+    public ClientStorageProviderResource(RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
+        this.auth = auth;
+        this.realm = realm;
+        this.adminEvent = adminEvent;
+    }
+
+    /**
+     * Need this for admin console to display simple name of provider when displaying client detail
+     *
+     * KEYCLOAK-4328
+     *
+     * @param id
+     * @return
+     */
+    @GET
+    @Path("{id}/name")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, String> getSimpleName(@PathParam("id") String id) {
+        auth.clients().requireList();
+
+        ComponentModel model = realm.getComponent(id);
+        if (model == null) {
+            throw new NotFoundException("Could not find component");
+        }
+        if (!model.getProviderType().equals(ClientStorageProvider.class.getName())) {
+            throw new NotFoundException("found, but not a ClientStorageProvider");
+        }
+
+        Map<String, String> data = new HashMap<>();
+        data.put("id", model.getId());
+        data.put("name", model.getName());
+        return data;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
index 1c5978e..00726e4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
@@ -31,6 +31,7 @@ import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.services.ForbiddenException;
+import org.keycloak.storage.StorageId;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -634,8 +635,8 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
     public Map<String, Boolean> getAccess(ClientModel client) {
         Map<String, Boolean> map = new HashMap<>();
         map.put("view", canView(client));
-        map.put("manage", canManage(client));
-        map.put("configure", canConfigure(client));
+        map.put("manage", StorageId.isLocalStorage(client) && canManage(client));
+        map.put("configure", StorageId.isLocalStorage(client) && canConfigure(client));
         return map;
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 88af6f6..b5b583b 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -100,6 +100,7 @@ import java.security.cert.X509Certificate;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -504,17 +505,62 @@ public class RealmAdminResource {
     public List<Map<String, String>> getClientSessionStats() {
         auth.realm().requireViewRealm();
 
-        List<Map<String, String>> data = new LinkedList<Map<String, String>>();
-        for (ClientModel client : realm.getClients()) {
-            long size = session.sessions().getActiveUserSessions(client.getRealm(), client);
-            if (size == 0) continue;
-            Map<String, String> map = new HashMap<>();
-            map.put("id", client.getId());
-            map.put("clientId", client.getClientId());
-            map.put("active", size + "");
-            data.add(map);
-        }
-        return data;
+        Map<String, Map<String, String>> data = new HashMap();
+        {
+            List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm);
+            Map<String, Long> activeCount = new HashMap<>();
+            // we have to iterate over all realm user sessions as clients coming from client storage provider might not be reachable from getClients()
+            for (UserSessionModel userSession : userSessions) {
+                for (String id : userSession.getAuthenticatedClientSessions().keySet()) {
+                    Long number = activeCount.get(id);
+                    if (number == null) {
+                        activeCount.put(id, new Long(1));
+                    } else {
+                        activeCount.put(id, number + 1);
+                    }
+                }
+            }
+            for (Map.Entry<String, Long> entry : activeCount.entrySet()) {
+                Map<String, String> map = new HashMap<>();
+                ClientModel client = realm.getClientById(entry.getKey());
+                map.put("id", client.getId());
+                map.put("clientId", client.getClientId());
+                map.put("active", entry.getValue().toString());
+                map.put("offline", "0");
+                data.put(client.getId(), map);
+
+            }
+        }
+        {
+            Map<String, Long> offlineCount = new HashMap<>();
+            // we have to iterate over all realm user sessions as clients coming from client storage provider might not be reachable from getClients()
+            List<UserSessionModel> offlineSessions = session.sessions().getOfflineUserSessions(realm);
+            for (UserSessionModel userSession : offlineSessions) {
+                for (String id : userSession.getAuthenticatedClientSessions().keySet()) {
+                    Long number = offlineCount.get(id);
+                    if (number == null) {
+                        offlineCount.put(id, new Long(1));
+                    } else {
+                        offlineCount.put(id, number + 1);
+                    }
+                }
+            }
+            for (Map.Entry<String, Long> entry : offlineCount.entrySet()) {
+                Map<String, String> map = data.get(entry.getKey());
+                if (map == null) {
+                    map = new HashMap<>();
+                    ClientModel client = realm.getClientById(entry.getKey());
+                    map.put("id", client.getId());
+                    map.put("clientId", client.getClientId());
+                    map.put("active", "0");
+                    data.put(client.getId(), map);
+                }
+                map.put("offline", entry.getValue().toString());
+            }
+        }
+        List<Map<String, String>> result = new LinkedList<>();
+        for (Map<String, String> item : data.values()) result.add(item);
+        return result;
     }
 
     /**
diff --git a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
index 53f38cc..d60d2f4 100644
--- a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
@@ -25,14 +25,13 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.storage.client.ClientLookupProvider;
 import org.keycloak.storage.client.ClientStorageProvider;
 import org.keycloak.storage.client.ClientStorageProviderFactory;
 import org.keycloak.storage.client.ClientStorageProviderModel;
-import org.keycloak.storage.user.UserLookupProvider;
 
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -163,9 +162,12 @@ public class ClientStorageManager implements ClientProvider {
         return session.clientLocalStorage().addClient(realm, id, clientId);
     }
 
+
+
+
     @Override
     public List<ClientModel> getClients(RealmModel realm) {
-        return session.clientLocalStorage().getClients(realm);
+       return session.clientLocalStorage().getClients(realm);
     }
 
     @Override
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
index 198014f..acf0db8 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
@@ -23,7 +23,6 @@ import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.storage.StorageId;
-import org.keycloak.storage.client.AbstractClientStorageAdapter;
 import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter;
 import org.keycloak.storage.client.ClientLookupProvider;
 import org.keycloak.storage.client.ClientStorageProvider;
@@ -55,13 +54,13 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl
     public ClientModel getClientById(String id, RealmModel realm) {
         StorageId storageId = new StorageId(id);
         final String clientId = storageId.getExternalId();
-        if (clientId.equals(clientId)) return new ClientAdapter(realm);
+        if (this.clientId.equals(clientId)) return new ClientAdapter(realm);
         return null;
     }
 
     @Override
     public ClientModel getClientByClientId(String clientId, RealmModel realm) {
-        if (clientId.equals(clientId)) return new ClientAdapter(realm);
+        if (this.clientId.equals(clientId)) return new ClientAdapter(realm);
         return null;
     }
 
@@ -155,7 +154,7 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl
 
         @Override
         public String getProtocol() {
-            return null;
+            return "openid-connect";
         }
 
         @Override
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
index 0684007..a1af7fe 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
@@ -132,6 +132,7 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
     public void testBrowser() throws Exception {
         String clientId = "hardcoded-client";
         testBrowser(clientId);
+        //Thread.sleep(10000000);
     }
 
     private void testBrowser(String clientId) {
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 3595dd9..f8ff2d0 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -173,6 +173,7 @@ realm-sessions=Realm Sessions
 revocation=Revocation
 logout-all=Logout all
 active-sessions=Active Sessions
+offline-sessions=Offline Sessions
 sessions=Sessions
 not-before=Not Before
 not-before.tooltip=Revoke any tokens issued before this date.
@@ -1343,6 +1344,8 @@ userStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of a user cache entry i
 user-origin-link=Storage Origin
 user-origin.tooltip=UserStorageProvider the user was loaded from
 user-link.tooltip=UserStorageProvider this locally stored user was imported from.
+client-origin-link=Storage Origin
+client-origin.tooltip=Provider the client was loaded from
 
 disable=Disable
 disableable-credential-types=Disableable Types
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 2e88f02..1625218 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -764,6 +764,15 @@ module.controller('ClientListCtrl', function($scope, realm, Client, serverInfo, 
         });
     };
 
+    $scope.searchClient = function() {
+        console.log('searchQuery!!! ' + $scope.search.clientId);
+        Client.query({realm: realm.realm, viewableOnly: true, clientId: $scope.search.clientId}).$promise.then(function(clients) {
+            $scope.numberOfPages = Math.ceil(clients.length/$scope.pageSize);
+            $scope.clients = clients;
+        });
+
+    };
+
     $scope.exportClient = function(client) {
         var clientCopy = angular.copy(client);
         delete clientCopy.id;
@@ -819,7 +828,7 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv
     }
 });
 
-module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
+module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, Components, ClientStorageOperations, $location, $modal, Dialog, Notifications) {
 
 
 
@@ -889,6 +898,25 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
     $scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
     $scope.disableCredentialsTab = client.publicClient;
 
+    if(client.origin) {
+        if ($scope.access.viewRealm) {
+            Components.get({realm: realm.realm, componentId: client.origin}, function (link) {
+                $scope.originName = link.name;
+                //$scope.originLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id;
+            })
+        }
+        else {
+            // KEYCLOAK-4328
+            ClientStorageOperations.simpleName.get({realm: realm.realm, componentId: client.origin}, function (link) {
+                $scope.originName = link.name;
+                //$scope.originLink = $location.absUrl();
+            })
+        }
+    } else {
+        console.log("origin is null");
+    }
+
+
     function updateProperties() {
         if (!$scope.client.attributes) {
             $scope.client.attributes = {};
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index b1b6304..5700792 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1813,6 +1813,16 @@ module.factory('UserStorageOperations', function($resource) {
 });
 
 
+module.factory('ClientStorageOperations', function($resource) {
+    var object = {}
+    object.simpleName = $resource(authUrl + '/admin/realms/:realm/client-storage/:componentId/name', {
+        realm : '@realm',
+        componentId : '@componentId'
+    });
+    return object;
+});
+
+
 module.factory('ClientRegistrationPolicyProviders', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/client-registration-policy/providers', {
         realm : '@realm',
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index c00601a..cbeb321 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -37,6 +37,13 @@
                 </div>
                 <kc-tooltip>{{:: 'client.enabled.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group clearfix block" data-ng-show="client.origin">
+                <label class="col-md-2 control-label">{{:: 'client-origin-link' | translate}}</label>
+                <div class="col-md-6">
+                    {{originName}}
+                </div>
+                <kc-tooltip>{{:: 'client-origin.tooltip' | translate}}</kc-tooltip>
+            </div>
             <div class="form-group clearfix block" data-ng-show="protocol != 'docker-v2'">
                 <label class="col-md-2 control-label" for="consentRequired">{{:: 'consent-required' | translate}}</label>
                 <div class="col-sm-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
index 03ebf5c..744e3c4 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
@@ -11,9 +11,9 @@
                     <div class="form-inline">
                         <div class="form-group">
                             <div class="input-group">
-                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.clientId" class="form-control search" onkeyup="if(event.keyCode === 13){$(this).next('I').click();}">
+                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.clientId" class="form-control search" onkeydown="if(event.keyCode === 13) document.getElementById('clientSearch').click()">
                                 <div class="input-group-addon">
-                                    <i class="fa fa-search" type="submit"></i>
+                                    <i class="fa fa-search" id="clientSearch" data-ng-click="searchClient()"></i>
                                 </div>
                             </div>
                         </div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html b/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
index e724d95..98b2284 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
@@ -18,12 +18,14 @@
         <tr>
             <th>{{:: 'client' | translate}}</th>
             <th>{{:: 'active-sessions' | translate}}</th>
+            <th>{{:: 'offline-sessions' | translate}}</th>
         </tr>
         </thead>
         <tbody>
         <tr data-ng-repeat="data in stats">
             <td><a href="#/realms/{{realm.realm}}/clients/{{data.id}}/sessions">{{data.clientId}}</a></td>
             <td>{{data.active}}</td>
+            <td>{{data.offline}}</td>
         </tr>
         </tbody>
     </table>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index 40aab54..c294692 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -9,11 +9,11 @@
     <ul class="nav nav-tabs"  data-ng-hide="create && !path[4]">
         <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'credentials'}"
-            data-ng-show="!client.publicClient && client.protocol == 'openid-connect'">
+            data-ng-show="!client.publicClient && client.protocol == 'openid-connect' && !client.origin">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a>
         </li>
         <li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
-        <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
+        <li ng-class="{active: path[4] == 'roles'}" data-ng-show="!client.origin"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">{{:: 'mappers' | translate}}</a>
             <kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
@@ -23,10 +23,10 @@
             <kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
         </li>
         <li ng-class="{active: path[4] == 'authz'}"
-            data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled">
+            data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled && !client.origin">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
                 translate}}</a></li>
-        <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2' && client.protocol != 'saml'"><a
+        <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2' && client.protocol != 'saml' && !client.origin"><a
                 href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a>
         </li>
     <!--    <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
@@ -40,9 +40,9 @@
             <kc-tooltip>{{:: 'offline-access.tooltip' | translate}}</kc-tooltip>
         </li>
 
-        <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
+        <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient && !client.origin"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
 
-        <li ng-class="{active: path[4] == 'installation'}">
+        <li ng-class="{active: path[4] == 'installation'}" data-ng-show="!client.origin">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">{{:: 'installation' | translate}}</a>
             <kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
         </li>