From 64f8d7ce2549429b9282cd6cb3e2aae1ca295d88 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 6 Dec 2017 16:00:23 -0500 Subject: [PATCH] KEYCLOAK-5350 --- .../InfinispanUserSessionProvider.java | 15 +- .../infinispan/UserSessionAdapter.java | 8 +- .../JpaUserSessionPersisterProvider.java | 26 ++- .../models/OfflineUserSessionModel.java | 27 +++ .../session/PersistentUserSessionAdapter.java | 25 ++- .../org/keycloak/testsuite/AssertEvents.java | 7 + .../FailableHardcodedStorageProvider.java | 209 ++++++++++++++++++ ...ilableHardcodedStorageProviderFactory.java | 55 +++++ .../storage/UserStorageFailureTest.java | 177 +++++++++++++++ ...eycloak.storage.UserStorageProviderFactory | 3 +- 10 files changed, 533 insertions(+), 19 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java create mode 100644 testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProvider.java create mode 100644 testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProviderFactory.java create mode 100644 testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java 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 eb49db4b2b..eb687a6c55 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 @@ -26,6 +26,7 @@ import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; @@ -839,12 +840,22 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setBrokerSessionId(userSession.getBrokerSessionId()); entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setIpAddress(userSession.getIpAddress()); - entity.setLoginUsername(userSession.getLoginUsername()); entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes()); entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore()); entity.setRememberMe(userSession.isRememberMe()); entity.setState(userSession.getState()); - entity.setUser(userSession.getUser().getId()); + if (userSession instanceof OfflineUserSessionModel) { + // this is a hack so that UserModel doesn't have to be available when offline token is imported. + // see related JIRA - KEYCLOAK-5350 and corresponding test + OfflineUserSessionModel oline = (OfflineUserSessionModel)userSession; + entity.setUser(oline.getUserId()); + // NOTE: Hack + // We skip calling entity.setLoginUsername(userSession.getLoginUsername()) + + } else { + entity.setLoginUsername(userSession.getLoginUsername()); + entity.setUser(userSession.getUser().getId()); + } entity.setStarted(userSession.getStarted()); entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index d0c2a4a804..a79d6fd58b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -176,7 +176,13 @@ public class UserSessionAdapter implements UserSessionModel { @Override public String getLoginUsername() { - return entity.getLoginUsername(); + if (entity.getLoginUsername() == null) { + // this is a hack so that UserModel doesn't have to be available when offline token is imported. + // see related JIRA - KEYCLOAK-5350 and corresponding test + return getUser().getUsername(); + } else { + return entity.getLoginUsername(); + } } public String getIpAddress() { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java index 64246e8d6d..fd18852eef 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -17,6 +17,7 @@ package org.keycloak.models.jpa.session; +import org.jboss.logging.Logger; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -42,6 +43,7 @@ import java.util.Map; * @author Marek Posolda */ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProvider { + private static final Logger logger = Logger.getLogger(JpaUserSessionPersisterProvider.class); private final KeycloakSession session; private final EntityManager em; @@ -205,15 +207,19 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv List userSessionIds = new ArrayList<>(); for (PersistentUserSessionEntity entity : results) { RealmModel realm = session.realms().getRealm(entity.getRealmId()); - UserModel user = session.users().getUserById(entity.getUserId(), realm); - - // Case when user was deleted in the meantime - if (user == null) { - onUserRemoved(realm, entity.getUserId()); - return loadUserSessions(firstResult, maxResults, offline); + try { + UserModel user = session.users().getUserById(entity.getUserId(), realm); + // Case when user was deleted in the meantime + if (user == null) { + onUserRemoved(realm, entity.getUserId()); + return loadUserSessions(firstResult, maxResults, offline); + } + } catch (Exception e) { + logger.debugv(e,"Failed to load user with id {0}", entity.getUserId()); } - result.add(toAdapter(realm, user, entity)); + + result.add(toAdapter(realm, entity)); userSessionIds.add(entity.getUserSessionId()); } @@ -247,14 +253,14 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv return result; } - private PersistentUserSessionAdapter toAdapter(RealmModel realm, UserModel user, PersistentUserSessionEntity entity) { + private PersistentUserSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionEntity entity) { PersistentUserSessionModel model = new PersistentUserSessionModel(); model.setUserSessionId(entity.getUserSessionId()); model.setLastSessionRefresh(entity.getLastSessionRefresh()); model.setData(entity.getData()); Map clientSessions = new HashMap<>(); - return new PersistentUserSessionAdapter(model, realm, user, clientSessions); + return new PersistentUserSessionAdapter(session, model, realm, entity.getUserId(), clientSessions); } private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) { @@ -263,7 +269,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv PersistentClientSessionModel model = new PersistentClientSessionModel(); model.setClientId(entity.getClientId()); model.setUserSessionId(userSession.getId()); - model.setUserId(userSession.getUser().getId()); + model.setUserId(userSession.getUserId()); model.setTimestamp(entity.getTimestamp()); model.setData(entity.getData()); return new PersistentAuthenticatedClientSessionAdapter(model, realm, client, userSession); diff --git a/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java b/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java new file mode 100644 index 0000000000..7ffae7d7d6 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/OfflineUserSessionModel.java @@ -0,0 +1,27 @@ +/* + * 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.models; + +/** + * Hacked extension to UserSessionModel so that user id can be obtain directly so + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface OfflineUserSessionModel extends UserSessionModel { + public String getUserId(); +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index 095a85782c..7a328543fa 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -19,7 +19,9 @@ package org.keycloak.models.session; import com.fasterxml.jackson.annotation.JsonProperty; import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; +import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -33,11 +35,14 @@ import java.util.Map; /** * @author Marek Posolda */ -public class PersistentUserSessionAdapter implements UserSessionModel { +public class PersistentUserSessionAdapter implements OfflineUserSessionModel { private final PersistentUserSessionModel model; - private final UserModel user; + private UserModel user; + private String userId; + private String username; private final RealmModel realm; + private KeycloakSession session; private final Map authenticatedClientSessions; private PersistentUserSessionData data; @@ -60,14 +65,16 @@ public class PersistentUserSessionAdapter implements UserSessionModel { this.model.setLastSessionRefresh(other.getLastSessionRefresh()); this.user = other.getUser(); + this.userId = this.user.getId(); this.realm = other.getRealm(); this.authenticatedClientSessions = other.getAuthenticatedClientSessions(); } - public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, Map clientSessions) { + public PersistentUserSessionAdapter(KeycloakSession session, PersistentUserSessionModel model, RealmModel realm, String userId, Map clientSessions) { + this.session = session; this.model = model; this.realm = realm; - this.user = user; + this.userId = userId; this.authenticatedClientSessions = clientSessions; } @@ -113,9 +120,17 @@ public class PersistentUserSessionAdapter implements UserSessionModel { @Override public UserModel getUser() { + if (user == null) { + user = session.users().getUserById(userId, realm); + } return user; } + @Override + public String getUserId() { + return userId; + } + @Override public RealmModel getRealm() { return realm; @@ -123,7 +138,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel { @Override public String getLoginUsername() { - return user.getUsername(); + return getUser().getUsername(); } @Override diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/AssertEvents.java index 02486fba52..a42f576d66 100755 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -349,6 +349,13 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { throw new AssertionError("No type received within timeout"); } } + public Event event() { + try { + return events.poll(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new AssertionError("No type received within timeout"); + } + } public Event assertEvent(Event actual) { if (expected.getError() != null && !expected.getType().toString().endsWith("_ERROR")) { diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProvider.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProvider.java new file mode 100644 index 0000000000..780d1c7e13 --- /dev/null +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProvider.java @@ -0,0 +1,209 @@ +/* + * 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.testsuite.federation.storage; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.credential.CredentialModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.UserModelDelegate; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.user.ImportedUserValidation; +import org.keycloak.storage.user.UserLookupProvider; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class FailableHardcodedStorageProvider implements UserStorageProvider, UserLookupProvider, ImportedUserValidation, CredentialInputUpdater, CredentialInputValidator { + + public static String username = "billb"; + public static String password = "password"; + public static String email = "billb@nowhere.com"; + public static String first = "Bill"; + public static String last = "Burke"; + public static MultivaluedHashMap attributes = new MultivaluedHashMap<>(); + + public static boolean fail; + + protected ComponentModel model; + protected KeycloakSession session; + protected boolean componentFail; + + public FailableHardcodedStorageProvider(ComponentModel model, KeycloakSession session) { + this.model = model; + this.session = session; + componentFail = model.getConfig().getFirst("fail") != null && model.getConfig().getFirst("fail").equalsIgnoreCase("true"); + } + + @Override + public boolean supportsCredentialType(String credentialType) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + return CredentialModel.PASSWORD.equals(credentialType); + } + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + if (!(input instanceof UserCredentialModel)) return false; + if (!user.getUsername().equals(username)) throw new RuntimeException("UNKNOWN USER!"); + + if (input.getType().equals(UserCredentialModel.PASSWORD)) { + password = ((UserCredentialModel)input).getValue(); + return true; + + } else { + return false; + } + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + + } + + @Override + public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + return Collections.EMPTY_SET; + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + return CredentialModel.PASSWORD.equals(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + if (!(input instanceof UserCredentialModel)) return false; + if (!user.getUsername().equals("billb")) throw new RuntimeException("UNKNOWN USER!"); + if (input.getType().equals(UserCredentialModel.PASSWORD)) { + return password != null && password.equals( ((UserCredentialModel)input).getValue()); + } else { + return false; + } + } + + private static class Delegate extends UserModelDelegate { + public Delegate(UserModel delegate) { + super(delegate); + } + + @Override + public void setUsername(String name) { + super.setUsername(name); + name = name; + } + + @Override + public void setSingleAttribute(String name, String value) { + super.setSingleAttribute(name, value); + attributes.putSingle(name, value); + } + + @Override + public void setAttribute(String name, List values) { + super.setAttribute(name, values); + attributes.put(name, values); + } + + @Override + public void removeAttribute(String name) { + super.removeAttribute(name); + attributes.remove(name); + } + + @Override + public void setFirstName(String firstName) { + super.setFirstName(firstName); + first = firstName; + } + + @Override + public void setLastName(String lastName) { + super.setLastName(lastName); + last = lastName; + } + + @Override + public void setEmail(String em) { + super.setEmail(em); + email = em; + } + } + + @Override + public UserModel validate(RealmModel realm, UserModel user) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + return new Delegate(user); + } + + @Override + public UserModel getUserById(String id, RealmModel realm) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + throw new RuntimeException("THIS IMPORTS SHOULD NEVER BE CALLED"); + } + + @Override + public UserModel getUserByUsername(String uname, RealmModel realm) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + if (!username.equals(uname)) return null; + UserModel local = session.userLocalStorage().getUserByUsername(uname, realm); + if (local != null && !model.getId().equals(local.getFederationLink())) { + throw new RuntimeException("local storage has wrong federation link"); + } + if (local != null) return new Delegate(local); + local = session.userLocalStorage().addUser(realm, uname); + local.setEnabled(true); + local.setFirstName(first); + local.setLastName(last); + local.setEmail(email); + local.setFederationLink(model.getId()); + for (String key : attributes.keySet()) { + List values = attributes.get(key); + if (values == null) continue; + local.setAttribute(key, values); + } + return new Delegate(local); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realm) { + if (fail || componentFail) throw new RuntimeException("FORCED FAILURE"); + return null; + } + + @Override + public void close() { + + } +} diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProviderFactory.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProviderFactory.java new file mode 100644 index 0000000000..46faec0244 --- /dev/null +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/FailableHardcodedStorageProviderFactory.java @@ -0,0 +1,55 @@ +/* + * 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.testsuite.federation.storage; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.storage.UserStorageProviderFactory; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class FailableHardcodedStorageProviderFactory implements UserStorageProviderFactory { + + public static final String PROVIDER_ID = "failable-hardcoded-storage"; + + @Override + public FailableHardcodedStorageProvider create(KeycloakSession session, ComponentModel model) { + return new FailableHardcodedStorageProvider(model, session); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + static List OPTIONS = new LinkedList<>(); + static { + ProviderConfigProperty prop = new ProviderConfigProperty("fail", "fail", "If on, provider will throw exception", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + OPTIONS.add(prop); + } + @Override + public List getConfigProperties() { + return OPTIONS; + } + +} diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java new file mode 100644 index 0000000000..d8d5fbd87a --- /dev/null +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java @@ -0,0 +1,177 @@ +/* + * 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.testsuite.federation.storage; + +import org.junit.After; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.common.constants.ServiceAccountConstants; +import org.keycloak.common.util.Time; +import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialAuthentication; +import org.keycloak.credential.UserCredentialStoreManager; +import org.keycloak.events.Details; +import org.keycloak.events.Event; +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.infinispan.UserAdapter; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.Constants; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +import java.util.Calendar; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserStorageFailureTest { + public static ComponentModel memoryProvider = null; + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setName("failure"); + model.setPriority(0); + model.setProviderId(FailableHardcodedStorageProviderFactory.PROVIDER_ID); + model.setParentId(appRealm.getId()); + memoryProvider = appRealm.addComponentModel(model); + + ClientModel offlineClient = appRealm.addClient("offline-client"); + offlineClient.setEnabled(true); + offlineClient.setDirectAccessGrantsEnabled(true); + offlineClient.setSecret("secret"); + HashSet redirects = new HashSet<>(); + redirects.add(Constants.AUTH_SERVER_ROOT + "/offline-client"); + offlineClient.setRedirectUris(redirects); + offlineClient.setServiceAccountsEnabled(true); + offlineClient.setFullScopeAllowed(true); + + UserModel serviceAccount = manager.getSession().users().addUser(appRealm, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + offlineClient.getClientId()); + serviceAccount.setEnabled(true); + RoleModel role = appRealm.getRole("offline_access"); + Assert.assertNotNull(role); + serviceAccount.grantRole(role); + serviceAccount.setServiceAccountClientLink(offlineClient.getClientId()); + + } + }); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected WebDriver driver; + + @WebResource + protected AppPage appPage; + + @WebResource + protected LoginPage loginPage; + + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + + + // this is a hack so that UserModel doesn't have to be available when offline token is imported. + // see related JIRA - KEYCLOAK-5350 and corresponding test + + /** + * KEYCLOAK-5350 + */ + @Test + public void testKeycloak5350() { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("offline-client"); + oauth.redirectUri(Constants.AUTH_SERVER_ROOT + "/offline-client"); + oauth.doLogin("billb", "password"); + + Event loginEvent = events.expectLogin() + .client("offline-client") + .detail(Details.REDIRECT_URI, Constants.AUTH_SERVER_ROOT + "/offline-client") + .event(); + + final String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret"); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String offlineTokenString = tokenResponse.getRefreshToken(); + RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString); + events.clear(); + + FailableHardcodedStorageProvider.fail = true; + // restart server to make sure we can still boot if user storage is down + keycloakRule.restartServer(); + + // test that once user storage provider is available again we can still access the token. + FailableHardcodedStorageProvider.fail = false; + tokenResponse = oauth.doRefreshTokenRequest(offlineTokenString, "secret"); + Assert.assertNotNull(tokenResponse.getAccessToken()); + token = oauth.verifyToken(tokenResponse.getAccessToken()); + offlineTokenString = tokenResponse.getRefreshToken(); + offlineToken = oauth.verifyRefreshToken(offlineTokenString); + events.clear(); + } + + @After + public void resetTimeoffset() { + Time.setOffset(0); + + } + + //@Test + public void testIDE() throws Exception { + Thread.sleep(100000000); + } + +} diff --git a/testsuite/integration-deprecated/src/test/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/testsuite/integration-deprecated/src/test/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory index dcc5143025..4e0892aa13 100644 --- a/testsuite/integration-deprecated/src/test/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory +++ b/testsuite/integration-deprecated/src/test/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -1,3 +1,4 @@ org.keycloak.testsuite.federation.sync.SyncDummyUserFederationProviderFactory org.keycloak.testsuite.federation.storage.UserPropertyFileStorageFactory -org.keycloak.testsuite.federation.storage.UserMapStorageFactory \ No newline at end of file +org.keycloak.testsuite.federation.storage.UserMapStorageFactory +org.keycloak.testsuite.federation.storage.FailableHardcodedStorageProviderFactory \ No newline at end of file