Merge pull request #2908 from mstruk/KEYCLOAK-2879-a

KEYCLOAK-2879 UserResource
This commit is contained in:
Stian Thorgersen 2016-06-02 18:53:53 +02:00
commit fba78f3e2a
6 changed files with 518 additions and 59 deletions

View file

@ -146,4 +146,8 @@ public interface UserResource {
@Path("consents/{client}")
public void revokeConsent(@PathParam("client") String clientId);
@POST
@Path("impersonation")
@Produces(MediaType.APPLICATION_JSON)
Map<String, Object> impersonate();
}

View file

@ -0,0 +1,44 @@
/*
* 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.pages;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ConsentPage extends AbstractPage {
@FindBy(id = "kc-login")
private WebElement submitButton;
public void confirm() {
submitButton.click();
}
@Override
public boolean isCurrent() {
return driver.getTitle().equalsIgnoreCase("grant access");
}
@Override
public void open() throws Exception {
}
}

View file

@ -294,6 +294,9 @@ public class ClientTest extends AbstractAdminTest {
Map<String, Long> offlineSessionCount = realm.clients().get(id).getOfflineSessionCount();
assertEquals(new Long(0), offlineSessionCount.get("count"));
List<UserSessionRepresentation> userSessions = realm.users().get(userId).getOfflineSessions(id);
assertEquals("There should be no offline sessions", 0, userSessions.size());
oauth.realm(REALM_NAME);
oauth.redirectUri(client.getRedirectUris().get(0));
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
@ -307,6 +310,17 @@ public class ClientTest extends AbstractAdminTest {
List<UserSessionRepresentation> offlineUserSessions = realm.clients().get(id).getOfflineUserSessions(0, 100);
assertEquals(1, offlineUserSessions.size());
assertEquals("testuser", offlineUserSessions.get(0).getUsername());
userSessions = realm.users().get(userId).getOfflineSessions(id);
assertEquals("There should be one offline session", 1, userSessions.size());
assertOfflineSession(offlineUserSessions.get(0), userSessions.get(0));
}
private void assertOfflineSession(UserSessionRepresentation expected, UserSessionRepresentation actual) {
assertEquals("id", expected.getId(), actual.getId());
assertEquals("userId", expected.getUserId(), actual.getUserId());
assertEquals("userName", expected.getUsername(), actual.getUsername());
assertEquals("clients", expected.getClients(), actual.getClients());
}
@Test

View file

@ -0,0 +1,300 @@
/*
* 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.admin;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.pages.ConsentPage;
import org.keycloak.testsuite.pages.LoginPage;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ConsentsTest extends AbstractKeycloakTest {
final static String REALM_PROV_NAME = "provider";
final static String REALM_CONS_NAME = "consumer";
final static String IDP_OIDC_ALIAS = "kc-oidc-idp";
final static String IDP_OIDC_PROVIDER_ID = "keycloak-oidc";
final static String CLIENT_ID = "brokerapp";
final static String CLIENT_SECRET = "secret";
final static String USER_LOGIN = "testuser";
final static String USER_EMAIL = "user@localhost.com";
final static String USER_PASSWORD = "password";
final static String USER_FIRSTNAME = "User";
final static String USER_LASTNAME = "Tester";
protected RealmRepresentation createProviderRealm() {
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm(REALM_PROV_NAME);
realm.setEnabled(true);
return realm;
}
protected RealmRepresentation createConsumerRealm() {
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm(REALM_CONS_NAME);
realm.setEnabled(true);
return realm;
}
protected List<ClientRepresentation> createProviderClients() {
ClientRepresentation client = new ClientRepresentation();
client.setId(CLIENT_ID);
client.setName(CLIENT_ID);
client.setSecret(CLIENT_SECRET);
client.setEnabled(true);
client.setConsentRequired(true);
client.setRedirectUris(Collections.singletonList(getAuthRoot() +
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint/*"));
client.setAdminUrl(getAuthRoot() +
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint");
return Collections.singletonList(client);
}
protected IdentityProviderRepresentation setUpIdentityProvider() {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
config.put("clientId", CLIENT_ID);
config.put("clientSecret", CLIENT_SECRET);
config.put("prompt", "login");
config.put("authorizationUrl", getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/auth");
config.put("tokenUrl", getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/token");
config.put("logoutUrl", getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/logout");
config.put("userInfoUrl", getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/userinfo");
config.put("defaultScope", "email profile");
config.put("backchannelSupported", "true");
return idp;
}
protected String getUserLogin() {
return USER_LOGIN;
}
protected String getUserPassword() {
return USER_PASSWORD;
}
protected String getUserEmail() {
return USER_EMAIL;
}
protected String getUserFirstName() {
return USER_FIRSTNAME;
}
protected String getUserLastName() {
return USER_LASTNAME;
}
protected String providerRealmName() {
return REALM_PROV_NAME;
}
protected String consumerRealmName() {
return REALM_CONS_NAME;
}
protected String getIDPAlias() {
return IDP_OIDC_ALIAS;
}
@Page
protected LoginPage accountLoginPage;
@Page
protected ConsentPage consentPage;
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation providerRealm = createProviderRealm();
RealmRepresentation consumerRealm = createConsumerRealm();
testRealms.add(providerRealm);
testRealms.add(consumerRealm);
}
@Before
public void createUser() {
log.debug("creating user for realm " + providerRealmName());
UserRepresentation user = new UserRepresentation();
user.setUsername(getUserLogin());
user.setEmail(getUserEmail());
user.setFirstName(getUserFirstName());
user.setLastName(getUserLastName());
user.setEmailVerified(true);
user.setEnabled(true);
RealmResource realmResource = adminClient.realm(providerRealmName());
String userId = createUserWithAdminClient(realmResource, user);
resetUserPassword(realmResource.users().get(userId), getUserPassword(), false);
}
@Before
public void addIdentityProviderToProviderRealm() {
log.debug("adding identity provider to realm " + consumerRealmName());
RealmResource realm = adminClient.realm(consumerRealmName());
realm.identityProviders().create(setUpIdentityProvider());
}
@Before
public void addClients() {
List<ClientRepresentation> clients = createProviderClients();
if (clients != null) {
RealmResource providerRealm = adminClient.realm(providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + providerRealmName());
providerRealm.clients().create(client);
}
}
}
protected String getAuthRoot() {
return suiteContext.getAuthServerInfo().getContextRoot().toString();
}
protected IdentityProviderRepresentation createIdentityProvider(String alias, String providerId) {
IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
identityProviderRepresentation.setAlias(alias);
identityProviderRepresentation.setProviderId(providerId);
identityProviderRepresentation.setEnabled(true);
return identityProviderRepresentation;
}
private void waitForPage(String title) {
long startAt = System.currentTimeMillis();
while (!driver.getTitle().toLowerCase().contains(title)
&& System.currentTimeMillis() - startAt < 200) {
try {
Thread.sleep(5);
} catch (InterruptedException ignore) {}
}
}
@Test
public void testConsents() {
driver.navigate().to(getAccountUrl(consumerRealmName()));
log.debug("Clicking social " + getIDPAlias());
accountLoginPage.clickSocial(getIDPAlias());
if (!driver.getCurrentUrl().contains("/auth/realms/" + providerRealmName() + "/")) {
log.debug("Not on provider realm page, url: " + driver.getCurrentUrl());
}
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + providerRealmName() + "/"));
log.debug("Logging in");
accountLoginPage.login(getUserLogin(), getUserPassword());
waitForPage("grant access");
Assert.assertTrue(consentPage.isCurrent());
consentPage.confirm();
Assert.assertTrue("We must be on correct realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + consumerRealmName() + "/"));
UsersResource consumerUsers = adminClient.realm(consumerRealmName()).users();
Assert.assertTrue("There must be at least one user", consumerUsers.count() > 0);
List<UserRepresentation> users = consumerUsers.search("", 0, 5);
UserRepresentation foundUser = null;
for (UserRepresentation user : users) {
if (user.getUsername().equals(getUserLogin()) && user.getEmail().equals(getUserEmail())) {
foundUser = user;
break;
}
}
Assert.assertNotNull("There must be user " + getUserLogin() + " in realm " + consumerRealmName(),
foundUser);
// get user with the same username from provider realm
RealmResource providerRealm = adminClient.realm(providerRealmName());
users = providerRealm.users().search(null, foundUser.getFirstName(), foundUser.getLastName(), null, 0, 1);
Assert.assertEquals("Same user should be in provider realm", 1, users.size());
String userId = users.get(0).getId();
UserResource userResource = providerRealm.users().get(userId);
// list consents
List<Map<String, Object>> consents = userResource.getConsents();
Assert.assertEquals("There should be one consent", 1, consents.size());
Map<String, Object> consent = consents.get(0);
Assert.assertEquals("Consent should be given to " + CLIENT_ID, CLIENT_ID, consent.get("clientId"));
// list sessions
List<UserSessionRepresentation> sessions = userResource.getUserSessions();
Assert.assertEquals("There should be one active session", 1, sessions.size());
// revoke consent
userResource.revokeConsent(CLIENT_ID);
// list consents
consents = userResource.getConsents();
Assert.assertEquals("There should be no consents", 0, consents.size());
// list sessions
sessions = userResource.getUserSessions();
Assert.assertEquals("There should be no active session", 0, sessions.size());
}
private String getAccountUrl(String realmName) {
return getAuthRoot() + "/auth/realms/" + realmName + "/account";
}
}

View file

@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
@ -29,25 +30,20 @@ import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.models.ImpersonationConstants;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.CredentialBuilder;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -85,21 +81,6 @@ public class ImpersonationTest extends AbstractKeycloakTest {
testRealms.add(realm.build());
}
private String createAdminToken(String username, String realm) {
try {
String password = username.equals("admin") ? "admin" : "password";
String clientId = realm.equals("master") ? Constants.ADMIN_CLI_CLIENT_ID : "myclient";
AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest(realm, username, password, null, clientId, null);
if (tokenResponse.getStatusCode() != 200) {
throw new RuntimeException("Failed to get token: " + tokenResponse.getErrorDescription());
}
events.clear();
return tokenResponse.getAccessToken();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void testImpersonateByMasterAdmin() {
// test that composite is set up right for impersonation role
@ -157,51 +138,65 @@ public class ImpersonationTest extends AbstractKeycloakTest {
}
protected void testSuccessfulImpersonation(String admin, String adminRealm) {
Client client = createClient(admin, adminRealm);
WebTarget impersonate = createImpersonateTarget(client);
Map data = impersonate.request().post(null, Map.class);
Assert.assertNotNull(data);
Assert.assertNotNull(data.get("redirect"));
// TODO Events not working
events.expect(EventType.IMPERSONATE)
.session(AssertEvents.isUUID())
.user(impersonatedUserId)
.detail(Details.IMPERSONATOR, admin)
.detail(Details.IMPERSONATOR_REALM, adminRealm)
.client((String) null).assertEvent();
Keycloak client = login(admin, adminRealm);
try {
Map data = client.realms().realm("test").users().get(impersonatedUserId).impersonate();
Assert.assertNotNull(data);
Assert.assertNotNull(data.get("redirect"));
client.close();
events.expect(EventType.IMPERSONATE)
.session(AssertEvents.isUUID())
.user(impersonatedUserId)
.detail(Details.IMPERSONATOR, admin)
.detail(Details.IMPERSONATOR_REALM, adminRealm)
.client((String) null).assertEvent();
} finally {
client.close();
}
}
protected void testForbiddenImpersonation(String admin, String adminRealm) {
Client client = createClient(admin, adminRealm);
WebTarget impersonate = createImpersonateTarget(client);
Response response = impersonate.request().post(null);
response.close();
client.close();
Keycloak client = createAdminClient(adminRealm, establishClientId(adminRealm), admin);
try {
client.realms().realm("test").users().get(impersonatedUserId).impersonate();
} catch (ClientErrorException e) {
Assert.assertTrue(e.getMessage().indexOf("403 Forbidden") != -1);
} finally {
client.close();
}
}
protected WebTarget createImpersonateTarget(Client client) {
UriBuilder authBase = UriBuilder.fromUri(getAuthServerRoot());
WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase));
WebTarget realmTarget = adminRealms.path("test");
return realmTarget.path("users").path(impersonatedUserId).path("impersonation");
Keycloak createAdminClient(String realm, String clientId, String username) {
return createAdminClient(realm, clientId, username, null);
}
protected Client createClient(String admin, String adminRealm) {
String token = createAdminToken(admin, adminRealm);
final String authHeader = "Bearer " + token;
ClientRequestFilter authFilter = new ClientRequestFilter() {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}
};
return javax.ws.rs.client.ClientBuilder.newBuilder().register(authFilter).build();
String establishClientId(String realm) {
return realm.equals("master") ? Constants.ADMIN_CLI_CLIENT_ID : "myclient";
}
Keycloak createAdminClient(String realm, String clientId, String username, String password) {
if (password == null) {
password = username.equals("admin") ? "admin" : "password";
}
return Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
realm, username, password, clientId);
}
private Keycloak login(String username, String realm) {
String clientId = establishClientId(realm);
Keycloak client = createAdminClient(realm, clientId, username);
client.tokenManager().grantToken();
// only poll for LOGIN event if realm is not master
// - since for master testing event listener is not installed
if (!AuthRealm.MASTER.equals(realm)) {
EventRepresentation e = events.poll();
Assert.assertEquals("Event type", EventType.LOGIN.toString(), e.getType());
Assert.assertEquals("Client ID", clientId, e.getClientId());
Assert.assertEquals("Username", username, e.getDetails().get("username"));
}
return client;
}
}

View file

@ -0,0 +1,102 @@
/*
* 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.admin;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.resources.AccountService;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.pages.AccountTotpPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.LoginPage;
import javax.ws.rs.core.UriBuilder;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class UserTotpTest extends TestRealmKeycloakTest {
private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8180/auth");
public static String ACCOUNT_REDIRECT = AccountService.loginRedirectUrl(BASE.clone()).build("test").toString();
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected AccountTotpPage totpPage;
@Page
protected AccountUpdateProfilePage profilePage;
@Page
protected LoginPage loginPage;
private TimeBasedOTP totp = new TimeBasedOTP();
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Test
public void setupTotp() {
totpPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=totp").assertEvent();
Assert.assertTrue(totpPage.isCurrent());
Assert.assertFalse(driver.getPageSource().contains("Remove Google"));
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
Assert.assertEquals("Mobile authenticator configured.", profilePage.getSuccess());
events.expectAccount(EventType.UPDATE_TOTP).assertEvent();
Assert.assertTrue(driver.getPageSource().contains("pficon-delete"));
List<UserRepresentation> users = adminClient.realms().realm("test").users().search("test-user@localhost", null, null, null, 0, 1);
String userId = users.get(0).getId();
adminClient.realms().realm("test").users().get(userId).removeTotp();
totpPage.open();
Assert.assertFalse(driver.getPageSource().contains("pficon-delete"));
AdminEventRepresentation event = testingClient.testing().pollAdminEvent();
Assert.assertNotNull(event);
Assert.assertEquals(OperationType.ACTION.name(), event.getOperationType());
Assert.assertEquals("users/" + userId + "/remove-totp", event.getResourcePath());
}
}