From e2b1c97e26fad9a1492906df47c79673359b3769 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 24 Feb 2017 12:52:36 +0100 Subject: [PATCH] KEYCLOAK-943 Added initial implementation for update profile --- .../services/resources/AccountService.java | 163 +++++++++++------- .../testsuite/account/AccountTest.java | 2 +- .../testsuite/account/ProfileTest.java | 66 +++++-- 3 files changed, 161 insertions(+), 70 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 9b36a45d71..a3c3fcbf67 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -60,12 +60,7 @@ import org.keycloak.services.validation.Validation; import org.keycloak.storage.ReadOnlyException; import org.keycloak.util.JsonSerialization; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.OPTIONS; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; +import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -259,27 +254,33 @@ public class AccountService extends AbstractSecuredLocalService { */ @Path("/") @GET + @Produces(MediaType.TEXT_HTML) public Response accountPage() { - List types = headers.getAcceptableMediaTypes(); - if (types.contains(MediaType.WILDCARD_TYPE) || (types.contains(MediaType.TEXT_HTML_TYPE))) { - return forwardToPage(null, AccountPages.ACCOUNT); - } else if (types.contains(MediaType.APPLICATION_JSON_TYPE)) { - requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); + return forwardToPage(null, AccountPages.ACCOUNT); + } - UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, auth.getUser()); - if (rep.getAttributes() != null) { - Iterator itr = rep.getAttributes().keySet().iterator(); - while (itr.hasNext()) { - if (itr.next().startsWith("keycloak.")) { - itr.remove(); - } + /** + * Get account information. + * + * @return + */ + @Path("/") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response accountPageJson() { + requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); + + UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, auth.getUser()); + if (rep.getAttributes() != null) { + Iterator itr = rep.getAttributes().keySet().iterator(); + while (itr.hasNext()) { + if (itr.next().startsWith("keycloak.")) { + itr.remove(); } } - - return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build(); - } else { - return Response.notAcceptable(Variant.VariantListBuilder.newInstance().mediaTypes(MediaType.TEXT_HTML_TYPE, MediaType.APPLICATION_JSON_TYPE).build()).build(); } + + return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build(); } public static UriBuilder totpUrl(UriBuilder base) { @@ -377,6 +378,8 @@ public class AccountService extends AbstractSecuredLocalService { UserModel user = auth.getUser(); + event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()); + List errors = Validation.validateUpdateProfileForm(realm.isEditUsernameAllowed(), formData); if (errors != null && !errors.isEmpty()) { setReferrerOnPage(); @@ -384,49 +387,15 @@ public class AccountService extends AbstractSecuredLocalService { } try { - if (realm.isEditUsernameAllowed()) { - String username = formData.getFirst("username"); + updateUsername(formData.getFirst("username"), user); + updateEmail(formData.getFirst("email"), user); - UserModel existing = session.users().getUserByUsername(username, realm); - if (existing != null && !existing.getId().equals(user.getId())) { - throw new ModelDuplicateException(Messages.USERNAME_EXISTS); - } - - user.setUsername(username); - } user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); - String email = formData.getFirst("email"); - String oldEmail = user.getEmail(); - boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; - if (emailChanged && !realm.isDuplicateEmailsAllowed()) { - UserModel existing = session.users().getUserByEmail(email, realm); - if (existing != null && !existing.getId().equals(user.getId())) { - throw new ModelDuplicateException(Messages.EMAIL_EXISTS); - } - } - - user.setEmail(email); - AttributeFormDataProcessor.process(formData, realm, user); - event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()).success(); - - if (emailChanged) { - user.setEmailVerified(false); - event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); - } - - if (realm.isRegistrationEmailAsUsername()) { - if (!realm.isDuplicateEmailsAllowed()) { - UserModel existing = session.users().getUserByEmail(email, realm); - if (existing != null && !existing.getId().equals(user.getId())) { - throw new ModelDuplicateException(Messages.USERNAME_EXISTS); - } - } - user.setUsername(email); - } + event.success(); setReferrerOnPage(); return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT); @@ -439,6 +408,82 @@ public class AccountService extends AbstractSecuredLocalService { } } + @Path("/") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response processAccountUpdateJson(UserRepresentation userRep) { + require(AccountRoles.MANAGE_ACCOUNT); + if (auth.isCookieAuthenticated()) { + throw new ForbiddenException(); + } + + UserModel user = auth.getUser(); + + event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()); + + updateUsername(userRep.getUsername(), user); + updateEmail(userRep.getEmail(), user); + + user.setFirstName(userRep.getFirstName()); + user.setLastName(userRep.getLastName()); + + if (userRep.getAttributes() != null) { + for (String k : user.getAttributes().keySet()) { + if (!userRep.getAttributes().containsKey(k)) { + user.removeAttribute(k); + } + } + + for (Map.Entry> e : userRep.getAttributes().entrySet()) { + user.setAttribute(e.getKey(), e.getValue()); + } + } + + event.success(); + + return Cors.add(request, Response.ok()).build(); + } + + private void updateUsername(String username, UserModel user) { + if (realm.isEditUsernameAllowed() && username != null) { + UserModel existing = session.users().getUserByUsername(username, realm); + if (existing != null && !existing.getId().equals(user.getId())) { + throw new ModelDuplicateException(Messages.USERNAME_EXISTS); + } + + user.setUsername(username); + } + } + + private void updateEmail(String email, UserModel user) { + String oldEmail = user.getEmail(); + boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; + if (emailChanged && !realm.isDuplicateEmailsAllowed()) { + UserModel existing = session.users().getUserByEmail(email, realm); + if (existing != null && !existing.getId().equals(user.getId())) { + throw new ModelDuplicateException(Messages.EMAIL_EXISTS); + } + } + + user.setEmail(email); + + if (emailChanged) { + user.setEmailVerified(false); + event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); + } + + if (realm.isRegistrationEmailAsUsername()) { + if (!realm.isDuplicateEmailsAllowed()) { + UserModel existing = session.users().getUserByEmail(email, realm); + if (existing != null && !existing.getId().equals(user.getId())) { + throw new ModelDuplicateException(Messages.USERNAME_EXISTS); + } + } + user.setUsername(email); + } + } + @Path("totp-remove") @GET public Response processTotpRemove(@QueryParam("stateChecker") String stateChecker) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 3f76556191..db5c4edd9c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -525,8 +525,8 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals("New last", profilePage.getLastName()); Assert.assertEquals("new@email.com", profilePage.getEmail()); - events.expectAccount(EventType.UPDATE_PROFILE).assertEvent(); events.expectAccount(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); + events.expectAccount(EventType.UPDATE_PROFILE).assertEvent(); // reset user for other tests profilePage.updateProfile("Tom", "Brady", "test-user@localhost"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java index e33447a1ec..63a2315faf 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java @@ -22,6 +22,8 @@ import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.jboss.arquillian.drone.api.annotation.Default; import org.jboss.arquillian.graphene.context.GrapheneContext; @@ -45,12 +47,14 @@ import org.keycloak.testsuite.pages.AccountApplicationsPage; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.runonserver.SerializationUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmRepUtil; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.util.JsonSerialization; import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; import org.openqa.selenium.JavascriptExecutor; @@ -68,6 +72,7 @@ import java.io.IOException; import java.net.URI; import java.util.Collections; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -146,22 +151,48 @@ public class ProfileTest extends AbstractTestRealmKeycloakTest { HttpResponse response = doGetProfile(token, null); assertEquals(200, response.getStatusLine().getStatusCode()); - JSONObject profile = new JSONObject(IOUtils.toString(response.getEntity().getContent())); + UserRepresentation profile = JsonSerialization.readValue(IOUtils.toString(response.getEntity().getContent()), UserRepresentation.class); - assertEquals("test-user@localhost", profile.getString("username")); - assertEquals("test-user@localhost", profile.getString("email")); - assertEquals("First", profile.getString("firstName")); - assertEquals("Last", profile.getString("lastName")); + assertEquals("test-user@localhost", profile.getUsername()); + assertEquals("test-user@localhost", profile.getEmail()); + assertEquals("First", profile.getFirstName()); + assertEquals("Last", profile.getLastName()); - JSONObject attributes = profile.getJSONObject("attributes"); - JSONArray attrValue = attributes.getJSONArray("key1"); - assertEquals(1, attrValue.length()); + Map> attributes = profile.getAttributes(); + List attrValue = attributes.get("key1"); + assertEquals(1, attrValue.size()); assertEquals("value1", attrValue.get(0)); - attrValue = attributes.getJSONArray("key2"); - assertEquals(1, attrValue.length()); + attrValue = attributes.get("key2"); + assertEquals(1, attrValue.size()); assertEquals("value2", attrValue.get(0)); } + @Test + public void updateProfile() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); + + UserRepresentation user = new UserRepresentation(); + user.setUsername("test-user@localhost"); + user.setFirstName("NewFirst"); + user.setLastName("NewLast"); + user.setEmail("NewEmail@localhost"); + + HttpResponse response = doUpdateProfile(token, null, JsonSerialization.writeValueAsString(user)); + assertEquals(200, response.getStatusLine().getStatusCode()); + + response = doGetProfile(token, null); + + UserRepresentation profile = JsonSerialization.readValue(IOUtils.toString(response.getEntity().getContent()), UserRepresentation.class); + + assertEquals("test-user@localhost", profile.getUsername()); + assertEquals("newemail@localhost", profile.getEmail()); + assertEquals("NewFirst", profile.getFirstName()); + assertEquals("NewLast", profile.getLastName()); + } + @Test public void getProfileCors() throws Exception { oauth.doLogin("test-user@localhost", "password"); @@ -274,6 +305,21 @@ public class ProfileTest extends AbstractTestRealmKeycloakTest { return client.execute(get); } + private HttpResponse doUpdateProfile(String token, String origin, String value) throws IOException { + HttpClient client = new DefaultHttpClient(); + HttpPost post = new HttpPost(UriBuilder.fromUri(getAccountURI()).build()); + if (token != null) { + post.setHeader(HttpHeaders.AUTHORIZATION, "bearer " + token); + } + if (origin != null) { + post.setHeader("Origin", origin); + } + post.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); + post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + post.setEntity(new StringEntity(value)); + return client.execute(post); + } + private String[] doGetProfileJs(String authServerRoot, String token) { UriBuilder uriBuilder = UriBuilder.fromUri(authServerRoot) .path(TestApplicationResource.class)