From b1d057fdff7cd73d237aef8808f0223a2c38794a Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Fri, 17 Apr 2015 06:31:19 -0700 Subject: [PATCH 01/18] [artifactory-release] Next development version [skip ci] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4cc88a45e0d..3db4174416d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.2.4-SNAPSHOT +version=2.2.5-SNAPSHOT From cdf511ff9d3cbbaf31e7d16325ad641e62111768 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 20 Apr 2015 09:39:42 -0600 Subject: [PATCH 02/18] Rename tests prior to feature implementation --- ... ResetPasswordControllerMockMvcTests.java} | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename uaa/src/test/java/org/cloudfoundry/identity/uaa/login/{ResetPasswordControllerIntegrationTests.java => ResetPasswordControllerMockMvcTests.java} (91%) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java similarity index 91% rename from uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerIntegrationTests.java rename to uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java index 360d2ad6e77..a9be6dcab1f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java @@ -21,8 +21,8 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.test.YamlServletProfileInitializerContextInitializer; -import org.junit.After; -import org.junit.Before; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; @@ -46,15 +46,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -public class ResetPasswordControllerIntegrationTests extends TestClassNullifier { +public class ResetPasswordControllerMockMvcTests extends TestClassNullifier { - XmlWebApplicationContext webApplicationContext; + static XmlWebApplicationContext webApplicationContext; - private MockMvc mockMvc; - private ExpiringCodeStore codeStore; + private static MockMvc mockMvc; + private static ExpiringCodeStore codeStore; - @Before - public void setUp() throws Exception { + @BeforeClass + public static void initResetPasswordTest() throws Exception { webApplicationContext = new XmlWebApplicationContext(); new YamlServletProfileInitializerContextInitializer().initializeContext(webApplicationContext, "login.yml,uaa.yml"); webApplicationContext.setConfigLocation("file:./src/main/webapp/WEB-INF/spring-servlet.xml"); @@ -67,8 +67,8 @@ public void setUp() throws Exception { codeStore = webApplicationContext.getBean(ExpiringCodeStore.class); } - @After - public void cleanUpAfterPasswordReset() throws Exception { + @AfterClass + public static void cleanUpAfterPasswordReset() throws Exception { Flyway flyway = webApplicationContext.getBean(Flyway.class); flyway.clean(); webApplicationContext.destroy(); From a79b89f6e4f66626914b029b7a15a423491f8013 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 20 Apr 2015 11:10:32 -0600 Subject: [PATCH 03/18] password reset link expiration logic 1. backwards compatible - if the link results in a user_id, we check last_modified date of the user and reject the link if the user has been modified 2. going forward - we store both the username and the user_id, thus we can see if the email(username) has been modified https://www.pivotaltracker.com/story/show/92065804 [#92065804] --- .../login/EmailResetPasswordServiceTests.java | 5 +- .../endpoints/PasswordResetEndpoints.java | 57 +++++++++- .../endpoints/PasswordResetEndpointsTest.java | 26 ++++- .../ResetPasswordControllerMockMvcTests.java | 103 +++++++++++++++++- 4 files changed, 179 insertions(+), 12 deletions(-) diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailResetPasswordServiceTests.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailResetPasswordServiceTests.java index 2602a5be0e5..8c795cd7eb8 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailResetPasswordServiceTests.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailResetPasswordServiceTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -15,6 +15,7 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.Map; import static org.hamcrest.Matchers.containsString; @@ -30,6 +31,7 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; +import org.cloudfoundry.identity.uaa.scim.ScimMeta; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordResetEndpoints; @@ -193,6 +195,7 @@ public void testForgotPasswordWhenTheCodeIsDenied() throws Exception { @Test public void testResetPassword() throws Exception { ScimUser user = new ScimUser("usermans-id","userman","firstName","lastName"); + user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.retrieve(eq("usermans-id"))).thenReturn(user); when(codeStore.retrieveCode(eq("secret_code"))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), "usermans-id")); diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoints.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoints.java index 6d33c03fbdc..716fe2f05cb 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoints.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoints.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -22,6 +22,8 @@ import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.JsonUtils.JsonUtilException; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.context.ApplicationEvent; @@ -49,6 +51,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; @Controller public class PasswordResetEndpoints implements ApplicationEventPublisherAware { @@ -85,7 +88,8 @@ public ResponseEntity> resetPassword(@RequestBody String emai } } ScimUser scimUser = results.get(0); - String code = expiringCodeStore.generateCode(scimUser.getId(), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME)).getCode(); + PasswordChange change = new PasswordChange(scimUser.getId(), scimUser.getUserName()); + String code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME)).getCode(); publish(new ResetPasswordRequestEvent(email, code, SecurityContextHolder.getContext().getAuthentication())); response.put("code", code); response.put("user_id", scimUser.getId()); @@ -139,20 +143,31 @@ private ResponseEntity> changePasswordUsernamePasswordAuthent } } - private ResponseEntity> changePasswordCodeAuthenticated(PasswordChange passwordChange) { + protected ResponseEntity> changePasswordCodeAuthenticated(PasswordChange passwordChange) { ExpiringCode expiringCode = expiringCodeStore.retrieveCode(passwordChange.getCode()); if (expiringCode == null) { return new ResponseEntity<>(BAD_REQUEST); } - String userId = expiringCode.getData(); + String userId; + String userName = null; + try { + PasswordChange change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); + userId = change.getUserId(); + userName = change.getUsername(); + } catch (JsonUtilException x) { + userId = expiringCode.getData(); + } ScimUser user = scimUserProvisioning.retrieve(userId); + Map userInfo = new HashMap<>(); try { + if (isUserModified(user, expiringCode.getExpiresAt(), userName)) { + return new ResponseEntity<>(BAD_REQUEST); + } if (!user.isVerified()) { scimUserProvisioning.verifyUser(userId, -1); } scimUserProvisioning.changePassword(userId, null, passwordChange.getNewPassword()); publish(new PasswordChangeEvent("Password changed", getUaaUser(user), SecurityContextHolder.getContext().getAuthentication())); - Map userInfo = new HashMap<>(); userInfo.put("user_id", user.getId()); userInfo.put("username", user.getUserName()); userInfo.put("email", user.getPrimaryEmail()); @@ -169,7 +184,19 @@ private ResponseEntity> changePasswordCodeAuthenticated(Passw } } - private UaaUser getUaaUser(ScimUser scimUser) { + protected boolean isUserModified(ScimUser user, Timestamp expiresAt, String userName) { + if (userName!=null) { + return ! userName.equals(user.getUserName()); + } + //left over from when all we stored in the code was the user ID + //here we will check the timestamp + //TODO - REMOVE THIS IN FUTURE RELEASE, ALL LINKS HAVE BEEN EXPIRED (except test created ones) + long codeCreated = expiresAt.getTime() - PASSWORD_RESET_LIFETIME; + long userModified = user.getMeta().getLastModified().getTime(); + return (userModified > codeCreated); + } + + protected UaaUser getUaaUser(ScimUser scimUser) { Date today = new Date(); return new UaaUser(scimUser.getId(), scimUser.getUserName(), "N/A", scimUser.getPrimaryEmail(), null, scimUser.getGivenName(), @@ -178,6 +205,16 @@ private UaaUser getUaaUser(ScimUser scimUser) { } public static class PasswordChange { + public PasswordChange() {} + + public PasswordChange(String userId, String username) { + this.userId = userId; + this.username = username; + } + + @JsonProperty("user_id") + private String userId; + @JsonProperty("username") private String username; @@ -221,6 +258,14 @@ public String getNewPassword() { public void setNewPassword(String newPassword) { this.newPassword = newPassword; } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } } protected void publish(ApplicationEvent event) { diff --git a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointsTest.java b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointsTest.java index 8aaf8ccabde..5d1701f261f 100644 --- a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointsTest.java +++ b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointsTest.java @@ -16,9 +16,12 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; +import org.cloudfoundry.identity.uaa.scim.ScimMeta; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordResetEndpoints.PasswordChange; import org.cloudfoundry.identity.uaa.test.MockAuthentication; +import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.codehaus.jackson.map.ObjectMapper; import org.junit.Before; import org.junit.Test; @@ -30,7 +33,9 @@ import java.sql.Timestamp; import java.util.Arrays; +import java.util.Date; +import static org.cloudfoundry.identity.uaa.scim.endpoints.PasswordResetEndpoints.PASSWORD_RESET_LIFETIME; import static org.hamcrest.Matchers.containsString; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -54,12 +59,22 @@ public void setUp() throws Exception { mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); Mockito.when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class))) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + 1000), "id001")); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), "id001")); + + PasswordChange change = new PasswordChange("id001", "user@example.com"); + Mockito.when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class))) + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), "id001")); + + change = new PasswordChange("id001", "user\"'@example.com"); + Mockito.when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class))) + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME), "id001")); + } @Test public void testCreatingAPasswordResetWhenTheUsernameExists() throws Exception { ScimUser user = new ScimUser("id001", "user@example.com", null, null); + user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); user.addEmail("user@example.com"); Mockito.when(scimUserProvisioning.query("userName eq \"user@example.com\" and origin eq \"" + Origin.UAA + "\"")) .thenReturn(Arrays.asList(user)); @@ -95,6 +110,7 @@ public void testCreatingAPasswordResetWhenTheUserHasNonUaaOrigin() throws Except .thenReturn(Arrays.asList()); ScimUser user = new ScimUser("id001", "user@example.com", null, null); + user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); user.addEmail("user@example.com"); user.setOrigin(Origin.LDAP); Mockito.when(scimUserProvisioning.query("userName eq \"user@example.com\"")) @@ -113,6 +129,7 @@ public void testCreatingAPasswordResetWhenTheUserHasNonUaaOrigin() throws Except @Test public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() throws Exception { ScimUser user = new ScimUser("id001", "user\"'@example.com", null, null); + user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); user.addEmail("user\"'@example.com"); Mockito.when(scimUserProvisioning.query("userName eq \"user\\\"'@example.com\" and origin eq \"" + Origin.UAA + "\"")) .thenReturn(Arrays.asList(user)); @@ -146,9 +163,10 @@ public void testCreatingAPasswordResetWithAUsernameContainingSpecialCharacters() @Test public void testChangingAPasswordWithAValidCode() throws Exception { Mockito.when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis()), "eyedee")); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis()+ PASSWORD_RESET_LIFETIME), "eyedee")); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); + scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); scimUser.addEmail("user@example.com"); Mockito.when(scimUserProvisioning.retrieve("eyedee")).thenReturn(scimUser); @@ -170,9 +188,10 @@ public void testChangingAPasswordWithAValidCode() throws Exception { @Test public void testChangingAPasswordForUnverifiedUser() throws Exception { Mockito.when(expiringCodeStore.retrieveCode("secret_code")) - .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis()), "eyedee")); + .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis()+ PASSWORD_RESET_LIFETIME), "eyedee")); ScimUser scimUser = new ScimUser("eyedee", "user@example.com", "User", "Man"); + scimUser.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); scimUser.addEmail("user@example.com"); scimUser.setVerified(false); Mockito.when(scimUserProvisioning.retrieve("eyedee")).thenReturn(scimUser); @@ -196,6 +215,7 @@ public void testChangingAPasswordForUnverifiedUser() throws Exception { @Test public void testChangingAPasswordWithAUsernameAndPassword() throws Exception { ScimUser user = new ScimUser("id001", "user@example.com", null, null); + user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); user.addEmail("user@example.com"); Mockito.when(scimUserProvisioning.query("userName eq \"user@example.com\"")) .thenReturn(Arrays.asList(user)); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java index a9be6dcab1f..3a7e7f2097a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java @@ -20,7 +20,9 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordResetEndpoints; import org.cloudfoundry.identity.uaa.test.YamlServletProfileInitializerContextInitializer; +import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -35,8 +37,10 @@ import org.springframework.web.context.support.XmlWebApplicationContext; import java.sql.Timestamp; +import java.util.Arrays; import java.util.List; +import static org.cloudfoundry.identity.uaa.scim.endpoints.PasswordResetEndpoints.PASSWORD_RESET_LIFETIME; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; @@ -74,12 +78,18 @@ public static void cleanUpAfterPasswordReset() throws Exception { webApplicationContext.destroy(); } + @Test - public void testResettingAPassword() throws Exception { + public void testResettingAPasswordUsingUsernameToEnsureNoModification() throws Exception { + List users = webApplicationContext.getBean(ScimUserProvisioning.class).query("username eq \"marissa\""); assertNotNull(users); assertEquals(1, users.size()); - ExpiringCode code = codeStore.generateCode(users.get(0).getId(), new Timestamp(System.currentTimeMillis()+50000)); + PasswordResetEndpoints.PasswordChange change = new PasswordResetEndpoints.PasswordChange(); + change.setUserId(users.get(0).getId()); + change.setUsername(users.get(0).getUserName()); + + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis()+ PASSWORD_RESET_LIFETIME)); MockHttpServletRequestBuilder post = post("/reset_password.do") .param("code", code.getCode()) @@ -101,4 +111,93 @@ public void testResettingAPassword() throws Exception { assertThat(principal.getEmail(), equalTo(users.get(0).getPrimaryEmail())); assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } + + @Test + public void testResettingAPasswordFailsWhenUsernameChanged() throws Exception { + + ScimUserProvisioning userProvisioning = webApplicationContext.getBean(ScimUserProvisioning.class); + List users = userProvisioning.query("username eq \"marissa\""); + assertNotNull(users); + assertEquals(1, users.size()); + ScimUser user = users.get(0); + PasswordResetEndpoints.PasswordChange change = new PasswordResetEndpoints.PasswordChange(); + change.setUserId(user.getId()); + change.setUsername(user.getUserName()); + + ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis()+50000)); + + String formerUsername = user.getUserName(); + user.setUserName("newusername"); + user = userProvisioning.update(user.getId(), user); + try { + MockHttpServletRequestBuilder post = post("/reset_password.do") + .param("code", code.getCode()) + .param("email", user.getPrimaryEmail()) + .param("password", "newpassword") + .param("password_confirmation", "newpassword"); + + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()); + } finally { + user.setUserName(formerUsername); + userProvisioning.update(user.getId(), user); + } + } + + @Test + public void testResettingAPasswordUsingTimestampForUserModification() throws Exception { + List users = webApplicationContext.getBean(ScimUserProvisioning.class).query("username eq \"marissa\""); + assertNotNull(users); + assertEquals(1, users.size()); + ExpiringCode code = codeStore.generateCode(users.get(0).getId(), new Timestamp(System.currentTimeMillis()+ PASSWORD_RESET_LIFETIME)); + + MockHttpServletRequestBuilder post = post("/reset_password.do") + .param("code", code.getCode()) + .param("email", users.get(0).getPrimaryEmail()) + .param("password", "newpassword") + .param("password_confirmation", "newpassword"); + + MvcResult mvcResult = mockMvc.perform(post) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("home")) + .andReturn(); + + SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + Authentication authentication = securityContext.getAuthentication(); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); + assertThat(principal.getId(), equalTo(users.get(0).getId())); + assertThat(principal.getName(), equalTo(users.get(0).getUserName())); + assertThat(principal.getEmail(), equalTo(users.get(0).getPrimaryEmail())); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + } + + @Test + public void testResettingAPasswordUsingTimestampUserModified() throws Exception { + ScimUserProvisioning userProvisioning = webApplicationContext.getBean(ScimUserProvisioning.class); + List users = userProvisioning.query("username eq \"marissa\""); + assertNotNull(users); + assertEquals(1, users.size()); + ScimUser user = users.get(0); + ExpiringCode code = codeStore.generateCode(user.getId(), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME)); + + MockHttpServletRequestBuilder post = post("/reset_password.do") + .param("code", code.getCode()) + .param("email", user.getPrimaryEmail()) + .param("password", "newpassword") + .param("password_confirmation", "newpassword"); + + if (Arrays.asList(webApplicationContext.getEnvironment().getActiveProfiles()).contains("mysql")) { + Thread.sleep(1050); + } else { + Thread.sleep(50); + } + + userProvisioning.update(user.getId(), user); + + mockMvc.perform(post) + .andExpect(status().isUnprocessableEntity()); + + + } } From de1d439a1d703a40f972d40b43d8eb68ea2f21f3 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 20 Apr 2015 16:01:24 -0700 Subject: [PATCH 04/18] Add optional parameter to identity-providers GET to retrieve only active IDPs [#92876196] https://www.pivotaltracker.com/story/show/92876196 Signed-off-by: Madhura Bhave --- .../uaa/zone/IdentityProviderEndpoints.java | 9 +++--- ...IdentityProviderEndpointsMockMvcTests.java | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java b/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java index 65160c9116e..ba27389650e 100644 --- a/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java +++ b/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java @@ -14,15 +14,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicLdapAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.LdapLoginAuthenticationManager; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.login.saml.IdentityProviderDefinition; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.cloudfoundry.identity.uaa.util.JsonUtils.JsonUtilException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.BadCredentialsException; @@ -32,6 +29,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.PrintWriter; @@ -85,8 +83,9 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str } @RequestMapping(method = GET) - public ResponseEntity> retrieveIdentityProviders() { - List identityProviderList = identityProviderProvisioning.retrieveAll(false,IdentityZoneHolder.get().getId()); + public ResponseEntity> retrieveIdentityProviders(@RequestParam(required = false) String retrieveActive) { + Boolean retrieveActiveIdps = Boolean.valueOf(retrieveActive); + List identityProviderList = identityProviderProvisioning.retrieveAll(retrieveActiveIdps, IdentityZoneHolder.get().getId()); return new ResponseEntity<>(identityProviderList, OK); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java index e2c3d975336..b9f0f172a48 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java @@ -55,6 +55,7 @@ import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; @@ -152,6 +153,15 @@ public void testCreateSamlProvider() throws Exception { @Test public void testEnsureWeRetrieveInactiveIDPsToo() throws Exception { + testRetrieveIdps(false); + } + + @Test + public void testRetrieveOnlyActiveIdps() throws Exception { + testRetrieveIdps(true); + } + + private void testRetrieveIdps(boolean retrieveActive) throws Exception { String clientId = RandomStringUtils.randomAlphabetic(6); BaseClientDetails client = new BaseClientDetails(clientId,null,"idps.write,idps.read","password",null); client.setClientSecret("test-client-secret"); @@ -163,11 +173,12 @@ public void testEnsureWeRetrieveInactiveIDPsToo() throws Exception { IdentityProvider identityProvider = MultitenancyFixture.identityProvider(randomOriginKey, IdentityZone.getUaa().getId()); IdentityProvider createdIDP = createIdentityProvider(null, identityProvider, accessToken, status().isCreated()); - MockHttpServletRequestBuilder requestBuilder = get("/identity-providers") - .header("Authorization", "Bearer" + accessToken) - .contentType(APPLICATION_JSON); + String retrieveActiveParam = retrieveActive ? "?retrieveActive=true" : ""; + MockHttpServletRequestBuilder requestBuilder = get("/identity-providers" + retrieveActiveParam) + .header("Authorization", "Bearer" + accessToken) + .contentType(APPLICATION_JSON); - int numberOfIdps = identityProviderProvisioning.retrieveAll(false, IdentityZone.getUaa().getId()).size(); + int numberOfIdps = identityProviderProvisioning.retrieveAll(retrieveActive, IdentityZone.getUaa().getId()).size(); MvcResult result = mockMvc.perform(requestBuilder).andExpect(status().isOk()).andReturn(); List identityProviderList = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference>() {}); @@ -180,8 +191,13 @@ public void testEnsureWeRetrieveInactiveIDPsToo() throws Exception { result = mockMvc.perform(requestBuilder).andExpect(status().isOk()).andReturn(); identityProviderList = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference>() { }); - assertEquals(numberOfIdps, identityProviderList.size()); - assertTrue(identityProviderList.contains(createdIDP)); + if (!retrieveActive) { + assertEquals(numberOfIdps, identityProviderList.size()); + assertTrue(identityProviderList.contains(createdIDP)); + } else { + assertEquals(numberOfIdps - 1, identityProviderList.size()); + assertFalse(identityProviderList.contains(createdIDP)); + } } private void createAndUpdateIdentityProvider(String accessToken, String zoneId) throws Exception { From 36d7ff1fe8f5d90196ba94d3b4d6101feb2b6346 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 21 Apr 2015 17:18:28 -0700 Subject: [PATCH 05/18] Document /identity-providers GET endpoint Signed-off-by: Chris Dutra --- docs/UAA-APIs.rst | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 7da9514320f..8c83566f199 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -892,8 +892,36 @@ You will be able to see all tokens used by these steps in the ``~/.uaac.yml`` fi Identity Provider API Documentation ----------------------------------- +================== ========================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== +Request ``GET /identity-providers/{id}`` (returns a single provider) or ``GET /identity-providers`` (returns an array of providers) +Header ``X-Identity-Zone-Id`` (if using zones..admin scope against default UAA zone) +Scopes Required ``zones..admin`` or ``idps.read`` +Request Parameters retrieveActive (optional parameter for /identity-providers). Set to true to retrieve only active Identity Providers +Response body *example* :: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "id":"50cf6125-4372-475e-94e8-c43f84111e75", + "originKey":"uaa", + "name":"internal", + "type":"internal", + "config":null, + "version":0, + "created":1426260091149, + "active":true, + "identityZoneId": + "testzone1", + "last_modified":1426260091149 + } + ] + +================== ========================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== + ================ ========================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== -Request ``POST /identity-providers`` or ``PUT /identity-providers/{id}`` or ``GET /identity-providers/{id}`` (returns a single provider) or ``GET /identity-providers`` (returns an array of providers) +Request ``POST /identity-providers`` or ``PUT /identity-providers/{id}`` Header ``X-Identity-Zone-Id`` (if using zones..admin scope against default UAA zone) Scopes Required ``zones..admin`` or ``idps.read`` and ``idps.write`` Request body *example* :: From c9c27725b8a73faa6def93867fd410c64c5d70dc Mon Sep 17 00:00:00 2001 From: Chris Dutra Date: Wed, 22 Apr 2015 13:39:29 -0700 Subject: [PATCH 06/18] Change parameter for list of identity providers to 'active_only' Signed-off-by: Madhura Bhave --- docs/UAA-APIs.rst | 2 +- .../identity/uaa/zone/IdentityProviderEndpoints.java | 6 +++--- .../mock/zones/IdentityProviderEndpointsMockMvcTests.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 8c83566f199..e6013b1cdb2 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -896,7 +896,7 @@ Identity Provider API Documentation Request ``GET /identity-providers/{id}`` (returns a single provider) or ``GET /identity-providers`` (returns an array of providers) Header ``X-Identity-Zone-Id`` (if using zones..admin scope against default UAA zone) Scopes Required ``zones..admin`` or ``idps.read`` -Request Parameters retrieveActive (optional parameter for /identity-providers). Set to true to retrieve only active Identity Providers +Request Parameters active_only (optional parameter for /identity-providers). Set to true to retrieve only active Identity Providers Response body *example* :: HTTP/1.1 200 OK diff --git a/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java b/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java index ba27389650e..138c411160e 100644 --- a/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java +++ b/uaa/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java @@ -83,9 +83,9 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str } @RequestMapping(method = GET) - public ResponseEntity> retrieveIdentityProviders(@RequestParam(required = false) String retrieveActive) { - Boolean retrieveActiveIdps = Boolean.valueOf(retrieveActive); - List identityProviderList = identityProviderProvisioning.retrieveAll(retrieveActiveIdps, IdentityZoneHolder.get().getId()); + public ResponseEntity> retrieveIdentityProviders(@RequestParam(value = "active_only", required = false) String activeOnly) { + Boolean retrieveActiveOnly = Boolean.valueOf(activeOnly); + List identityProviderList = identityProviderProvisioning.retrieveAll(retrieveActiveOnly, IdentityZoneHolder.get().getId()); return new ResponseEntity<>(identityProviderList, OK); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java index b9f0f172a48..e89559c904d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java @@ -173,7 +173,7 @@ private void testRetrieveIdps(boolean retrieveActive) throws Exception { IdentityProvider identityProvider = MultitenancyFixture.identityProvider(randomOriginKey, IdentityZone.getUaa().getId()); IdentityProvider createdIDP = createIdentityProvider(null, identityProvider, accessToken, status().isCreated()); - String retrieveActiveParam = retrieveActive ? "?retrieveActive=true" : ""; + String retrieveActiveParam = retrieveActive ? "?active_only=true" : ""; MockHttpServletRequestBuilder requestBuilder = get("/identity-providers" + retrieveActiveParam) .header("Authorization", "Bearer" + accessToken) .contentType(APPLICATION_JSON); From 786ae0594c7dad1193ded29368459281d8dd1fbb Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 21 Apr 2015 16:25:01 -0700 Subject: [PATCH 07/18] Add lastModified field to oauth_client_details - migrate and normalize database columns [#93127142] https://www.pivotaltracker.com/story/show/93127142 [#93032394] https://www.pivotaltracker.com/story/show/93032394 Signed-off-by: Chris Dutra Signed-off-by: Madhura Bhave --- .../JdbcQueryableClientDetailsService.java | 13 +++--- .../identity/uaa/util/JsonUtils.java | 10 ++++- .../JdbcIdentityProviderProvisioning.java | 4 +- .../zone/JdbcIdentityZoneProvisioning.java | 4 +- .../MultitenantJdbcClientDetailsService.java | 18 ++++++-- ...1__Add_Last_Modified_To_Client_Details.sql | 1 + ...1__Add_Last_Modified_To_Client_Details.sql | 5 +++ ...1__Add_Last_Modified_To_Client_Details.sql | 1 + ...titenantJdbcClientDetailsServiceTests.java | 27 ++++++++---- docs/UAA-APIs.rst | 9 ++-- .../ClientAdminEndpointsMockMvcTests.java | 43 +++++++++++++++++++ 11 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_1_1__Add_Last_Modified_To_Client_Details.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_1_1__Add_Last_Modified_To_Client_Details.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_1_1__Add_Last_Modified_To_Client_Details.sql diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/JdbcQueryableClientDetailsService.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/JdbcQueryableClientDetailsService.java index d856635c09d..2e298334719 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/JdbcQueryableClientDetailsService.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/JdbcQueryableClientDetailsService.java @@ -38,15 +38,13 @@ public class JdbcQueryableClientDetailsService extends AbstractQueryable T readValue(String s, Class clazz) throws JsonUtilException } } - public static T readValue(String s, TypeReference typeReference) { + public static T readValue(String s, TypeReference typeReference) { try { return objectMapper.readValue(s, typeReference); } catch (IOException e) { @@ -52,6 +52,14 @@ public static T readValue(String s, TypeReference typeReference) { } } + public static T convertValue(Object object, Class toClazz) throws JsonUtilException { + try { + return objectMapper.convertValue(object, toClazz); + } catch (IllegalArgumentException e) { + throw new JsonUtilException(e); + } + } + public static class JsonUtilException extends RuntimeException { private static final long serialVersionUID = -4804245225960963421L; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java index 71d8c238ca6..812ad6469b6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java @@ -34,11 +34,11 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisioning { - public static final String ID_PROVIDER_FIELDS = "id,version,created,lastModified,name,origin_key,type,config,identity_zone_id,active"; + public static final String ID_PROVIDER_FIELDS = "id,version,created,lastmodified,name,origin_key,type,config,identity_zone_id,active"; public static final String CREATE_IDENTITY_PROVIDER_SQL = "insert into identity_provider(" + ID_PROVIDER_FIELDS + ") values (?,?,?,?,?,?,?,?,?,?)"; - public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastModified,name,type,config,active".replace(",","=?,")+"=?"; + public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active".replace(",","=?,")+"=?"; public static final String IDENTITY_PROVIDERS_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider where identity_zone_id=?"; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java index 206c8dad2cb..115e6e33b79 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java @@ -28,9 +28,9 @@ public class JdbcIdentityZoneProvisioning implements IdentityZoneProvisioning { - public static final String ID_ZONE_FIELDS = "id,version,created,lastModified,name,subdomain,description"; + public static final String ID_ZONE_FIELDS = "id,version,created,lastmodified,name,subdomain,description"; - public static final String ID_ZONE_UPDATE_FIELDS = "version,lastModified,name,subdomain,description".replace(",","=?,")+"=?"; + public static final String ID_ZONE_UPDATE_FIELDS = "version,lastmodified,name,subdomain,description".replace(",","=?,")+"=?"; public static final String CREATE_IDENTITY_ZONE_SQL = "insert into identity_zone(" + ID_ZONE_FIELDS + ") values (?,?,?,?,?,?,?)"; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java index c992cd5f5ac..09c0b293ba8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java @@ -14,7 +14,10 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -58,7 +61,7 @@ public class MultitenantJdbcClientDetailsService extends JdbcClientDetailsServic private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, " + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " - + "refresh_token_validity, additional_information, autoapprove"; + + "refresh_token_validity, additional_information, autoapprove, lastmodified"; private static final String CLIENT_FIELDS = "client_secret, " + CLIENT_FIELDS_FOR_UPDATE; @@ -70,7 +73,7 @@ public class MultitenantJdbcClientDetailsService extends JdbcClientDetailsServic private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ? and identity_zone_id = ?"; private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (" + CLIENT_FIELDS - + ", client_id, identity_zone_id) values (?,?,?,?,?,?,?,?,?,?,?,?)"; + + ", client_id, identity_zone_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?)"; private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details " + "set " + CLIENT_FIELDS_FOR_UPDATE.replaceAll(", ", "=?, ") + "=? where client_id = ? and identity_zone_id = ?"; @@ -187,7 +190,8 @@ private Object[] getFieldsForUpdate(ClientDetails clientDetails) { clientDetails.getAuthorities() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails .getAuthorities()) : null, clientDetails.getAccessTokenValiditySeconds(), clientDetails.getRefreshTokenValiditySeconds(), json, getAutoApproveScopes(clientDetails), - clientDetails.getClientId(), IdentityZoneHolder.get().getId() }; + new Timestamp(System.currentTimeMillis()), + clientDetails.getClientId(), IdentityZoneHolder.get().getId()}; } private String getAutoApproveScopes(ClientDetails clientDetails) { @@ -262,6 +266,8 @@ public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException { if (rs.getObject(9) != null) { details.setRefreshTokenValiditySeconds(rs.getInt(9)); } + + String json = rs.getString(10); if (json != null) { try { @@ -276,6 +282,12 @@ public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException { if (scopes != null) { details.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(scopes)); } + + // lastModified + if (rs.getObject(12) != null) { + details.addAdditionalInformation("lastModified", rs.getTimestamp(12)); + } + return details; } } diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_1_1__Add_Last_Modified_To_Client_Details.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_1_1__Add_Last_Modified_To_Client_Details.sql new file mode 100644 index 00000000000..71dd195d027 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_1_1__Add_Last_Modified_To_Client_Details.sql @@ -0,0 +1 @@ +ALTER TABLE oauth_client_details ADD COLUMN lastmodified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_1_1__Add_Last_Modified_To_Client_Details.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_1_1__Add_Last_Modified_To_Client_Details.sql new file mode 100644 index 00000000000..2bad673fb4a --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_1_1__Add_Last_Modified_To_Client_Details.sql @@ -0,0 +1,5 @@ +ALTER TABLE oauth_client_details ADD COLUMN lastmodified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; + +ALTER TABLE identity_provider CHANGE lastModified lastmodified TIMESTAMP NULL; + +ALTER TABLE identity_zone CHANGE lastModified lastmodified TIMESTAMP NULL; \ No newline at end of file diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_1_1__Add_Last_Modified_To_Client_Details.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_1_1__Add_Last_Modified_To_Client_Details.sql new file mode 100644 index 00000000000..71dd195d027 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_1_1__Add_Last_Modified_To_Client_Details.sql @@ -0,0 +1 @@ +ALTER TABLE oauth_client_details ADD COLUMN lastmodified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsServiceTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsServiceTests.java index 5414e5e9f50..c491ea5644d 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsServiceTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsServiceTests.java @@ -6,8 +6,11 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -35,9 +38,9 @@ public class MultitenantJdbcClientDetailsServiceTests { private EmbeddedDatabase db; - private static final String SELECT_SQL = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity from oauth_client_details where client_id=?"; + private static final String SELECT_SQL = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, lastmodified from oauth_client_details where client_id=?"; - private static final String INSERT_SQL = "insert into oauth_client_details (client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, autoapprove, identity_zone_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + private static final String INSERT_SQL = "insert into oauth_client_details (client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, autoapprove, identity_zone_id, lastmodified) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; private IdentityZone otherIdentityZone; @@ -75,7 +78,7 @@ public void testLoadingClientForNonExistingClientId() { @Test public void testLoadingClientIdWithNoDetails() { int rowsInserted = jdbcTemplate.update(INSERT_SQL, "clientIdWithNoDetails", null, null, - null, null, null, null, null, null, null, IdentityZoneHolder.get().getId()); + null, null, null, null, null, null, null, IdentityZoneHolder.get().getId(), new Timestamp(System.currentTimeMillis())); assertEquals(1, rowsInserted); @@ -96,8 +99,11 @@ public void testLoadingClientIdWithNoDetails() { @Test public void testLoadingClientIdWithAdditionalInformation() { + + Timestamp lastModifiedDate = new Timestamp(System.currentTimeMillis()); + jdbcTemplate.update(INSERT_SQL, "clientIdWithAddInfo", null, null, - null, null, null, null, null, null, null, IdentityZoneHolder.get().getId()); + null, null, null, null, null, null, null, IdentityZoneHolder.get().getId(), lastModifiedDate); jdbcTemplate .update("update oauth_client_details set additional_information=? where client_id=?", "{\"foo\":\"bar\"}", "clientIdWithAddInfo"); @@ -106,15 +112,20 @@ public void testLoadingClientIdWithAdditionalInformation() { .loadClientByClientId("clientIdWithAddInfo"); assertEquals("clientIdWithAddInfo", clientDetails.getClientId()); - assertEquals(Collections.singletonMap("foo", "bar"), - clientDetails.getAdditionalInformation()); + + Map additionalInfoMap = new HashMap<>(); + additionalInfoMap.put("foo", "bar"); + additionalInfoMap.put("lastModified", lastModifiedDate); + + assertEquals(additionalInfoMap, clientDetails.getAdditionalInformation()); + assertEquals(lastModifiedDate, clientDetails.getAdditionalInformation().get("lastModified")); } @Test public void testLoadingClientIdWithSingleDetails() { jdbcTemplate.update(INSERT_SQL, "clientIdWithSingleDetails", "mySecret", "myResource", "myScope", "myAuthorizedGrantType", - "myRedirectUri", "myAuthority", 100, 200, "true", IdentityZoneHolder.get().getId()); + "myRedirectUri", "myAuthority", 100, 200, "true", IdentityZoneHolder.get().getId(), new Timestamp(System.currentTimeMillis())); ClientDetails clientDetails = service .loadClientByClientId("clientIdWithSingleDetails"); @@ -148,7 +159,7 @@ public void testLoadingClientIdWithMultipleDetails() { "mySecret", "myResource1,myResource2", "myScope1,myScope2", "myAuthorizedGrantType1,myAuthorizedGrantType2", "myRedirectUri1,myRedirectUri2", "myAuthority1,myAuthority2", - 100, 200, "read,write", IdentityZoneHolder.get().getId()); + 100, 200, "read,write", IdentityZoneHolder.get().getId(), new Timestamp(System.currentTimeMillis())); ClientDetails clientDetails = service .loadClientByClientId("clientIdWithMultipleDetails"); diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index e6013b1cdb2..5dc75adb47e 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -2091,14 +2091,16 @@ Response body *example* :: "scope" : ["uaa.none"], "resource_ids" : ["none"], "authorities" : ["cloud_controller.read","cloud_controller.write","scim.read"], - "authorized_grant_types" : ["client_credentials"] + "authorized_grant_types" : ["client_credentials"], + "lastModified" : 1426260091149 }, "bar": { "client_id" : "bar", "scope" : ["cloud_controller.read","cloud_controller.write","openid"], "resource_ids" : ["none"], "authorities" : ["uaa.none"], - "authorized_grant_types" : ["authorization_code"] + "authorized_grant_types" : ["authorization_code"], + "lastModified" : 1426260091145 }} ============== =========================================================================== @@ -2119,7 +2121,8 @@ Response body *example*:: "scope" : ["uaa.none"], "resource_ids" : ["none"], "authorities" : ["cloud_controller.read","cloud_controller.write","scim.read"], - "authorized_grant_types" : ["client_credentials"] + "authorized_grant_types" : ["client_credentials"], + "lastModified" : 1426260091145 } =============== =============================================================== diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/clients/ClientAdminEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/clients/ClientAdminEndpointsMockMvcTests.java index 3b0edb72d38..1809ba0588d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/clients/ClientAdminEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/clients/ClientAdminEndpointsMockMvcTests.java @@ -25,7 +25,9 @@ import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.test.YamlServletProfileInitializerContextInitializer; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -48,6 +50,7 @@ import javax.servlet.http.HttpServletResponse; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Map; @@ -1006,6 +1009,46 @@ public void testCreateAsWritePermissions() throws Exception { result.andExpect(status().isCreated()); } + @Test + public void testGetClientDetailsSortedByLastModified() throws Exception{ + + ClientDetails adminsClient = createReadWriteClient(adminToken); + + String token = testClient.getClientCredentialsOAuthAccessToken( + + adminsClient.getClientId(), + "secret", + "clients.read"); + + MockHttpServletRequestBuilder get = get("/oauth/clients") + .header("Authorization", "Bearer " + token) + .param("sortBy", "lastModified") + .param("sortOrder", "descending") + .accept(APPLICATION_JSON); + + MvcResult result = mockMvc.perform(get).andExpect(status().isOk()).andReturn(); + String body = result.getResponse().getContentAsString(); + + Collection clientDetails = JsonUtils.> readValue(body, new TypeReference>() { + }).getResources(); + + assertNotNull(clientDetails); + + Date lastDate = null; + + for(ClientDetails clientDetail : clientDetails){ + assertTrue(clientDetail.getAdditionalInformation().containsKey("lastModified")); + + Date currentDate = JsonUtils.convertValue(clientDetail.getAdditionalInformation().get("lastModified"), Date.class); + + if(lastDate != null){ + assertTrue(currentDate.getTime() <= lastDate.getTime()); + } + + lastDate = currentDate; + } + } + private Approval[] getApprovals(String token, String clientId) throws Exception { String filter = "client_id eq \""+clientId+"\""; From f544d68cb7c14dc6619cf34e48d0b3f1df8c3ce3 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 23 Apr 2015 11:17:39 -0600 Subject: [PATCH 08/18] Add in option to 1. Disable open redirect 2. Reconfigure the logout redirect URL --- .../identity/uaa/UaaConfiguration.java | 4 +- login/src/main/resources/login-ui.xml | 7 ++-- uaa/src/main/resources/login.yml | 5 +++ .../main/webapp/WEB-INF/spring-servlet.xml | 5 --- .../identity/uaa/login/BootstrapTests.java | 36 ++++++++++++++-- .../identity/uaa/login/LoginMockMvcTests.java | 42 ++++++++++++++++++- 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java b/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java index dbe61e07351..b1b2774c2a8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -73,6 +73,8 @@ public class UaaConfiguration { @Valid public Map login; @Valid + public Map logout; + @Valid public Map links; @Valid public Map smtp; diff --git a/login/src/main/resources/login-ui.xml b/login/src/main/resources/login-ui.xml index c1606b8145a..9fa70c18a9d 100644 --- a/login/src/main/resources/login-ui.xml +++ b/login/src/main/resources/login-ui.xml @@ -1,6 +1,6 @@ @@ -394,7 +395,7 @@ - + diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index 8802f6e780c..66d36afebc1 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -37,6 +37,11 @@ links: #notifications: # url: http://localhost:3001 +#logout: +# redirect: +# url: /login +# parameter: +# disable: true login: # Enable create account and forgot password links on the Login Server (enabled by default) diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 8fffcca377b..252c3720492 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -180,11 +180,6 @@ - - - - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index 0de41a6f07f..7f6766ef50c 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -44,6 +44,8 @@ import org.springframework.security.saml.log.SAMLDefaultLogger; import org.springframework.security.saml.websso.WebSSOProfileConsumer; import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext; import org.springframework.web.servlet.ViewResolver; @@ -51,6 +53,8 @@ import javax.servlet.RequestDispatcher; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -128,8 +132,8 @@ public void testRootContextDefaults() throws Exception { public void testInternalHostnames() throws Exception { String uaa = "uaa.some.test.domain.com"; String login = uaa.replace("uaa", "login"); - System.setProperty("uaa.url", "https://"+uaa+":555/uaa"); - System.setProperty("login.url", "https://"+login+":555/uaa"); + System.setProperty("uaa.url", "https://" + uaa + ":555/uaa"); + System.setProperty("login.url", "https://" + login + ":555/uaa"); context = getServletContext(null, "login.yml","uaa.yml", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); IdentityZoneResolvingFilter filter = context.getBean(IdentityZoneResolvingFilter.class); Set defaultHostnames = new HashSet<>(Arrays.asList(uaa,login, "localhost")); @@ -168,11 +172,37 @@ public void testSamlProfileNoData() throws Exception { System.setProperty("login.saml.metadataTrustCheck", "false"); context = getServletContext("default", "login.yml","uaa.yml", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); assertEquals(3600, context.getBean("webSSOprofileConsumer", WebSSOProfileConsumerImpl.class).getMaxAuthenticationAge()); - Assume.assumeTrue(context.getEnvironment().getProperty("login.idpMetadataURL")==null); + Assume.assumeTrue(context.getEnvironment().getProperty("login.idpMetadataURL") == null); assertNotNull(context.getBean("viewResolver", ViewResolver.class)); assertNotNull(context.getBean("samlLogger", SAMLDefaultLogger.class)); assertFalse(context.getBean(IdentityProviderConfigurator.class).isLegacyMetadataTrustCheck()); assertEquals(0, context.getBean(IdentityProviderConfigurator.class).getIdentityProviderDefinitions().size()); + SimpleUrlLogoutSuccessHandler handler = context.getBean(SimpleUrlLogoutSuccessHandler.class); + Method getDefaultTargetUrl = ReflectionUtils.findMethod(SimpleUrlLogoutSuccessHandler.class, "getDefaultTargetUrl"); + getDefaultTargetUrl.setAccessible(true); + Method isAlwaysUseDefaultTargetUrl = ReflectionUtils.findMethod(SimpleUrlLogoutSuccessHandler.class, "isAlwaysUseDefaultTargetUrl"); + isAlwaysUseDefaultTargetUrl.setAccessible(true); + assertEquals(true, ReflectionUtils.invokeMethod(isAlwaysUseDefaultTargetUrl, handler)); + assertEquals("/login", ReflectionUtils.invokeMethod(getDefaultTargetUrl, handler)); + } + + @Test + public void testLogoutRedirectConfiguration() throws Exception { + System.setProperty("logout.redirect.parameter.disable", "false"); + System.setProperty("logout.redirect.url", "/login?parameter=true"); + try { + context = getServletContext("default", "login.yml", "uaa.yml", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); + SimpleUrlLogoutSuccessHandler handler = context.getBean(SimpleUrlLogoutSuccessHandler.class); + Method getDefaultTargetUrl = ReflectionUtils.findMethod(SimpleUrlLogoutSuccessHandler.class, "getDefaultTargetUrl"); + getDefaultTargetUrl.setAccessible(true); + Method isAlwaysUseDefaultTargetUrl = ReflectionUtils.findMethod(SimpleUrlLogoutSuccessHandler.class, "isAlwaysUseDefaultTargetUrl"); + isAlwaysUseDefaultTargetUrl.setAccessible(true); + assertEquals(false, ReflectionUtils.invokeMethod(isAlwaysUseDefaultTargetUrl, handler)); + assertEquals("/login?parameter=true", ReflectionUtils.invokeMethod(getDefaultTargetUrl, handler)); + } finally { + System.clearProperty("logout.redirect.parameter.disable"); + System.clearProperty("logout.redirect.url"); + } } @Test diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index 122cd2b99fe..3bb26c81dec 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -48,6 +48,7 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.PortResolverImpl; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.savedrequest.DefaultSavedRequest; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.test.web.servlet.MockMvc; @@ -72,7 +73,6 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.TEXT_HTML; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -127,6 +127,46 @@ public void testLogin() throws Exception { .andExpect(model().attributeExists("prompts")); } + @Test + public void testLogOut() throws Exception { + mockMvc.perform(get("/logout.do")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login")); + } + + @Test + public void testLogOutIgnoreRedirectParameter() throws Exception { + mockMvc.perform(get("/logout.do").param("redirect", "https://www.google.com")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login")); + } + + @Test + public void testLogOutEnableRedirectParameter() throws Exception { + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = webApplicationContext.getBean(SimpleUrlLogoutSuccessHandler.class); + logoutSuccessHandler.setAlwaysUseDefaultTargetUrl(false); + try { + mockMvc.perform(get("/logout.do").param("redirect", "https://www.google.com")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://www.google.com")); + } finally { + logoutSuccessHandler.setAlwaysUseDefaultTargetUrl(true); + } + } + + @Test + public void testLogOutChangeUrlValue() throws Exception { + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = webApplicationContext.getBean(SimpleUrlLogoutSuccessHandler.class); + logoutSuccessHandler.setDefaultTargetUrl("https://www.google.com"); + try { + mockMvc.perform(get("/logout.do")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://www.google.com")); + } finally { + logoutSuccessHandler.setDefaultTargetUrl("/login"); + } + } + @Test public void testLoginWithAnalytics() throws Exception { mockEnvironment.setProperty("analytics.code", "secret_code"); From b86128081b5cd22cf05d252b9bd7668e3d144ea7 Mon Sep 17 00:00:00 2001 From: Chris Dutra Date: Thu, 23 Apr 2015 17:23:48 -0700 Subject: [PATCH 09/18] Separate sample app for implicit grant Signed-off-by: Madhura Bhave --- samples/implicit/.gitignore | 1 + samples/implicit/README.md | 38 ++++ samples/implicit/application.yml | 17 ++ samples/implicit/build.gradle | 27 +++ samples/implicit/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/implicit/gradlew | 164 ++++++++++++++++++ samples/implicit/gradlew.bat | 90 ++++++++++ samples/implicit/manifest.yml | 11 ++ samples/implicit/pom.xml | 40 +++++ .../samples/implicit/Application.java | 38 ++++ .../implicit/SSLValidationDisabler.java | 43 +++++ .../src/main/resources/application.yml | 13 ++ samples/implicit/src/main/resources/log4j.xml | 23 +++ .../src/main/resources/public/implicit.html | 55 ++++++ .../src/main/resources/templates/index.html | 18 ++ uaa/src/main/resources/uaa.yml | 2 +- 18 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 samples/implicit/.gitignore create mode 100644 samples/implicit/README.md create mode 100644 samples/implicit/application.yml create mode 100644 samples/implicit/build.gradle create mode 100644 samples/implicit/gradle.properties create mode 100644 samples/implicit/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/implicit/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/implicit/gradlew create mode 100644 samples/implicit/gradlew.bat create mode 100644 samples/implicit/manifest.yml create mode 100644 samples/implicit/pom.xml create mode 100644 samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/Application.java create mode 100644 samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/SSLValidationDisabler.java create mode 100644 samples/implicit/src/main/resources/application.yml create mode 100644 samples/implicit/src/main/resources/log4j.xml create mode 100644 samples/implicit/src/main/resources/public/implicit.html create mode 100644 samples/implicit/src/main/resources/templates/index.html diff --git a/samples/implicit/.gitignore b/samples/implicit/.gitignore new file mode 100644 index 00000000000..b83d22266ac --- /dev/null +++ b/samples/implicit/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/samples/implicit/README.md b/samples/implicit/README.md new file mode 100644 index 00000000000..ef86cccbeba --- /dev/null +++ b/samples/implicit/README.md @@ -0,0 +1,38 @@ +# Implicit Grant Sample Application + +This application is a sample for how you can set up your own application that uses an implicit grant type. The application is written in java and uses spring framework. +Implicit grants are typically used for Single-page javascript apps. + +## Quick Start + +Start your UAA and Implicit Sample Application + + $ git clone git@github.com:cloudfoundry/uaa.git + $ cd uaa + $ ./gradlew run + +Verify that the uaa has started by going to http://localhost:8080/uaa + +Start the implicit sample application + +### Using Gradle: + + $ cd samples/implicit + $ ./gradlew run + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.implicit.Application : Started Application in 4.937 seconds (JVM running for 5.408) + + +### Using Maven + + $ cd samples/implicit + $ mvn package + $ java -jar target/implicit-sample-1.0.0-SNAPSHOT.jar + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.implicit.Application : Started Application in 4.937 seconds (JVM running for 5.408) + +You can start the implicit grant flow by going to http://localhost:8889 + +Login with the pre-created UAA user/password of "marissa/koala" \ No newline at end of file diff --git a/samples/implicit/application.yml b/samples/implicit/application.yml new file mode 100644 index 00000000000..74a1636201e --- /dev/null +++ b/samples/implicit/application.yml @@ -0,0 +1,17 @@ +server: + port: 8889 +idServiceUrl: ${ID_SERVICE_URL:https://localhost:8080/uaa} +spring: + thymeleaf: + cache: false + oauth2: + client: + clientId: oauth_showcase_implicit_grant + clientSecret: secret + accessTokenUri: ${idServiceUrl}/oauth/token + userAuthorizationUri: ${idServiceUrl}/oauth/authorize + resource: + jwt.keyUri: ${idServiceUrl}/token_key + +logging.level: + org.springframework.security: DEBUG \ No newline at end of file diff --git a/samples/implicit/build.gradle b/samples/implicit/build.gradle new file mode 100644 index 00000000000..a3cc8c1121f --- /dev/null +++ b/samples/implicit/build.gradle @@ -0,0 +1,27 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.2.RELEASE") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: 'spring-boot' + +repositories { + mavenCentral() +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'org.springframework.cloud:spring-cloud-starter-oauth2:1.0.0.RELEASE' +} + +task wrapper(type: Wrapper) { + gradleVersion = '1.12' +} + diff --git a/samples/implicit/gradle.properties b/samples/implicit/gradle.properties new file mode 100644 index 00000000000..8d0c7be9623 --- /dev/null +++ b/samples/implicit/gradle.properties @@ -0,0 +1 @@ +version=1.0.0-SNAPSHOT diff --git a/samples/implicit/gradle/wrapper/gradle-wrapper.jar b/samples/implicit/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b7612167031001b7b84baf2a959e8ea8ad03c011 GIT binary patch literal 51017 zcmaI7W0WY(vMt)SZQHhOcduS;+qP}nwr$(CZEH2&I&bfL-u=$q_vNUpQ9mL_Wky9t z&WM<$APo!x1poj60q`KZF9Ptl0sYtQZ-e~XWkpp4X(i>v=z#$g{vp^9t%kz;S3u=& zNBQ3cWd-FV#YB}==w!tnWv3=(q-p8qVWnxQW~OEvl^B+o_l_T?XvZX{Wv8hnX#k-v zLX1+5iZm$O&`C>N`ww7dj2*6IL-PlpPVEyN>#oD(JjuU9F4&>RtrkQfFSerWU{tTQdH z_y5pxtmab;+TYJ__gBULWeWeL<$r7Nf2rla*Qo67=wxiI;9&b#Sx)B0j(?xr+y$MT z%#3ZE%nkLOY#sikgkoiDTO>gQA2f>4(fNaNz3SwR6%Uo;2-|r*EXa|epfs{&vJiL^ zXlxG0Zeq{KB;R6PtHN;pK78XW&e%#C?G;h zzE~o`tkY+OmhF}!THQA9@lFwE-Jq{Ncy~)jpMI!82hB2Gs#SPnOQ6RAKm9?<75=-} zE!ZFjQ*u?9En|Rj*O_IdnzR0)7*`A^M!cxpm6N=G;gRhZ?_!5zYQ&x@`*7`&>suh8(vV55ruH`4wv-#{>(SUWrEQWJ zRtmaNweT0zLf_f#(yL^3utc>2!Yhs9Wxs&_=}bl-oLC$ zG`j!q)`AK7nL0l~LF|Ikc{aH3s)Pa-RCv;9Wnz=!zHs8p1jp|SMdD7zgcwi#e1G)X z#s@$<^E~r_fbc1xCS{d}NIWMy{WX(Bv96CEtUJM?X{r>|NKB}{ZJ?Nxu4W3)JL&1o zSYP%UB-r%%d-_s%Ks__5ID}lOZsM*0A%qoc;Leb~U26R$DYA_u>bvknIaI(-0lYm3 zO>5Fx+WC6z$?CSx7xqf>-(vCFE6++*6wNIbnjo6%8rQ0eLz5NZDi8#a@$!Dp8LGc8<4CyY@JqOikVL^ZNj)4^#vwPK~=2>`~@OhEYQ3>4<5)g(Ha7 z5$v}I!~t|8cqob~naK`FLrTLWYJR+Y2vX^8jMvx}KP?E#&8E04<~oJgU954ivP{-h zYRovwc6LlKY)4ZYH=IZ1OruMCdc^CSE!Jb_=zD?=S~wi^2Cu$C^Rcv-8o@>b4no zqbJC=k2`DgmI^VOu3@0r32)#9uouWV48^z^^;{aE+5S-U!2Jq~Fi$`o#xNg9+!psm zIb{DNAWY{FPDn(VmtbB1hDO)Z)r_MU*Q4f$0Vh$#_oL(?!5v`1YfhBcFnVGc^iDma zzxfLA2o{Jh1M3M2OpW6b9(V)$owgcGBmzUE7UlvID>*|D(tbIBghoO3Lb`a-Ta!wu z>81P&0+l{52)z88bLkEt+!4k%l;&l?=89Cvp+wc?h5nyrigTd7ISdK_@bMQJF#l&W z6?HSTa&|O#F%~noG8Qy6Gy;NLe#5)5wD@21RWhXVdQ3j?R>o_9o!F_~U$cmR-n1Osft)f+;RO8pw6%e?Ksc zNuPs3k2kd2nwipr3-^xqb9(#p!N&jnXBid%{xFfCCBG2}v1n-FSlkss2j}%r1cA>f zVp7un0y6K{mAN1%X-W@)T;Xo4Kfnx#B5@ci2lf!N8=HjkET}!)?4NuP1_~5rK%?Lc zED_oes`x=W1gx1~1|S{yg+3TQX-F$YG2~pc&Lqm$rylaoP9%RwgOpB_8A(g1#pqTn zH8bKZ;}wz_q64ZiTyhK0RUuJZ*eWtaJ1YqUrKHMcGCOiutd_BqodlnnEkaE2Qxx!I z$*@02+>lLDB3LP>6*?me11pl%z?@azn3*GXO4T#kQp0pS)eDo=Cz>4Uvx<$JS=nqT z-@7b^H|UL?3A-Sq&G{atSqEGN&mww<0AOZJhSsO-_qN+GAz%CQmX4kKO4RWPcJEVOGt_V+WVqph)&sQG94AW^W-CD?wrkXt8c`pw;FZ8JnY$zHV zD9E0f2=e}3QqjyMc=KGr3DJ~p$&BTY@i!RcfpC;CuO?!qrq|zQ62nlh{oTY=%(POw zU5JmC8gf}3E*;%I?z{4+vN#B=HXMb;mOKIVu)@+2LH%Va-bzT2!Hfjt5J&1yC zEUhonr;Fs!xQj@CQA#v*uYSZ>YpBvkE8!kXCpErL*{6%}P*$cv_vZbE63f8rw1F_6 z={m-72qyvp$JYLdD5b(EWZB9{Yu?Hr8YW%MsHUCxCQH?l|mH(#_E>y1d=QDmSJ=ZBwQ}8gypIL;P0t`G7oa77Osg9 zdf7kugeOcsYq+tqO8+vf7I$^yf$}%#lb7YlZvjq zN~+7tXguWjWzob+E2*RJaC`t;%~qc2qP|60J|bPJP<@AWZ9>27%t*M2AAx1)<@QUOvYQ5 zSfzxlHtkUIhTE!1+V)AV+!i`Dn{77Yps>Fd@9>m}YOpfu(Cy>~qK~qIh~Z}$uL@DN z-7enpQvt`Bfp7b6D3fOU*co8?WBn;rf&#Qk6gXkqJHxjqRkBkKbSQOvV3())DDfb` zd&I%%n|ns+_tRDQzF?-A%P`HLs?$d{+SsqMg(!6JN-ns>KW=9AscPXp+ZBqf9DXpM_-N_238}(YYwj6Ptkq%Jx3$`vWNXSOX_O~4oYTk(O zv~mJCM-#JU{Bhx#AceB0meFkt(zgrtqIn#T?JZd`AUMU>c`efLZ8X&>b z?gM#>-`K11|Ge8?Ai~6R1&qdtGw%)swgsul93b#E&;J6_EyUf;1KvHL@abE8S`F7V z+edS69sd*9#XtThvWxrZOE{;@0UoHyD=_D!F#w!VST{CdL~2GYrxf^!hO^XU!Y%BL z4UfD!im8_{25qDZz87hcFzJvX`H1tKoZ^RsQb*|`TLa{I99%(cv- z>+gZ06~gWuY(x)QS$5K5K^qNBE_qGb9h=3n*}Mx`l)|*UwTW4_StO20#Yk}8#`A*z z&r`v$*1tpVFL*%!`@e#hU;m1pgl%n1%uSsg^qtIYZT<~j5;koX1rS0^6FKB?*=O=; zX-@_6V>BJ45hCt!_gb7Vn4teWGq^Y$|mh{{Wcs2DeSlh$DVnC zf=|+)$C-G!ncy{Ra}X%xcK92GG6gtm&NXukTkdBZ-#S{=oTLQ458wkGfi*=a6Vzj2%6=&8;ZS(BOvnj>_r^|x$1aFZ2O zy2^eusV#7CBW@8*QLDW9b*hl$@D4JIXtH>kzVzpsb32_PsS8xCmFQ3>6#8Mi&8~0Q zvB^WP#V(JI`!7SF9u)`}_;*;pmhz#nP*fjO#z6$I<8M z_6Y>^!H7ZW^e>I0)b0RrX0Y^KFnMoa=*Wd-o}T2GdW3H--5Y77b+QqpPYDWE2IL}Zhl)gvks;8-RaYe3>_pam9q#wrkwz+w>mirf)!S(`Blo&x9k zRSCm}0<9nfZNddf@Qk2YpD_tEfE+X-5{?D&paj*134Y@pAzI+g0#K3>xCRk0!8g!? zv{Qq{yT_H5t!D!&NeQf^iTyzT(^M6`am|naVrj@&TuxamH1o_)`DoW0up`FuzB`+EC5NEcP+CM=9B z#*#Hu7QxQ?M*5fphHCi0K^JP2xX315-|w{BwS+LA&$x--cgG37PHwX z)E~mQh=d$`6=gSr#f<5&F>+NDpU#A%oHHqto5}VF^pa`Xtf0(vDG2o}P4YpyDr%5KHOJ=k{YK<0T^% z)2h7Us`{}YV?+zYIR{if>uQ8r07~X@Ogh_UEMtcbk_hR?iU7Kk95m;=kBxkfx(iP6 zb35$;ncpFrc4vle&jvN?r73mLa!N^i7an5L&(g}coq}iV)mx4WW2Nq?5!75vdW%-( zL8$>TrcDV?X6JSBi}tD?J6n0BC{8sXJ|%kXCTc1W+!l>oY7ESc4dX8G+%eZdr;7tn zrdEb24R=z`gN{+hVQ}E?l3KW+T8V(u)N5(FA^QftMf@Ap27;qRd~^1=_!VywqoZy4 z2gta&0l&RMW}R{R(5ZTs?3EpQ23Dzh=I?2H7Vmj|6zru(W8WkNovz{2{g+;Pfx3~T z;m{km&T;6_)w4Eg;(*1;YV|fF${tvyVSSklPm7r`V??)mB)l7)R_iAuMlGKv>77}> z@+?!;tviEVOAk+jkDMiyj zX;xx5*rb>u;Cm-~3|m=Lo-*|`#pfbGJMA}i6=TI5=eyQ1eH@w(N`_T}>XV8F;;4z4 z+cB)s2#<$lngDgtJ)`(>_& zLBX*z=q3vD%=qNMqflZPv%C&{Glw*wkDih{89U zRq^=23qE|S3oSkgv&4!<-hBc2&VG@;jYCAy3uc#`?uppyB3{1LZU^$6C1%Fmj`IeQ zr{u}g=O=GmZnB}&$J4Q)&ab>8Wac(my9d35?+~@{iG>CT>W;l)Y@%%SL$*W5AKya1 zN1o9b{Lv#op(wb7dkF&SA&=23?|o(p z^yN0MDa`RVeV{-jm?p%14p-8KjEyitb`cu3MJM%&7=s72F#C+)VU8-AmGyX}MO3kR zeow~VVq?FxJtDX@EZrtQ4bE}p&jtRjza%%ww}U2*er>R|Ek;iJ`nY&W#*3F{?AHTk z#@mNfWDiBZ7%3khCtqs^d%(VfXBsc`hAPZ4aG-VYx}w-6)O*hQfaKredl%=QfiPv1EE%k5>F zqGx#4R{TDjqKA6l^NsJY4S5*8;Egc1i};|%4>)loLMdmCW^*Y8SCiQZ&_Qlhm04Mh zM!FdUr=5qJjWJz2gWkwAwMOQ&Q98JNeQU`WuznnSLY7op?MbOa0Pbl)6ws47#AZFh zhMvM$9JS8Y#N{L0%B04}LU&vx!q|C7X_{Irn3jhX1x+C|0b7ZN@)W+xyP8w(y}5wNAYTz zaiT&fg$$G{&J1pP{v}nVGd;FgB(Ao!i`>{#o0_v^pamU#m+>OJ6>%o3`s#^@U`|#~ z0C3)TSg90+j7fu}b*E{N1x9M{NsZ`7klNQpRTG&Z+s*TNM>q#C86CILOSE1MRANZK z$8AQF2M2-T?sF%?;eWX?$B8mZ{T70XrgAj~`|?#s^+n30lY}cIQWyfEc+>mvtJ&pi{AERUPVV$|0xL6@ zZOSFrP7B$orHXrOv^`aVrr-DWx|x(oKtS#oigPLbpJ~U!XWJ3ITpOBR5rrn;N3q z_olWZb`!&GHf#5)NH$uFt2o>@lBPl4t6_*4TZ6ksTcasaz0Aa~oLZ)qu}Sx2J4E(x z)3G-t!e2H;W$8qCV{hQ-W+*=-)(uQ_Ly{i$(-GVsjUD!peM7t927skI$Ht^i;r(jn zcvj~Qhc$a0Nd8E+tU30^xcdqoihXyT9eX`JZHDV&{k?I(w6Gj~^8v2@Wv1>JU%{dJ zG@rAMe(+#M9@Y)2gZj7b{D&;*lf{J2UwrbO=^VXUB7E>6)z3nx7rc zkXL#1pg_7RtEIJ#lK@vkmw1!EcWwqlk=ys$h)DM?r?eJQmm-!1C1_Vn^?Q9h}8wMx^PDE zo;q{jJXd%Cc=Le0tyd||WE2R2*4Gpr0{TW+k%>?s<810%<8>|BdNYNkE7c*Ew> z@>Ia;@g;ExQxGEQ^uXz*`3I9kI z5Xktv5Xm<}{9d@6AwLBBLM}M1Cn2<|Jy3uREj17yust|4@T{oAz@pm{el?Lkms1{} zX)8T^_k*=D*SA8YLGu;C;PQ$dS0r#=@#8u!SB)6An7| zW*y=_RK?kyvenmPbP2F4?c14u4F4^#`lwffS zZ(om6j2;8a-~bt3j*wXmJ#rJp`_~I2)E~<^kXL{pacWwtX0W_xEJYCIA5V zd;O=QGL9A_&HMQh=fx)3MKa!m7CCHkULR|Z zU31|dTN8P6#*wqpSMNvM+t1$D9^2lB!Bkz+0@}}e0?~8%qW2P(-Gmc)>G{v}sBaz# z=cf{(-4_Jk!)F4}GkT+I`r>!FxSfJquyfC+UxFTS-x?Xcif6WgDuTY`nm;=Ex2f~| zbNp0K@_-+v!QZ43(NDF48nQoRk3V>MKXKpus2{Zi)urr#DzZPc(?1fAz`y_K#vl9u zJqf3`S1?dc0n$O%fg@kRE||PX9>O;a_$$#J=M5MOUKFtdR~4M1Hq>j0Q3rNKP@~kS zv`Mx60yk%01u;jjRcm9D@DvTu)r+fm)zomtIsFsoo-!?Hs@r#45$l+m~B!6Wy{E?8SXrGbd9q;rhDHaF5HH?HHLbAzP-i6=W%MV)w`PO_s_W(1|}XP z7w?3})vLhasm`9~GSD#SFq~?M8hXMjLG3mnGPi{MQ->yf4%ib-iNHEbW>A7=tif-l zv532vf);&_Yf5WvBG$?U_OWp3;3d{5?@XTP;YC!UDT5t}i!r?$C~Wz(KCVt>o;Cl9 z&Dibfpd?Qg+7!e_i^3J5c-DIY825O~iWJfvTi$wqJ>1=6B^#RF)or3;s=;YS^0cqw zCDaOMu8#62JyGMT&II!zJLhSmG>T+#qabUzp&mm7Moy!{yxnrBr#|}V~%AmkN zcZV-ne=VZFcv0p;fCvze5q2aHutj&Z?RhfHFQ16%8Bcv8_&tVdr+Parqm zP%_xov?62W+RFf37P;lxiiV^S=V4MtBhxm8kC)RNles2hr%Ye4S@j08YLpAC9^F(8 zT}a=C3^?>Iq1zL>{d#a+U`iD;X;8YzT4QFM``GMunWhr&Zzf#D!&XLK&mTNZ3I;}M zH0nIaq?E?HoiQbWo=8husr8TP^LP^NN7RMdmIT@G2|&M+MQAW7+C{@7Z+SW8_Cg`J zGwnr)$wEv9I{oeO^%D8V!5%^ZNA7~2cgJ=gEVNDQ^ z0$Ct#dSPM14Gu7IP)|%OR^%;_4$>5*367RxIxeCei==1s3mjKgm{|a9sxTZ@Y9yx` z@+u$V1fH;^h`91HeR~SBo;Lp4U&9jTMiBaHAYueHohq`bn`>_C;}A9?vDH!Mrs-DH zen=*Af`xq8;FXPaenXPzI{791m%T_xdbP^;@yanjQ?)W{HZtd) zMgQfKe0u~;^+0E%W?5S;%Iha6biqs2n+i_o(xV0iU(O>lOYnXi=faR&7u}X4t0eQf zb3O-m`m5sZB;@GCZfPlTKgc}Pp1zKi7;y#3an0mBv4xUx6YnNByFU~UcGJWSYMi=i z)*{iximr>a)3ydzly215=zyg}6>nb=@slnY{sCk0-V!SQBpU>p8OPTZsmv>EG#=8i z@4AJ?T5hiLLEl5{nAodz0udAU*pTWgK%JIDpJX8fUGY}%p;HZ(Qehs}q-bhf*&f+_ zr_pj0E;;rQGT%Yz*z>oto6xZ&o-#+pLTb_25*=@DF%I6MwOKM{=6<@?HjI3_+AsBE z${e+K6byprT!pu$iPwAWvA$txa=Hgmhpf&Lhp0m$Qz5LzK9lu@iUOS+0Wez$Hj<&l zSJM{M6p3Wo^_K7JN)lzq+Vluf+&t3SLaLkALPl+AAPN(8y3v3z6e4{(E1BTaDU~-G z9qfQ$l$W*ACp)ZPhroXM24tJa6lGN8>uMau514$F4>W}}l&0Gol@FXgxfAerfmFS^ z3JS_i2Yp-#NNDeb!Tfm-;GmrFkD*L#x@vf;eDpW&A`~^+7b)=p9lulY2j8U+;^89$ z;~aDfS5bXbj$`j|q4-N44nKn?@Q-5&S8B8A>_Eq-B(m~PY`X6~okvzh zPf5HOC-8<@%%35BV+z`Dw;g}Pa!9m81w30oGV23?1HqPDj1+j(*w8rix(ET%wcuPo z%pT{Up-z&KLYs#~^JuEepCq*4&Gu%*1W4XkT%gO02NqPPji<^U=kOwVY$HoDr!+YK z8uha_sraReXo6rT)?*O1;DAHdX1jVKDzv}dUh^ia6{V7_Db;&v#OC*WmiOqk=_!n$ z^#YQlKGpM=;sB^j3;=qZxf9>~Vx+Y%7EuK1t`rk%?E5gGi9oS&lIM%g4>+9NmU#hu0{HE-Sk0=<^yLwy~3D8#xfFB0CQXu%DEI!$jN7Uoslc2{6GhCWj_)CFcdko7>X8VPD7hH*=@=BMN5FWRmOiew{TWoLmP3TLsnXt$0w(0L9P3KB z39(R<;(Us$XsiS!o*4;o4Ko~p6eBmk@hte+ltRGgX;bsWd|2V}vB*GyZ;MTyy0gpd zm-9QrZ?p$mCD9_zBWQO`^fHH>3Z2r6S^9WWQ~f?NH>j9ouWCQYIoR5_WsCRIr>`djw|=@m1G4C;@HA@Eb$A( zrb4?FZ54ULCArYDR5)%8*3PX)4Oni6MrIEb1d9G~;3q^6m?u1PcO`p|6UeEgF;Dty z|6TFgyjz{3=XlT!1`zNd`gt}dcKFurj>XIM^UC{Wx7-3d&1hT8RQ{vf(&$tPYuI<) z0!^v2F?W33o{#I5th-Cx0P+0|LxgWUj$;nz1;`H6!%g!6#XW!*79~(`9D_glZ)hKg zA0RWU^Cle<{7}y}0Smc95?Y(ts!iP0W&lef-3e75H&e)I?~LFW{4qan#~pBowgJ+V zC%Zr+m-O+k6GW=w8dTV5W{U{$^b03pKd2T_Zd92gL^~5F^QV| zsq{ygr7z^>hmK4264`ajDHGL?O|XAj3NuS_ADbSTu62vF<3%@c+&iIz74)ofZa5s?FIebxIKd2SDLUKwbmkyt16?M7f

N(DARnfPMFG|;(rIGZH1uB1rN;y7UlJ8 znb!RwO#5WS_%cPPlwSw94PTKP64}}J!~E1}ch9``2jvUrB*#@up8L&*`tqgq ztTAS@^33w-5hFZAH&{j)?l0<)9X(d`N|yLk!E=iT%PD-FTJ!|hqyj%RFFP`SaL;|_ z$(+VYhKm)K@Oe~+eZK(6ElQNdfimBrdKjeVl57_7p^|)jrzt|wyRonj$j9Bji!


f)EE4q$t9yObmyG04W0z zwEAra3WPkn>3Fb9s8^;JsD2U=J6G7s&CUebJEw+&=yoE*5?$-5j`#sxPl0^y-Nr5f zqODD$5Yec*Q%egs>4=qVe6nv3ffzozQ=Q){+(I-_?VSTi`{dk4@98oU1+Xtx3~B}K zYz=68@z@s5-T1<7tXF^>56vl# z58_Nqz!y-iJ+67h#|Q=}D!^}=s*eO@Y-td)8erJkZxGjR*`DK^rOIeP2<-!0FE>;# z_OWgl^eNX*lnJve=cWYR46lVZ=AlV|p3D5zU?m3qyx2*j8=?;SN-bYo6*=D1Sg&7Y3#^FT*@Odq-Z;-7>QjtU_L^=fm4iA4$-Q>6hmA zRIlHQF-50@Wj_&2!!tn^AW)czs8ybBv96Z%y6@Zh++jREJP+vPs$8uLU;6v)1AzzoXl^+Y(8ayi``o z&7K1gRo6gigwS58-c-J= zk~u7=EQOuN0vTL~k)W5tRTNoAFD8;wMuU8oLTCO5`(q=uhmAg@)=PHx5BZWJV3$9>@tgfGF?th{E_!`6G3pbet--D+P?`sQ$q;Zx z%t`hA!7mSNYBqbm(<&6CGMIfI1yAS~T5gA6nXvS$h>h>wN#+4=OY?AM^bd_h=<%PE%0+efQ zT519u=4vMv>vGC$(Oiv-Z@$I?SJ}mxji%pfti(2zDbrPwfIBq0P-pPg!!Jv~tQD1F zTv)BN(-QI>NYha>=3JRhcV%iBL`_Q!Xat>vr_(U{?OHzS0)dsC3fx41)1>vdi15?TlT$mal%d(AU}&53zZYUUxEieJ z<+tX27~QLrZAwE*+E3R9Nj6(q7ssUbir40&RAiUQoTDqy^EO$UV-mM7JtLpMVa>cui;PDaQ8w<{p=jt0S%`-6Dhm zWls zyQ*A00#tGit}tCy=>eITPG#jlVU^%6m0z(zZ*XiX8}_S9yL!tt5P{BQ)qc?qmxZiH!IXdss0`rYp5n# z9oA?qNyp`%*q~AVu8Y8;=Hg%;;Z)__( z=xL^@qCVbhoXRP`|Kt{~g;>p7?6~^|7h0Y!jnMPgs3+a3u{Wu_ka<&$KAaHcve?M5 z*W=6Q_Nruw^4y&qKoQxui1+J>U{eY#N`&ah(VE59r_frKCL!uBcmUJN`EDfJR-r6p z##}B56~NoHD1Jdi++w6l`B4nLXBk2hSZ&KgKW@C{A7i#pk#9z^H& zPZIxP#3CC)9s^{IT?H!89Y0(MV&8k#CA%C6Dg~9j=gA=V0?ZXa@*QB7`I9n@4yOTE zItq!LXP4b{JHSw50JHrxJtvG0&pqxwvfKDCWSaxo1}y407iZgen=BF{$sIEKCwYajZAQ_j`7Wo3m7%)cYm94mCz{x< zte2=rYlGL>GhB0ITZamFt$Xl?vrot#zt`2%g1gCH-|PbF{ay_cuYeoFr%!t|e-|id zDCDAT3dAnKOZy4lQZ8BjKoHk}F*p143&wCtu2q?%UDE1MKEIknUX5^^o!?R^St54< zyDkS?@9_V0hy9`i6T*L)49#c&02Kc@dH8SQji{Tkq4R%p?~~PC^{|&Qf3k_i8yxJz ztW5X)AON`=jG3h+gv@w=N_5oaltNq1e|M~*8)b83Go49jnyJ%TOQO$#;-1@>g~PT9 zNN>(9bidMVd(O$ed%K#R7ilfrh3+0`IH9j-9wp!Qc5mi1c> zw9!Ur;6w@aTNv!=5um|0bP}q#(DlYBMP@wJUVc13(Ai}N0KTI=qiH5XJ+_CNV zNQZO=A6+AM37@!5%yb@IDuCRkyz?@3u?M`4fBInZA@qYAf5*Xu4z`g8z(>x+Lfm#M zIvs?mv(oA^KRD}xyhaYg2QgB!z>ke*KRnMf;6)vH;Y95brsK!#86tY|1c(#8iGbur z?I|~SvqXs(u2C4}ro1yV;b{Af-u;e$XlLNV7p3nZkm|u0PQ5c`yrx$4Le-5txO@`> z*;T3QDy3)TT3Bs1Zn8DA8%>G-#vLRU9_%JAG=wtv>TKH9FjbqBbtYP;MZs@)#}1*n~=)XfJe-TDf9D-jJB{iK$w(g16V)y*_UNhqV5!X}8mbdyTE=p{I>UHuV-sbK#}b96 z&^f1XpDOFbmcb#UTd|+izeSDM8wb9HN`azk+;yvyz50~bN$91h|E!viQ#|#Embdn| zN35bAR1CW47}}(Qozx}PFG0Ch?$)R$$vP;Ld8yPxZNBgn7>b~K+L7Ib<|yKYEd%59 z7}QI=Y^lYJYSR{T7%g1D%mpjWRw+4739FpVl7J4G-|F5gOH6}3BG#OgETFAn@rK3c z8!D2>?jH_QimsmC7`rA*H*ekeW;oGX!zi>5Q1rt7s;Efvn zO|?tR7x`%U^TNe5NGZYt38ym%h+qM^%!2kKROY;hf}yZRfsgP@#eik?#oCNgGnJp* z0rww?`rUH3J*w<21FlrN`5J8%kSQ&J-{jPk#CFS38y#hvU0S)>8DC{5@fe^$7m{hC zO0q;;BU|b#%(z5)0#tu`yUJE`Q=v2}&$f*hG+mN5D49nzVv+j>&pclt%>C$X?nQ_x1>RX!2; zk%IY6%Cn6oD%WBvRiX?cBdXU*4%RW0^$W`e`sh&k`{m1N4yhaHs`Dz}F!6rfJ8YF5 zoEFps7^pXrt9}Ym_)vykG>h?xra7lR|8lsyi~vqOWOumJtSF>1~RNWW!4#>2b8XQ=thuT1fi6eN#2^v|}E&rvRR>c+bwm z2QFq@s(LpD`54^VJk3Nu#9WMqx9X@OT7foumh$2z-CV!ynNv(_Pn%lKL#esBfQX;H z1nuD!8$UfV`rKA5pWazsrBR%KZMf~W?K z(AbBjx)Yo9+J*R*Neucs*z+JB)hlu+oIYb-a&k-ABVt)mhKjdbT9huyGbXk?>-y!- z0(QkGlMPjT>DH8FqCa4HvnETjbyE{g540?X+hV%Ip}LztD50lqjEo| zPx*FYM)E=}7j^pcd|1O{D|A>0Rj|(05E;f?w4QDLBnr z?c9uk9uYbs^QAcb2O@tEhmfqr=i(&r1P!sHJ8=JOZrlnhAL!uC>W#CcaOKt;Gszmh z!>Gv|s<_s!Z)=`kWuM$+-`osmcQbEwtKRIS&+J?Xo9UoC@bGUwcggbT;$;X<4!+JE zG#(`n&Of(9myY>rBMey347i&Oy~Esra=@Y|oDh-JrRbs;!e2r>dT0g09`z<6(e+QS zg>}}A_n!k1*)gXGjmk|Bp{4Z|1+HzwxkNmYhASO0oU)qE{yY=V@9*M!WU1aH>dh~dAVC3@ysAiglYcrUWP5-gLWNqRPAqe z^B>(kzh8y&WqoYsJJ~>bgbuHWluQwmW)5%e+^AJYmujIk-Qd5>skriFtlx?qc9Sdz zD8@ONpc{Goib=ts)VUZU4N2YSdJr>zc?Ka>0TMWv&4tbZzRX?#83<5k|x5DoqdM5+EW5dGKhYyXC${}r46cRkw;c@*^< zd{W1~8;ls+O0W)17DwjPp zRLwgt&MpBsdX+mO)MJNs9D21oBzm2T;cAB$CRF-SYLqS|(dMn%k;P(4(kwdHSIJ^2TjZz%#_H}N++3cLG(h2F%W zuQ1n_+&nB^dA?=}edrx>{3YOq9tKn#7NmvY<#hfRc+Bw)PeH6DqYEJdc}H%xyZ$%d z3c9vV@U%$%rMQWx=Qh&(w*B1QtBFO}rMFA0LT4M&PA8%)l*^@A2uB+E0mw-|fUq&6CFMb^f~e^-hV4NpL_ zG}Ev>9;82|UDVl&n9~s1(z+}>RIvo}SZGL%fz+>XL_!Wi;o+R)^QPB5A-nra9!4|b z9p8G<7)cgKH&~5C8BLUwP2N*Tr5eKdBcGv=niowuFG`zu#OEzGNPiy`qPxF}b>J2ga@n8Q+i#2dDK~g~5ALr@C%8>tsZB#x zw_)74%99h8?h$l;c7?Ng2eU;%`#L&YGLen3Qhk1-G7;9;MoPM!dvs^&A5AyZsjj1h zM!rB^m~un$2mLd-U-3+#;xg+>y-We=#TM$jW*`5pG{jeu{#!HR1%GSI!y2boLv~5hy3W553S&&&T z5u|~{UrS0k$cadZ&eeAt8>A#s{*Y<0=ify(CgS7{RW?`H8{fU8d z@)}Q@=be+ro2d}$xyEVGhBn_D{k)+g(V`SYx0_9M^Ut;TO7e8)7AC-fnK&$K}ru2%w{>TNOOo4 zHy0>z?sp#h8gU@>q&&JSq}qKM&nkHlsN>t%ckHr7cwy~!xSZ_9DRr_xMv%o_mTV~h z4`b&TT?x0X>56UJww+XL+qUggDzJY-`^?Ri%iT;t zTGRFQ`rdezhA5`VMuMq4P*Tu?tHOST#N$ZCSJWTH5#ZI(WxRH=;jiN>u$pE9cL z98HeORByGr!#qTztg6ui|Y(v^qXBd`6|#3HmJBE_``TzGCc+{s7{R=$_NxddPK{FcS+9td=A~<7{wWUIEo{cuDYLTl zg+JjIa!sn*L}mA9{j_|tzO|mrF+}Y#XyE~|dvIsxb+5N`ozqje{HEOgZ;Mh3lF>sY)WZ|M~1LJ(9nL>1ij)E1&!BX$8R3?=S$Y<)L z^HC=xwEc>CCVg5MNRsGjS@&cQ092Yy^u_tUHRKVvu*!&2l_~cubzB;H+hnyd6BU-R zrin?Z9lZbuda4szuGA2Vqd{>ly`;HI?(=FkI4HG5z>aYtPkihJ!5A^TdrGx%b< z5NFLR5d(TNijz!@17lQAZPg3*4UPLss2J(79Q_;$VcLugsvI`UQjzcK-~5u7o^EZJ z-*bjD(EoQN#6SF!|4m!@HU_C8sUm;zFcL6Cgpq4hkXJSYk@q)N`jG=_SO|fDG&VVl zW$8PmgvLXJY`C^BuXZkbH@XN@Av|PcW$iWl+!%g^eL`7ZO%VSSC>VzN-1avGg4mT2?Uk@+(2ai(|4{DnoS_LklCtv^+A}fpBKhg*TyPyT*-Y{f<>;wIA z4;c;*ZX05=5=!>AtQfHW#vCu}W=9<_3yB1}%wmv$r&gP-pgUuj!CDULOeN(g z642NRX)azS-3DrWYQn0M?%+77b&Hi0HFXx^>gqH{xKQR#R)scA4Y9$>jotd9K@c?D ziUuLUg6KK>D3%4qNM zkeYIyw8}efDy5XNAHegla|tfxxg_y%#FD0ZO6@_zRKOyW$zW%YhNyO+h)4(z{p@xm z9+lmIOEG|^0bJPTmQlv_FTN&t$xW8LYYB`=R`o$reX|CDyYLK-6+%L=68;W9$4861 zg~-vy2b!ow_4K=f`$994SwJMyZYkK9!XM-1Q@1lnz1^~g58oM?nQ%8ZT%|8pq8>n+i_?G$|CM3YMETR7RsDmfTsewm@0)k(<)x`y@_Hypo+> zD^uX;O570WN}WCz%hUorP+-;rYBdHpJEXuB+2rgo(?L+B>&}aMS@zZx+7RNK!c?!z z!j`T%jZ1qtt9GHMmUduJYTyu6b}Hbm%AbGW%?7n9A=tq9 zM?Txh419O`2FRZ0gQt|dqUe;oLaUX$B5l2%RR-L>j`~1Qw(em0@CC@qr7>sq;mgp% z3XI{9)}x<81sb-v3&{{G@}-v8Q7L=|>3Ac`T0$f#eT)-GU7SaxSR@k_Q|Nx5Vjzih zkS0qZhjj01UI}{TH_cCcC+OKll0)MyRqa;nE2OHK2 zZ)Q3av0?&AP7jl8Q3hF0&ga33%ZsarRe)&jxss?5)o`g@CiVdk8X0+3lEzemXmJ)5 zcusCJB)K(T_Hidpio4BbN~C57TGFZphplzph1XLel+7c~L=O*IJlRi%0SHYB z$&nI~Bp=}e1b-@EnNf!*Jfcq_7rqGVkyAlrVJKlG4&-ezoqS+EcA1M@orO_4l2zWI zI`g`lzZxR-8TY)S5%SrLkI5DaA2duMrl`g^Pnt$Ou6`A1*SNj1@Q~>Wc%H48`d(h@ zY2O^>8nh+!Zd8+S9f&SZoy7a@KzlSM1H-|9GaCW=JSMh>@=C*jJVRp z$#C=&&QBjnhTxti%8ib_Jc5WjS9s^AXex221Rj^H$z>r)k{9$i!fF^9P zfF{X(WpEQv9z{=>MscHyYXW|f*OajQdHkeUZZa5t^8>Rd8{BCq4uv4;{F^p{2%{@{ z(lc$GP_WYu`_)60axe*Ko}gSZU%>DN=}izjA3wN`pj0iD?*$Acj<0r1c^H{*Ft4rd z1Iv5BjXZToLd4EV^!X0)KKaUwdaom3yjWsDP&%w2Zit{>U=Mc17Dbt~pgUjycdDJk z<>@FjSX&UUv;df%KzTr!1R0@66rBX|9yKeyN%71QpnE{FOB;9&s=R5xzTo+pjipF^ z<&v7v(u6X$QCe(Ak={aWQ5+ETyd{{sQeZTlXumGGqdB8l7-14O4spg$(g*{t0AW=? z7$Ua|rx+I%^T=Fy16_CpXkMj{WFrS?n}_jBKO9`IT+J7?7JN;JOJjhyA>79bjjTox zI*&;4hxR>yPg>lTl<;A$GF45Wh|YUx<-O(aUXmD^=$z$n>jD&Z>av+MinjFo-eyBC z?f`B$F0u0M9xMIUHrQV85MoGim!xL7P~mk%B3q%N_l{gt7-byBUtj+Q8#*siVXFuD@k0gZ#}D@ZS;P33IX7Y{TSI*-A@gsu zfw_V6{~2gcRM3>f`lj(rnrtjMEwpr+ozaMUhpdgMoTMwj7s`QClJh?6aiv3#47XvC zra#&?PRkwp^X2eKc$h#J)(RZ=O=hgQruKcdy|}~ZK~0&`2bvnYsj<$4aj1A#ypV!=Zs<^-F6grsBbIPS{qi8Q|xe1oAIc|h&x8WZCP`PAuKU(XVE3_tJ9WUgIWed2`o9JWQvI5B4t?I#ulXDX=`QNyvkwk?N)7 z6e-{9cY#ymT^Yxpdm91oHOyH0q}Vgdee06f82L-=*;-!|^;hz_2W8|$dpp$~w(WvZ zM#etSMqINvJL&D}HekXH{!d>GUkoX*Yle$_ndQ%4(dUC2;=+re(Q%S1l;DLy>c-B9 zSQhW5ZtqhG@R)GefrIdJrRd>Jed+zw*^U|7-M`14@{b4Lkw1KqITTcReXzqh^=*_OobQX!Py4 z=n09bH=$gv{O&xvVUx~Z5;pv{WY*R9lBaF|Rm@ro3?}3F9(eSC`SF9`e-4Cyg-!*k zm}_Ev8&O5Y(q_JG6ZEy;S=_GhIf;#-s7wZC1kwPoBC?oqvoqKUU|k`GvB`d6dV8Vy zoRfIEQ=#}$i*?&_DUFt39Ph_A+y~tUl=T)DxVRn(}el2HcN` z9X!rg0z&luJnSSV*fGpoPTK9fFh5unVH{K|9Fh)VFwskUXE*ZlmV({7C|0c*gaKuo z?7pMIS18P`l0C^5(qOu?Z)phA?~82%EBCD0{JAYE`3453dOICcWPexFVXIu>wk*ij z(oMS0mDx+R?}_F`>-w-V)dGI<}$A+x2y$*<90 z>#)=M?u%rPYr0>x_z8x-SzjjKPf>x(EOQK+rH^VFv}tL>spI zLVJw4=he85x+IAtV>7A3=O^P75oXN9KN^?orE`!rlUZtpzZBLHHTCTkuf)Z~Hfk;W z?CG*-OS%?0GErPmx_dk>_2PdvFEgP%0#B~Z*K9`Y%dqS0P=3JKuqgkT-4TaEQtG7q zF2w(4vYc=LF~kTpUVM(+#F3R}+;9$g)E`%(AmWBL8XoQk3^p0bL{hGmfx`IF6lX{` za!z_)=Oodaov+${PH|jy;8=8?IEtH#W)i#U-yiaFxr&&`Vh~y(mBa<;e(Jh-uBejl z9}6)_x!9R?i>;zU2w>22^@q>DILy@7A>!9FPXO+OY-I>84TX4+&cXrBqY4 zAL`z+U+TtGfvJ#G^THLORaR#xE2mtw#u_uw&Re|PqKx@Ymcv$l2d4sb~p4UZCQNea@(9`B>C1-lb3LwrO?;ymZ!8gUo zN}HenRt6jRxiVta1`8D&;Jk7SA?Eh;eIADbMl;HTUm9MbAYI1fnl;+BjfCF8`?fPK zVWfRLGsNqyr59jj>fXBun_92#h3EqC?d}{N5qEtZ^}yHiReXyzKG+N^>>SY}n=;Lz8> z3vPt-zyhSppg)QenIKN^>QZ9?Do5UspnauZFk`eRpzU6*jkoE(@mTo9(g0iNHl*>D+#{_q8+p6%3TD<*DmjA$)zz`0Rxrka zuoP3!DU)Lm0<#6|9#X{#cqd<=dO@xvsKt8aQj2D_16sGX50$b0{f!L0Y&L(_6s6%x(HXo^pA@)vj#w0h zxT1&9zTSa}VNMl|avD2?&#{3wTPnfo7tQ(lH#gG_T%n8n_jxP({rHQd`VYb=jq5+q zK{Wq`PPO|c4ba;CZ@8%9>Mii; z63O`YJHhx)3(>!y4V=xbjA$H;4UNt1oc{JUSY-Ka`tad9u2jqL@t29gXj_%b9K`1> z8@pEt81(>}Uf(&T8Jkl#QfHP)q&_=@BnJvIz|RF+g2Wp2Ga~l|>?c5$%GS!^x&XAI z7#d`4tz?hi0F=o-wrAoM!3Ns%*&*vX5kK)85<2}gt7!Cz_smhOsE^4ES|i|3Np`es zTT=mwCG3V!{*=zg#3M*CefE*;d-$aM5zz2qyY0G4HS$_I*$@kru%l1*B3l}-{}JX1 z<6o%oa%BtKd_=0}ZIV$Fx1zB5ZoHiEH^c2;-@x~W{@;9ExbMIJMt(K4GXEDC?q4sD z8NN^A{w6^welr3Y{`2Mh27fto4V?sSt*yVQI#T8~#@|FJmv2z)e|vNjm9)R9u*f_> zP~lWfA=Z^!({Km*PHViW0%G7ZW&&jhv;9NH7)#cA$T5f;DXg21+3la+_!Je&a3h}EFpS7f2;GrIle4=64_S3i2=^pRd0%Jh# zJWfc&sGtZuHj zaBN&?A%N-JUREYu045jVeV>zG(8Y0S%J+5Fpl6)ELyNuP#XSzco=H&H^^;VI61#E! z-ctRQ>RS&x-a;UdoXBMnZ^u*@VO44Q@y0KM>}nPXriV$@Ksp4VCLDIYAt{zdoj+MA zpOyC}qC(XE0u>vL7LtW5L1Y%FU>~r&34U`m2T5hb?+#Hh=R;JYgnlGLNnxA0S<~Gv zD;tof=;j-oP(B$8!Olu{gg(TVHpo}>Otx>ZtSLFUJ1m*M{zM-oBEf)qx@c#v87XC78)PQn1XbZ6voRUKZ7Vbgn zcPXJU2NZv>qviGuMpV>lv*W$v!!y}D`)~ssh7Sf86bd-DvT543u1u*JmR^(4zOK*gbMjMEDuvs!>0Y=oE!Ra1tvZ zt{u8pxRXaz@FgaG$^qnMdJM7!7~utq?wS1>`400ylj`+v`;>wJ1Ww8KvU~a#M!ElU z+5P8dl{R;DG`BI8wfn1V<##as_Q^XL|Fa>Vs4Y9EhdlH(#oAVRW|V{9#fX;BiEkL< z>r3iK#~{PpqvxjzSCPuHp*V}WMb~jNi1mT5BbG;W(+js9%$QY7-N9}>?@vQSQmn37~FOfezEfHlAZItBPql%M1Q{=pKZ4` za{-gw&guX76MRF5sdeudwohxNu6&?uf~rVoogs2JO}X4&o&rnY>4P-tXA;F|7yIQ zCT*B9jq@yFQCUh7eFTs!(%7R@&Mk8W3DBh!&kUvt>zzqfPq4vs|mrvoZRUMB8d>? zuXy=<4AC!3Mr9g*-253N_2fb?g=@&}lW!R0*dkcQcCD?LtZ!1sLl<${Xi!}Ql}xZP zfnomYJ3ydnt|sG83_`#`z==V4!D~+7L3(@sBiCz(toN-TXc*fFmvqV%UGFNl4vt;i zG{1{OCKgyWPEBOVJOX68@JHD`l(SG5iyP#=!Y`{`a+oMTCiXLXGfBWn!7y12{M4`C zb~r$RrJM;@)-AHQv=>;cb|XK?ND>R+N6_eTeQiM@21!yJBANvG*bdNbf9^$M&$S@u zpmOv4l#iPFY?e*DJclwkFAtvc62wael-4KoT_+f;*{T7m`DilIVN+gSuNl+XD|7;h z*AZ5qViIKm!VofJCow~KL88(JzC}~%6`p0f7ovRpTki1JYVW9!W{mw_0sh|y&tLfY ze>s#mE=Ib^3Y2?ZoQQ(V7 zgcO8A)FL23hi(>K6hW)Ij9ex?S9g!3gL&QnhLR4}f5TlSTq*@DE!dql)1SKBuhwig z?}w)7wtgVrjCaEy!k}Bs)aDq@_y&t-m(%8h3Z=_(qO16b#L8Flz{f2S(l${^*RvCfSf~> z>9Zn|+=zYA=G+Tf2n+K@%Zb+^KzJ8if&w4f{Mf9}vTRW0sk7iO<;ugnc^Xso6yiuE zC|p8^Pda;h8Hj5OSpWKg5%g%>hrq8GTK7O#Ht}=y5Ras}EpWL=VX$lM-eM7|)P`ka z!A2ZM0{^!WplofGq5qD%Zj|wkW_y$^M;G*d=>iY#oHu;gUOq5sjER``(L{}XpLv@? z1r2JS8(kwh?&BYbH1stG%pU#cATvyp*UTP99sz%mT~r=*7%d3NcKy{_qCve9CEL5UI{;F>6cGm9+<54iq+%D zWr8Q-64F)ZSy<$Er0clj;vbTjHb9*q2TS#$ZW*{?Z|YN^tZ@yEn2_-V0P^>F@Wbcv zH}n8xV_yDdw)!j9kBMMzMZYsr$?tgoACj!U#rpTo?ria_Q=K^bf*(J#c0zNMz{%?Yz8HdTm`>D027vgI1Ui=07R5{1T3 zeSJS`{cQv3_VeZybQ^jC5ptPPe@%Ep*uR_O$gh~k?=|yGayFDp`T$W8s1vez;o+FCh+tXE$dLg-=5@e!e=_C0fbbB)onMG&GGkOJI@WL? zWPM8L{V9hY97S@TXi^E@)R#p&h8*=g@rObb;KHtPrS7WM zo3+=mP$maI$ANLQGasC?=Gx~jrTBHu zKfkXy5Bcs3er^}*`vK*lrs z#!JIsEbZr9mU#ay-{3HuR4SuNeL7b2$}OagkS>a7dDsOa)j3BG(=53y;B9|vERx%D6wc-0K52!H4E~^If4hhxHJgpeRj>xu zAPCVb^jeXUgg$TO{{+C;zlo^k8gDM0(xuEXv#zu5+gq`(xx(0f5j&q^(+1dnde^s5 zv#tbTiQyjWWe-@s{FHX_cqXc)Kr^lK#|o!h#K|@Ka9mSIW6Q4G6=PS5)ag+oHMtIu zL?p6%wW8zP*;&((X0ifu3}@lOn;(NxoUY--TBq(nn0FW+A{y%F_SP5NiF!uchU88h zk#$GI-E7Yost%|7%BZ&`k_*} zxWMUbu|A8l(4e)ja5=9$p)`lFCGcU-yZ3~{Sur$Rx3w+KdPM;uyNB^RbEBv%nq(s% zgVkBUalXJh63d>aYEys3!U|!522qBJ-G7U+1%!>t4OI|w(m<>?p}Q%McFU3-);KYn z!|5RBml8^U{lpw`%%foep}f# z%X?t*5(sO-0!9?(QhXCGQFi}=384p2wV0b?k0$lI^}BNK7Vx}5V%aw0CPK?2(4)og zD3Ycq?4kt_yNR(=en9ClbNu@vG&=9!ZM&U}X3X=e<}}4t=yN4=wshAbIOm#b&i7U* zZXRWjown56x#>igCB@~*I?3x8LDL%CUA|nEK{}b>JR%#R0W7iZ(Z{t((57r?FUClH ze~P<%NZ5(KZgk3ks2bFyTr<)hCs{34cqA7SF@8Y0IqVeP>fm5PjhLSl-!}n9(S0@? zM1q9#bSrg5*`uATcQAu8Mj5c8M>NE~2EFIH7?PHp@Ft?H*C0f|-dQlq> zlpJ2K3xcL^np%4c`Y6~B@7}!Z6opx}@|y7*eZ-ig>u4V`ni|B1KB+`KJ(56^{6_Dy z*pOvhCrG~<@^4O(IwdXiaqQ`_i#2I81o+59;gH31sdTDWSL{Z1(28?~z?~h&U|Ut8 zTuyfvFc zE-<@8?wvWEMph9jhoH*7A>I@^ON=q==LS&c4x_w5XAO&Qk;o9Jbi>Mbi-7RFER11S0kejO>|V_8=u^;<5Yd1?CR1h zyOSGm%;FqCLoPw$CIOY;rtVqM_oi!CmLYJ5H{K5wjk z>{;2Cv!rtUiOu?|!S?WCg_k$ujEVQ?I~6EXVC!Sd38;z^)lI#DK)m0m3Gc>3|{#lV6t$3G{|ahW@qQ)1{l`&XeaKz@ zK#FO9NJYC~Rek74z4P zQ%A$EDxVKfx$dY4=0l_VTUEG266#_2T5>~(F+?2+wb(q4 zYxJZYYer&+7jMv7BfL{+ZJ5t2w6oIywX$Uy?MklI+|qKEIXQ^6=?X#I!}!CNHYWG@{9*is zcN~z{nD#*YYwAIwTjSjZ@78X5gsW*kx>;>3&?n3iY;&?S3kwzPN{=Z0h49~z_=>Hp zdz(G6I(K22m_Cp@YrW28(}Of15K6GmPPOV($nFcISAa&xRhZ6_cWK=DiC;L4Cm2Pq z)zDZs>Z`;Strl#VXO+S*Tk_{=3AtzMc~O3s^c&Fdt3`sg;%wrNg9zO?-Q{N_{2dFL}Qs^g-O1C29~ zM^%dPbcVmXY&)5`z3-io39gg!H-20wX_!~VWbg)G`vU{`-(T)ZxC9c!CI0Z$=LXLH zNhyE)o;e)XP&J-GU=MHu#U*-6)<#QkG3ipWH~>}f+~sK_#O?338nze?jK)AdoeA6% z-G)scEZ=$$9{mwtu?-=COf5-XPT77L|z=L ziVf#msb?+)&v92M9v1-pj2_r;*#N0Ospd3U2aqG2LfnhJf;9a0Y(D;cMvXho$?q-89d!S&LxM*&}lJZtOcT7_gytU!U?Z*SNcE+SWDjP)O;dQ0Nz7h-d)J zjV)$`X-;3I=6#eJav<$SQhLmgjdz6E5G&o|mkRJFhv`F$)aLAxqUN^@&_7Mh_0TUM zul<;x!%wmnHG17)MGMns-mqW_N`nCqB%?#Uvhk$VJyHpL{D>TE1X!r0Vi3aXg?&{E zib00SRaR&iewrt_MG(vLX0H8cpqinT>e4j?i)pCk31~RS?OlDw-N)gKi6Kn)`|gM! zFunl?dW*2V`SCuY6dy~KBkKJy{qc*0*6340i{gb!UMeKd)SkA5Q&PuBd}pcAlaR2t z>-%z@2j*>K_UN7;sZcR>P0_>YMB7)+daa;cKSz~%9QO<3yZOBA%F^UKVb)wOj&myTp9$z=wZe$d{X4k$zJ^Ha zsDKFta^THB#e56I1#^UJl|_|ewbT!1-#R~_I_@hE3gH?Qd%x#bUi$@2U&&qtSA9fP zj8^I-i{e8kvlg;8Y+e8G+~WQEdd2chzOlyUq9-xrjAE5?*5led?uIrAyf1PaC$R&% zgIMpUxp9*mT!UB-qBP_e;fz8$aA9}%o(y1CEtqdfiEMmUqptJ6cHcv zL^LYjKTc9lnr874?JPf}jI!A;Vm4J17)sD#RxUQMM0{NQgHvh)vp{`VgssUI-bdyx zAb(+CEY6g90!D(n3SWcCGVhQ|nvUsAgkjGpKRxQM>DnVE7PO(LJ}uFdq#8I_ zb^K%OT1v4u#V9EWhSTjSJ$+es$IQd;zF&XQ3PkUiCb?2ZqM__Aoh>#P*3 zvC&~b&qPzec1p=9KL-NTY1TEfKL4bqnk82XanZc5<`Sf~sThnMSMx(v6hr|j@)%yro| zzSV34>Q0`9juA2lSFNOo`fvYE$j1;-5i?52%iXMqH%MGPsh+pzp8~FivPNDd+eBXD zu!~yJXU0uj3wdjhkNSW7WUov8fCOHlv%@dY?iq9~1-A6?=o&R4XVLX`jx1eqoOKP9 zdQ_h^de{hEw!$fugS{MfqLN&-6viudU3ACQI6d)Fv)VnPcp!B_5DnT~&1F>gHu+NVko=Cn_P3n4>;mzLyx(HH!33^}c+z;To%NhS(<^ybXXd2HkbnZo^g zDtb}^E>32?>Y_MQlt~CtA+ZTy9b+oaQt_3~RR z^Sh+sC$^sMvNP-sfHp^~9Hk*?UtQFF21yXzV{Qp7d_=hjwdjnq1V*_9*VYvq`1pzM zm=<~X8;WA z=c`tw$jnK$P~o@?nBWAssI`o73@1-`ThCD@JL~wNOhz={85ExtZ>a?W&JmB2=yraD z;<^CDonf#2pfurCQMZ(esgis0RGa0{*D+ZP?ihioKNq_a(r=N|^fO10I}1wh@)}T_ zePe#))kFri{W*<;!(V=CBwMFA7=gE;=y_VLnk9uj}v7mlz`59+-<+97z zDs6(4E!?%2`~-3YcZ&4^zB96^vucE~Y5qqRK1q+t;Br6hrN>IJHubcRi$M$qu6|ZE z!lC-7_8^BAnfsw#>>mL-kkM*)hylBBmm@8}j3E%ZM#UZ-m*6s{9vzvc)Xk_Oz+ol zj(P4V;t*sVElsx;hz2GgYvhYnB{Egrh;(@I3$=jC5--aNn**9yrnw`pdtQmNEUImv zbpM(YWmsuV6@RO}MgM6_{h#}o{})LyQ9=7JTPjbzdcBLZbVJ&%YXpS|g$=SUF`?N{ zWIxDQ?q^o6%#U&ulzp+vVpwMK(F~>uB-qEQ06YUlbP*DSz|n|pYI=cP)s84N~cp4Onv2v5L*T@;Dm0}NaOTX!--sRJhK2lDBb=2v?K zmR%zO`a6#!!)hD$ncy3Y>(kZijS2#6gjvLXAqprOHpFl7CPh423oPyX1m)@>ad}x7^|FQ9x<(3n9-G zsV0MawQly66UV*8u;dREi6gFS`hEm$oEly9wQU42RWK-h21`e3-28MMC~T0V=-R_x zhHy;zzM(E~$Lv*^9$81b?Seau7UsnnGZ}p}UR0l4ny{6`qnGwPIndCQP?e_*B*2Kl z{U2uitRUML+3jlB4`ihFjqO$0Pl}bYr8x z3I6}o_f`MtM_mi~PPhi^gbga#^#Z8#*`+nW5H!ATtYX+EE*M<4o&|^;kjo{=M_@3I zuM$r#kc_jDc}k?UNxv_>X!B|FvdsPr@;OWX3~RgUWI{y5w0Qm9`t7e+d&_d13iNYsHybBBE?jQX~tJu4AQx z&#D>?`Eg341WI2eN0DEBrR`9M^-4*?ZI*v;!bQR@$q;$c#Jj=DD4#jIa=qKEjyeP7 z!1qCx6^{V5cfa0vnxb64>mjbK$@Xo9e8$h2SZbZN2@+_m9c<&BOF?^n&y<6j*E~shk*Fba`rwlV~ zeJ88EP?cQH7D50rqJp`odZvU8H;vcs>5)erm}7B1tfEAHx%%*VIROoZ!vCwwr2SB+ z@HxO5BF{e@&j_RZHA`8x1oG1I(qck6#)64mzJ63#F+ruVteB=)g61Hv#0tKn8y07+ z)<&-jM^dD5Q`VE|0<&*iqS;cSAg-iY;tri--K7P_S6if(oF|KIl*JLNY9)P! z<;olE;Vgg;E64Lm=ZkOqG^*90I`zTL-`by~o6B**CsP6TND?Le7XL`pw~Xaxu-G;>;@M_ckag7OK^bA*$Yb5xg*k+ zN^Fb97}e4=;P@G~t9;k<-Nvd=d*I3W{Rf|t-87zO{z;Do?Z#^lf+@}55o*#(HRq&( z#8C9Lp^r>cDsaYEYeQ7_b}=@vAm`*Z1P)$zu=6`K7Nt+sokmFqEQ6M?S zfd^=D6?Gc#;RkNf{j{DlxpwV_(@cj{!2p7M3JA)dS$h~PPM=69P9J}$3!$dk0{jew z1KEZtoJp0jFRfZ4jZ2X^Q|cL?QGj6rCPCan*UfD)VH*vw22o;dvyPldgtU=#RuD=sQfZ*D|8F1im}vUFW2L+6DGWVR#CvyRnIM+Dsl zsw#bmdru&Ga6nIgP}1MYz&OGF5q^3ujWTSjj`X4Z;RB%#sg9#qI0^Q6zf4G9fY>7f zTGsF>F9pzdvQ*n@`38Be>wH(w62g1bkC}Sf!Wm6fNNq2S)w%Cpym;-Gm{4fqV4V57JPtj3k6l6t`M3bfVzm{k53x2qbn0 zu@i<2LeXN1Gp)|}A*>2%g93_g3bP}m6s5B7J^TJx2@PF zz{Fxc{Q8IY;*VtlhT;y5?JX@st?aHy-7KsY zSK4HD{A8sDb2Qwomb<5QnEOBVfm_gDM3_O(sw711WJ9nR1i#!J?tjeS5)FbeOen_Q zu-A`n!#{!4rEM$Hqa&oUqI+R7#r3e$Oznsh&Kw_6WCf?Ip=i{@sAT8%L!8xn1d-)Y z@~jZC+1bg+N!*{ni<)GLNK~dh)?Cj3c-bI<#2&Gwrt10qN>K8(r0cg&9{3$ zY;G{c8&Bc3g_-TI4rcxetY&GEK&(8j;yrW;X#rI$C8)y}Fgku6R{;77Cckmq+pC_HR67oV>NUE8V`#lRS>LMpVg)@!@<&*)6csYv=->IX z&$>oMfG~Ljsg=V$MNBtBJeSp16u|A%=9bz&050!?I`txTK;rmQso{EY)4QtFHNQ&` z?t2!Fxn?`xZM-^5Aq$>zUKKR^|+6~n5R_|EEadbuo z3q}xoTdHj`u=Z{n@sV(+#{G229Gz((AqfMT!2Bi+1wvD@`Ne?Gy67BDySxY%?2iV4 zI|-zmOOYI#6P@ceV(W^(PXPVZSP$oCvGV#(wV=LJEzbWepucajw=gzz`hNQ7)%Jfo zc>m4Dm>DN2+xrtaa0&!i$cs;|FYmk?7!%2Pk4Qd(DvucbNsudv!8#Zk2;xgZm6Y}! z;FEk0xr||1Xpj2xB!gq?-lfR)imv*{W3A>-R4jL^!`ehqir@=u7w{D%1W0cYF;z>~ z04c?`jGA>sf-fh+;jGBMwv%YB4~5IFc8{7=u>y&b`K|Jt&R0;GFKU z1Yla0@L1@v1z=jLFcn0}iz`{_!@n7W7_C&BB)%m@;BR3>HdVT|K@&;z*I26b9JDJKJsp-inNVkZv73I#bX)pN9pbKlef zsiWXZ>vJ(9(bbM1-`+k;v4JNJLQIabVQG;>OU#(^y6nwDDlUU?mcLxO@?TvlQRP+xHj-Rsfc!lY(x3M^TNy9Y`!o)W<~MiczO>l!{RP*dz~5bzmRQE#pdW57mq?)|r>)nwMsa zkdGHrx#)v0U;G6USD%PRH~BjpQGI=&vqlJ&YySpYEYv{g-H$ptL9aZKd>jC(7Z`!J z;11nI@SP|@;0>LvmzRA+M=J#q+bWhKNPKT5wLO8;*O?;p|nPYF*Q z!b@qP^{Z>#!PJHpo)7?3oiN;p#1|3YDkvl@?gwZOcu4X-DMre8Kq>@$Af-g5MsgVn z$eB)IQx!P`Ls+A8^$WU_0*G&^_J&_<=GkO$FHN!)Nv_V(#N4_&&iB$yNK7> zm)Ft$M07TnjF98=1pY?kdv$XS{Yu>+&Ng%%(N7i$vN@l#r*i+ z{Ev0AMIV@!m|(XY!)-G89JK+@QbY^sP@pT!crP_ClnmoF7 zpVchAd3kBbq65FCj65q`!`G~Wy~d37wDzS-*owrgo;zXG_d-Auv+Zoj)ABgLv<1H& z$Wx7%sq_lHX9abOS2L}jOuj|Uq%&md))Yp3&R0J|Z30}UjJL*YWS&e6+Ieke2>moqYvV zmD}32bW3-mba#o;9nuZb-QC^Y-QC^YCEYC`U4lqS{M)PN(}Uh~&-mXl_INjAu;yH~ zS3K)k^WjNYn#)CP5JJWhO6o6+vgtnKudXto{CGf@L;bFD#W<|546sJhF#?%6Vc1fb zYflruQ3er)eKD;fUQnu0W4?XhPYqZ1g|fc1NWH^JDPpr~(CnRvj2IrW_?940W#{Oa-j7eik%NKi1yS7V!CQg- z4O3=BhqR{Prp8$bCv|GubJw|vVgpJp7S7^kPT8pK9k_(t=vZZD(Vsn)dnWJPo%f|WcQ z6t;tBgn9gxO1hzsUZ^k37zo5z-L!R^hg3&7HDkrgSC!|2W|sI4a;Ng) z9O1%|Gu$2o%Q1EWAj)G zHJYF=HO-KcCVB{ess6TAL!>V*p~2U=5i*IN5uDHCJ)l(l1Z@}}130X&}8ucp1 zw?GR^R~+}FN4LS(2j?+ek=IQ>QQv|2SZptQ>Jg71S|Y z+bTXng^S{`&8ZWYVh~68f-#aw-mpAyK*!4G6e()}PIAFASHL)CGS1 zQbcqV!Em==_fsDVdOf8+Bl=`)kTSW?WoCSm_HnU}FMvnzaUtGpWkh1kR`xRJsp%TH zR`L0I&2GxT=TwC66-!1F4+j2*7L#obH;~z#XD=e>^$^+kvsyEKP&;!)h_QWJxbW%` zg*L~X(BAi(me~uT1UIV9A*z-SLS^yFM13LJI~CY>(YkyRX_#UC(S7=2Vpdc-WkGe5 zGr~0zF~Z4bx@YB<)oqUY+wYaZSH&9)@8EnohGRD&d3(Hix9>Oy**S z^G=M}GeX;KZtQIC*gKYD62^184r>%IGT6+Kf|nkToG!nvoddrAj{60g9FVMQAbCiwXA0b+-}=(gNky$|v-Iw|Pjg@Xu|I)6oP z;b_URSEt>0lloP^OYY3*M5L$d>(N#CdCYW6uEvhjbegaVuFo36QH_h0h1qzi{Ry$@rLSPqa@kOm`kU4d=xf|C`o<>a z`?5}3Y-Y^;eDb(QQS8gNJ13~VFq9wq`3M1Z9_wd!Ev3mtuoS|zNq^B17b#NpQlHSW zw}``bewEZrj~3S$Tb?k8-5XBB7(GZR+ynzk388$P(GcX{)7Ye*iY;zyCc&1aDpx&K zw}MAFnU>W}Oha+U$Z9&@kQgZ9VSj8I6|?xB%hK> zMtP`qWt2I?{?v8XQdEtARi~qfrrr}#agCIGB)3qif9mQplH#OLy|PFwE6C=dy%9ZPVi0VjX9a5S>qh7DZ2v#9(vKujb!2Y`vj5EcW28 z^RrGMrNE#~;Jw2GQ&Af|yGL@TCZu)7pgttkoJKsqik=hmDnRvd`7}F;M4c05i?$k) zt`#%wX8R^e`-^9fF|rcWac{K-w0GLtjM$CPkWcxf+?v>na3uIo)S%3h)~@K@s<>&Q z29ysvW_Zs%_E$yVwFBHP5Zo|r>L;9nDU=l4Lj^A<3Ja!8??qc@F-yIT6nLBy@w)D` zE(8uMK0NYWkU9Al?LNC9!#8ccmz$W8%1IecCzirTW%x2h-o}*Qo)MfpOwpLo&k}nz z$);x2N@#%}X23CMAwt`GVVC@Ns|5DOd2hz&&T&wYttx;7H`m)edF7>80Ta%S3!T~u zUjc3|R3_{V_p7>q{Ml>12fXCx-dqHUS&;gEu3%!$dl$wX63U>#NxUU~wFvG`w6Z2* zm3wMb6!IaGvb7o+e9-lT8E+(5w;|(jvx(j&_}n6FU&(^u$lnmfdlvT78VB6DF;T(; zF)BFTq~XTdUZnT52+GDL^EFjeXB2lD+Ha)dB~$j=xa`3i70iP2df{<=CP4KECbuJm zUM1-HjO9Apm7SSbdds8otn8E6MY3{^LCp+qymRt)tN%UEYuC-~o?6)Ik9xs}@A307 z`G$!3o2?h`{Cn`n%a5|aP?4nYP^E+lf!D*B@HV)@SY)ZyMR%vsRD=?uf;^UuDD7k! z)kUanUnmb&D)1{~bBP^aAo=Zc)`q!57hjR{;!8uMWs*f?VnpOfrt@<)CT^;Uilj@a zuLyHGy*S=9n;A{BWk1cMyar$DiI~uEhwX@%dabr!v7u=6rh{+(EWnoQG2DFf%>?zG zVsqS^{N>LtzY<@%M7~=c^sktj|J3G_KcREM<_!O0QCHkY*PjbU*^~pWMIw)MPh^VS zE7k*=(mT?)BpL4Adge4`-u%ANgi&Od2DYbuM?hVA@2QG=cskFx(Doe-m6e z-vyq(Ya`Pqi%8Xh$h_(6gc+gnuAO`GKxw&g1HF{fD<^HKgh) z1{FWLA&@J7Oi|sc5Z+dhz?)Hz6~ey!iqP?9dpoFmiz1LU!e_s5MO*%J*2-ZJGTJN)?|!iy zBk;S=fQ2?paKb)nD0P;|umN5lpxf;XsS22GbBn1t0v0 z*!7bFJ>Ulepw}iqB_k=G-ga%a_I!F+nqLjU#3!Y8J8gI894X_NjXkx(xz?i7br3p6 zcV21LAeo{d5p9i2(KkI4S0JomP7$L+v283(NC%m0_7twWa!VPN@k$r zvifupB*g30?XOdd)xzz|+c0=a!Iw&Xhi2xttIr ztF0u;8z(800i9S@b1W*0r44BvI<%}sPj5ujYTVwph13Y|M2dtW^5PTTkP&7@hiV1a z(jqd;5uR2TmL018N-%5XyVgO_>F!T=`)uP})ptS@feC+FwRC#fv)94~sIA9f%Hf*o zmS%qPsxfSBTm1rWo%hWL#XX@rj{C%Q-~^ObT<*S4@t&yaVDI81;j~YM_RT)cRen9b zFms03xB-%YSl|j;TqmmSC{ZZ&DiuOFaW5vs&URf`e*2-|Og9cq2q?_V^XuuP3x+C8 zxUsl5M;zLoTZX%5B>efv1>Y@VG(qq9R0N?{z|Q$<k$DaGhZqcMJyk(=q- z)Hfi7OD^{~IxG>aQ?J&9R1VI)k7nLhO0f;}MrHe)!YVhYHBZC^AGsaysg(H{P0dR)0X$(Mm+;>5oPEV-5grf|uFo=W zq~yFG{f3X{YrS8~ttmkpQM@(3r@Y6ug~>Ygd>uZlsfAm#22oko*=KzV|7j4!VZ%m< zWRLPzGp8==fzR^@@iq*k`nWRG(5bDw8PUixF2>Xz)_fsJw0+c$MA zsP*hnA52~e9=D+g`n?cor)KT|985zB^fKUbp>Tbi=(RSwbOBnJznYFFA?I=_s%hu- zk|J@JDi<%PRvg+c6DI8ZiV89y#vImp6~W~H04n?#h&GX-mC`ZMcfpE+=uyCx*n%b4 zl$3<0lZtVawQDt#o9PBC8v5cOag+}nvUWGCLy@Ni!oFRNrv)9N8xnP1Bh`O;dcyq` z-0=XeVF%_zToRcy7BQaLCUav2as2tf zmU+p9?ltDIdu@h*3-wOOg!}VjJHv{rok=*uH}JRElWU;OUvP3A?z=pmxGrnLzn;ng zSW(FVXVHxR#)`_L54h6*-4ns)yOF9a?muqJ1CI^ZhkM}IWP#%_-rO*;S=oW~(yow0 z{K&RJt@I8ixU=QV4gzN{uGL)$gG94~@H^r4UNw~DR7ZK!OE{0SU#+aO@2=S_XLPjR zKYQCB430XDRh_s`DxyeVLKx-*M}j67D^JDn@xZ-ih^u7Nk)_A64;`+CuyK1yf7R|i zsamb6iQT$^AEC|2S?ULto{zTGU&Z4H+YVGgX@z@k?Q8Ty3R-uf^%^(ln^d>Eqnvfc zAyg$p2t9W7-h~WU01sT{Ht#sqE7>`f=*1Z0h2n%@k`R(?9+Dqw$8=OIVgsS`EDzE# zs5|woTTgFm^sGSoZZRw9vtY`oG3i>X$H0QLqw^IucGJzoFH0LSWMZ+nM7s2f(qxBF zU#&-+kKe2{pWYXdTq%M(xXi}m(Bg^o$%E6C;%+3y3~g7U#-@SYP%w()=t23^Z&-vh z-*-lp(JyT?(uTZ$5zy+YK9Yl<98B!L&40^yTDRNr**SG>K}(jQ-wu`aT&Z8eze(o{ zLC=v@eTLTA^(qf5o0B4DezW97hYykjj&Uu^ zmxTCU6rvY>?|JJVE04&dZ!-s9zZiLD1TP|M_4PPHyr#_rOmh3IRM6E}@NgCLXh5P~t85aBz?pP__)FEZHZ!hN>^dXL)V6qqD zQ&y-$J|*um=(Wx68mG+(*Y4Q+(>HJ2feJSl3Cc5LNp$j~c$EEZ$mOOI1M;*8;o$U) zil?aZEfv$%rz|ylK>XRRQed0vxE`WZpF5F+I@+azgqngrDEM%QS!*f$Q-sUC67 zr;wY`zckk1qtl%?RV+Piu=jn8KV{>!KR;Mm+-#@bB1?jFIQHaOe+$Q{N9MZS++D%` z3KH~K#Uy>bHu<#$TX!*Mz5Hht>Jt{-5Y`oVUrn|!QlO-KNX-SF<&BJar;yuFG_iZ% zeBO#J6UV`4{`2TlPOmeHlLd~Zy_w{V&@iFVyXaxhoYg^jvYKnTKdGEWAAE(DuyFqB zuHIU^Ju%=y@m?%2TnmH48Y5~aDx3;dTcgO(u~Y|>5*B$iFXMDslJA-${hj;(oH%`D zaQI>3)Th`iYw`owet_dCr%N^-!~4`XjLsth?B+Qsxckp}PXf9)Ial;B4sDw9t}ce1 zTBujRCyyO6Nl}gRi+0Ah{9oNS!rsqeNW}JwXh=#X&E*bCtI>p~1A`i>V- zVt85~VDjC0mtPBt`uoHh)Z!4{qsBb4_>Y;oa&9mYm{e)?@tw|uInv#rWT&H*S<@Qf z@tlH3WB`+LzT*oX75V@dE!3TrwB4lB`@H!>vUf__3sS}jI^0q2p3r2k3-c_`#;9(y zm3=M2&E_N{gG7mvDf;&ms=c|*(HN^ITxtSXtVVuOw=jP{Zg3>olzRsVr-%jf>>20p z!}?!xE&t}kvrIwN0ZSg`;W?~0L(-0S7Bfki2rMcvTv@#lHIwM{%)23hq97wmdrzn& z6n#tmK>kOc)ADT>%|jS7H8I7Ed)$ZTb{Sp-x|Mm#-J5Eciw36_9$!=4uD2>QJs>Q> z-Uzb=EEB~Or}p8ll?pe8ND-G3jTU5_O>#kDmWn71^dqpG-5|7c$Fp8Re98lbuLrN> z^0jj9Bjs#$BG@Ece-!SWC$ffp$jvvoMDk(9S=2^|56d2=i=MQ#Yf~61*>NM*BoQId zL5V$&*EV)tLxR3c*QZw-$IvI&V2IOJ4nrSSc@@`SDoehQk}oYUp(C14i(02xk7OgC_Bn)sCtm4d~Frq5af*t4juo+`NNp{N$W2Z$0Q6>XfP7c80IY# zY$PGLEl3g!#`Ox}738!0AA3_76q(7Os3b(3v+>Nyx09P?Dw2!!*du}47A_2JdTj%W zn27nS;P65hVEYdc!?1CET8=(rA;A!oT8cZP$=7@TPNi1|^OO%UDnXiCx}Qo@x{5Np zSMieHkm2%-pI=!JtQnF|5OsZ+uB)Px8k5BsHqy%TRCmT|Fy$0;%(jp6g|`!jR{@18 z$|W}4@!ZLTXi}bv!0hp5kba`OdR>+^w)@_gWNrrFLg&`Ntm2w%sg4r=zFH$~Pfu-0 z$wwOhN7BIZ!|uqVfqE0o<(|F=rlQJ;l1(|;O+ti|(r1{Xn~LU*s@-&%b;V?HZRK#d zv&;e3j^^Mh`D%48Mj|x;SL?=}#1& z@kv}dlI`1$ld@x+5TonwJ@&DGih%65;Ho>FppxWTmAKx?@VVy&fhQ^Y!r4ub-+5Y= zP>d2J?C zY>b76d6Q?rbupl}!R@<kQ;53$8I`;~jIgvDP2}Q`UK`+oXl^{~;}{{6wil2(8m4dhUXHf%Xo7e_ z5$+-5v3^o8**`_EGWghanAy%9>%9Pphz!%)9ni}c{OK5>TF5s2W{lFsTA6^7>q~wsF$7Le! zzKf%>PMLCLx0g$ z0GOMpejm7g^NalT!b6y>>VMQd>Kmw!iSpD?c*xMRsE32mTi=#|(i0l>=7RfJ@Np{p zf18x45j~*CN!qV7gG3|w-2mtJ<3}`kC?c*&Y3yGC=b`KDj2(}zk3jDbDam!;zLf?z zEb+qM(q&|C`v<3pP~}E?ruS1*kyxp^pB6S!c3gd|&fRPehJZ$Ld2ec^$dg|^a+$bx z?pvhjdq_>WR|MUotue9zt>5gP;9WCB#aH1RUKzU0koQuw8%aZ%he>lm4nei`2-<&~c&;}#aiPJmwBW1nLo`25)$-GJageJql_80k<$#)9s zGGq1MwS+`Sa0bPX;TyU@gx!WQ@OlslT*3STRT;fXAddECygyfWa(nxL_~+j zl)?=?h)G!oG-YWUPhx`=6|Y(w-!(VTz$SX^MXY2?X{R*pF z#HoCefoqb<$=}QLEV<$3W51KXo{mexd|^PsSh)X5AmtiZn`%Lv zKc!IB8v89loUM@2J9QuE$D#I;Zgmr;HaGF9l%WZqY_B1jDF;tUL|7t87i0Fa1acX{ zSbOe#N60+K(7`#vY)!!P}6zha!F9g|N)B_qjnIwd#>rc ztgk*GjD8cMiNMJVkLiZ*FH5?yhd70J=Y3hEZk5R7ZCoUmlW{#z#EXZFJDadHJ3x+i zR&r6lmtb0%#=qmvG55 zOkExg$13OzEjuT35khBBcU$lh{9!L&d&29bs>Z8KB@1k3jE%idvdMSYIeEDIau4C@ zR7x7z3kSzheQv~?laYc&LZ3$n!Pi#{j><@(&-{|EKwCo~icr}mMdD;eEMA8pG(l~W zj0v=2SwBl()o(z}QQz$|ynY{uk>BdtNv zCLRa|Nsl5Y=eovf<2EjwJ(cM^Cb(1E!-p_hN55t2&$J_Q`*{b_1xcS(MlV{Ax^dK3 zf{@h&nsR9$M&h{k7fr7U)6z{|;S_{UV#^db7=ZRz_j%c>&~OQ4zcq~TH>}UlCcf1?l5iSG7TZcSS#u=wO zE*S{nBk=Oes+=Mb@cKYD)q^Q&d)p8%bI|!^=B-BVIfCt%Zkl71h#Vt0<>cp-F?{I= zP}N|YZ31Zf*A{;nt@!!wWAMu{xY7+&=FG^d)=Hm#lAt3&Kgt}$F& z+*G90m$ESBa9Qgx1*TnhvE&;a80Cv>+dNP%xKd8qz!lJl91$bF9#~05O+Jj?*dkep zL(HTrvrzD&O1iajL>S;!IF6S{=`LFNezZ&}xhqmK`yKRLyD`!%DfG-xc}q<-hq$>c z?d56x0Vo2?im2K_GKPDdE~FRWRvpH#O4DRww>dVX80u~Tp4gMlP-^StO1>k4MF$j~TPpRMe1sh6 zb2*3jd;RiFqU{_2+#g*5JA!|uB*&|#Z(*$`Xk=|;D{H6yqyE3|e)|;6WC4LU+|8>k zt!fN~;o|BSgnhU*AbCo-5nvD_k%G)(Sr!^3l%{8O=NCGL5Wm0rH~77m<$T+bKsR@RPYAqPgh$hvPyD^Yq`)4UH z7OPa4fTnzYiFtewD2YXe&)pI@?S$sfGflQothbB5*lc9!I!5;~d{wE|Fs34HXa(d8 z1hc$+m}v1k)n@4-Tb2CQB8O{|=kBw`koz2WlMRfxuYx{4Uja+rftQeN12<>KGmfEH z54BQbX-b3@@&bZ|Ul*lts*&W}-*I7hkz8x2kD1+eq!S6F-$Nmr9EgK+(kTwpgqqqD z=&jW{ViiE>M1Zt3H zan#K*V`*ADOq~y3mDf{!074f#pSfEn*0l#yx{esTN2qb(;+7SBLa41Cz6e9AlIy~( z&)w@3^A>r$%k3Hj1ZFDmf^@3KuOf^!VzeyE!f^os7n z8b2$0Aqpo-gK*txDcc1vbx%L&04}szKD3JYKDkKa@C<8GUkg z6gZ7dmtm&(mfjjzgtXKaM1?K2fREPK$>=;+a>pnK@W4j2t=hbF~B zX|{BBUDT{x)%pTvDkAL-_Ss$2siY4Eh|knCxKlOy$PW;4JRNBa2=RJg8U-5ZzGR5J zc9pnijlRSG`xp}Qj5Fz*8}%JvjAs9RjFxq>vDGvCH#UW|=n>H#ZWRCVg%Y#6`Z?oS zffZ(6Wd2@1^hh*x8QQn|?#X!5Q7@56rg_&y6}@2q*E5hvjSz}>dX-ZdKaW3-rZ%;D zw6+6v0+}Wdz=si;qGT!ijslyaW(p8^48I~7htgfTjpVK$84%7~GQe9CUquXd;joUC zIkgGl;GFrcsUGJ{sLPl<5DgKUCkERk-Oo0mST29Gv zC_DH|N31?Sm|W&F0pMJrOq|}g1BxhKTR@^_<%aInW&QX}>^#TDj%9U|m2hR#Ha&&_ z~LO*9X*SDjokS72wU0y1ee; z{s3(GY(NlenT26tEZcYs)1GREnh7#VG5sQY-dC>8ikFVFYl67co@*aZBlK(eE!pjB z7tz&s1b4_;ugs)U^=)_jd{?U8W|cTc1ui5#66BR|v_tYGz^kruhowd#@vYl|x9{3| zpD7Jyl}CP*FPK{P?N)N#Z!s;}+mrnW7G0@vnjRvyam^c^s*(GO-I!GpN}0A5S_F!D zippRUlF}=cx295zjN}$>t-vPuJ$XxG8m8$^B+N%Gf{MZRwE6KV8|aU6gPiQO6E8jA zmeBfh7#^BoXJjB3KO#$1x^($rHJ);{U56zl#+#GD!zg>pdmyu1s!G7PI348}W(h2) z=5w8#tb2yv?w$}3wJN!v` z-PyZ@b>f9#i3_j&A%KieGhAr?D7k9%)J&gK_C1*wmEqv1$fj6)e{NaQ$8(g73+4=_ z^h~4zuCT2J%LF}4^4CRLeYNa4{$Hu^YxoJjqW`^H3D!eiaRF8v!%%;;KmOjW{&TGK zUltpOGShDn9wb#EbAd#`dO5ZAGO2695Gx(Y=OnMiEQtrrXM>ak0O0HE^hDVefcw{ra51UjT75kZB4Db<#UfxSEI7 z-%_lz(sNmjk%G4~(UTLnU%=5y$~W*UFdn2Z-Aszb3@^d(MA}rC+Dn7M5+~y5(UPd# z_9{Y?5^{N2G~Jv`46AB9MdsPo0lL{k(}xN_Vh!r55PW`>OZDG~_w(BOO{ttUjeZ^IdVTP&2; z2i8kvj6M+4ZDf!lhN2=doFN4$9S_PqHRc0fiKh?}2|e*Um2Ox(Mq740AFcP3Q3gxR z`4%PvvD^m4alT@W?{QOdmh4*J)dRZ<5es;PG3(94+R5(6w=j<(?hq-)DVI)VVbc##HAY+6 zzC0)%bd&N3pxYCvLrFRCLssy|GEbi*3V(%LD&U-$;fPn zqdt)15>z9e2V#grSm=^1BE++AQkMv`2eo-r@ZJ;LF_6SN}_LGe@5LQXc$6YNr&@sXX>xwMLIE|aTo=5B|i!ik{dY}uGBWam#lU|%q zYC^0{T}{}%O=Npyz>QE15jhuV>f*A7ce(HS-apqV-wjygg=(~XMw1!wA+ZmvtdJd@ zcVXDL!;jt*hM$xSWb|V5;N_5f_WUa%=fLiHZc=RwAP?g{1k$#IJ@rpQDzM0S==wqV zuQs2k@FdSjX9xiXc>%x&AMp91!w2jezS%A5 z{J&%pJdz@U0D{d>Y6F%W(OF!a{d&jI}FEa3CvNAhp4S}!cX0_+zN zM5tbbqX9~92`Kr0n&1a$t=Gf<0DQWv11#_yop|hQ4FUSIMmj&mm7g;19x2%r16Y;; zAEF;@-(Iy|_JBP6p$H;>mErp?^C=!^k@Kn=pajx@)YN~&dj~KU{{ioNU~+kY1iY|? znVz_Xg^8WzpP_L!)tCDKAh&>C`t3ycucy`v0`ia00Nq_vI~%|)GC?C#Jsuq$JsTSd zEpsh{zoH#H&AU#8SMxhS-Vp!>y}#!j_xT?Q6u-yD0N6V8jSTDn64n4Y_dn-d+6~ue z8&D;ffV|WFm2tqk)@u-;E%1kGdD@=BfPf9A|1IO@hXg%X0N)TRKsLYKTl(v%_4`q>gm}1pXm5g)bQ@uPecGzKL9GlUy%PPK0d%O^9R)L{rCs=w@m-JBAy0N{R_I* zD@gEiR!<|wLgV_sx^ z=C zzWm)Q_GzY{IvxHb_?-MN2!0)<{#=5mE?++h=F)y6_-mH0@h4Sa;XhIRkGb^eDceujy`q1D{V`zt z!w`yT~*Y9Ibd^H}j4%`dP&3iSQ;4`bg`L*Y++h3bES t|KH7qPwVTc?c*oWQQiM74F8)Y \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/implicit/gradlew.bat b/samples/implicit/gradlew.bat new file mode 100644 index 00000000000..8a0b282aa68 --- /dev/null +++ b/samples/implicit/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/implicit/manifest.yml b/samples/implicit/manifest.yml new file mode 100644 index 00000000000..a264d15421d --- /dev/null +++ b/samples/implicit/manifest.yml @@ -0,0 +1,11 @@ +--- +applications: + - name: implicit-sample + memory: 512M + instances: 1 + path: target/implicit-sample-0.0.1-SNAPSHOT.jar + env: + SKIP_SSL_VALIDATION: "true" + ID_SERVICE_URL: https://uaa.10.244.0.34.xip.io + CLIENT_ID: oauth_showcase_implicit_grant + CLIENT_SECRET: secret diff --git a/samples/implicit/pom.xml b/samples/implicit/pom.xml new file mode 100644 index 00000000000..228d131736b --- /dev/null +++ b/samples/implicit/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-starter-parent + 1.0.0.RELEASE + + + org.cloudfoundry.identity + implicit-sample + 1.0.0-SNAPSHOT + jar + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + UTF-8 + 1.7 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/Application.java b/samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/Application.java new file mode 100644 index 00000000000..1f8b5f5bb08 --- /dev/null +++ b/samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/Application.java @@ -0,0 +1,38 @@ +package org.cloudfoundry.identity.samples.implicit; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +@Controller +public class Application { + + public static void main(String[] args) { + if ("true".equals(System.getenv("SKIP_SSL_VALIDATION"))) { + SSLValidationDisabler.disableSSLValidation(); + } + SpringApplication.run(Application.class, args); + } + + @Value("${idServiceUrl}") + private String idServiceUrl; + + @RequestMapping("/") + public String index(HttpServletRequest request, Model model) { + request.getSession().invalidate(); + model.addAttribute("idServiceUrl", idServiceUrl); + model.addAttribute("thisUrl", UrlUtils.buildFullRequestUrl(request)); + return "index"; + } +} \ No newline at end of file diff --git a/samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/SSLValidationDisabler.java b/samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/SSLValidationDisabler.java new file mode 100644 index 00000000000..f24684598a2 --- /dev/null +++ b/samples/implicit/src/main/java/org/cloudfoundry/identity/samples/implicit/SSLValidationDisabler.java @@ -0,0 +1,43 @@ +package org.cloudfoundry.identity.samples.implicit; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class SSLValidationDisabler { + public static void disableSSLValidation() { + TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } }; + + // Install the all-trusting trust manager + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } catch (GeneralSecurityException e) { + } + } + +} diff --git a/samples/implicit/src/main/resources/application.yml b/samples/implicit/src/main/resources/application.yml new file mode 100644 index 00000000000..2b74ef4eca9 --- /dev/null +++ b/samples/implicit/src/main/resources/application.yml @@ -0,0 +1,13 @@ +security: + ignored: /favicon.ico, / + basic: + enabled: false +idServiceUrl: ${ID_SERVICE_URL} +spring.oauth2: + client: + accessTokenUri: ${ID_SERVICE_URL}/oauth/token + userAuthorizationUri: ${ID_SERVICE_URL}/oauth/authorize + clientId: ${CLIENT_ID} + clientSecret: ${CLIENT_SECRET} + resource: + jwt.keyUri: ${ID_SERVICE_URL}/token_key \ No newline at end of file diff --git a/samples/implicit/src/main/resources/log4j.xml b/samples/implicit/src/main/resources/log4j.xml new file mode 100644 index 00000000000..c0c7e9f5697 --- /dev/null +++ b/samples/implicit/src/main/resources/log4j.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/implicit/src/main/resources/public/implicit.html b/samples/implicit/src/main/resources/public/implicit.html new file mode 100644 index 00000000000..425832ef48e --- /dev/null +++ b/samples/implicit/src/main/resources/public/implicit.html @@ -0,0 +1,55 @@ + + + +Implicit sample + + + + + + +

Implicit sample

+

This is a static HTML page! The server only saw a request for /implicit.html. Everything after the # in the address bar is stuff that only your browser can see.

+

Here's the result of calling /userinfo:

+

+  

Your access token is:

+

+  

What do you want to do?

+
+ + \ No newline at end of file diff --git a/samples/implicit/src/main/resources/templates/index.html b/samples/implicit/src/main/resources/templates/index.html new file mode 100644 index 00000000000..08384479c98 --- /dev/null +++ b/samples/implicit/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ + + + +Implicit sample + + + +

Implicit sample

+

What do you want to do?

+ + + \ No newline at end of file diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index fdc835f09cf..b31367230f5 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -2,7 +2,7 @@ # if any of these exist: # [$UAA_CONFIG_URL, $UAA_CONFIG_PATH/uaa.yml, $CLOUDFOUNDRY_CONFIG_PATH/uaa.yml] -#spring_profiles: mysql,default +spring_profiles: mysql,default #spring_profiles: postgresql,default #spring_profiles: ldap,default,hsqldb #spring_profiles: saml From 9047cab94c6b231636e0f726e2a1768945109772 Mon Sep 17 00:00:00 2001 From: Chris Dutra Date: Fri, 24 Apr 2015 16:19:06 -0700 Subject: [PATCH 10/18] Document sample apps [#93245770] https://www.pivotaltracker.com/story/show/93245770 Signed-off-by: Madhura Bhave --- samples/authcode/README.md | 38 ++++++++++++++++++++++++++++++++++++++ samples/authcode/pom.xml | 2 +- samples/implicit/README.md | 6 +++--- samples/password/README.md | 38 ++++++++++++++++++++++++++++++++++++++ samples/password/pom.xml | 2 +- 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 samples/authcode/README.md create mode 100644 samples/password/README.md diff --git a/samples/authcode/README.md b/samples/authcode/README.md new file mode 100644 index 00000000000..8d09c4b3710 --- /dev/null +++ b/samples/authcode/README.md @@ -0,0 +1,38 @@ +# Authorization Code Sample Application + +This application is a sample for how you can set up your own application that uses an authorization code grant type. The application is written in java and uses Spring Cloud Security for the SSO flow. +Authorization code grant is the most common OAuth flow. + +## Quick Start + +Start your UAA + + $ git clone git@github.com:cloudfoundry/uaa.git + $ cd uaa + $ ./gradlew run + +Verify that the uaa has started by going to http://localhost:8080/uaa + +Start the authcode sample application + +### Using Gradle: + + $ cd samples/authcode + $ ./gradlew run + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.authcode.Application : Started Application in 4.937 seconds (JVM running for 5.408) + + +### Using Maven: + + $ cd samples/authcode + $ mvn package + $ java -jar target/authcode-sample-1.0.0-SNAPSHOT.jar + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.authcode.Application : Started Application in 4.937 seconds (JVM running for 5.408) + +You can start the authcode grant flow by going to http://localhost:8889 + +Login with the pre-created UAA user/password of "marissa/koala" \ No newline at end of file diff --git a/samples/authcode/pom.xml b/samples/authcode/pom.xml index a94bd52b6be..fd61e0aae6f 100644 --- a/samples/authcode/pom.xml +++ b/samples/authcode/pom.xml @@ -10,7 +10,7 @@ org.cloudfoundry.identity authcode-sample - 0.0.1-SNAPSHOT + 1.0.0-SNAPSHOT jar diff --git a/samples/implicit/README.md b/samples/implicit/README.md index ef86cccbeba..f4ee8926ae6 100644 --- a/samples/implicit/README.md +++ b/samples/implicit/README.md @@ -1,11 +1,11 @@ # Implicit Grant Sample Application -This application is a sample for how you can set up your own application that uses an implicit grant type. The application is written in java and uses spring framework. +This application is a sample for how you can set up your own application that uses an implicit grant type. The application is written in java and uses Spring framework. Implicit grants are typically used for Single-page javascript apps. ## Quick Start -Start your UAA and Implicit Sample Application +Start your UAA $ git clone git@github.com:cloudfoundry/uaa.git $ cd uaa @@ -24,7 +24,7 @@ Start the implicit sample application >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.implicit.Application : Started Application in 4.937 seconds (JVM running for 5.408) -### Using Maven +### Using Maven: $ cd samples/implicit $ mvn package diff --git a/samples/password/README.md b/samples/password/README.md new file mode 100644 index 00000000000..b183fcc6ae9 --- /dev/null +++ b/samples/password/README.md @@ -0,0 +1,38 @@ +# Password Grant Sample Application + +This application is a sample for how you can set up your own application that uses a password grant type. The application is written in java uses the Spring framework. +Password grant is typically used when there is a high degree of trust between the resource owner and the client. + +## Quick Start + +Start your UAA + + $ git clone git@github.com:cloudfoundry/uaa.git + $ cd uaa + $ ./gradlew run + +Verify that the uaa has started by going to http://localhost:8080/uaa + +Start the password grant sample application + +### Using Gradle: + + $ cd samples/password + $ ./gradlew run + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.password.Application : Started Application in 4.937 seconds (JVM running for 5.408) + + +### Using Maven: + + $ cd samples/password + $ mvn package + $ java -jar target/password-sample-1.0.0-SNAPSHOT.jar + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.password.Application : Started Application in 4.937 seconds (JVM running for 5.408) + +You can start the password grant flow by going to http://localhost:8889 + +Login with the pre-created UAA user/password of "marissa/koala" \ No newline at end of file diff --git a/samples/password/pom.xml b/samples/password/pom.xml index 1e9213ac60a..5ea09c3a56e 100644 --- a/samples/password/pom.xml +++ b/samples/password/pom.xml @@ -10,7 +10,7 @@ org.cloudfoundry.identity password-grant-sample - 0.0.1-SNAPSHOT + 1.0.0-SNAPSHOT jar From aa786014a621e628ea26a2de528906bd39b1fed4 Mon Sep 17 00:00:00 2001 From: Chris Dutra Date: Fri, 24 Apr 2015 17:12:18 -0700 Subject: [PATCH 11/18] Implicit grant type sample app should not have client secret Signed-off-by: Madhura Bhave --- samples/implicit/application.yml | 1 - samples/implicit/src/main/resources/application.yml | 1 - uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml | 1 - 3 files changed, 3 deletions(-) diff --git a/samples/implicit/application.yml b/samples/implicit/application.yml index 74a1636201e..a930973ec9b 100644 --- a/samples/implicit/application.yml +++ b/samples/implicit/application.yml @@ -7,7 +7,6 @@ spring: oauth2: client: clientId: oauth_showcase_implicit_grant - clientSecret: secret accessTokenUri: ${idServiceUrl}/oauth/token userAuthorizationUri: ${idServiceUrl}/oauth/authorize resource: diff --git a/samples/implicit/src/main/resources/application.yml b/samples/implicit/src/main/resources/application.yml index 2b74ef4eca9..558fafe1d9d 100644 --- a/samples/implicit/src/main/resources/application.yml +++ b/samples/implicit/src/main/resources/application.yml @@ -8,6 +8,5 @@ spring.oauth2: accessTokenUri: ${ID_SERVICE_URL}/oauth/token userAuthorizationUri: ${ID_SERVICE_URL}/oauth/authorize clientId: ${CLIENT_ID} - clientSecret: ${CLIENT_SECRET} resource: jwt.keyUri: ${ID_SERVICE_URL}/token_key \ No newline at end of file diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml index bf626aa55ab..e6210f0d816 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml @@ -157,7 +157,6 @@ - From 67f671c03f19117a621ab8d8aa5b5be1f53e1359 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 24 Apr 2015 12:53:49 -0600 Subject: [PATCH 12/18] Fix fragment handling https://www.pivotaltracker.com/story/show/93165866 [#93165866] --- .../UaaAuthorizationEndpoint.java | 11 ++-- .../uaa/mock/token/TokenMvcMockTests.java | 54 +++++++++++++++++-- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authorization/UaaAuthorizationEndpoint.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authorization/UaaAuthorizationEndpoint.java index 6c10ce10b28..83c00d70e62 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authorization/UaaAuthorizationEndpoint.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authorization/UaaAuthorizationEndpoint.java @@ -274,7 +274,7 @@ private ModelAndView getImplicitGrantResponse(AuthorizationRequest authorization if (accessToken == null) { throw new UnsupportedResponseTypeException("Unsupported response type: token"); } - return new ModelAndView(new RedirectView(appendAccessToken(authorizationRequest, accessToken, authentication), false, true, + return new ModelAndView(new RedirectView(appendAccessToken(authorizationRequest, accessToken, authentication, true), false, true, false)); } catch (OAuth2Exception e) { if (authorizationRequest.getResponseTypes().contains("token") || fallbackToAuthcode == false) { @@ -307,17 +307,14 @@ private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequ private String appendAccessToken(AuthorizationRequest authorizationRequest, OAuth2AccessToken accessToken, - Authentication authUser) { + Authentication authUser, + boolean fragment) { String requestedRedirect = authorizationRequest.getRedirectUri(); if (accessToken == null) { throw new InvalidRequestException("An implicit grant could not be made"); } - boolean fragment = true; - if (requestedRedirect.contains("#")) { - fragment = false; - } StringBuilder url = new StringBuilder(); url.append("token_type=").append(encode(accessToken.getTokenType())); @@ -359,7 +356,7 @@ private String appendAccessToken(AuthorizationRequest authorizationRequest, } } - UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(requestedRedirect); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestedRedirect); if (fragment) { String existingFragment = builder.build(true).getFragment(); if (StringUtils.hasText(existingFragment)) { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index e283ed2fa93..b18e3228f38 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -188,7 +188,7 @@ private IdentityProvider setupIdentityProvider(String origin) { } protected void setUpClients(String id, String authorities, String scopes, String grantTypes, Boolean autoapprove) { - setUpClients(id,authorities,scopes,grantTypes,autoapprove,null); + setUpClients(id, authorities, scopes, grantTypes, autoapprove, null); } protected void setUpClients(String id, String authorities, String scopes, String grantTypes, Boolean autoapprove, String redirectUri) { setUpClients(id, authorities, scopes, grantTypes, autoapprove, redirectUri, null); @@ -438,8 +438,8 @@ public void testOpenIdTokenHybridFlowWithNoImplicitGrantWhenLenient() throws Exc SecurityContextHolder.getContext().setAuthentication(auth); MockHttpSession session = new MockHttpSession(); session.setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - new MockSecurityContext(auth) + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + new MockSecurityContext(auth) ); String state = new RandomValueStringGenerator().generate(); @@ -621,6 +621,54 @@ public void testAuthorizationCodeGrantWithEncodedRedirectURL() throws Exception assertEquals(redirectUri, location); } + @Test + public void testImplicitGrantWithFragmentInRedirectURL() throws Exception { + String redirectUri = "https://example.com/dashboard/?appGuid=app-guid#test"; + testImplicitGrantRedirectUri(redirectUri, "&"); + } + + @Test + public void testImplicitGrantWithNoFragmentInRedirectURL() throws Exception { + String redirectUri = "https://example.com/dashboard/?appGuid=app-guid"; + testImplicitGrantRedirectUri(redirectUri, "#"); + } + + protected void testImplicitGrantRedirectUri(String redirectUri, String delim) throws Exception { + String clientId = "authclient-"+new RandomValueStringGenerator().generate(); + String scopes = "openid"; + setUpClients(clientId, scopes, scopes, GRANT_TYPES, true, redirectUri); + String username = "authuser"+new RandomValueStringGenerator().generate(); + String userScopes = "openid"; + ScimUser developer = setUpUser(username, userScopes); + String basicDigestHeaderValue = "Basic " + + new String(org.apache.commons.codec.binary.Base64.encodeBase64((clientId + ":" + SECRET).getBytes())); + UaaPrincipal p = new UaaPrincipal(developer.getId(),developer.getUserName(),developer.getPrimaryEmail(), Origin.UAA,"", IdentityZoneHolder.get().getId()); + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(p, "", UaaAuthority.USER_AUTHORITIES); + Assert.assertTrue(auth.isAuthenticated()); + + SecurityContextHolder.getContext().setAuthentication(auth); + MockHttpSession session = new MockHttpSession(); + session.setAttribute( + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + new MockSecurityContext(auth) + ); + + String state = new RandomValueStringGenerator().generate(); + MockHttpServletRequestBuilder authRequest = get("/oauth/authorize") + .header("Authorization", basicDigestHeaderValue) + .session(session) + .param(OAuth2Utils.RESPONSE_TYPE, "token") + .param(OAuth2Utils.SCOPE, "openid") + .param(OAuth2Utils.STATE, state) + .param(OAuth2Utils.CLIENT_ID, clientId) + .param(OAuth2Utils.REDIRECT_URI, redirectUri); + + MvcResult result = mockMvc.perform(authRequest).andExpect(status().is3xxRedirection()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertTrue(location.startsWith(redirectUri + delim + "token_type=bearer&access_token")); + } + + @Test public void testOpenIdToken() throws Exception { String clientId = "testclient"+new RandomValueStringGenerator().generate(); From bd024d0ac6f47252681976c463c5f2afdc35988a Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 27 Apr 2015 14:40:47 -0700 Subject: [PATCH 13/18] Comment mysql profile in yaml file Signed-off-by: Chris Dutra --- uaa/src/main/resources/uaa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index b31367230f5..fdc835f09cf 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -2,7 +2,7 @@ # if any of these exist: # [$UAA_CONFIG_URL, $UAA_CONFIG_PATH/uaa.yml, $CLOUDFOUNDRY_CONFIG_PATH/uaa.yml] -spring_profiles: mysql,default +#spring_profiles: mysql,default #spring_profiles: postgresql,default #spring_profiles: ldap,default,hsqldb #spring_profiles: saml From 46725743f4aa99c0283f2cbb1698b2485c425d23 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 27 Apr 2015 10:48:39 -0700 Subject: [PATCH 14/18] Read internalHostnames from yaml instead of deriving it from uaa.url and login.url [#93243380] https://www.pivotaltracker.com/story/show/93243380 Signed-off-by: Chris Dutra --- .../identity/uaa/UaaConfiguration.java | 1 + .../uaa/zone/IdentityZoneResolvingFilter.java | 12 +++++---- .../zone/IdentityZoneResolvingFilterTest.java | 6 ++--- .../main/webapp/WEB-INF/spring-servlet.xml | 6 ++--- .../identity/uaa/login/BootstrapTests.java | 27 ++++++++++++++----- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java b/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java index b1b2774c2a8..6390823dda8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/UaaConfiguration.java @@ -47,6 +47,7 @@ public class UaaConfiguration { @Pattern(regexp = "(default|postgresql|hsqldb|mysql|oracle)") public String platform; public String spring_profiles; + public String internalHostnames; @URL(message = "issuer.uri must be a valid URL") public String issuerUri; public boolean dump_requests; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java index f9367174527..7ccf68c4853 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.zone; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.web.filter.OncePerRequestFilter; @@ -20,6 +21,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -36,7 +38,6 @@ public class IdentityZoneResolvingFilter extends OncePerRequestFilter { private IdentityZoneProvisioning dao; - private Set internalHostnames = new HashSet<>(); @Override @@ -81,11 +82,12 @@ public void setIdentityZoneProvisioning(IdentityZoneProvisioning dao) { this.dao = dao; } - public void setInternalHostnames(Set hostnames) { - internalHostnames = Collections.unmodifiableSet(hostnames); + @Value("${internalHostnames:localhost}") + public void setInternalHostnames(String hostnames) { + this.internalHostnames.addAll(Arrays.asList(hostnames.split("[ ,]+"))); } - public Set getInternalHostnames() { - return internalHostnames; + public void setInternalHostnames(Set hostnames) { + this.internalHostnames.addAll(Collections.unmodifiableSet(hostnames)); } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java index 3e934c6d462..1e316e5ed0e 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java @@ -52,8 +52,8 @@ private void assertFindsCorrectSubdomain(final String expectedSubdomain, final S IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(); IdentityZoneProvisioning dao = Mockito.mock(IdentityZoneProvisioning.class); filter.setIdentityZoneProvisioning(dao); - filter.setInternalHostnames(new HashSet<>(Arrays.asList(StringUtils.commaDelimitedListToStringArray(internalHostnames)))); - + filter.setInternalHostnames(internalHostnames); + IdentityZone identityZone = new IdentityZone(); identityZone.setSubdomain(expectedSubdomain); Mockito.when(dao.retrieveBySubdomain(Mockito.eq(expectedSubdomain))).thenReturn(identityZone); @@ -86,7 +86,7 @@ public void holderIsNotSetWithNonMatchingIdentityZone() throws Exception { IdentityZoneProvisioning dao = Mockito.mock(IdentityZoneProvisioning.class); FilterChain chain = Mockito.mock(FilterChain.class); filter.setIdentityZoneProvisioning(dao); - filter.setInternalHostnames(new HashSet<>(new LinkedList<>(Arrays.asList(uaaHostname)))); + filter.setInternalHostnames(uaaHostname); IdentityZone identityZone = new IdentityZone(); identityZone.setSubdomain(incomingSubdomain); diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 252c3720492..04387130bc7 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -99,14 +99,14 @@ - + #{T(org.cloudfoundry.identity.uaa.util.UaaUrlUtils).getHostForURI(@uaaUrl)} #{T(org.cloudfoundry.identity.uaa.util.UaaUrlUtils).getHostForURI(@loginUrl)} localhost - - + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index 7f6766ef50c..8bf0c45b322 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -102,6 +102,7 @@ public void cleanup() throws Exception { System.clearProperty("spring.profiles.active"); System.clearProperty("uaa.url"); System.clearProperty("login.url"); + System.clearProperty("internalHostnames"); if (context != null) { context.close(); } @@ -119,25 +120,39 @@ public void cleanup() throws Exception { @Test public void testRootContextDefaults() throws Exception { + String uaa = "uaa.some.test.domain.com"; + String login = uaa.replace("uaa", "login"); + System.setProperty("uaa.url", "https://"+uaa+":555/uaa"); + System.setProperty("login.url", "https://"+login+":555/uaa"); context = getServletContext(null, "login.yml","uaa.yml", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); assertNotNull(context.getBean("viewResolver", ViewResolver.class)); assertNotNull(context.getBean("resetPasswordController", ResetPasswordController.class)); assertEquals(864000, context.getBean("webSSOprofileConsumer", WebSSOProfileConsumerImpl.class).getMaxAuthenticationAge()); IdentityZoneResolvingFilter filter = context.getBean(IdentityZoneResolvingFilter.class); - Set defaultHostnames = new HashSet<>(Arrays.asList("localhost")); - assertEquals(filter.getInternalHostnames(), defaultHostnames); + Set defaultHostnames = new HashSet<>(Arrays.asList(uaa, login, "localhost")); + + Field internalHostnames = filter.getClass().getDeclaredField("internalHostnames"); + internalHostnames.setAccessible(true); + + assertEquals(internalHostnames.get(filter), defaultHostnames); + } @Test public void testInternalHostnames() throws Exception { String uaa = "uaa.some.test.domain.com"; String login = uaa.replace("uaa", "login"); - System.setProperty("uaa.url", "https://" + uaa + ":555/uaa"); - System.setProperty("login.url", "https://" + login + ":555/uaa"); + System.setProperty("uaa.url", "https://"+uaa+":555/uaa"); + System.setProperty("login.url", "https://"+login+":555/uaa"); + System.setProperty("internalHostnames", "some-other-hostname.com"); context = getServletContext(null, "login.yml","uaa.yml", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); IdentityZoneResolvingFilter filter = context.getBean(IdentityZoneResolvingFilter.class); - Set defaultHostnames = new HashSet<>(Arrays.asList(uaa,login, "localhost")); - assertEquals(filter.getInternalHostnames(), defaultHostnames); + Set defaultHostnames = new HashSet<>(Arrays.asList(uaa, login, "localhost", "some-other-hostname.com")); + + Field internalHostnames = filter.getClass().getDeclaredField("internalHostnames"); + internalHostnames.setAccessible(true); + + assertEquals(internalHostnames.get(filter), defaultHostnames); } @Test From 6215fb0fa4573278172117fd7279c32d628e9c0f Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 28 Apr 2015 10:49:10 -0700 Subject: [PATCH 15/18] Separate sample app for client credentials Signed-off-by: Chris Dutra --- samples/authcode/application.yml | 2 +- samples/client_credentials/.gitignore | 1 + samples/client_credentials/README.md | 36 ++++ samples/client_credentials/application.yml | 14 ++ samples/client_credentials/build.gradle | 27 +++ samples/client_credentials/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/client_credentials/gradlew | 164 ++++++++++++++++++ samples/client_credentials/gradlew.bat | 90 ++++++++++ samples/client_credentials/manifest.yml | 11 ++ samples/client_credentials/pom.xml | 40 +++++ .../clientcredentials/Application.java | 91 ++++++++++ .../SSLValidationDisabler.java | 43 +++++ .../src/main/resources/application.yml | 13 ++ .../src/main/resources/log4j.xml | 23 +++ .../templates/client_credentials.html | 42 +++++ .../src/main/resources/templates/index.html | 17 ++ samples/implicit/application.yml | 2 +- 19 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 samples/client_credentials/.gitignore create mode 100644 samples/client_credentials/README.md create mode 100644 samples/client_credentials/application.yml create mode 100644 samples/client_credentials/build.gradle create mode 100644 samples/client_credentials/gradle.properties create mode 100644 samples/client_credentials/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/client_credentials/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/client_credentials/gradlew create mode 100644 samples/client_credentials/gradlew.bat create mode 100644 samples/client_credentials/manifest.yml create mode 100644 samples/client_credentials/pom.xml create mode 100644 samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/Application.java create mode 100644 samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/SSLValidationDisabler.java create mode 100644 samples/client_credentials/src/main/resources/application.yml create mode 100644 samples/client_credentials/src/main/resources/log4j.xml create mode 100644 samples/client_credentials/src/main/resources/templates/client_credentials.html create mode 100644 samples/client_credentials/src/main/resources/templates/index.html diff --git a/samples/authcode/application.yml b/samples/authcode/application.yml index 262a69d61a8..7543f0cefc7 100644 --- a/samples/authcode/application.yml +++ b/samples/authcode/application.yml @@ -1,5 +1,5 @@ server: - port: 8889 + port: 8888 idServiceUrl: ${ID_SERVICE_URL:https://localhost:8080/uaa} spring: thymeleaf: diff --git a/samples/client_credentials/.gitignore b/samples/client_credentials/.gitignore new file mode 100644 index 00000000000..b83d22266ac --- /dev/null +++ b/samples/client_credentials/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/samples/client_credentials/README.md b/samples/client_credentials/README.md new file mode 100644 index 00000000000..33608d4761e --- /dev/null +++ b/samples/client_credentials/README.md @@ -0,0 +1,36 @@ +# Client Credentials Sample Application + +This application is a sample for how you can set up your own application that uses a client credentials grant type. The application is written in java and uses the Spring framework. +Client Credentials grant is typically used for service to service applications. + +## Quick Start + +Start your UAA + + $ git clone git@github.com:cloudfoundry/uaa.git + $ cd uaa + $ ./gradlew run + +Verify that the uaa has started by going to http://localhost:8080/uaa + +Start the authcode sample application + +### Using Gradle: + + $ cd samples/client_credentials + $ ./gradlew run + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.clientcredentials.Application : Started Application in 4.937 seconds (JVM running for 5.408) + + +### Using Maven: + + $ cd samples/client_credentials + $ mvn package + $ java -jar target/clientcredentials-sample-1.0.0-SNAPSHOT.jar + >> 2015-04-24 15:39:15.862 INFO 88632 --- [main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 + >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) + >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.clientcredentials.Application : Started Application in 4.937 seconds (JVM running for 5.408) + +You can start the client credentials grant flow by going to http://localhost:8889 \ No newline at end of file diff --git a/samples/client_credentials/application.yml b/samples/client_credentials/application.yml new file mode 100644 index 00000000000..f7aa3965856 --- /dev/null +++ b/samples/client_credentials/application.yml @@ -0,0 +1,14 @@ +server: + port: 8888 +idServiceUrl: ${ID_SERVICE_URL:https://localhost:8080/uaa} +spring: + thymeleaf: + cache: false + oauth2: + client: + authorizationUri: ${idServiceUrl}/oauth/authorize + accessTokenUri: ${idServiceUrl}/oauth/token + clientId: oauth_showcase_client_credentials + clientSecret: secret +logging.level: + org.springframework.security: DEBUG \ No newline at end of file diff --git a/samples/client_credentials/build.gradle b/samples/client_credentials/build.gradle new file mode 100644 index 00000000000..a3cc8c1121f --- /dev/null +++ b/samples/client_credentials/build.gradle @@ -0,0 +1,27 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.2.RELEASE") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: 'spring-boot' + +repositories { + mavenCentral() +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'org.springframework.cloud:spring-cloud-starter-oauth2:1.0.0.RELEASE' +} + +task wrapper(type: Wrapper) { + gradleVersion = '1.12' +} + diff --git a/samples/client_credentials/gradle.properties b/samples/client_credentials/gradle.properties new file mode 100644 index 00000000000..8d0c7be9623 --- /dev/null +++ b/samples/client_credentials/gradle.properties @@ -0,0 +1 @@ +version=1.0.0-SNAPSHOT diff --git a/samples/client_credentials/gradle/wrapper/gradle-wrapper.jar b/samples/client_credentials/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b7612167031001b7b84baf2a959e8ea8ad03c011 GIT binary patch literal 51017 zcmaI7W0WY(vMt)SZQHhOcduS;+qP}nwr$(CZEH2&I&bfL-u=$q_vNUpQ9mL_Wky9t z&WM<$APo!x1poj60q`KZF9Ptl0sYtQZ-e~XWkpp4X(i>v=z#$g{vp^9t%kz;S3u=& zNBQ3cWd-FV#YB}==w!tnWv3=(q-p8qVWnxQW~OEvl^B+o_l_T?XvZX{Wv8hnX#k-v zLX1+5iZm$O&`C>N`ww7dj2*6IL-PlpPVEyN>#oD(JjuU9F4&>RtrkQfFSerWU{tTQdH z_y5pxtmab;+TYJ__gBULWeWeL<$r7Nf2rla*Qo67=wxiI;9&b#Sx)B0j(?xr+y$MT z%#3ZE%nkLOY#sikgkoiDTO>gQA2f>4(fNaNz3SwR6%Uo;2-|r*EXa|epfs{&vJiL^ zXlxG0Zeq{KB;R6PtHN;pK78XW&e%#C?G;h zzE~o`tkY+OmhF}!THQA9@lFwE-Jq{Ncy~)jpMI!82hB2Gs#SPnOQ6RAKm9?<75=-} zE!ZFjQ*u?9En|Rj*O_IdnzR0)7*`A^M!cxpm6N=G;gRhZ?_!5zYQ&x@`*7`&>suh8(vV55ruH`4wv-#{>(SUWrEQWJ zRtmaNweT0zLf_f#(yL^3utc>2!Yhs9Wxs&_=}bl-oLC$ zG`j!q)`AK7nL0l~LF|Ikc{aH3s)Pa-RCv;9Wnz=!zHs8p1jp|SMdD7zgcwi#e1G)X z#s@$<^E~r_fbc1xCS{d}NIWMy{WX(Bv96CEtUJM?X{r>|NKB}{ZJ?Nxu4W3)JL&1o zSYP%UB-r%%d-_s%Ks__5ID}lOZsM*0A%qoc;Leb~U26R$DYA_u>bvknIaI(-0lYm3 zO>5Fx+WC6z$?CSx7xqf>-(vCFE6++*6wNIbnjo6%8rQ0eLz5NZDi8#a@$!Dp8LGc8<4CyY@JqOikVL^ZNj)4^#vwPK~=2>`~@OhEYQ3>4<5)g(Ha7 z5$v}I!~t|8cqob~naK`FLrTLWYJR+Y2vX^8jMvx}KP?E#&8E04<~oJgU954ivP{-h zYRovwc6LlKY)4ZYH=IZ1OruMCdc^CSE!Jb_=zD?=S~wi^2Cu$C^Rcv-8o@>b4no zqbJC=k2`DgmI^VOu3@0r32)#9uouWV48^z^^;{aE+5S-U!2Jq~Fi$`o#xNg9+!psm zIb{DNAWY{FPDn(VmtbB1hDO)Z)r_MU*Q4f$0Vh$#_oL(?!5v`1YfhBcFnVGc^iDma zzxfLA2o{Jh1M3M2OpW6b9(V)$owgcGBmzUE7UlvID>*|D(tbIBghoO3Lb`a-Ta!wu z>81P&0+l{52)z88bLkEt+!4k%l;&l?=89Cvp+wc?h5nyrigTd7ISdK_@bMQJF#l&W z6?HSTa&|O#F%~noG8Qy6Gy;NLe#5)5wD@21RWhXVdQ3j?R>o_9o!F_~U$cmR-n1Osft)f+;RO8pw6%e?Ksc zNuPs3k2kd2nwipr3-^xqb9(#p!N&jnXBid%{xFfCCBG2}v1n-FSlkss2j}%r1cA>f zVp7un0y6K{mAN1%X-W@)T;Xo4Kfnx#B5@ci2lf!N8=HjkET}!)?4NuP1_~5rK%?Lc zED_oes`x=W1gx1~1|S{yg+3TQX-F$YG2~pc&Lqm$rylaoP9%RwgOpB_8A(g1#pqTn zH8bKZ;}wz_q64ZiTyhK0RUuJZ*eWtaJ1YqUrKHMcGCOiutd_BqodlnnEkaE2Qxx!I z$*@02+>lLDB3LP>6*?me11pl%z?@azn3*GXO4T#kQp0pS)eDo=Cz>4Uvx<$JS=nqT z-@7b^H|UL?3A-Sq&G{atSqEGN&mww<0AOZJhSsO-_qN+GAz%CQmX4kKO4RWPcJEVOGt_V+WVqph)&sQG94AW^W-CD?wrkXt8c`pw;FZ8JnY$zHV zD9E0f2=e}3QqjyMc=KGr3DJ~p$&BTY@i!RcfpC;CuO?!qrq|zQ62nlh{oTY=%(POw zU5JmC8gf}3E*;%I?z{4+vN#B=HXMb;mOKIVu)@+2LH%Va-bzT2!Hfjt5J&1yC zEUhonr;Fs!xQj@CQA#v*uYSZ>YpBvkE8!kXCpErL*{6%}P*$cv_vZbE63f8rw1F_6 z={m-72qyvp$JYLdD5b(EWZB9{Yu?Hr8YW%MsHUCxCQH?l|mH(#_E>y1d=QDmSJ=ZBwQ}8gypIL;P0t`G7oa77Osg9 zdf7kugeOcsYq+tqO8+vf7I$^yf$}%#lb7YlZvjq zN~+7tXguWjWzob+E2*RJaC`t;%~qc2qP|60J|bPJP<@AWZ9>27%t*M2AAx1)<@QUOvYQ5 zSfzxlHtkUIhTE!1+V)AV+!i`Dn{77Yps>Fd@9>m}YOpfu(Cy>~qK~qIh~Z}$uL@DN z-7enpQvt`Bfp7b6D3fOU*co8?WBn;rf&#Qk6gXkqJHxjqRkBkKbSQOvV3())DDfb` zd&I%%n|ns+_tRDQzF?-A%P`HLs?$d{+SsqMg(!6JN-ns>KW=9AscPXp+ZBqf9DXpM_-N_238}(YYwj6Ptkq%Jx3$`vWNXSOX_O~4oYTk(O zv~mJCM-#JU{Bhx#AceB0meFkt(zgrtqIn#T?JZd`AUMU>c`efLZ8X&>b z?gM#>-`K11|Ge8?Ai~6R1&qdtGw%)swgsul93b#E&;J6_EyUf;1KvHL@abE8S`F7V z+edS69sd*9#XtThvWxrZOE{;@0UoHyD=_D!F#w!VST{CdL~2GYrxf^!hO^XU!Y%BL z4UfD!im8_{25qDZz87hcFzJvX`H1tKoZ^RsQb*|`TLa{I99%(cv- z>+gZ06~gWuY(x)QS$5K5K^qNBE_qGb9h=3n*}Mx`l)|*UwTW4_StO20#Yk}8#`A*z z&r`v$*1tpVFL*%!`@e#hU;m1pgl%n1%uSsg^qtIYZT<~j5;koX1rS0^6FKB?*=O=; zX-@_6V>BJ45hCt!_gb7Vn4teWGq^Y$|mh{{Wcs2DeSlh$DVnC zf=|+)$C-G!ncy{Ra}X%xcK92GG6gtm&NXukTkdBZ-#S{=oTLQ458wkGfi*=a6Vzj2%6=&8;ZS(BOvnj>_r^|x$1aFZ2O zy2^eusV#7CBW@8*QLDW9b*hl$@D4JIXtH>kzVzpsb32_PsS8xCmFQ3>6#8Mi&8~0Q zvB^WP#V(JI`!7SF9u)`}_;*;pmhz#nP*fjO#z6$I<8M z_6Y>^!H7ZW^e>I0)b0RrX0Y^KFnMoa=*Wd-o}T2GdW3H--5Y77b+QqpPYDWE2IL}Zhl)gvks;8-RaYe3>_pam9q#wrkwz+w>mirf)!S(`Blo&x9k zRSCm}0<9nfZNddf@Qk2YpD_tEfE+X-5{?D&paj*134Y@pAzI+g0#K3>xCRk0!8g!? zv{Qq{yT_H5t!D!&NeQf^iTyzT(^M6`am|naVrj@&TuxamH1o_)`DoW0up`FuzB`+EC5NEcP+CM=9B z#*#Hu7QxQ?M*5fphHCi0K^JP2xX315-|w{BwS+LA&$x--cgG37PHwX z)E~mQh=d$`6=gSr#f<5&F>+NDpU#A%oHHqto5}VF^pa`Xtf0(vDG2o}P4YpyDr%5KHOJ=k{YK<0T^% z)2h7Us`{}YV?+zYIR{if>uQ8r07~X@Ogh_UEMtcbk_hR?iU7Kk95m;=kBxkfx(iP6 zb35$;ncpFrc4vle&jvN?r73mLa!N^i7an5L&(g}coq}iV)mx4WW2Nq?5!75vdW%-( zL8$>TrcDV?X6JSBi}tD?J6n0BC{8sXJ|%kXCTc1W+!l>oY7ESc4dX8G+%eZdr;7tn zrdEb24R=z`gN{+hVQ}E?l3KW+T8V(u)N5(FA^QftMf@Ap27;qRd~^1=_!VywqoZy4 z2gta&0l&RMW}R{R(5ZTs?3EpQ23Dzh=I?2H7Vmj|6zru(W8WkNovz{2{g+;Pfx3~T z;m{km&T;6_)w4Eg;(*1;YV|fF${tvyVSSklPm7r`V??)mB)l7)R_iAuMlGKv>77}> z@+?!;tviEVOAk+jkDMiyj zX;xx5*rb>u;Cm-~3|m=Lo-*|`#pfbGJMA}i6=TI5=eyQ1eH@w(N`_T}>XV8F;;4z4 z+cB)s2#<$lngDgtJ)`(>_& zLBX*z=q3vD%=qNMqflZPv%C&{Glw*wkDih{89U zRq^=23qE|S3oSkgv&4!<-hBc2&VG@;jYCAy3uc#`?uppyB3{1LZU^$6C1%Fmj`IeQ zr{u}g=O=GmZnB}&$J4Q)&ab>8Wac(my9d35?+~@{iG>CT>W;l)Y@%%SL$*W5AKya1 zN1o9b{Lv#op(wb7dkF&SA&=23?|o(p z^yN0MDa`RVeV{-jm?p%14p-8KjEyitb`cu3MJM%&7=s72F#C+)VU8-AmGyX}MO3kR zeow~VVq?FxJtDX@EZrtQ4bE}p&jtRjza%%ww}U2*er>R|Ek;iJ`nY&W#*3F{?AHTk z#@mNfWDiBZ7%3khCtqs^d%(VfXBsc`hAPZ4aG-VYx}w-6)O*hQfaKredl%=QfiPv1EE%k5>F zqGx#4R{TDjqKA6l^NsJY4S5*8;Egc1i};|%4>)loLMdmCW^*Y8SCiQZ&_Qlhm04Mh zM!FdUr=5qJjWJz2gWkwAwMOQ&Q98JNeQU`WuznnSLY7op?MbOa0Pbl)6ws47#AZFh zhMvM$9JS8Y#N{L0%B04}LU&vx!q|C7X_{Irn3jhX1x+C|0b7ZN@)W+xyP8w(y}5wNAYTz zaiT&fg$$G{&J1pP{v}nVGd;FgB(Ao!i`>{#o0_v^pamU#m+>OJ6>%o3`s#^@U`|#~ z0C3)TSg90+j7fu}b*E{N1x9M{NsZ`7klNQpRTG&Z+s*TNM>q#C86CILOSE1MRANZK z$8AQF2M2-T?sF%?;eWX?$B8mZ{T70XrgAj~`|?#s^+n30lY}cIQWyfEc+>mvtJ&pi{AERUPVV$|0xL6@ zZOSFrP7B$orHXrOv^`aVrr-DWx|x(oKtS#oigPLbpJ~U!XWJ3ITpOBR5rrn;N3q z_olWZb`!&GHf#5)NH$uFt2o>@lBPl4t6_*4TZ6ksTcasaz0Aa~oLZ)qu}Sx2J4E(x z)3G-t!e2H;W$8qCV{hQ-W+*=-)(uQ_Ly{i$(-GVsjUD!peM7t927skI$Ht^i;r(jn zcvj~Qhc$a0Nd8E+tU30^xcdqoihXyT9eX`JZHDV&{k?I(w6Gj~^8v2@Wv1>JU%{dJ zG@rAMe(+#M9@Y)2gZj7b{D&;*lf{J2UwrbO=^VXUB7E>6)z3nx7rc zkXL#1pg_7RtEIJ#lK@vkmw1!EcWwqlk=ys$h)DM?r?eJQmm-!1C1_Vn^?Q9h}8wMx^PDE zo;q{jJXd%Cc=Le0tyd||WE2R2*4Gpr0{TW+k%>?s<810%<8>|BdNYNkE7c*Ew> z@>Ia;@g;ExQxGEQ^uXz*`3I9kI z5Xktv5Xm<}{9d@6AwLBBLM}M1Cn2<|Jy3uREj17yust|4@T{oAz@pm{el?Lkms1{} zX)8T^_k*=D*SA8YLGu;C;PQ$dS0r#=@#8u!SB)6An7| zW*y=_RK?kyvenmPbP2F4?c14u4F4^#`lwffS zZ(om6j2;8a-~bt3j*wXmJ#rJp`_~I2)E~<^kXL{pacWwtX0W_xEJYCIA5V zd;O=QGL9A_&HMQh=fx)3MKa!m7CCHkULR|Z zU31|dTN8P6#*wqpSMNvM+t1$D9^2lB!Bkz+0@}}e0?~8%qW2P(-Gmc)>G{v}sBaz# z=cf{(-4_Jk!)F4}GkT+I`r>!FxSfJquyfC+UxFTS-x?Xcif6WgDuTY`nm;=Ex2f~| zbNp0K@_-+v!QZ43(NDF48nQoRk3V>MKXKpus2{Zi)urr#DzZPc(?1fAz`y_K#vl9u zJqf3`S1?dc0n$O%fg@kRE||PX9>O;a_$$#J=M5MOUKFtdR~4M1Hq>j0Q3rNKP@~kS zv`Mx60yk%01u;jjRcm9D@DvTu)r+fm)zomtIsFsoo-!?Hs@r#45$l+m~B!6Wy{E?8SXrGbd9q;rhDHaF5HH?HHLbAzP-i6=W%MV)w`PO_s_W(1|}XP z7w?3})vLhasm`9~GSD#SFq~?M8hXMjLG3mnGPi{MQ->yf4%ib-iNHEbW>A7=tif-l zv532vf);&_Yf5WvBG$?U_OWp3;3d{5?@XTP;YC!UDT5t}i!r?$C~Wz(KCVt>o;Cl9 z&Dibfpd?Qg+7!e_i^3J5c-DIY825O~iWJfvTi$wqJ>1=6B^#RF)or3;s=;YS^0cqw zCDaOMu8#62JyGMT&II!zJLhSmG>T+#qabUzp&mm7Moy!{yxnrBr#|}V~%AmkN zcZV-ne=VZFcv0p;fCvze5q2aHutj&Z?RhfHFQ16%8Bcv8_&tVdr+Parqm zP%_xov?62W+RFf37P;lxiiV^S=V4MtBhxm8kC)RNles2hr%Ye4S@j08YLpAC9^F(8 zT}a=C3^?>Iq1zL>{d#a+U`iD;X;8YzT4QFM``GMunWhr&Zzf#D!&XLK&mTNZ3I;}M zH0nIaq?E?HoiQbWo=8husr8TP^LP^NN7RMdmIT@G2|&M+MQAW7+C{@7Z+SW8_Cg`J zGwnr)$wEv9I{oeO^%D8V!5%^ZNA7~2cgJ=gEVNDQ^ z0$Ct#dSPM14Gu7IP)|%OR^%;_4$>5*367RxIxeCei==1s3mjKgm{|a9sxTZ@Y9yx` z@+u$V1fH;^h`91HeR~SBo;Lp4U&9jTMiBaHAYueHohq`bn`>_C;}A9?vDH!Mrs-DH zen=*Af`xq8;FXPaenXPzI{791m%T_xdbP^;@yanjQ?)W{HZtd) zMgQfKe0u~;^+0E%W?5S;%Iha6biqs2n+i_o(xV0iU(O>lOYnXi=faR&7u}X4t0eQf zb3O-m`m5sZB;@GCZfPlTKgc}Pp1zKi7;y#3an0mBv4xUx6YnNByFU~UcGJWSYMi=i z)*{iximr>a)3ydzly215=zyg}6>nb=@slnY{sCk0-V!SQBpU>p8OPTZsmv>EG#=8i z@4AJ?T5hiLLEl5{nAodz0udAU*pTWgK%JIDpJX8fUGY}%p;HZ(Qehs}q-bhf*&f+_ zr_pj0E;;rQGT%Yz*z>oto6xZ&o-#+pLTb_25*=@DF%I6MwOKM{=6<@?HjI3_+AsBE z${e+K6byprT!pu$iPwAWvA$txa=Hgmhpf&Lhp0m$Qz5LzK9lu@iUOS+0Wez$Hj<&l zSJM{M6p3Wo^_K7JN)lzq+Vluf+&t3SLaLkALPl+AAPN(8y3v3z6e4{(E1BTaDU~-G z9qfQ$l$W*ACp)ZPhroXM24tJa6lGN8>uMau514$F4>W}}l&0Gol@FXgxfAerfmFS^ z3JS_i2Yp-#NNDeb!Tfm-;GmrFkD*L#x@vf;eDpW&A`~^+7b)=p9lulY2j8U+;^89$ z;~aDfS5bXbj$`j|q4-N44nKn?@Q-5&S8B8A>_Eq-B(m~PY`X6~okvzh zPf5HOC-8<@%%35BV+z`Dw;g}Pa!9m81w30oGV23?1HqPDj1+j(*w8rix(ET%wcuPo z%pT{Up-z&KLYs#~^JuEepCq*4&Gu%*1W4XkT%gO02NqPPji<^U=kOwVY$HoDr!+YK z8uha_sraReXo6rT)?*O1;DAHdX1jVKDzv}dUh^ia6{V7_Db;&v#OC*WmiOqk=_!n$ z^#YQlKGpM=;sB^j3;=qZxf9>~Vx+Y%7EuK1t`rk%?E5gGi9oS&lIM%g4>+9NmU#hu0{HE-Sk0=<^yLwy~3D8#xfFB0CQXu%DEI!$jN7Uoslc2{6GhCWj_)CFcdko7>X8VPD7hH*=@=BMN5FWRmOiew{TWoLmP3TLsnXt$0w(0L9P3KB z39(R<;(Us$XsiS!o*4;o4Ko~p6eBmk@hte+ltRGgX;bsWd|2V}vB*GyZ;MTyy0gpd zm-9QrZ?p$mCD9_zBWQO`^fHH>3Z2r6S^9WWQ~f?NH>j9ouWCQYIoR5_WsCRIr>`djw|=@m1G4C;@HA@Eb$A( zrb4?FZ54ULCArYDR5)%8*3PX)4Oni6MrIEb1d9G~;3q^6m?u1PcO`p|6UeEgF;Dty z|6TFgyjz{3=XlT!1`zNd`gt}dcKFurj>XIM^UC{Wx7-3d&1hT8RQ{vf(&$tPYuI<) z0!^v2F?W33o{#I5th-Cx0P+0|LxgWUj$;nz1;`H6!%g!6#XW!*79~(`9D_glZ)hKg zA0RWU^Cle<{7}y}0Smc95?Y(ts!iP0W&lef-3e75H&e)I?~LFW{4qan#~pBowgJ+V zC%Zr+m-O+k6GW=w8dTV5W{U{$^b03pKd2T_Zd92gL^~5F^QV| zsq{ygr7z^>hmK4264`ajDHGL?O|XAj3NuS_ADbSTu62vF<3%@c+&iIz74)ofZa5s?FIebxIKd2SDLUKwbmkyt16?M7f

N(DARnfPMFG|;(rIGZH1uB1rN;y7UlJ8 znb!RwO#5WS_%cPPlwSw94PTKP64}}J!~E1}ch9``2jvUrB*#@up8L&*`tqgq ztTAS@^33w-5hFZAH&{j)?l0<)9X(d`N|yLk!E=iT%PD-FTJ!|hqyj%RFFP`SaL;|_ z$(+VYhKm)K@Oe~+eZK(6ElQNdfimBrdKjeVl57_7p^|)jrzt|wyRonj$j9Bji!


f)EE4q$t9yObmyG04W0z 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 t|KH7qPwVTc?c*oWQQiM74F8)Y \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/client_credentials/gradlew.bat b/samples/client_credentials/gradlew.bat new file mode 100644 index 00000000000..8a0b282aa68 --- /dev/null +++ b/samples/client_credentials/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/client_credentials/manifest.yml b/samples/client_credentials/manifest.yml new file mode 100644 index 00000000000..a107743438f --- /dev/null +++ b/samples/client_credentials/manifest.yml @@ -0,0 +1,11 @@ +--- +applications: + - name: clientcredentials-sample + memory: 512M + instances: 1 + path: target/clientcredentials-sample-0.0.1-SNAPSHOT.jar + env: + SKIP_SSL_VALIDATION: "true" + ID_SERVICE_URL: https://uaa.10.244.0.34.xip.io + CLIENT_ID: oauth_showcase_client_credentials + CLIENT_SECRET: secret diff --git a/samples/client_credentials/pom.xml b/samples/client_credentials/pom.xml new file mode 100644 index 00000000000..81769f88a62 --- /dev/null +++ b/samples/client_credentials/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-starter-parent + 1.0.0.RELEASE + + + org.cloudfoundry.identity + clientcredentials-sample + 1.0.0-SNAPSHOT + jar + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + UTF-8 + 1.7 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/Application.java b/samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/Application.java new file mode 100644 index 00000000000..0399ba4ba9e --- /dev/null +++ b/samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/Application.java @@ -0,0 +1,91 @@ +package org.cloudfoundry.identity.samples.clientcredentials; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.codec.binary.Base64; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +@Controller +public class Application { + + public static void main(String[] args) { + if ("true".equals(System.getenv("SKIP_SSL_VALIDATION"))) { + SSLValidationDisabler.disableSSLValidation(); + } + SpringApplication.run(Application.class, args); + } + + @Autowired + private ObjectMapper objectMapper; + + @Value("${idServiceUrl}") + private String uaaLocation; + + @Autowired + @Qualifier("clientCredentialsRestTemplate") + private OAuth2RestTemplate clientCredentialsRestTemplate; + + @RequestMapping("/") + public String index(HttpServletRequest request, Model model) { + return "index"; + } + + @RequestMapping("/client_credentials") + public String clientCredentials(Model model) throws Exception { + Object clientResponse = clientCredentialsRestTemplate.getForObject("{uaa}/oauth/clients", Object.class, + uaaLocation); + model.addAttribute("clients", clientResponse); + model.addAttribute("token", getToken(clientCredentialsRestTemplate.getOAuth2ClientContext())); + return "client_credentials"; + } + + @Configuration + @EnableConfigurationProperties + @EnableOAuth2Client + public static class Config { + @Bean + @ConfigurationProperties(prefix = "spring.oauth2.client") + ClientCredentialsResourceDetails clientCredentialsResourceDetails() { + return new ClientCredentialsResourceDetails(); + } + + @Bean + OAuth2RestTemplate clientCredentialsRestTemplate() { + OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(clientCredentialsResourceDetails()); + return restTemplate; + } + } + + private Map getToken(OAuth2ClientContext clientContext) throws Exception { + if (clientContext.getAccessToken() != null) { + String tokenBase64 = clientContext.getAccessToken().getValue().split("\\.")[1]; + return objectMapper.readValue(Base64.decodeBase64(tokenBase64), new TypeReference>() { + }); + } + return null; + } +} \ No newline at end of file diff --git a/samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/SSLValidationDisabler.java b/samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/SSLValidationDisabler.java new file mode 100644 index 00000000000..fb03cbdada2 --- /dev/null +++ b/samples/client_credentials/src/main/java/org/cloudfoundry/identity/samples/clientcredentials/SSLValidationDisabler.java @@ -0,0 +1,43 @@ +package org.cloudfoundry.identity.samples.clientcredentials; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class SSLValidationDisabler { + public static void disableSSLValidation() { + TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } }; + + // Install the all-trusting trust manager + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } catch (GeneralSecurityException e) { + } + } + +} diff --git a/samples/client_credentials/src/main/resources/application.yml b/samples/client_credentials/src/main/resources/application.yml new file mode 100644 index 00000000000..2b74ef4eca9 --- /dev/null +++ b/samples/client_credentials/src/main/resources/application.yml @@ -0,0 +1,13 @@ +security: + ignored: /favicon.ico, / + basic: + enabled: false +idServiceUrl: ${ID_SERVICE_URL} +spring.oauth2: + client: + accessTokenUri: ${ID_SERVICE_URL}/oauth/token + userAuthorizationUri: ${ID_SERVICE_URL}/oauth/authorize + clientId: ${CLIENT_ID} + clientSecret: ${CLIENT_SECRET} + resource: + jwt.keyUri: ${ID_SERVICE_URL}/token_key \ No newline at end of file diff --git a/samples/client_credentials/src/main/resources/log4j.xml b/samples/client_credentials/src/main/resources/log4j.xml new file mode 100644 index 00000000000..c0c7e9f5697 --- /dev/null +++ b/samples/client_credentials/src/main/resources/log4j.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/client_credentials/src/main/resources/templates/client_credentials.html b/samples/client_credentials/src/main/resources/templates/client_credentials.html new file mode 100644 index 00000000000..16edce77dc3 --- /dev/null +++ b/samples/client_credentials/src/main/resources/templates/client_credentials.html @@ -0,0 +1,42 @@ + + + + Client Credentials + + + + +

Client Credentials

+ +

You've used a client_credentials grant to access protected data on UAA. Here's the result of calling /oauth/clients:

+

Clients

+ + + + + + + + + + + + +
Client IDAuthorized Grant TypesScopesAuthorities
client_idauthorized_grant_typesscopesauthorities
+

This is the token that was used:

+

+
+

What do you want to do?

+
+ + + \ No newline at end of file diff --git a/samples/client_credentials/src/main/resources/templates/index.html b/samples/client_credentials/src/main/resources/templates/index.html new file mode 100644 index 00000000000..efe110ab5be --- /dev/null +++ b/samples/client_credentials/src/main/resources/templates/index.html @@ -0,0 +1,17 @@ + + + + Client Credentials Sample + + + +

Client Credentials sample

+

What do you want to do?

+ + + + \ No newline at end of file diff --git a/samples/implicit/application.yml b/samples/implicit/application.yml index a930973ec9b..b290570bb53 100644 --- a/samples/implicit/application.yml +++ b/samples/implicit/application.yml @@ -1,5 +1,5 @@ server: - port: 8889 + port: 8888 idServiceUrl: ${ID_SERVICE_URL:https://localhost:8080/uaa} spring: thymeleaf: From c7c321fc3bb38c69fe90f8a83df0d74dab85a4a2 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 28 Apr 2015 11:28:03 -0700 Subject: [PATCH 16/18] Update README files for sample apps Signed-off-by: Chris Dutra --- samples/authcode/README.md | 2 +- samples/client_credentials/README.md | 2 +- samples/implicit/README.md | 2 +- samples/password/README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/authcode/README.md b/samples/authcode/README.md index 8d09c4b3710..b795294f8d8 100644 --- a/samples/authcode/README.md +++ b/samples/authcode/README.md @@ -33,6 +33,6 @@ Start the authcode sample application >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.authcode.Application : Started Application in 4.937 seconds (JVM running for 5.408) -You can start the authcode grant flow by going to http://localhost:8889 +You can start the authcode grant flow by going to http://localhost:8888 Login with the pre-created UAA user/password of "marissa/koala" \ No newline at end of file diff --git a/samples/client_credentials/README.md b/samples/client_credentials/README.md index 33608d4761e..10f57833e78 100644 --- a/samples/client_credentials/README.md +++ b/samples/client_credentials/README.md @@ -33,4 +33,4 @@ Start the authcode sample application >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.clientcredentials.Application : Started Application in 4.937 seconds (JVM running for 5.408) -You can start the client credentials grant flow by going to http://localhost:8889 \ No newline at end of file +You can start the client credentials grant flow by going to http://localhost:8888 \ No newline at end of file diff --git a/samples/implicit/README.md b/samples/implicit/README.md index f4ee8926ae6..7fd530161fe 100644 --- a/samples/implicit/README.md +++ b/samples/implicit/README.md @@ -33,6 +33,6 @@ Start the implicit sample application >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.implicit.Application : Started Application in 4.937 seconds (JVM running for 5.408) -You can start the implicit grant flow by going to http://localhost:8889 +You can start the implicit grant flow by going to http://localhost:8888 Login with the pre-created UAA user/password of "marissa/koala" \ No newline at end of file diff --git a/samples/password/README.md b/samples/password/README.md index b183fcc6ae9..009313e523f 100644 --- a/samples/password/README.md +++ b/samples/password/README.md @@ -33,6 +33,6 @@ Start the password grant sample application >> 2015-04-24 15:39:15.947 INFO 88632 --- [main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8888 (http) >> 2015-04-24 15:39:15.948 INFO 88632 --- [main] o.c.i.samples.password.Application : Started Application in 4.937 seconds (JVM running for 5.408) -You can start the password grant flow by going to http://localhost:8889 +You can start the password grant flow by going to http://localhost:8888 Login with the pre-created UAA user/password of "marissa/koala" \ No newline at end of file From c0714331ce0fec50df2a6e938a644fee8ebf62ce Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Tue, 28 Apr 2015 11:55:07 -0700 Subject: [PATCH 17/18] [artifactory-release] Next development version [skip ci] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3db4174416d..819d6aab3be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.2.5-SNAPSHOT +version=2.2.6-SNAPSHOT From 8d998bde74482edaa48a0fedda57625f32642b33 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 28 Apr 2015 13:02:48 -0600 Subject: [PATCH 18/18] Bump release version to 2.2.5 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 819d6aab3be..bcc6e51b451 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.2.6-SNAPSHOT +version=2.2.5