From 41779f09cb2a1c0dcadd3d3b71f4b4c140ba26e5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 13 Jan 2016 11:47:19 -0700 Subject: [PATCH 01/25] Add description field to a scim group https://www.pivotaltracker.com/story/show/109738682 [#109738682] --- .../identity/uaa/scim/ScimGroup.java | 19 +++++- .../scim/impl/ScimGroupJsonDeserializer.java | 6 +- .../scim/impl/ScimGroupJsonSerializer.java | 6 +- .../identity/uaa/scim/ScimGroupTests.java | 52 +++++++++++++++ .../scim/jdbc/JdbcScimGroupProvisioning.java | 52 ++++++++------- .../hsqldb/V3_0_1__Add_Group_Description.sql | 15 +++++ .../mysql/V3_0_1__Add_Group_Description.sql | 15 +++++ .../V3_0_1__Add_Group_Description.sql | 15 +++++ .../jdbc/JdbcScimGroupProvisioningTests.java | 55 ++++++++++------ .../ScimGroupEndpointsMockMvcTests.java | 64 +++++++++++++++++-- 10 files changed, 246 insertions(+), 53 deletions(-) create mode 100644 model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimGroupTests.java create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_1__Add_Group_Description.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_1__Add_Group_Description.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_1__Add_Group_Description.sql diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimGroup.java b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimGroup.java index 7a7fc5eba92..8148a35b553 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimGroup.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimGroup.java @@ -26,6 +26,7 @@ public class ScimGroup extends ScimCore { private String displayName; private String zoneId; + private String description; private List members; @@ -56,6 +57,14 @@ public ScimGroup setMembers(List members) { return this; } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + public ScimGroup() { this(null); } @@ -72,7 +81,13 @@ public ScimGroup(String id, String name, String zoneId) { @Override public String toString() { - return String.format("(Group id: %s, name: %s, created: %s, modified: %s, version: %s, members: %s)", getId(), - displayName, getMeta().getCreated(), getMeta().getLastModified(), getVersion(), members); + return String.format("(Group id: %s, name: %s, description: %s, created: %s, modified: %s, version: %s, members: %s)", + getId(), + displayName, + description, + getMeta().getCreated(), + getMeta().getLastModified(), + getVersion(), + members); } } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonDeserializer.java b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonDeserializer.java index ffd6c02d377..1e53b859902 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonDeserializer.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonDeserializer.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"). @@ -52,6 +52,10 @@ public ScimGroup deserialize(JsonParser jp, DeserializationContext ctxt) throws group.setId(jp.readValueAs(String.class)); } else if ("displayname".equalsIgnoreCase(fieldName)) { group.setDisplayName(jp.readValueAs(String.class)); + } else if ("description".equalsIgnoreCase(fieldName)) { + group.setDescription(jp.readValueAs(String.class)); + } else if ("zoneId".equalsIgnoreCase(fieldName)) { + group.setZoneId(jp.readValueAs(String.class)); } else if ("meta".equalsIgnoreCase(fieldName)) { group.setMeta(jp.readValueAs(ScimMeta.class)); } else if ("schemas".equalsIgnoreCase(fieldName)) { diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonSerializer.java b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonSerializer.java index 8382a363ad8..d25adfef8f0 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonSerializer.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimGroupJsonSerializer.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"). @@ -44,11 +44,13 @@ public void serialize(ScimGroup group, JsonGenerator jgen, SerializerProvider pr } } - Map groupJson = new HashMap(); + Map groupJson = new HashMap<>(); addNonNull(groupJson, "meta", group.getMeta()); addNonNull(groupJson, "schemas", group.getSchemas()); addNonNull(groupJson, "id", group.getId()); addNonNull(groupJson, "displayName", group.getDisplayName()); + addNonNull(groupJson, "zoneId", group.getZoneId()); + addNonNull(groupJson, "description", group.getDescription()); for (Map.Entry> entry : roles.entrySet()) { addNonNull(groupJson, entry.getKey(), entry.getValue()); diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimGroupTests.java b/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimGroupTests.java new file mode 100644 index 00000000000..4c887c675ac --- /dev/null +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimGroupTests.java @@ -0,0 +1,52 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.scim; + +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ScimGroupTests { + private static final String GROUP_BEFORE_DESCRIPTION = "{\"meta\":{\"version\":0,\"created\":\"2016-01-13T09:01:33.909Z\"},\"zoneId\":\"zoneId\",\"displayName\":\"name\",\"schemas\":[\"urn:scim:schemas:core:1.0\"],\"id\":\"id\"}"; + ScimGroup group; + + @Before + public void setUp() { + group = new ScimGroup("id","name","zoneId"); + } + + @Test + public void testDeSerializeWithoutDescription() { + group = JsonUtils.readValue(GROUP_BEFORE_DESCRIPTION, ScimGroup.class); + assertEquals("id", group.getId()); + assertEquals("name", group.getDisplayName()); + assertEquals("zoneId", group.getZoneId()); + assertNull(group.getDescription()); + } + + @Test + public void testSerializeWithDescription() { + group.setDescription("description"); + String json = JsonUtils.writeValueAsString(group); + group = JsonUtils.readValue(json, ScimGroup.class); + assertEquals("id", group.getId()); + assertEquals("name", group.getDisplayName()); + assertEquals("zoneId", group.getZoneId()); + assertEquals("description", group.getDescription()); + } +} \ No newline at end of file diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java index 295b7aed403..c79c785ae2a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioning.java @@ -1,6 +1,6 @@ /******************************************************************************* * Cloud Foundry - * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. @@ -56,17 +56,18 @@ public Log getLogger() { return logger; } - public static final String GROUP_FIELDS = "id,displayName,created,lastModified,version,identity_zone_id"; + public static final String GROUP_FIELDS = "id,displayName,description,created,lastModified,version,identity_zone_id"; public static final String GROUP_TABLE = "groups"; public static final String GROUP_MEMBERSHIP_TABLE = "group_membership"; public static final String EXTERNAL_GROUP_TABLE = "external_group_mapping"; - public static final String ADD_GROUP_SQL = String.format("insert into %s ( %s ) values (?,?,?,?,?,?)", GROUP_TABLE, - GROUP_FIELDS); + public static final String ADD_GROUP_SQL = String.format("insert into %s ( %s ) values (?,?,?,?,?,?,?)", + GROUP_TABLE, + GROUP_FIELDS); public static final String UPDATE_GROUP_SQL = String.format( - "update %s set version=?, displayName=?, lastModified=? where id=? and version=?", GROUP_TABLE); + "update %s set version=?, displayName=?, description=?, lastModified=? where id=? and version=?", GROUP_TABLE); public static final String GET_GROUP_SQL = String.format("select %s from %s where id=? and identity_zone_id=?", GROUP_FIELDS, GROUP_TABLE); @@ -133,12 +134,14 @@ public ScimGroup create(final ScimGroup group) throws InvalidScimResourceExcepti jdbcTemplate.update(ADD_GROUP_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { - ps.setString(1, id); - ps.setString(2, group.getDisplayName()); - ps.setTimestamp(3, new Timestamp(new Date().getTime())); - ps.setTimestamp(4, new Timestamp(new Date().getTime())); - ps.setInt(5, group.getVersion()); - ps.setString(6, group.getZoneId()); + int pos = 1; + ps.setString(pos++, id); + ps.setString(pos++, group.getDisplayName()); + ps.setString(pos++, group.getDescription()); + ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); + ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); + ps.setInt(pos++, group.getVersion()); + ps.setString(pos++, group.getZoneId()); } }); } catch (DuplicateKeyException ex) { @@ -156,11 +159,13 @@ public ScimGroup update(final String id, final ScimGroup group) throws InvalidSc int updated = jdbcTemplate.update(UPDATE_GROUP_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { - ps.setInt(1, group.getVersion() + 1); - ps.setString(2, group.getDisplayName()); - ps.setTimestamp(3, new Timestamp(new Date().getTime())); - ps.setString(4, id); - ps.setInt(5, group.getVersion()); + int pos = 1; + ps.setInt(pos++, group.getVersion() + 1); + ps.setString(pos++, group.getDisplayName()); + ps.setString(pos++, group.getDescription()); + ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); + ps.setString(pos++, id); + ps.setInt(pos++, group.getVersion()); } }); if (updated != 1) { @@ -209,13 +214,16 @@ private static final class ScimGroupRowMapper implements RowMapper { @Override public ScimGroup mapRow(ResultSet rs, int rowNum) throws SQLException { - String id = rs.getString(1); - String name = rs.getString(2); - Date created = rs.getTimestamp(3); - Date modified = rs.getTimestamp(4); - int version = rs.getInt(5); - String zoneId = rs.getString(6); + int pos = 1; + String id = rs.getString(pos++); + String name = rs.getString(pos++); + String description = rs.getString(pos++); + Date created = rs.getTimestamp(pos++); + Date modified = rs.getTimestamp(pos++); + int version = rs.getInt(pos++); + String zoneId = rs.getString(pos++); ScimGroup group = new ScimGroup(id, name, zoneId); + group.setDescription(description); ScimMeta meta = new ScimMeta(created, modified, version); group.setMeta(meta); return group; diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_1__Add_Group_Description.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_1__Add_Group_Description.sql new file mode 100644 index 00000000000..b32ae67a141 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_1__Add_Group_Description.sql @@ -0,0 +1,15 @@ +-- +-- Cloud Foundry +-- Copyright (c) [2016] Pivotal Software, Inc. All Rights Reserved. +-- +-- This product is licensed to you under the Apache License, Version 2.0 (the "License"). +-- You may not use this product except in compliance with the License. +-- +-- This product includes a number of subcomponents with +-- separate copyright notices and license terms. Your use of these +-- subcomponents is subject to the terms and conditions of the +-- subcomponent's license, as noted in the LICENSE file. +-- + +-- add zone id to the groups table +ALTER TABLE groups ADD COLUMN description varchar(255); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_1__Add_Group_Description.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_1__Add_Group_Description.sql new file mode 100644 index 00000000000..b32ae67a141 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_1__Add_Group_Description.sql @@ -0,0 +1,15 @@ +-- +-- Cloud Foundry +-- Copyright (c) [2016] Pivotal Software, Inc. All Rights Reserved. +-- +-- This product is licensed to you under the Apache License, Version 2.0 (the "License"). +-- You may not use this product except in compliance with the License. +-- +-- This product includes a number of subcomponents with +-- separate copyright notices and license terms. Your use of these +-- subcomponents is subject to the terms and conditions of the +-- subcomponent's license, as noted in the LICENSE file. +-- + +-- add zone id to the groups table +ALTER TABLE groups ADD COLUMN description varchar(255); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_1__Add_Group_Description.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_1__Add_Group_Description.sql new file mode 100644 index 00000000000..b32ae67a141 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_1__Add_Group_Description.sql @@ -0,0 +1,15 @@ +-- +-- Cloud Foundry +-- Copyright (c) [2016] Pivotal Software, Inc. All Rights Reserved. +-- +-- This product is licensed to you under the Apache License, Version 2.0 (the "License"). +-- You may not use this product except in compliance with the License. +-- +-- This product includes a number of subcomponents with +-- separate copyright notices and license terms. Your use of these +-- subcomponents is subject to the terms and conditions of the +-- subcomponent's license, as noted in the LICENSE file. +-- + +-- add zone id to the groups table +ALTER TABLE groups ADD COLUMN description varchar(255); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java index 9e235fd7245..d95f0272c3c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupProvisioningTests.java @@ -1,6 +1,6 @@ /******************************************************************************* * Cloud Foundry - * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. @@ -12,12 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.scim.jdbc; -import java.sql.Timestamp; -import java.util.Arrays; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; @@ -27,7 +21,14 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.Before; import org.junit.Test; -import org.springframework.util.StringUtils; + +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.util.StringUtils.hasText; public class JdbcScimGroupProvisioningTests extends JdbcTestBase { @@ -52,13 +53,22 @@ private void validateGroupCount(int expected) { assertEquals(expected, existingGroupCount); } - private void validateGroup(ScimGroup group, String name) { + private void validateGroup(ScimGroup group, String name, String zoneId) { + + } + private void validateGroup(ScimGroup group, String name, String zoneId, String description) { assertNotNull(group); assertNotNull(group.getId()); assertNotNull(group.getDisplayName()); - if (StringUtils.hasText(name)) { + if (hasText(name)) { assertEquals(name, group.getDisplayName()); } + if (hasText(description)) { + assertEquals(description, group.getDescription()); + } + if (hasText(zoneId)) { + assertEquals(zoneId, group.getZoneId()); + } } @Test @@ -66,7 +76,7 @@ public void canRetrieveGroups() throws Exception { List groups = dao.retrieveAll(); assertEquals(3, groups.size()); for (ScimGroup g : groups) { - validateGroup(g, null); + validateGroup(g, null, IdentityZoneHolder.get().getId()); } } @@ -118,7 +128,7 @@ public void cannotRetrieveGroupsWithWrongFilter() { @Test public void canRetrieveGroup() throws Exception { ScimGroup group = dao.retrieve("g1"); - validateGroup(group, "uaa.user"); + validateGroup(group, "uaa.user", IdentityZoneHolder.get().getId()); } @Test(expected = ScimResourceNotFoundException.class) @@ -129,12 +139,13 @@ public void cannotRetrieveNonExistentGroup() { @Test public void canCreateGroup() throws Exception { ScimGroup g = new ScimGroup(null, "test.1", IdentityZoneHolder.get().getId()); + g.setDescription("description-create"); ScimGroupMember m1 = new ScimGroupMember("m1", ScimGroupMember.Type.USER, ScimGroupMember.GROUP_MEMBER); ScimGroupMember m2 = new ScimGroupMember("m2", ScimGroupMember.Type.USER, ScimGroupMember.GROUP_ADMIN); g.setMembers(Arrays.asList(m1, m2)); g = dao.create(g); validateGroupCount(4); - validateGroup(g, "test.1"); + validateGroup(g, "test.1", IdentityZoneHolder.get().getId(), "description-create"); } @Test @@ -164,11 +175,12 @@ public void canUpdateGroup() throws Exception { ScimGroupMember m2 = new ScimGroupMember("g2", ScimGroupMember.Type.USER, ScimGroupMember.GROUP_ADMIN); g.setMembers(Arrays.asList(m1, m2)); g.setDisplayName("uaa.none"); + g.setDescription("description-update"); - g = dao.update("g1", g); + dao.update("g1", g); g = dao.retrieve("g1"); - validateGroup(g, "uaa.none"); + validateGroup(g, "uaa.none", IdentityZoneHolder.get().getId(), "description-update"); } @Test @@ -181,12 +193,13 @@ private ScimGroup addGroup(String id, String name) { TestUtils.assertNoSuchUser(jdbcTemplate, "id", id); //"id,displayName,created,lastModified,version,identity_zone_id" jdbcTemplate.update(dao.ADD_GROUP_SQL, - id, - name, - new Timestamp(System.currentTimeMillis()), - new Timestamp(System.currentTimeMillis()), - 0, - IdentityZoneHolder.get().getId()); + id, + name, + name+"-description", + new Timestamp(System.currentTimeMillis()), + new Timestamp(System.currentTimeMillis()), + 0, + IdentityZoneHolder.get().getId()); return dao.retrieve(id); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java index 3696105a69a..854a8615545 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java @@ -62,13 +62,13 @@ import java.util.Set; import java.util.stream.Collectors; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -318,7 +318,7 @@ public void testGroupOperations_as_Zone_Admin() throws Exception { .header("Authorization", "bearer " + zoneAdminToken) .accept(APPLICATION_JSON); - Assert.assertEquals(group, JsonUtils.readValue( + assertEquals(group, JsonUtils.readValue( getMockMvc().perform(get) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), @@ -617,6 +617,60 @@ public void testCreateExternalGroupMapUsingId() throws Exception { checkGetExternalGroups(); } + @Test + public void test_create_and_update_group_description() throws Exception { + String name = new RandomValueStringGenerator().generate(); + ScimGroup group = new ScimGroup(name); + group.setZoneId("some-other-zone"); + group.setDescription(name+"-description"); + + String content = JsonUtils.writeValueAsString(group); + MockHttpServletRequestBuilder action = MockMvcRequestBuilders.post("/Groups") + .header("Authorization", "Bearer " + scimWriteToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(content); + + ScimGroup newGroup = + JsonUtils.readValue( + getMockMvc().perform(action) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(), + ScimGroup.class + ); + assertNotNull(newGroup); + assertNotNull(newGroup.getId()); + assertEquals(IdentityZone.getUaa().getId(), newGroup.getZoneId()); + assertEquals(group.getDisplayName(), newGroup.getDisplayName()); + assertEquals(group.getDescription(), newGroup.getDescription()); + + group.setDescription(name+"-description-updated"); + newGroup.setDescription(group.getDescription()); + + content = JsonUtils.writeValueAsString(newGroup); + action = MockMvcRequestBuilders.put("/Groups/"+newGroup.getId()) + .header("Authorization", "Bearer " + scimWriteToken) + .header("If-Match", newGroup.getVersion()) + .contentType(MediaType.APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(content); + + newGroup = + JsonUtils.readValue( + getMockMvc().perform(action) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + ScimGroup.class + ); + + assertNotNull(newGroup); + assertNotNull(newGroup.getId()); + assertEquals(IdentityZone.getUaa().getId(), newGroup.getZoneId()); + assertEquals(group.getDisplayName(), newGroup.getDisplayName()); + assertEquals(group.getDescription(), newGroup.getDescription()); + + } + protected ResultActions createGroup(String id, String name, String externalName) throws Exception { ScimGroupExternalMember em = new ScimGroupExternalMember(); if (id!=null) em.setGroupId(id); @@ -795,7 +849,7 @@ public void get_group_membership() throws Exception { .andExpect(status().isOk()) .andReturn(); ScimGroupMember scimGroupMember = JsonUtils.readValue(mvcResult.getResponse().getContentAsString(), ScimGroupMember.class); - Assert.assertNotNull(scimGroupMember); + assertNotNull(scimGroupMember); assertEquals(scimUser.getId(), scimGroupMember.getMemberId()); } @@ -906,7 +960,7 @@ public void update_member_in_group() throws Exception { .header("Content-Type", APPLICATION_JSON_VALUE) .content(updatedMember)) .andExpect(status().isOk()); - Assert.assertNotNull(updatedMember); + assertNotNull(updatedMember); MockHttpServletRequestBuilder get = get("/Groups/" + groupId + "/members/" + scimGroupMember.getMemberId()) .header("Authorization", "Bearer " + scimReadToken); From 07c33712e86af90585f604400008219a1cf5471e Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 13 Jan 2016 16:41:15 -0700 Subject: [PATCH 02/25] Bump next develop version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4950f0ded39..a6b377530ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=3.0.0 +version=3.0.1-SNAPSHOT From 0e8bb79583d432065053991c34966b2995200e37 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 14 Jan 2016 15:20:27 -0700 Subject: [PATCH 03/25] Implement a "provider description" field for IdentityProviders. No DB changes needed. Add support for LDAP in Yaml config Add support for SAML in Yaml config --- .../AbstractIdentityProviderDefinition.java | 10 + .../SamlIdentityProviderDefinition.java | 176 ++++-------------- .../UaaIdentityProviderDefinition.java | 36 ++-- .../SamlIdentityProviderConfigurator.java | 13 +- .../identity/uaa/util/LdapUtils.java | 10 + .../config/IdentityProviderBootstrapTest.java | 4 + .../InvitationsControllerTest.java | 5 +- .../uaa/login/LoginInfoEndpointTests.java | 11 +- .../IdentityProviderConfiguratorTests.java | 30 ++- .../SamlIdentityProviderDefinitionTests.java | 5 +- .../provider/saml/SamlRedirectUtilsTest.java | 5 +- .../identity/uaa/util/DomainFilterTest.java | 10 +- ...JdbcIdentityProviderProvisioningTests.java | 23 ++- .../identity/uaa/BootstrapTests.java | 4 + .../identity/uaa/login/BootstrapTests.java | 11 +- .../login/InvitationsServiceMockMvcTests.java | 5 +- .../identity/uaa/login/LoginMockMvcTests.java | 30 ++- ...IdentityProviderEndpointsMockMvcTests.java | 38 ++-- .../test/resources/test/bootstrap/login.yml | 1 + uaa/src/test/resources/test/bootstrap/uaa.yml | 1 + 20 files changed, 176 insertions(+), 252 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractIdentityProviderDefinition.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractIdentityProviderDefinition.java index 8f6698cc1f1..b2d00fbb1ee 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractIdentityProviderDefinition.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractIdentityProviderDefinition.java @@ -19,9 +19,11 @@ public class AbstractIdentityProviderDefinition { public static final String EMAIL_DOMAIN_ATTR = "emailDomain"; + public static final String PROVIDER_DESCRIPTION = "providerDescription"; private List emailDomain; private Map additionalConfiguration; + private String providerDescription; public List getEmailDomain() { return emailDomain; @@ -41,6 +43,14 @@ public AbstractIdentityProviderDefinition setAdditionalConfiguration(Map emailDomain = getEmailDomain() != null ? new ArrayList<>(getEmailDomain()) : null; List externalGroupsWhitelist = getExternalGroupsWhitelist() != null ? new ArrayList<>(getExternalGroupsWhitelist()) : null; Map attributeMappings = getAttributeMappings() != null ? new HashMap(getAttributeMappings()) : null; - return Builder.get() - .setMetaDataLocation(metaDataLocation) - .setIdpEntityAlias(idpEntityAlias) - .setNameID(nameID) - .setAssertionConsumerIndex(assertionConsumerIndex) - .setMetadataTrustCheck(metadataTrustCheck) - .setShowSamlLink(showSamlLink) - .setLinkText(linkText) - .setIconUrl(iconUrl) - .setZoneId(zoneId) - .setAddShadowUserOnLogin(addShadowUserOnLogin) - .setEmailDomain(emailDomain) - .setExternalGroupsWhitelist(externalGroupsWhitelist) - .setAttributeMappings(attributeMappings) - .build(); + SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition(); + def.setMetaDataLocation(metaDataLocation); + def.setIdpEntityAlias(idpEntityAlias); + def.setZoneId(zoneId); + def.setNameID(nameID); + def.setAssertionConsumerIndex(assertionConsumerIndex); + def.setMetadataTrustCheck(metadataTrustCheck); + def.setShowSamlLink(showSamlLink); + def.setLinkText(linkText); + def.setIconUrl(iconUrl); + def.setAddShadowUserOnLogin(addShadowUserOnLogin); + def.setEmailDomain(emailDomain); + def.setExternalGroupsWhitelist(externalGroupsWhitelist); + def.setAttributeMappings(attributeMappings); + def.setAdditionalConfiguration(getAdditionalConfiguration()); + def.setProviderDescription(getProviderDescription()); + return def; } @JsonIgnore @@ -115,48 +117,54 @@ public String getMetaDataLocation() { return metaDataLocation; } - public void setMetaDataLocation(String metaDataLocation) { + public SamlIdentityProviderDefinition setMetaDataLocation(String metaDataLocation) { this.metaDataLocation = metaDataLocation; + return this; } public String getIdpEntityAlias() { return idpEntityAlias; } - public void setIdpEntityAlias(String idpEntityAlias) { + public SamlIdentityProviderDefinition setIdpEntityAlias(String idpEntityAlias) { this.idpEntityAlias = idpEntityAlias; + return this; } public String getNameID() { return nameID; } - public void setNameID(String nameID) { + public SamlIdentityProviderDefinition setNameID(String nameID) { this.nameID = nameID; + return this; } public int getAssertionConsumerIndex() { return assertionConsumerIndex; } - public void setAssertionConsumerIndex(int assertionConsumerIndex) { + public SamlIdentityProviderDefinition setAssertionConsumerIndex(int assertionConsumerIndex) { this.assertionConsumerIndex = assertionConsumerIndex; + return this; } public boolean isMetadataTrustCheck() { return metadataTrustCheck; } - public void setMetadataTrustCheck(boolean metadataTrustCheck) { + public SamlIdentityProviderDefinition setMetadataTrustCheck(boolean metadataTrustCheck) { this.metadataTrustCheck = metadataTrustCheck; + return this; } public boolean isShowSamlLink() { return showSamlLink; } - public void setShowSamlLink(boolean showSamlLink) { + public SamlIdentityProviderDefinition setShowSamlLink(boolean showSamlLink) { this.showSamlLink = showSamlLink; + return this; } public String getSocketFactoryClassName() { @@ -173,8 +181,7 @@ public String getSocketFactoryClassName() { } } - public void setSocketFactoryClassName(String socketFactoryClassName) { - this.socketFactoryClassName = socketFactoryClassName; + public SamlIdentityProviderDefinition setSocketFactoryClassName(String socketFactoryClassName) { if (socketFactoryClassName!=null && socketFactoryClassName.trim().length()>0) { try { Class.forName( @@ -188,38 +195,44 @@ public void setSocketFactoryClassName(String socketFactoryClassName) { throw new IllegalArgumentException(e); } } + this.socketFactoryClassName = socketFactoryClassName; + return this; } public String getLinkText() { return StringUtils.hasText(linkText) ? linkText : idpEntityAlias; } - public void setLinkText(String linkText) { + public SamlIdentityProviderDefinition setLinkText(String linkText) { this.linkText = linkText; + return this; } public String getIconUrl() { return iconUrl; } - public void setIconUrl(String iconUrl) { + public SamlIdentityProviderDefinition setIconUrl(String iconUrl) { this.iconUrl = iconUrl; + return this; } public String getZoneId() { return zoneId; } - public void setZoneId(String zoneId) { + public SamlIdentityProviderDefinition setZoneId(String zoneId) { this.zoneId = zoneId; + return this; } public boolean isAddShadowUserOnLogin() { return addShadowUserOnLogin; } - public void setAddShadowUserOnLogin(boolean addShadowUserOnLogin) { + public SamlIdentityProviderDefinition setAddShadowUserOnLogin(boolean addShadowUserOnLogin) { this.addShadowUserOnLogin = addShadowUserOnLogin; + return this; } @Override @@ -260,111 +273,4 @@ public String toString() { '}'; } - public static class Builder { - - private String metaDataLocation; - private String idpEntityAlias; - private String zoneId; - private String nameID; - private int assertionConsumerIndex; - private boolean metadataTrustCheck; - private boolean showSamlLink; - private String linkText; - private String iconUrl; - private boolean addShadowUserOnLogin = true; - private List emailDomain; - private List externalGroupsWhitelist; - private Map attributeMappings; - - private Builder(){} - - public static Builder get() { - return new Builder(); - } - - public SamlIdentityProviderDefinition build() { - SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition(); - - def.setMetaDataLocation(metaDataLocation); - def.setIdpEntityAlias(idpEntityAlias); - def.setZoneId(zoneId); - def.setNameID(nameID); - def.setAssertionConsumerIndex(assertionConsumerIndex); - def.setMetadataTrustCheck(metadataTrustCheck); - def.setShowSamlLink(showSamlLink); - def.setLinkText(linkText); - def.setIconUrl(iconUrl); - def.setAddShadowUserOnLogin(addShadowUserOnLogin); - def.setEmailDomain(emailDomain); - def.setExternalGroupsWhitelist(externalGroupsWhitelist); - def.setAttributeMappings(attributeMappings); - - return def; - } - - public Builder setAttributeMappings(Map attributeMappings) { - this.attributeMappings = attributeMappings; - return this; - } - - public Builder setMetaDataLocation(String metaDataLocation) { - this.metaDataLocation = metaDataLocation; - return this; - } - - public Builder setIdpEntityAlias(String idpEntityAlias) { - this.idpEntityAlias = idpEntityAlias; - return this; - } - - public Builder setZoneId(String zoneId) { - this.zoneId = zoneId; - return this; - } - - public Builder setNameID(String nameID) { - this.nameID = nameID; - return this; - } - - public Builder setAssertionConsumerIndex(int assertionConsumerIndex) { - this.assertionConsumerIndex = assertionConsumerIndex; - return this; - } - - public Builder setMetadataTrustCheck(boolean metadataTrustCheck) { - this.metadataTrustCheck = metadataTrustCheck; - return this; - } - - public Builder setShowSamlLink(boolean showSamlLink) { - this.showSamlLink = showSamlLink; - return this; - } - - public Builder setLinkText(String linkText) { - this.linkText = linkText; - return this; - } - - public Builder setIconUrl(String iconUrl) { - this.iconUrl = iconUrl; - return this; - } - - public Builder setAddShadowUserOnLogin(boolean addShadowUserOnLogin) { - this.addShadowUserOnLogin = addShadowUserOnLogin; - return this; - } - - public Builder setEmailDomain(List emailDomain) { - this.emailDomain = emailDomain; - return this; - } - - public Builder setExternalGroupsWhitelist(List externalGroupsWhitelist) { - this.externalGroupsWhitelist = externalGroupsWhitelist; - return this; - } - } -} + } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/UaaIdentityProviderDefinition.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/UaaIdentityProviderDefinition.java index 6d6908c3afe..f9621bdad24 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/UaaIdentityProviderDefinition.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/UaaIdentityProviderDefinition.java @@ -1,21 +1,17 @@ /******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. - * - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - * - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.provider; -import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.provider.LockoutPolicy; -import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) @@ -54,11 +50,11 @@ public void setLockoutPolicy(LockoutPolicy lockoutPolicy) { this.lockoutPolicy = lockoutPolicy; } - public boolean isDisableInternalUserManagement() { - return disableInternalUserManagement; - } + public boolean isDisableInternalUserManagement() { + return disableInternalUserManagement; + } - public void setDisableInternalUserManagement(boolean disableInternalUserManagement) { - this.disableInternalUserManagement = disableInternalUserManagement; - } + public void setDisableInternalUserManagement(boolean disableInternalUserManagement) { + this.disableInternalUserManagement = disableInternalUserManagement; + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderConfigurator.java index a62ccdd6ac2..a280f339c53 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderConfigurator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderConfigurator.java @@ -26,7 +26,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.security.saml.metadata.ExtendedMetadata; import org.springframework.security.saml.metadata.ExtendedMetadataDelegate; -import org.springframework.util.StringUtils; import java.net.URI; import java.net.URISyntaxException; @@ -44,8 +43,10 @@ import java.util.TimerTask; import static org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; +import static org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition.PROVIDER_DESCRIPTION; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; +import static org.springframework.util.StringUtils.hasText; public class SamlIdentityProviderConfigurator implements InitializingBean { private static Log logger = LogFactory.getLog(SamlIdentityProviderConfigurator.class); @@ -154,10 +155,10 @@ public synchronized ExtendedMetadataDelegate[] addSamlIdentityProviderDefinition if (providerDefinition==null) { throw new NullPointerException(); } - if (!StringUtils.hasText(providerDefinition.getIdpEntityAlias())) { + if (!hasText(providerDefinition.getIdpEntityAlias())) { throw new NullPointerException("SAML IDP Alias must be set"); } - if (!StringUtils.hasText(providerDefinition.getZoneId())) { + if (!hasText(providerDefinition.getZoneId())) { throw new NullPointerException("IDP Zone Id must be set"); } for (SamlIdentityProviderDefinition def : getIdentityProviderDefinitions()) { @@ -305,11 +306,15 @@ public void setIdentityProviders(Map> providers) { String linkText = (String)((Map)entry.getValue()).get("linkText"); String iconUrl = (String)((Map)entry.getValue()).get("iconUrl"); String zoneId = (String)((Map)entry.getValue()).get("zoneId"); + String providerDescription = (String)((Map)entry.getValue()).get(PROVIDER_DESCRIPTION); Boolean addShadowUserOnLogin = (Boolean)((Map)entry.getValue()).get("addShadowUserOnLogin"); List emailDomain = (List) saml.get(EMAIL_DOMAIN_ATTR); List externalGroupsWhitelist = (List) saml.get(EXTERNAL_GROUPS_WHITELIST); Map attributeMappings = (Map) saml.get(ATTRIBUTE_MAPPINGS); SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition(); + if (hasText(providerDescription)) { + def.setProviderDescription(providerDescription); + } if (alias==null) { throw new IllegalArgumentException("Invalid IDP - alias must not be null ["+metaDataLocation+"]"); } @@ -328,7 +333,7 @@ public void setIdentityProviders(Map> providers) { def.setEmailDomain(emailDomain); def.setExternalGroupsWhitelist(externalGroupsWhitelist); def.setAttributeMappings(attributeMappings); - def.setZoneId(StringUtils.hasText(zoneId) ? zoneId : IdentityZone.getUaa().getId()); + def.setZoneId(hasText(zoneId) ? zoneId : IdentityZone.getUaa().getId()); def.setAddShadowUserOnLogin(addShadowUserOnLogin==null?true:addShadowUserOnLogin); toBeFetchedProviders.add(def); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/LdapUtils.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/LdapUtils.java index d6b53566e98..85a38b21b46 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/LdapUtils.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/LdapUtils.java @@ -25,6 +25,9 @@ import java.util.List; import java.util.Map; +import static org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition.PROVIDER_DESCRIPTION; +import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.LDAP_PREFIX; + public final class LdapUtils { private LdapUtils() {} @@ -83,6 +86,8 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldap return definition; } + + if (ldapConfig.get(LdapIdentityProviderDefinition.LDAP_EMAIL_DOMAIN)!=null) { definition.setEmailDomain((List) ldapConfig.get(LdapIdentityProviderDefinition.LDAP_EMAIL_DOMAIN)); } @@ -155,6 +160,11 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldap definition.addAttributeMapping(entry.getKey().substring(LDAP_ATTR_MAP_PREFIX.length()), entry.getValue()); } } + + if (ldapConfig.get(LDAP_PREFIX+PROVIDER_DESCRIPTION)!=null && ldapConfig.get(LDAP_PREFIX+PROVIDER_DESCRIPTION) instanceof String) { + definition.setProviderDescription((String)ldapConfig.get(LDAP_PREFIX+PROVIDER_DESCRIPTION)); + } + return definition; } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java index c5134e9625e..c3795782b7e 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java @@ -42,6 +42,7 @@ import java.util.Map; import static org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; +import static org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition.PROVIDER_DESCRIPTION; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.KEYSTONE; @@ -85,6 +86,8 @@ public void testLdapBootstrap() throws Exception { IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, new MockEnvironment()); HashMap ldapConfig = new HashMap<>(); ldapConfig.put(EMAIL_DOMAIN_ATTR, Arrays.asList("test.domain")); + final String idpDescription = "Test LDAP Provider Description"; + ldapConfig.put(PROVIDER_DESCRIPTION, idpDescription); List attrMap = new ArrayList<>(); attrMap.add("value"); ldapConfig.put(EXTERNAL_GROUPS_WHITELIST, attrMap); @@ -104,6 +107,7 @@ public void testLdapBootstrap() throws Exception { assertEquals("test.domain", ldapProvider.getConfig().getEmailDomain().get(0)); assertEquals(Arrays.asList("value"), ldapProvider.getConfig().getExternalGroupsWhitelist()); assertEquals("first_name", ldapProvider.getConfig().getAttributeMappings().get("given_name")); + assertEquals(idpDescription, ldapProvider.getConfig().getProviderDescription()); } @Test diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java index 3787f2c648b..1d9f8994a08 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java @@ -159,14 +159,13 @@ public void acceptInvitePage_for_unverifiedSamlUser() throws Exception { when(expiringCodeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); when(expiringCodeStore.generateCode(anyString(), anyObject(), eq(null))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData), null)); IdentityProvider provider = new IdentityProvider(); - SamlIdentityProviderDefinition definition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition definition = new SamlIdentityProviderDefinition() .setMetaDataLocation("http://test.saml.com") .setIdpEntityAlias("test-saml") .setNameID("test") .setLinkText("testsaml") .setIconUrl("test.com") - .setZoneId(IdentityZone.getUaa().getId()) - .build(); + .setZoneId(IdentityZone.getUaa().getId()); provider.setConfig(definition); provider.setType(OriginKeys.SAML); when(providerProvisioning.retrieveByOrigin(eq("test-saml"), anyString())).thenReturn(provider); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java index d26ac88e8a7..2aab0fe3e3c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java @@ -6,12 +6,10 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.codestore.InMemoryExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.login.LoginInfoEndpoint; -import org.cloudfoundry.identity.uaa.login.Prompt; -import org.cloudfoundry.identity.uaa.provider.saml.LoginSamlAuthenticationToken; -import org.cloudfoundry.identity.uaa.provider.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.saml.LoginSamlAuthenticationToken; +import org.cloudfoundry.identity.uaa.provider.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; @@ -475,15 +473,14 @@ private List getIdps() { } private SamlIdentityProviderDefinition createIdentityProviderDefinition(String idpEntityAlias, String zoneId) { - SamlIdentityProviderDefinition idp1 = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition idp1 = new SamlIdentityProviderDefinition() .setMetaDataLocation("metadataLocation for " + idpEntityAlias) .setIdpEntityAlias(idpEntityAlias) .setNameID("nameID for " + idpEntityAlias) .setMetadataTrustCheck(true) .setLinkText("link text for " + idpEntityAlias) .setIconUrl("icon url for " + idpEntityAlias) - .setZoneId(zoneId) - .build(); + .setZoneId(zoneId); idp1.setIdpEntityAlias(idpEntityAlias); idp1.setShowSamlLink(true); idp1.setZoneId(zoneId); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/IdentityProviderConfiguratorTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/IdentityProviderConfiguratorTests.java index d2613c7e05c..5c5d0481e57 100755 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/IdentityProviderConfiguratorTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/IdentityProviderConfiguratorTests.java @@ -181,7 +181,7 @@ public static void initializeOpenSAML() throws Exception { public void setUp() throws Exception { conf = new SamlIdentityProviderConfigurator(); conf.setParserPool(new BasicParserPool()); - singleAdd = SamlIdentityProviderDefinition.Builder.get() + singleAdd = new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(xmlWithoutID, new RandomValueStringGenerator().generate())) .setIdpEntityAlias(singleAddAlias) .setNameID("sample-nameID") @@ -189,9 +189,8 @@ public void setUp() throws Exception { .setMetadataTrustCheck(true) .setLinkText("sample-link-test") .setIconUrl("sample-icon-url") - .setZoneId("uaa") - .build(); - singleAddWithoutHeader = SamlIdentityProviderDefinition.Builder.get() + .setZoneId("uaa"); + singleAddWithoutHeader = new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(xmlWithoutHeader, new RandomValueStringGenerator().generate())) .setIdpEntityAlias(singleAddAlias) .setNameID("sample-nameID") @@ -199,8 +198,7 @@ public void setUp() throws Exception { .setMetadataTrustCheck(true) .setLinkText("sample-link-test") .setIconUrl("sample-icon-url") - .setZoneId("uaa") - .build(); + .setZoneId("uaa"); } private static Map> parseYaml(String sampleYaml) { @@ -303,7 +301,7 @@ public void testGetIdentityProviderDefinitionsForZone() throws Exception { String zoneId = UUID.randomUUID().toString(); IdentityZone zone = MultitenancyFixture.identityZone(zoneId, "test-zone"); - SamlIdentityProviderDefinition samlIdentityProviderDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition samlIdentityProviderDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(xml) .setIdpEntityAlias("zoneIdpAlias") .setNameID("sample-nameID") @@ -311,8 +309,7 @@ public void testGetIdentityProviderDefinitionsForZone() throws Exception { .setMetadataTrustCheck(true) .setLinkText("sample-link-test") .setIconUrl("sample-icon-url") - .setZoneId(zoneId) - .build(); + .setZoneId(zoneId); conf.addSamlIdentityProviderDefinition(samlIdentityProviderDefinition); List idps = conf.getIdentityProviderDefinitionsForZone(zone); @@ -338,7 +335,7 @@ public void testGetIdentityProviderDefinititonsForAllowedProviders() throws Exce public void testReturnAllIdpsInZoneForClientWithNoAllowedProviders() throws Exception { conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); - SamlIdentityProviderDefinition samlIdentityProviderDefinitionInOtherZone = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition samlIdentityProviderDefinitionInOtherZone = new SamlIdentityProviderDefinition() .setMetaDataLocation(xml) .setIdpEntityAlias("zoneIdpAlias") .setNameID("sample-nameID") @@ -346,8 +343,7 @@ public void testReturnAllIdpsInZoneForClientWithNoAllowedProviders() throws Exce .setMetadataTrustCheck(true) .setLinkText("sample-link-test") .setIconUrl("sample-icon-url") - .setZoneId("other-zone-id") - .build(); + .setZoneId("other-zone-id"); try { conf.addSamlIdentityProviderDefinition(samlIdentityProviderDefinitionInOtherZone); } catch (MetadataProviderException e) { @@ -362,7 +358,7 @@ public void testReturnNoIdpsInZoneForClientWithNoAllowedProviders() throws Excep conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); String xmlMetadata = String.format(xmlWithoutID, new RandomValueStringGenerator().generate()); - SamlIdentityProviderDefinition samlIdentityProviderDefinitionInOtherZone = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition samlIdentityProviderDefinitionInOtherZone = new SamlIdentityProviderDefinition() .setMetaDataLocation(xmlMetadata) .setIdpEntityAlias("zoneIdpAlias") .setNameID("sample-nameID") @@ -370,8 +366,7 @@ public void testReturnNoIdpsInZoneForClientWithNoAllowedProviders() throws Excep .setMetadataTrustCheck(true) .setLinkText("sample-link-test") .setIconUrl("sample-icon-url") - .setZoneId("other-zone-id") - .build(); + .setZoneId("other-zone-id"); conf.addSamlIdentityProviderDefinition(samlIdentityProviderDefinitionInOtherZone); List clientIdps = conf.getIdentityProviderDefinitions(null, IdentityZoneHolder.get()); @@ -487,13 +482,12 @@ public void testDuplicate_EntityID_IsRejected() throws Exception { conf.afterPropertiesSet(); testGetIdentityProviderDefinitions(3, false); - SamlIdentityProviderDefinition def = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition() .setMetaDataLocation("http://simplesamlphp.identity.cf-app.com/saml2/idp/metadata.php") .setIdpEntityAlias("simplesamlphp-url-2") .setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") .setZoneId(IdentityZone.getUaa().getId()) - .setShowSamlLink(true) - .build(); + .setShowSamlLink(true); //duplicate entityID - different alias ExtendedMetadataDelegate[] delegate = null; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java index b61e5f70d68..dc6cd59f873 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java @@ -20,7 +20,7 @@ public class SamlIdentityProviderDefinitionTests { @Before public void createDefinition() { - definition = SamlIdentityProviderDefinition.Builder.get() + definition = new SamlIdentityProviderDefinition() .setMetaDataLocation("location") .setIdpEntityAlias("alias") .setNameID("nameID") @@ -28,8 +28,7 @@ public void createDefinition() { .setShowSamlLink(false) .setLinkText("link test") .setIconUrl("url") - .setZoneId("zoneId") - .build(); + .setZoneId("zoneId"); } @Test diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlRedirectUtilsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlRedirectUtilsTest.java index b3f44533b76..0482c14bb6c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlRedirectUtilsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlRedirectUtilsTest.java @@ -24,14 +24,13 @@ public class SamlRedirectUtilsTest { @Test public void testGetIdpRedirectUrl() throws Exception { SamlIdentityProviderDefinition definition = - SamlIdentityProviderDefinition.Builder.get() + new SamlIdentityProviderDefinition() .setMetaDataLocation("http://some.meta.data") .setIdpEntityAlias("simplesamlphp-url") .setNameID("nameID") .setMetadataTrustCheck(true) .setLinkText("link text") - .setZoneId(IdentityZone.getUaa().getId()) - .build(); + .setZoneId(IdentityZone.getUaa().getId()); String url = SamlRedirectUtils.getIdpRedirectUrl(definition, "login.identity.cf-app.com"); Assert.assertEquals("saml/discovery?returnIDParam=idp&entityID=login.identity.cf-app.com&idp=simplesamlphp-url&isPassive=true", url); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java index b1042a09708..aa917101f4d 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java @@ -91,24 +91,22 @@ public void setUp() throws Exception { client = new BaseClientDetails("clientid","", "", "","",""); uaaDef = new UaaIdentityProviderDefinition(null, null); ldapDef = new LdapIdentityProviderDefinition(); - samlDef1 = SamlIdentityProviderDefinition.Builder.get() + samlDef1 = new SamlIdentityProviderDefinition() .setMetaDataLocation(idpMetaData) .setIdpEntityAlias("") .setNameID("") .setMetadataTrustCheck(true) .setLinkText("") .setIconUrl("") - .setZoneId(IdentityZone.getUaa().getId()) - .build(); - samlDef2 = SamlIdentityProviderDefinition.Builder.get() + .setZoneId(IdentityZone.getUaa().getId()); + samlDef2 = new SamlIdentityProviderDefinition() .setMetaDataLocation(idpMetaData) .setIdpEntityAlias("") .setNameID("") .setMetadataTrustCheck(true) .setLinkText("") .setIconUrl("") - .setZoneId(IdentityZone.getUaa().getId()) - .build(); + .setZoneId(IdentityZone.getUaa().getId()); configureTestData(); } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java index 5ebf86ceece..5a18469fd87 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java @@ -2,12 +2,13 @@ import org.apache.commons.lang.RandomStringUtils; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; -import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdpAlreadyExistsException; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.After; @@ -21,11 +22,11 @@ import java.util.Map; import java.util.UUID; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; public class JdbcIdentityProviderProvisioningTests extends JdbcTestBase { @@ -76,7 +77,7 @@ public void test_cannot_delete_uaa_providers() { //action try to delete uaa provider //should not do anything assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=?", new Object[] {IdentityZoneHolder.get().getId()}, Integer.class), is(4)); - IdentityProvider uaa = db.retrieveByOrigin(OriginKeys.UAA, IdentityZoneHolder.get().getId()); + IdentityProvider uaa = db.retrieveByOrigin(UAA, IdentityZoneHolder.get().getId()); db.onApplicationEvent(new EntityDeletedEvent<>(uaa)); assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=?", new Object[] {IdentityZoneHolder.get().getId()}, Integer.class), is(4)); } @@ -85,8 +86,11 @@ public void test_cannot_delete_uaa_providers() { public void testCreateAndUpdateIdentityProviderInDefaultZone() throws Exception { String zoneId = IdentityZone.getUaa().getId(); String originKey = RandomStringUtils.randomAlphabetic(6); - IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zoneId); - + IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zoneId); + String providerDescription = "Test Description"; + idp.setConfig(new UaaIdentityProviderDefinition(null,null)); + idp.getConfig().setProviderDescription(providerDescription); + idp.setType(UAA); IdentityProvider createdIdp = db.create(idp); Map rawCreatedIdp = jdbcTemplate.queryForMap("select * from identity_provider where id = ?",createdIdp.getId()); @@ -94,26 +98,27 @@ public void testCreateAndUpdateIdentityProviderInDefaultZone() throws Exception assertEquals(idp.getOriginKey(), createdIdp.getOriginKey()); assertEquals(idp.getType(), createdIdp.getType()); assertEquals(idp.getConfig(), createdIdp.getConfig()); + assertEquals(providerDescription, createdIdp.getConfig().getProviderDescription()); assertEquals(idp.getName(), rawCreatedIdp.get("name")); assertEquals(idp.getOriginKey(), rawCreatedIdp.get("origin_key")); assertEquals(idp.getType(), rawCreatedIdp.get("type")); - assertEquals(idp.getConfig(), JsonUtils.readValue((String)rawCreatedIdp.get("config"), AbstractIdentityProviderDefinition.class)); + assertEquals(idp.getConfig(), JsonUtils.readValue((String)rawCreatedIdp.get("config"), UaaIdentityProviderDefinition.class)); assertEquals(zoneId, rawCreatedIdp.get("identity_zone_id").toString().trim()); idp.setId(createdIdp.getId()); idp.setLastModified(new Timestamp(System.currentTimeMillis())); idp.setName("updated name"); idp.setCreated(createdIdp.getCreated()); - idp.setConfig(new AbstractIdentityProviderDefinition()); + idp.setConfig(new UaaIdentityProviderDefinition()); idp.setOriginKey("new origin key"); - idp.setType("new type"); + idp.setType(UAA); idp.setIdentityZoneId("somerandomID"); createdIdp = db.update(idp); assertEquals(idp.getName(), createdIdp.getName()); assertEquals(rawCreatedIdp.get("origin_key"), createdIdp.getOriginKey()); - assertEquals(OriginKeys.UNKNOWN, createdIdp.getType()); //we don't allow other types anymore + assertEquals(UAA, createdIdp.getType()); //we don't allow other types anymore assertEquals(idp.getConfig(), createdIdp.getConfig()); assertEquals(idp.getLastModified().getTime()/1000, createdIdp.getLastModified().getTime()/1000); assertEquals(Integer.valueOf(rawCreatedIdp.get("version").toString())+1, createdIdp.getVersion()); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java index 868a8d5782d..42dd930d43b 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java @@ -166,6 +166,10 @@ protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throw public RequestDispatcher getNamedDispatcher(String path) { return new MockRequestDispatcher("/"); } + + public String getVirtualServerName() { + return null; + } }; context.setServletContext(servletContext); MockServletConfig servletConfig = new MockServletConfig(servletContext); 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 44a19d03326..f79fc066953 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 @@ -25,7 +25,9 @@ import org.cloudfoundry.identity.uaa.oauth.UaaTokenServices; import org.cloudfoundry.identity.uaa.oauth.UaaTokenStore; import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.LockoutPolicy; import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; @@ -457,7 +459,14 @@ public void testBootstrappedIdps_and_ExcludedClaims_and_CorsConfig() throws Exce assertNotNull(providerProvisioning.retrieveByOrigin(def.getIdpEntityAlias(), IdentityZone.getUaa().getId())); } - assertNotNull(providerProvisioning.retrieveByOrigin(OriginKeys.LDAP, IdentityZone.getUaa().getId())); + IdentityProvider ldapProvider = + providerProvisioning.retrieveByOrigin(OriginKeys.LDAP, IdentityZone.getUaa().getId()); + assertNotNull(ldapProvider); + assertEquals("Test LDAP Provider Description", ldapProvider.getConfig().getProviderDescription()); + + IdentityProvider samlProvider = + providerProvisioning.retrieveByOrigin("okta-local", IdentityZone.getUaa().getId()); + assertEquals("Test Okta Preview 1 Description", samlProvider.getConfig().getProviderDescription()); CorsFilter filter = context.getBean(CorsFilter.class); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java index 3b25cd1d84a..1007fe05516 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java @@ -319,13 +319,12 @@ protected IdentityProvider createIdentityProvider(IdentityZoneCreationResult zon } protected SamlIdentityProviderDefinition getSamlIdentityProviderDefinition(IdentityZoneCreationResult zone, String entityID) { - return SamlIdentityProviderDefinition.Builder.get() + return new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(utils.IDP_META_DATA, entityID)) .setIdpEntityAlias(entityID) .setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") .setLinkText("Test Saml Provider") - .setZoneId(zone.getIdentityZone().getId()) - .build(); + .setZoneId(zone.getIdentityZone().getId()); } public URL inviteUser(String email, String userInviteToken, String subdomain, String clientId, String expectedOrigin) throws Exception { 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 8e93205128a..03de6aacc27 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 @@ -616,13 +616,12 @@ public void testSamlLoginLinksShowActiveProviders() throws Exception { String zoneAdminToken = identityZoneCreationResult.getZoneAdminToken(); String metadata = String.format(MockMvcUtils.IDP_META_DATA, new RandomValueStringGenerator().generate()); - SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(metadata) .setIdpEntityAlias(activeAlias) .setLinkText("Active SAML Provider") .setShowSamlLink(true) - .setZoneId(identityZone.getId()) - .build(); + .setZoneId(identityZone.getId()); IdentityProvider activeIdentityProvider = new IdentityProvider(); activeIdentityProvider.setType(OriginKeys.SAML); activeIdentityProvider.setName("Active SAML Provider"); @@ -632,12 +631,11 @@ public void testSamlLoginLinksShowActiveProviders() throws Exception { mockMvcUtils.createIdpUsingWebRequest(getMockMvc(), identityZone.getId(), zoneAdminToken, activeIdentityProvider, status().isCreated()); metadata = String.format(MockMvcUtils.IDP_META_DATA, new RandomValueStringGenerator().generate()); - SamlIdentityProviderDefinition inactiveSamlIdentityProviderDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition inactiveSamlIdentityProviderDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(metadata) .setIdpEntityAlias(inactiveAlias) .setLinkText("You should not see me") - .setZoneId(identityZone.getId()) - .build(); + .setZoneId(identityZone.getId()); IdentityProvider inactiveIdentityProvider = new IdentityProvider(); inactiveIdentityProvider.setType(OriginKeys.SAML); inactiveIdentityProvider.setName("Inactive SAML Provider"); @@ -664,12 +662,11 @@ public void testSamlRedirectWhenTheOnlyProvider() throws Exception { String zoneAdminToken = identityZoneCreationResult.getZoneAdminToken(); String metadata = String.format(MockMvcUtils.IDP_META_DATA, new RandomValueStringGenerator().generate()); - SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(metadata) .setIdpEntityAlias(alias) .setLinkText("Active SAML Provider") - .setZoneId(identityZone.getId()) - .build(); + .setZoneId(identityZone.getId()); IdentityProvider activeIdentityProvider = new IdentityProvider(); activeIdentityProvider.setType(OriginKeys.SAML); activeIdentityProvider.setName("Active SAML Provider"); @@ -723,12 +720,11 @@ public void testNoCreateAccountLinksWhenUAAisNotAllowedProvider() throws Excepti IdentityZone identityZone = identityZoneCreationResult.getIdentityZone(); String zoneAdminToken = identityZoneCreationResult.getZoneAdminToken(); - SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition3 = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition3 = new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(IdentityProviderConfiguratorTests.xmlWithoutID, "http://example3.com/saml/metadata")) .setIdpEntityAlias(alias3) .setLinkText("Active3 SAML Provider") - .setZoneId(identityZone.getId()) - .build(); + .setZoneId(identityZone.getId()); IdentityProvider activeIdentityProvider3 = new IdentityProvider(); activeIdentityProvider3.setType(OriginKeys.SAML); activeIdentityProvider3.setName("Active 3 SAML Provider"); @@ -737,12 +733,11 @@ public void testNoCreateAccountLinksWhenUAAisNotAllowedProvider() throws Excepti activeIdentityProvider3.setOriginKey(alias3); activeIdentityProvider3 = mockMvcUtils.createIdpUsingWebRequest(getMockMvc(), identityZone.getId(), zoneAdminToken, activeIdentityProvider3, status().isCreated()); - SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition2 = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition activeSamlIdentityProviderDefinition2 = new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(IdentityProviderConfiguratorTests.xmlWithoutID, "http://example2.com/saml/metadata")) .setIdpEntityAlias(alias2) .setLinkText("Active2 SAML Provider") - .setZoneId(identityZone.getId()) - .build(); + .setZoneId(identityZone.getId()); IdentityProvider activeIdentityProvider2 = new IdentityProvider(); activeIdentityProvider2.setType(OriginKeys.SAML); activeIdentityProvider2.setName("Active 2 SAML Provider"); @@ -798,13 +793,12 @@ public void testDeactivatedProviderIsRemovedFromSamlLoginLinks() throws Exceptio String zoneAdminToken = identityZoneCreationResult.getZoneAdminToken(); String metadata = String.format(MockMvcUtils.IDP_META_DATA, new RandomValueStringGenerator().generate()); - SamlIdentityProviderDefinition samlIdentityProviderDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition samlIdentityProviderDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(metadata) .setIdpEntityAlias(alias) .setLinkText("SAML Provider") .setShowSamlLink(true) - .setZoneId(identityZone.getId()) - .build(); + .setZoneId(identityZone.getId()); IdentityProvider identityProvider = new IdentityProvider(); identityProvider.setType(OriginKeys.SAML); identityProvider.setName("SAML Provider"); 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 47ce3b7ada4..d307b45dbdb 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 @@ -16,21 +16,21 @@ import org.apache.commons.lang.RandomStringUtils; import org.cloudfoundry.identity.uaa.audit.AuditEventType; import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; -import org.cloudfoundry.identity.uaa.provider.saml.IdentityProviderConfiguratorTests; -import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; +import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.saml.IdentityProviderConfiguratorTests; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestApplicationEventListener; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.cloudfoundry.identity.uaa.provider.IdentityProvider; -import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; -import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.zone.event.IdentityProviderModifiedEvent; import org.junit.After; import org.junit.Before; @@ -57,7 +57,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class IdentityProviderEndpointsMockMvcTests extends InjectedMockContextTest { @@ -110,10 +109,9 @@ public void test_Create_and_Delete_SamlProvider() throws Exception { provider.setIdentityZoneId(IdentityZone.getUaa().getId()); provider.setType(OriginKeys.SAML); provider.setOriginKey(origin); - SamlIdentityProviderDefinition samlDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(metadata) - .setLinkText("Test SAML Provider") - .build(); + .setLinkText("Test SAML Provider"); samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); List externalGroupsWhitelist = new ArrayList<>(); externalGroupsWhitelist.add("value"); @@ -317,13 +315,12 @@ public void test_Create_Duplicate_Saml_Identity_Provider_In_Other_Zone() throws IdentityProvider identityProvider = MultitenancyFixture.identityProvider(origin1, zone.getId()); identityProvider.setType(OriginKeys.SAML); - SamlIdentityProviderDefinition providerDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition providerDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(IdentityProviderConfiguratorTests.xmlWithoutID, "http://www.okta.com/" + identityProvider.getOriginKey())) .setIdpEntityAlias(identityProvider.getOriginKey()) .setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") .setLinkText("IDPEndpointsMockTests Saml Provider:" + identityProvider.getOriginKey()) - .setZoneId(zone.getId()) - .build(); + .setZoneId(zone.getId()); identityProvider.setConfig(providerDefinition); IdentityProvider createdIDP = createIdentityProvider(zone.getId(), identityProvider, userAccessToken, status().isCreated()); @@ -335,13 +332,12 @@ public void test_Create_Duplicate_Saml_Identity_Provider_In_Other_Zone() throws assertEquals(identityProvider.getConfig().getZoneId(), createdIDP.getConfig().getZoneId()); identityProvider.setOriginKey(origin2); - providerDefinition = SamlIdentityProviderDefinition.Builder.get() + providerDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(providerDefinition.getMetaDataLocation()) .setIdpEntityAlias(identityProvider.getOriginKey()) .setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") .setLinkText("IDPEndpointsMockTests Saml Provider:" + identityProvider.getOriginKey()) - .setZoneId(zone.getId()) - .build(); + .setZoneId(zone.getId()); identityProvider.setConfig(providerDefinition); createIdentityProvider(zone.getId(), identityProvider, userAccessToken, status().isConflict()); @@ -360,13 +356,12 @@ public void test_Create_Duplicate_Saml_Identity_Provider_In_Default_Zone() throw IdentityProvider identityProvider = MultitenancyFixture.identityProvider(origin1, IdentityZone.getUaa().getId()); identityProvider.setType(OriginKeys.SAML); - SamlIdentityProviderDefinition providerDefinition = SamlIdentityProviderDefinition.Builder.get() + SamlIdentityProviderDefinition providerDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(String.format(IdentityProviderConfiguratorTests.xmlWithoutID, "http://www.okta.com/" + identityProvider.getOriginKey())) .setIdpEntityAlias(identityProvider.getOriginKey()) .setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") .setLinkText("IDPEndpointsMockTests Saml Provider:" + identityProvider.getOriginKey()) - .setZoneId(IdentityZone.getUaa().getId()) - .build(); + .setZoneId(IdentityZone.getUaa().getId()); identityProvider.setConfig(providerDefinition); IdentityProvider createdIDP = createIdentityProvider(null, identityProvider, userAccessToken, status().isCreated()); @@ -376,13 +371,12 @@ public void test_Create_Duplicate_Saml_Identity_Provider_In_Default_Zone() throw assertEquals(identityProvider.getOriginKey(), createdIDP.getOriginKey()); identityProvider.setOriginKey(origin2); - providerDefinition = SamlIdentityProviderDefinition.Builder.get() + providerDefinition = new SamlIdentityProviderDefinition() .setMetaDataLocation(providerDefinition.getMetaDataLocation()) .setIdpEntityAlias(identityProvider.getOriginKey()) .setNameID("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") .setLinkText("IDPEndpointsMockTests Saml Provider:" + identityProvider.getOriginKey()) - .setZoneId(IdentityZone.getUaa().getId()) - .build(); + .setZoneId(IdentityZone.getUaa().getId()); identityProvider.setConfig(providerDefinition); createIdentityProvider(null, identityProvider, userAccessToken, status().isConflict()); diff --git a/uaa/src/test/resources/test/bootstrap/login.yml b/uaa/src/test/resources/test/bootstrap/login.yml index 0374c29820f..5fa0aabb5a8 100644 --- a/uaa/src/test/resources/test/bootstrap/login.yml +++ b/uaa/src/test/resources/test/bootstrap/login.yml @@ -36,6 +36,7 @@ login: showSamlLoginLink: true linkText: 'Okta Preview 1' iconUrl: 'http://link.to/icon.jpg' + providerDescription: 'Test Okta Preview 1 Description' okta-local-2: idpMetadata: | MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG diff --git a/uaa/src/test/resources/test/bootstrap/uaa.yml b/uaa/src/test/resources/test/bootstrap/uaa.yml index 303006ef50b..3edecab6906 100644 --- a/uaa/src/test/resources/test/bootstrap/uaa.yml +++ b/uaa/src/test/resources/test/bootstrap/uaa.yml @@ -7,6 +7,7 @@ ldap: password: 'password' searchBase: '' searchFilter: 'cn={0}' + providerDescription: 'Test LDAP Provider Description' jwt: token: claims: From 949745e92f5ff7dc3195d56b87e29739d92fde2a Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 14 Jan 2016 15:38:38 -0700 Subject: [PATCH 04/25] [skip ci] add documentation for IDP description field --- docs/UAA-APIs.rst | 3 +++ uaa/src/main/resources/login.yml | 1 + uaa/src/main/resources/uaa.yml | 1 + 3 files changed, 5 insertions(+) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index ebb957f3762..292ae5c50cb 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1229,6 +1229,7 @@ Fields *Available Fields* :: countFailuresWithin int Required Amount of time in seconds for which past login failures are counted, starting from the current time, 0+ disableInternalUserManagement boolean Optional When set to true, user management is disabled for this provider, defaults to false emailDomain List Optional List of email domains associated with the UAA provider. If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, wherein the email will be associated with the UAA provider. Wildcards supported. + providerDescription String Optional Human readable name/description of this provider SAML Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition ====================== ====================== ======== ================================================================================================================================================================================================================================================================================================================================================================================================================================================= @@ -1244,6 +1245,7 @@ Fields *Available Fields* :: emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. attributeMappings Map Optional List of UAA attributes mapped to attributes in the SAML assertion. Currently we support mapping given_name, family_name, email, phone_number and external_groups. Also supports custom user attributes to be populated in the id_token when the `user_attributes` scope is requested. The attributes are pulled out of the user records and have the format `user.attribute.: ` externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the `roles` scope is requested. + providerDescription String Optional Human readable name/description of this provider LDAP Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition ====================== ====================== ======== ================================================================================================================================================================================================= @@ -1266,6 +1268,7 @@ Fields *Available Fields* :: emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. attributeMappings Map Optional List of UAA attributes mapped to attributes from LDAP. Currently we support mapping given_name, family_name, email, phone_number and external_groups. externalGroupsWhitelist List Optional List of external groups (`DN` distinguished names`) that can be included in the ID Token if the `roles` scope is requested. See `UAA-LDAP.md UAA-LDAP.md`_ for more information + providerDescription String Optional Human readable name/description of this provider Curl Example POST (Creating a SAML provider):: diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index 348ac5fc6ad..bbdfd81fd03 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -160,6 +160,7 @@ login: # attributeMappings: # given_name: firstName # family_name: surname +# providerDescription: 'Human readable description of this provider' # okta-local-2: # idpMetadata: | # MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 79ac381083e..f12b351decd 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -99,6 +99,7 @@ # phone_number: telephonenumber # user.attribute.employeeCostCenter: costCenter # user.attribute.terribleBosses: uaaManager +# providerDescription: 'Human readable description of this provider' #ldap: From f459da773f9b40bd6ec38ba1fc27e4468a8fea13 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 14 Jan 2016 15:46:22 -0700 Subject: [PATCH 05/25] [skip ci] Fix documentation errors --- docs/UAA-APIs.rst | 91 +++++++++++------------------------------------ 1 file changed, 21 insertions(+), 70 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 292ae5c50cb..1c9319003a7 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -811,20 +811,20 @@ Fields *Available Fields* :: tokenPolicy TokenPolicy Optional Various fields pertaining to the JWT access and refresh tokens. See `Token Policy` section below for details. samlConfig SamlConfig Optional Various fields pertaining to SAML identity provider configuration. See ``SamlConfig`` section below for details. - Token Policy ``TokenPolicy`` (part of Identity Zone Configuration - See class org.cloudfoundry.identity.uaa.zone.TokenPolicy) - ===================== ==================== ======== ======================================================================================================================================================================== - accessTokenValidity int Optional How long the access token is valid for in seconds. - refreshTokenValidity int Optional How long the refresh token is valid for seconds. + Token Policy ``TokenPolicy`` (part of Identity Zone Configuration - See class org.cloudfoundry.identity.uaa.zone.TokenPolicy) + ===================== ==================== ======== ======================================================================================================================================================================== + accessTokenValidity int Optional How long the access token is valid for in seconds. + refreshTokenValidity int Optional How long the refresh token is valid for seconds. - SAML Identity Provider Configuration ``SamlConfig`` (part of Identity Zone Configuration - See class org.cloudfoundry.identity.uaa.zone.SamlConfig) - ===================== ==================== ======== ======================================================================================================================================================================== - requestSigned Boolean Optional Exposed SAML metadata property. If ``true``, the service provider will sign all outgoing authentication requests. Defaults to ``true``. - wantAssertionSigned Boolean Optional Exposed SAML metadata property. If ``true``, all assertions received by the SAML provider must be signed. Defaults to ``true``. - certificate String Optional Exposed SAML metadata property. The certificate used to sign all communications. Reserved for future use. - privateKey String Optional Exposed SAML metadata property. The SAML provider's private key. Reserved for future use. - privateKeyPassword String Optional Exposed SAML metadata property. The SAML provider's private key password. Reserved for future use. + SAML Identity Provider Configuration ``SamlConfig`` (part of Identity Zone Configuration - See class org.cloudfoundry.identity.uaa.zone.SamlConfig) + ===================== ==================== ======== ======================================================================================================================================================================== + requestSigned Boolean Optional Exposed SAML metadata property. If ``true``, the service provider will sign all outgoing authentication requests. Defaults to ``true``. + wantAssertionSigned Boolean Optional Exposed SAML metadata property. If ``true``, all assertions received by the SAML provider must be signed. Defaults to ``true``. + certificate String Optional Exposed SAML metadata property. The certificate used to sign all communications. Reserved for future use. + privateKey String Optional Exposed SAML metadata property. The SAML provider's private key. Reserved for future use. + privateKeyPassword String Optional Exposed SAML metadata property. The SAML provider's private key password. Reserved for future use. - ===================== ==================== ======== ======================================================================================================================================================================== + ===================== ==================== ======== ======================================================================================================================================================================== Curl Example POST (Token contains ``zones.write`` scope) :: @@ -1386,6 +1386,7 @@ Response body *example* (a provider contains the fields defined above) :: 400 - Bad Request - Invalid configuration - result contains stack trace 403 - Forbidden - insufficient scope 500 - Internal Server Error - error information will only be in server logs + ================ ========================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== @@ -1983,10 +1984,11 @@ __ http://www.simplecloud.info/specs/draft-scim-api-01.html#create-resource { "displayName":"uaa.admin", "members":[ - { "type":"USER","authorities":["READ"],"value":"3ebe4bda-74a2-40c4-8b70-f771d9bc8b9f","origin":"uaa" } - ] + { "type":"USER","authorities":["READ"],"value":"3ebe4bda-74a2-40c4-8b70-f771d9bc8b9f","origin":"uaa" } + ] } + The ``displayName`` is unique in the UAA, but is allowed to change. Each group also has a fixed primary key which is a UUID (stored in the ``id`` field of the core schema). The origin value shows what identity provider was responsible for making the connection between the user and the group. For example, if this relationship came from an LDAP user, it would have origin=ldap. @@ -2036,7 +2038,8 @@ See `SCIM - Modifying with PUT - + (optional) ``If-Match`` the ``ETag`` (version id) for the value to update + + (optional) ``If-Match`` the ``ETag`` (version id) for the value to update + * Request Body:: Host: example.com @@ -2282,10 +2285,10 @@ The API ``GET /Groups/External/list`` is deprecated "itemsPerPage":100, "totalResults":5, "schemas":["urn:scim:schemas:core:1.0"] - } + } - * Response Codes:: +* Response Codes:: 200 - Results retrieved successfully 401 - Unauthorized @@ -2462,6 +2465,7 @@ Response body *example* :: "n":"ANJufZdrvYg5zG61x36pDq59nVUN73wSanA7hVCtN3ftT2Rm1ZTQqp5KSCfLMhaaVvJY51sHj+/i4lqUaM9CO32G93fE44VfOmPfexZeAwa8YDOikyTrhP7sZ6A4WUNeC4DlNnJF4zsznU7JxjCkASwpdL6XFwbRSzGkm6b9aM4vIewyclWehJxUGVFhnYEzIQ65qnr38feVP9enOVgQzpKsCJ+xpa8vZ/UrscoG3/IOQM6VnLrGYAyyCGeyU1JXQW/KlNmtA5eJry2Tp+MD6I34/QsNkCArHOfj8H9tXz/oc3/tVkkR252L/Lmp0TtIGfHpBmoITP9h+oKiW6NpyCc=", "e":"AQAB" } + ================ ======================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================= The algorithm ("alg") tells the caller how to use the value (it is the @@ -2904,57 +2908,4 @@ Response Headers :: Management Endpoints ==================== -Basic Metrics: ``GET /varz`` ----------------------------- - -Authentication is via HTTP basic using credentials that are configured -via ``varz.username`` and ``varz.password``. The ``/varz`` endpoint pulls -data out of the JMX ``MBeanServer``, exposing selected nuggets directly -for ease of use, and providing links to more detailed metrics. - -* Request: ``GET /varz`` -* Response Body:: - { - "type": "UAA", - "links": { - "Users": "http://localhost:8080/uaa/varz/Users", - "JMImplementation": "http://localhost:8080/uaa/varz/JMImplementation", - "spring.application": "http://localhost:8080/uaa/varz/spring.application", - "com.sun.management": "http://localhost:8080/uaa/varz/com.sun.management", - "Catalina": "http://localhost:8080/uaa/varz/Catalina", - "env": "http://localhost:8080/uaa/varz/env", - "java.lang": "http://localhost:8080/uaa/varz/java.lang", - "java.util.logging": "http://localhost:8080/uaa/varz/java.util.logging" - }, - "mem": 19173496, - "memory": { - "verbose": false, - "non_heap_memory_usage": { - "max": 184549376, - "committed": 30834688, - "init": 19136512, - "used": 30577744 - }, - "object_pending_finalization_count": 0, - "heap_memory_usage": { - "max": 902299648, - "committed": 84475904, - "init": 63338496, - "used": 19173496 - } - }, - "token_store": { - "refresh_token_count": 0, - "access_token_count": 0, - "flush_interval": 1000 - }, - "audit_service": { - "user_authentication_count": 0, - "user_not_found_count": 0, - "principal_authentication_failure_count": 1, - "principal_not_found_count": 0, - "user_authentication_failure_count": 0 - }, - "spring.profiles.active": [] - } From 7b9de952786c12728fb24064080e465105d37c65 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 19 Jan 2016 11:13:52 -0700 Subject: [PATCH 06/25] Provide 409 (conflict) when trying to create an IDP that violates a unique key https://www.pivotaltracker.com/story/show/110114032 [#110114032] --- .../uaa/provider/IdentityProviderEndpoints.java | 15 ++++++++++++--- .../IdentityProviderEndpointsMockMvcTests.java | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java index 2031879846c..43ad213a76e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java @@ -47,6 +47,8 @@ import java.util.List; import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.EXPECTATION_FAILED; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.OK; @@ -97,8 +99,15 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden samlConfigurator.addSamlIdentityProviderDefinition(definition); body.setConfig(definition); } - IdentityProvider createdIdp = identityProviderProvisioning.create(body); - return new ResponseEntity<>(createdIdp, HttpStatus.CREATED); + try { + IdentityProvider createdIdp = identityProviderProvisioning.create(body); + return new ResponseEntity<>(createdIdp, CREATED); + } catch (IdpAlreadyExistsException e) { + return new ResponseEntity<>(body, CONFLICT); + } catch (Exception x) { + logger.debug("Unable to create IdentityProvider["+x.getMessage()+"].", x); + return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); + } } @RequestMapping(value = "{id}", method = DELETE) @@ -187,7 +196,7 @@ public ResponseEntity testIdentityProvider(@RequestBody IdentityProvider @ExceptionHandler(MetadataProviderException.class) public ResponseEntity handleMetadataProviderException(MetadataProviderException e) { if (e.getMessage().contains("Duplicate")) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT); + return new ResponseEntity<>(e.getMessage(), CONFLICT); } else { return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } 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 d307b45dbdb..ddca2d4b085 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 @@ -124,6 +124,7 @@ public void test_Create_and_Delete_SamlProvider() throws Exception { IdentityProvider created = createIdentityProvider(null, provider, accessToken, status().isCreated()); assertNotNull(created.getConfig()); + createIdentityProvider(null, created, accessToken, status().isConflict()); SamlIdentityProviderDefinition samlCreated = created.getConfig(); assertEquals(Arrays.asList("test.com", "test2.com"), samlCreated.getEmailDomain()); assertEquals(externalGroupsWhitelist, samlCreated.getExternalGroupsWhitelist()); From b9c2d7dd3e0c5c71726825e8dda94effed5f2df9 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Thu, 14 Jan 2016 15:02:14 -0800 Subject: [PATCH 07/25] Show scope description from database if present. [#111718508] https://www.pivotaltracker.com/story/show/111718508 Signed-off-by: Madhura Bhave --- .../identity/uaa/oauth/AccessController.java | 146 +++++++++--------- .../scim/bootstrap/ScimGroupBootstrap.java | 85 +++++++--- .../templates/web/access_confirmation.html | 6 +- uaa/src/main/resources/uaa.yml | 2 +- .../WEB-INF/jsp/access_confirmation.jsp | 108 ------------- uaa/src/main/webapp/WEB-INF/jsp/home.jsp | 54 ------- uaa/src/main/webapp/WEB-INF/jsp/login.jsp | 64 -------- .../webapp/WEB-INF/spring/oauth-endpoints.xml | 1 + .../webapp/WEB-INF/spring/scim-endpoints.xml | 1 + .../integration/feature/AppApprovalIT.java | 25 +++ 10 files changed, 164 insertions(+), 328 deletions(-) delete mode 100644 uaa/src/main/webapp/WEB-INF/jsp/access_confirmation.jsp delete mode 100644 uaa/src/main/webapp/WEB-INF/jsp/home.jsp delete mode 100644 uaa/src/main/webapp/WEB-INF/jsp/login.jsp diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AccessController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AccessController.java index 48ce977cc69..a6ecde4f490 100755 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AccessController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AccessController.java @@ -12,40 +12,42 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.oauth; -import java.security.Principal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.authentication.Origin; -import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.approval.Approval; import org.cloudfoundry.identity.uaa.approval.ApprovalStore; +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; +import org.cloudfoundry.identity.uaa.scim.ScimGroup; +import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.AuthorizationRequest; -import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.SessionStatus; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.WebRequest; +import javax.servlet.http.HttpServletRequest; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + /** * Controller for retrieving the model for and displaying the confirmation page * for access to a protected resource. @@ -66,6 +68,8 @@ public class AccessController { private ApprovalStore approvalStore = null; + private ScimGroupProvisioning groupProvisioning; + /** * Explicitly requests caller to point back to an authorization endpoint on * "https", even if the incoming request is @@ -87,6 +91,15 @@ public void setApprovalStore(ApprovalStore approvalStore) { this.approvalStore = approvalStore; } + public ScimGroupProvisioning getGroupProvisioning() { + return groupProvisioning; + } + + public AccessController setGroupProvisioning(ScimGroupProvisioning groupProvisioning) { + this.groupProvisioning = groupProvisioning; + return this; + } + @RequestMapping("/oauth/confirm_access") public String confirm(Map model, final HttpServletRequest request, Principal principal, SessionStatus sessionStatus) throws Exception { @@ -110,8 +123,6 @@ public String confirm(Map model, final HttpServletRequest reques BaseClientDetails modifiableClient = new BaseClientDetails(client); modifiableClient.setClientSecret(null); model.put("auth_request", clientAuthRequest); - model.put("client", modifiableClient); // TODO: remove this once it - // has gone from jsp pages model.put("redirect_uri", getRedirectUri(modifiableClient, clientAuthRequest)); Map additionalInfo = client.getAdditionalInformation(); @@ -166,29 +177,19 @@ else if (autoApproved instanceof Boolean && (Boolean) autoApproved || "true".equ } } - model.put("approved_scopes", getScopes(modifiableClient, approvedScopes)); - model.put("denied_scopes", getScopes(modifiableClient, deniedScopes)); - model.put("undecided_scopes", getScopes(modifiableClient, undecidedScopes)); + List> approvedScopeDetails = getScopes(approvedScopes); + model.put("approved_scopes", approvedScopeDetails); + List> undecidedScopeDetails = getScopes(undecidedScopes); + model.put("undecided_scopes", undecidedScopeDetails); + List> deniedScopeDetails = getScopes(deniedScopes); + model.put("denied_scopes", deniedScopeDetails); - // For backward compatibility with older login servers - List> combinedScopes = new ArrayList>(); - if (model.get("approved_scopes") != null) { - @SuppressWarnings("unchecked") - List> scopes = (List>) model.get("approved_scopes"); - combinedScopes.addAll(scopes); - } - if (model.get("denied_scopes") != null) { - @SuppressWarnings("unchecked") - List> scopes = (List>) model.get("denied_scopes"); - combinedScopes.addAll(scopes); - } - if (model.get("undecided_scopes") != null) { - @SuppressWarnings("unchecked") - List> scopes = (List>) model.get("undecided_scopes"); - combinedScopes.addAll(scopes); - } + List> allScopes = new ArrayList<>(); + allScopes.addAll(approvedScopeDetails); + allScopes.addAll(undecidedScopeDetails); + allScopes.addAll(deniedScopeDetails); - model.put("scopes", combinedScopes); + model.put("scopes", allScopes); model.put("message", "To confirm or deny access POST to the following locations with the parameters requested."); @@ -221,48 +222,48 @@ else if (autoApproved instanceof Boolean && (Boolean) autoApproved || "true".equ } - private List> getScopes(ClientDetails client, ArrayList scopes) { + private List> getScopes(ArrayList scopes) { List> result = new ArrayList>(); for (String scope : scopes) { - if (!scope.contains(".")) { - HashMap map = new HashMap(); - map.put("code", SCOPE_PREFIX + scope); - map.put("text", "Access your data with scope '" + scope + "'"); - result.add(map); - } - else { - HashMap map = new HashMap(); - String value = SCOPE_PREFIX + scope; - String resource = scope.substring(0, scope.lastIndexOf(".")); - if (OriginKeys.UAA.equals(resource)) { - // special case: don't need to prompt for internal uaa - // scopes - continue; + + + String[] tokens = scope.split("\\."); + String resource = tokens[0]; + + if(OriginKeys.UAA.equals(resource)) { continue; } + + HashMap map = new HashMap(); + String code = SCOPE_PREFIX + scope; + map.put("code", code); + + Optional group = groupProvisioning.query(String.format("displayName eq '%s'", scope)).stream().findFirst(); + group.ifPresent(g -> { + String description = g.getDescription(); + if (StringUtils.hasText(description)) { + map.put("text", description); } - String access = scope.substring(scope.lastIndexOf(".") + 1); - map.put("code", value); - map.put("text", "Access your '" + resource + "' resources with scope '" + access + "'"); - result.add(map); - } + }); + map.putIfAbsent("text", scope); + + result.add(map); } - Collections.sort(result, new Comparator>() { - @Override - public int compare(Map o1, Map o2) { - String code1 = o1.get("code"); - String code2 = o2.get("code"); - if (code1.startsWith(SCOPE_PREFIX + "password") || code1.startsWith(SCOPE_PREFIX + "openid")) { - code1 = "aaa" + code1; - } - if (code2.startsWith(SCOPE_PREFIX + "password") || code2.startsWith(SCOPE_PREFIX + "openid")) { - code2 = "aaa" + code2; - } - return code1.compareTo(code2); + Collections.sort(result, (map1, map2) -> { + String code1 = map1.get("code"); + String code2 = map2.get("code"); + int i; + if (0 != (i = codeIsPasswordOrOpenId(code2) - codeIsPasswordOrOpenId(code1))) { + return i; } + return code1.compareTo(code2); }); return result; } + private int codeIsPasswordOrOpenId(String code) { + return code.startsWith(SCOPE_PREFIX + "password") || code.startsWith(SCOPE_PREFIX + "openid") ? 1 : 0; + } + private String getRedirectUri(ClientDetails client, AuthorizationRequest clientAuth) { String result = null; if (clientAuth.getRedirectUri() != null) { @@ -311,5 +312,4 @@ private String getPath(HttpServletRequest request, String path) { protected String extractScheme(HttpServletRequest request) { return useSsl != null && useSsl ? "https" : request.getScheme(); } - } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java index 288698a56de..2732e85a1b3 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.scim.bootstrap; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -19,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,6 +35,8 @@ import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.util.StringUtils; public class ScimGroupBootstrap implements InitializingBean { @@ -49,30 +53,47 @@ public class ScimGroupBootstrap implements InitializingBean { private final ScimUserProvisioning scimUserProvisioning; + private Set defaultUserGroups = Collections.EMPTY_SET; + private Set commaSeparatedGroups = Collections.EMPTY_SET; + private static final String USER_BY_NAME_FILTER = "username eq \"%s\""; private static final String GROUP_BY_NAME_FILTER = "displayName eq \"%s\""; private final Log logger = LogFactory.getLog(getClass()); + private final PropertySource messages; + public ScimGroupBootstrap(ScimGroupProvisioning scimGroupProvisioning, ScimUserProvisioning scimUserProvisioning, ScimGroupMembershipManager membershipManager) { this.scimGroupProvisioning = scimGroupProvisioning; this.scimUserProvisioning = scimUserProvisioning; this.membershipManager = membershipManager; - groups = new HashSet(); - groupMembers = new HashMap>(); - groupAdmins = new HashMap>(); + groups = new HashSet<>(); + groupMembers = new HashMap<>(); + groupAdmins = new HashMap<>(); + + PropertySource messagesPropertySource; + String messagesFilename = "messages.properties"; + try { + messagesPropertySource = new ResourcePropertySource(messagesFilename); + } catch(IOException ex) { + messagesPropertySource = new PropertySource.StubPropertySource(messagesFilename); + } + messages = messagesPropertySource; } /** * Specify the list of groups to create as a comma-separated list of * group-names * - * @param groups + * @param commaSeparatedGroups */ - public void setGroups(String groups) { - this.groups = StringUtils.commaDelimitedListToSet(groups); + public void setGroups(String commaSeparatedGroups) { + this.commaSeparatedGroups = StringUtils.commaDelimitedListToSet(commaSeparatedGroups); + this.groups = new HashSet<>(); + this.groups.addAll(this.commaSeparatedGroups); + this.groups.addAll(this.defaultUserGroups); } /** @@ -107,28 +128,32 @@ public void setGroupMembers(List membershipInfo) { @Override public void afterPropertiesSet() throws Exception { - for (String g : groups) { - addGroup(g); + List groupInfos = groups.stream().filter(n -> StringUtils.hasText(n)).map(n -> getOrCreateGroup(n)).collect(Collectors.toList()); + for (int i = 0; i < groupInfos.size(); i++) { + ScimGroup g = groupInfos.get(i); + String description = (String) messages.getProperty("scope." + g.getDisplayName()); + if (StringUtils.hasText(description)) { + g.setDescription(description); + groupInfos.set(i, scimGroupProvisioning.update(g.getId(), g)); + } } - for (String g : groups) { + + for (ScimGroup g : groupInfos) { addMembers(g); } } - private void addMembers(String g) { - ScimGroup group = getGroup(g); - if (group == null) { - addGroup(g); - } - List members = getMembers(groupMembers.get(g), ScimGroupMember.GROUP_MEMBER); - members.addAll(getMembers(groupAdmins.get(g), ScimGroupMember.GROUP_ADMIN)); - logger.debug("adding members: " + members + " into group: " + g); + private void addMembers(ScimGroup group) { + String name = group.getDisplayName(); + List members = getMembers(groupMembers.get(name), ScimGroupMember.GROUP_MEMBER); + members.addAll(getMembers(groupAdmins.get(name), ScimGroupMember.GROUP_ADMIN)); + logger.debug("adding members: " + members + " into group: " + name); for (ScimGroupMember member : members) { try { membershipManager.addMember(group.getId(), member); } catch (MemberAlreadyExistsException ex) { - logger.debug(member.getMemberId() + " already is member of group " + g); + logger.debug(member.getMemberId() + " already is member of group " + name); } } } @@ -138,7 +163,7 @@ private List getMembers(Set names, List emptyList(); } - List members = new ArrayList(); + List members = new ArrayList<>(); for (String name : names) { ScimCore member = getScimResourceId(name); if (member != null) { @@ -184,16 +209,26 @@ ScimGroup getGroup(String name) { return null; } - private void addGroup(String name) { - if (name.isEmpty()) { - return; - } + private ScimGroup getOrCreateGroup(String name) { logger.debug("adding group: " + name); ScimGroup g = new ScimGroup(null,name,IdentityZoneHolder.get().getId()); try { - scimGroupProvisioning.create(g); + g = scimGroupProvisioning.create(g); } catch (ScimResourceAlreadyExistsException ex) { - logger.debug("group " + g + " already exists, ignoring..."); + logger.debug("group " + g + " already exists, retrieving..."); + g = getGroup(name); } + return g; + } + + public Set getDefaultUserGroups() { + return defaultUserGroups; + } + + public void setDefaultUserGroups(Set defaultUserGroups) { + this.defaultUserGroups = defaultUserGroups; + this.groups = new HashSet<>(); + this.groups.addAll(this.commaSeparatedGroups); + this.groups.addAll(this.defaultUserGroups); } } diff --git a/server/src/main/resources/templates/web/access_confirmation.html b/server/src/main/resources/templates/web/access_confirmation.html index 6022853707e..9b027dee272 100644 --- a/server/src/main/resources/templates/web/access_confirmation.html +++ b/server/src/main/resources/templates/web/access_confirmation.html @@ -28,7 +28,7 @@

Cloudbees

  • - +
  • @@ -36,7 +36,7 @@

    Cloudbees

  • - +
  • @@ -44,7 +44,7 @@

    Cloudbees

  • - +
  • diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index f12b351decd..8a108ed788b 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -290,4 +290,4 @@ oauth: # - GET # - PUT # - POST -# - DELETE \ No newline at end of file +# - DELETE diff --git a/uaa/src/main/webapp/WEB-INF/jsp/access_confirmation.jsp b/uaa/src/main/webapp/WEB-INF/jsp/access_confirmation.jsp deleted file mode 100644 index 4adaa316ea7..00000000000 --- a/uaa/src/main/webapp/WEB-INF/jsp/access_confirmation.jsp +++ /dev/null @@ -1,108 +0,0 @@ -<%-- - - Cloud Foundry 2012.02.03 Beta - Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved. - - This product is licensed to you under the Apache License, Version 2.0 (the "License"). - You may not use this product except in compliance with the License. - - This product includes a number of subcomponents with - separate copyright notices and license terms. Your use of these - subcomponents is subject to the terms and conditions of the - subcomponent's license, as noted in the LICENSE file. - ---%> -<%@ page session="false"%> - -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> - - - - -Access Confirmation | Cloud Foundry - - -
    -
    - -
    -

    Sorry

    -

    There was an error. The request for authorization was invalid.

    -
    -
    - - -

    Please Confirm

    -
    - -
    -

    - Do you authorize the application '${client_id}' at ${redirect_uri} to access your Cloud - Foundry resources? -

    - - -

    You have already approved '${client_id}' with access to the following:

    - -
    - -
    -
    - -

    You have already denied '${client_id}' access to the following:

    - -
    - -
    -
    - -

    Do you want to allow '${client_id}' to:

    - -
    - -
    -
    - - -

    Do you wish to change these selections?

    -
    -

    If you do not recognize the application or the URL in the - link above you should deny access by unchecking all these boxes.

    -
    - - -
    - -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - - - diff --git a/uaa/src/main/webapp/WEB-INF/jsp/home.jsp b/uaa/src/main/webapp/WEB-INF/jsp/home.jsp deleted file mode 100644 index 2ce7939f458..00000000000 --- a/uaa/src/main/webapp/WEB-INF/jsp/home.jsp +++ /dev/null @@ -1,54 +0,0 @@ -<%-- - - Cloud Foundry 2012.02.03 Beta - Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved. - - This product is licensed to you under the Apache License, Version 2.0 (the "License"). - You may not use this product except in compliance with the License. - - This product includes a number of subcomponents with - separate copyright notices and license terms. Your use of these - subcomponents is subject to the terms and conditions of the - subcomponent's license, as noted in the LICENSE file. - ---%> -<%@ page session="false"%> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> - - - -Success | Cloud Foundry - - -
    -
    -
    -

    Success

    - -

    Your account login is working and you have authenticated.

    - - -
    -

    But there was an error.

    -
    -
    - -

    You are logged in.

    - -

    - - Logout   -

    - -
    -
    -
    - Copyright © - - Pivotal Software, Inc. All rights reserved. -
    -
    - - diff --git a/uaa/src/main/webapp/WEB-INF/jsp/login.jsp b/uaa/src/main/webapp/WEB-INF/jsp/login.jsp deleted file mode 100644 index 8791f839bd7..00000000000 --- a/uaa/src/main/webapp/WEB-INF/jsp/login.jsp +++ /dev/null @@ -1,64 +0,0 @@ -<%-- - - Cloud Foundry 2012.02.03 Beta - Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved. - - This product is licensed to you under the Apache License, Version 2.0 (the "License"). - You may not use this product except in compliance with the License. - - This product includes a number of subcomponents with - separate copyright notices and license terms. Your use of these - subcomponents is subject to the terms and conditions of the - subcomponent's license, as noted in the LICENSE file. - ---%> -<%@ page session="false"%> - -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> - - - - -UAA Login | Cloud Foundry - - - -
    -
    -

    Sign in to zone "${zone_name}" with your CloudFoundry credentials.

    -
    " method="POST" novalidate> -
    - -
    Sorry, we couldn't verify your email and - password.
    -
    - - - -
    - -
    -
    -
    -

    - If you are reading this you are probably in the wrong place because - the UAA does not support a branded UI out of the box. To login to - Pivotal Web Services - click here. If you were - re-directed here by another application, please contact the owner of - that application and tell them to use the Login Server as UI entry - point. -

    -
    -
    - - - diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index 433d6b5f06b..d547bb484bb 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -353,6 +353,7 @@ + diff --git a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml index 2114242cb0f..b8f002b1ab0 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml @@ -255,6 +255,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java index 0e63aaa735f..143e8eaf95f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertEquals; import org.cloudfoundry.identity.uaa.ServerRunning; +import org.cloudfoundry.identity.uaa.resources.SearchResults; +import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestAccountSetup; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; @@ -32,6 +34,10 @@ import org.openqa.selenium.WebDriver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.test.OAuth2ContextConfiguration; @@ -57,6 +63,8 @@ public class AppApprovalIT { @Rule public TestAccountSetup testAccountSetup = TestAccountSetup.standard(serverRunning, testAccounts); + public RestOperations restTemplate; + @Autowired @Rule public IntegrationTestRule integrationTestRule; @@ -73,6 +81,8 @@ public class AppApprovalIT { @Before @After public void logout_and_clear_cookies() { + restTemplate = serverRunning.getRestTemplate(); + try { webDriver.get(baseUrl + "/logout.do"); }catch (org.openqa.selenium.TimeoutException x) { @@ -85,6 +95,20 @@ public void logout_and_clear_cookies() { @Test public void testApprovingAnApp() throws Exception { + ResponseEntity> getGroups = restTemplate.exchange(baseUrl + "/Groups?filter=displayName eq '{displayName}'", + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + }, + "cloud_controller.read"); + ScimGroup group = getGroups.getBody().getResources().stream().findFirst().get(); + + group.setDescription("Read about your clouds."); + HttpHeaders headers = new HttpHeaders(); + headers.add("If-Match", Integer.toString(group.getVersion())); + HttpEntity request = new HttpEntity(group, headers); + restTemplate.exchange(baseUrl + "/Groups/{group-id}", HttpMethod.PUT, request, Object.class, group.getId()); + ScimUser user = createUnapprovedUser(); // Visit app @@ -100,6 +124,7 @@ public void testApprovingAnApp() throws Exception { webDriver.findElement(By.xpath("//label[text()='Change your password']/preceding-sibling::input")).click(); webDriver.findElement(By.xpath("//label[text()='Translate user ids to names and vice versa']/preceding-sibling::input")).click(); + webDriver.findElement(By.xpath("//label[text()='Read about your clouds.']/preceding-sibling::input")); webDriver.findElement(By.xpath("//button[text()='Authorize']")).click(); From 7f41a15d1cd84e93268e5f309e431a83747faf71 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Mon, 18 Jan 2016 17:28:37 -0800 Subject: [PATCH 08/25] Bootstrap group descriptions from YML. [#111718508] https://www.pivotaltracker.com/story/show/111718508 Signed-off-by: Madhura Bhave --- .../scim/bootstrap/ScimGroupBootstrap.java | 102 ++++++++++++------ .../identity/uaa/util/MapCollector.java | 51 +++++++++ .../bootstrap/ScimGroupBootstrapTests.java | 50 +++++++++ .../identity/uaa/util/PredicateMatcher.java | 49 +++++++++ .../identity/uaa/login/BootstrapTests.java | 15 +++ uaa/src/test/resources/test/bootstrap/uaa.yml | 2 + 6 files changed, 235 insertions(+), 34 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/util/MapCollector.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/util/PredicateMatcher.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java index 2732e85a1b3..1f4f7533290 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrap.java @@ -12,16 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.scim.bootstrap; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.scim.ScimCore; @@ -33,15 +23,25 @@ import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.MemberAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; +import org.cloudfoundry.identity.uaa.util.MapCollector; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.env.PropertySource; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.util.StringUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + public class ScimGroupBootstrap implements InitializingBean { - private Set groups; + private Map groups; private Map> groupMembers; @@ -53,8 +53,8 @@ public class ScimGroupBootstrap implements InitializingBean { private final ScimUserProvisioning scimUserProvisioning; - private Set defaultUserGroups = Collections.EMPTY_SET; - private Set commaSeparatedGroups = Collections.EMPTY_SET; + private Map defaultUserGroups = Collections.EMPTY_MAP; + private Map configuredGroups = Collections.EMPTY_MAP; private static final String USER_BY_NAME_FILTER = "username eq \"%s\""; @@ -62,25 +62,42 @@ public class ScimGroupBootstrap implements InitializingBean { private final Log logger = LogFactory.getLog(getClass()); - private final PropertySource messages; + private PropertySource messageSource; + private String messagePropertyNameTemplate = "scope.%s"; + + public String getMessagePropertyNameTemplate() { + return messagePropertyNameTemplate; + } + + public void setMessagePropertyNameTemplate(String messagePropertyNameTemplate) { + this.messagePropertyNameTemplate = messagePropertyNameTemplate; + } public ScimGroupBootstrap(ScimGroupProvisioning scimGroupProvisioning, ScimUserProvisioning scimUserProvisioning, ScimGroupMembershipManager membershipManager) { this.scimGroupProvisioning = scimGroupProvisioning; this.scimUserProvisioning = scimUserProvisioning; this.membershipManager = membershipManager; - groups = new HashSet<>(); + groups = new HashMap<>(); groupMembers = new HashMap<>(); groupAdmins = new HashMap<>(); + } - PropertySource messagesPropertySource; - String messagesFilename = "messages.properties"; - try { - messagesPropertySource = new ResourcePropertySource(messagesFilename); - } catch(IOException ex) { - messagesPropertySource = new PropertySource.StubPropertySource(messagesFilename); + public PropertySource getMessageSource() { + if(messageSource == null) { + String messagesFilename = "messages.properties"; + try { + messageSource = new ResourcePropertySource(messagesFilename); + } catch(IOException ex) { + messageSource = new PropertySource.StubPropertySource(messagesFilename); + } } - messages = messagesPropertySource; + + return messageSource; + } + + public void setMessageSource(PropertySource messageSource) { + this.messageSource = messageSource; } /** @@ -90,10 +107,14 @@ public ScimGroupBootstrap(ScimGroupProvisioning scimGroupProvisioning, ScimUserP * @param commaSeparatedGroups */ public void setGroups(String commaSeparatedGroups) { - this.commaSeparatedGroups = StringUtils.commaDelimitedListToSet(commaSeparatedGroups); - this.groups = new HashSet<>(); - this.groups.addAll(this.commaSeparatedGroups); - this.groups.addAll(this.defaultUserGroups); + this.configuredGroups = StringUtils.commaDelimitedListToSet(commaSeparatedGroups).stream() + .map(g -> g.split("\\|")) + .collect(new MapCollector<>( + gd -> StringUtils.trimWhitespace(gd[0]), + gd -> gd.length > 1 ? StringUtils.trimWhitespace(gd[1]) : null) + ); + + setCombinedGroups(); } /** @@ -113,7 +134,7 @@ public void setGroupMembers(List membershipInfo) { } Set users = StringUtils.commaDelimitedListToSet(fields[1]); String groupName = fields[0]; - groups.add(groupName); + groups.putIfAbsent(groupName, null); boolean groupAdmin = (3 <= fields.length && "write".equalsIgnoreCase(fields[2])) ? true : false; if (groupAdmin) { @@ -128,10 +149,10 @@ public void setGroupMembers(List membershipInfo) { @Override public void afterPropertiesSet() throws Exception { - List groupInfos = groups.stream().filter(n -> StringUtils.hasText(n)).map(n -> getOrCreateGroup(n)).collect(Collectors.toList()); + List groupInfos = groups.keySet().stream().filter(n -> StringUtils.hasText(n)).map(n -> getOrCreateGroup(n)).collect(Collectors.toList()); for (int i = 0; i < groupInfos.size(); i++) { ScimGroup g = groupInfos.get(i); - String description = (String) messages.getProperty("scope." + g.getDisplayName()); + String description = groups.get(g.getDisplayName()); if (StringUtils.hasText(description)) { g.setDescription(description); groupInfos.set(i, scimGroupProvisioning.update(g.getId(), g)); @@ -222,13 +243,26 @@ private ScimGroup getOrCreateGroup(String name) { } public Set getDefaultUserGroups() { - return defaultUserGroups; + return defaultUserGroups.keySet(); } public void setDefaultUserGroups(Set defaultUserGroups) { - this.defaultUserGroups = defaultUserGroups; - this.groups = new HashSet<>(); - this.groups.addAll(this.commaSeparatedGroups); - this.groups.addAll(this.defaultUserGroups); + this.defaultUserGroups = defaultUserGroups.stream() + .collect(new MapCollector<>( + g -> g, + g -> (String) getMessageSource().getProperty(String.format(messagePropertyNameTemplate, g)) + )); + + setCombinedGroups(); } + + private void setCombinedGroups() { + this.groups = new HashMap<>(); + this.groups.putAll(this.defaultUserGroups); + + this.configuredGroups.entrySet().stream() + .filter(e -> StringUtils.hasText(e.getValue()) || !groups.containsKey(e.getKey())) + .forEach(e -> groups.put(e.getKey(), e.getValue())); + } + } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/MapCollector.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/MapCollector.java new file mode 100644 index 00000000000..1dca67410ef --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/MapCollector.java @@ -0,0 +1,51 @@ +package org.cloudfoundry.identity.uaa.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +/** +* Created by pivotal on 1/18/16. +*/ +public class MapCollector implements Collector, Map> { + + private final Function keyMapper; + private final Function valueMapper; + + public MapCollector(Function keyMapper, Function valueMapper) { + + this.keyMapper = keyMapper; + this.valueMapper = valueMapper; + } + + @Override + public Supplier> supplier() { + return HashMap::new; + } + + @Override + public BiConsumer, T> accumulator() { + return (m, item) -> m.put(keyMapper.apply(item), valueMapper.apply(item)); + } + + @Override + public BinaryOperator> combiner() { + return (left, right) -> { throw new IllegalStateException(String.format("Duplicate key %s", left)); }; + } + + @Override + public Function, Map> finisher() { + return m -> m; + } + + @Override + public Set characteristics() { + return Collections.singleton(Characteristics.UNORDERED); + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrapTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrapTests.java index 1f2f4546809..eb0839e10da 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrapTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimGroupBootstrapTests.java @@ -13,20 +13,28 @@ package org.cloudfoundry.identity.uaa.scim.bootstrap; import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory; +import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupMembershipManager; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupProvisioning; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.test.TestUtils; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.cloudfoundry.identity.uaa.util.PredicateMatcher; import org.junit.Before; import org.junit.Test; +import org.springframework.core.env.MapPropertySource; import org.springframework.jdbc.core.JdbcTemplate; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; public class ScimGroupBootstrapTests extends JdbcTestBase { @@ -100,4 +108,46 @@ public void canAddMembers() throws Exception { assertEquals(3, bootstrap.getGroup("org1.engg").getMembers().size()); assertEquals(2, mDB.getMembers(bootstrap.getGroup("org1.dev").getId(), ScimGroupMember.Role.WRITER).size()); } + + @Test + public void stripsWhitespaceFromGroupNamesAndDescriptions() throws Exception { + bootstrap.setGroups("print|Access the network printer, something| Do something else "); + bootstrap.afterPropertiesSet(); + + ScimGroup group; + assertNotNull(group = bootstrap.getGroup("something")); + assertNotNull(group = gDB.retrieve(group.getId())); + assertEquals("something", group.getDisplayName()); + assertEquals("Do something else", group.getDescription()); + } + + @Test + public void prefersNonBlankYmlOverMessagesProperties() throws Exception { + // set up default groups + HashMap defaults = new HashMap<>(); + defaults.put("records.read", ""); + defaults.put("pets.cat", "Access the cat"); + defaults.put("pets.dog", "Dog your data"); + bootstrap.setMessageSource(new MapPropertySource("messages.properties", defaults)); + bootstrap.setMessagePropertyNameTemplate("%s"); + bootstrap.setDefaultUserGroups(defaults.keySet()); + + // set up configured groups + bootstrap.setGroups("print|Access the network printer,records.read|Read important data,pets.cat|Pet the cat,pets.dog,fish.nemo"); + + bootstrap.afterPropertiesSet(); + + List groups = gDB.retrieveAll(); + + // print: only specified in the configured groups, so it should get its description from there + assertThat(groups, PredicateMatcher.has(group -> "print".equals(group.getDisplayName()) && "Access the network printer".equals(group.getDescription()))); + // records.read: exists in the message property source but should get its description from configuration + assertThat(groups, PredicateMatcher.has(group -> "records.read".equals(group.getDisplayName()) && "Read important data".equals(group.getDescription()))); + // pets.cat: read: exists in the message property source but should get its description from configuration + assertThat(groups, PredicateMatcher.has(group -> "pets.cat".equals(group.getDisplayName()) && "Pet the cat".equals(group.getDescription()))); + // pets.dog: specified in configuration with no description, so it should retain the default description + assertThat(groups, PredicateMatcher.has(group -> "pets.dog".equals(group.getDisplayName()) && "Dog your data".equals(group.getDescription()))); + // fish.nemo: never gets a description + assertThat(groups, PredicateMatcher.has(group -> "fish.nemo".equals(group.getDisplayName()) && group.getDescription() == null)); + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/PredicateMatcher.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/PredicateMatcher.java new file mode 100644 index 00000000000..165e50c621f --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/PredicateMatcher.java @@ -0,0 +1,49 @@ +package org.cloudfoundry.identity.uaa.util; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.util.function.Predicate; + +import static org.hamcrest.Matchers.hasItem; + +public class PredicateMatcher extends BaseMatcher { + + private PredicateMatcher(){} + + private Predicate predicate; + + public static PredicateMatcher is(Predicate predicate) { + PredicateMatcher matcher = new PredicateMatcher<>(); + matcher.predicate = predicate; + return matcher; + } + + public static PredicateMatcher[] are(Predicate... predicates) { + PredicateMatcher[] matchers = new PredicateMatcher[predicates.length]; + for(int i = 0; i < predicates.length; i++) { + matchers[i] = is(predicates[i]); + } + return matchers; + } + + public static Matcher> has(Predicate predicate) { + PredicateMatcher itemMatcher = is(predicate); + return hasItem(itemMatcher); + } + + @Override + public boolean matches(Object item) { + try { + return predicate.test((T) item); + } catch(ClassCastException ex) { + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("match for a predicate"); + } +} 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 f79fc066953..654017887f0 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 @@ -34,7 +34,10 @@ import org.cloudfoundry.identity.uaa.provider.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.provider.saml.ZoneAwareMetadataGenerator; import org.cloudfoundry.identity.uaa.resources.jdbc.SimpleSearchQueryConverter; +import org.cloudfoundry.identity.uaa.scim.ScimGroup; +import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; import org.cloudfoundry.identity.uaa.security.web.CorsFilter; +import org.cloudfoundry.identity.uaa.util.PredicateMatcher; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneResolvingFilter; @@ -83,6 +86,9 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -436,6 +442,15 @@ public void testDefaultInternalHostnamesAndNoDBSettings() throws Exception { } } + @Test + public void bootstrap_scim_groups_from_yaml() throws Exception { + context = getServletContext(null, "login.yml", "test/bootstrap/uaa.yml", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); + ScimGroupProvisioning scimGroupProvisioning = context.getBean("scimGroupProvisioning", ScimGroupProvisioning.class); + List scimGroups = scimGroupProvisioning.retrieveAll(); + assertThat(scimGroups, PredicateMatcher.has(g -> g.getDisplayName().equals("pony") && "The magic of friendship".equals(g.getDescription()))); + assertThat(scimGroups, PredicateMatcher.has(g -> g.getDisplayName().equals("cat") && "The cat".equals(g.getDescription()))); + } + @Test public void testBootstrappedIdps_and_ExcludedClaims_and_CorsConfig() throws Exception { diff --git a/uaa/src/test/resources/test/bootstrap/uaa.yml b/uaa/src/test/resources/test/bootstrap/uaa.yml index 3edecab6906..5d27575aed1 100644 --- a/uaa/src/test/resources/test/bootstrap/uaa.yml +++ b/uaa/src/test/resources/test/bootstrap/uaa.yml @@ -38,3 +38,5 @@ cors: - POST - PUT default: *xhr +scim: + groups: pony|The magic of friendship,cat|The cat From 1da6b7977cca904d868b8ddb34f37a11db88aec1 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 19 Jan 2016 14:15:22 -0800 Subject: [PATCH 09/25] Enable customization of legal footer text. [#109739844] https://www.pivotaltracker.com/story/show/109739844 --- .../resources/templates/web/layouts/main.html | 9 ++++----- uaa/src/main/resources/login.yml | 6 +++--- .../identity/uaa/login/LoginMockMvcTests.java | 15 ++++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/server/src/main/resources/templates/web/layouts/main.html b/server/src/main/resources/templates/web/layouts/main.html index 14be36df1a3..ee131b2da2e 100644 --- a/server/src/main/resources/templates/web/layouts/main.html +++ b/server/src/main/resources/templates/web/layouts/main.html @@ -1,14 +1,13 @@ + copyright=(${@environment.getProperty('login.branding.footerLegalText')} ?: ('Copyright © ' + ${companyName} + ', Inc. ' + ${#dates.year(#dates.createNow())}) + '. All Rights Reserved.')"> - [[${pivotal and isUaa ? 'Pivotal' : (isUaa ? 'Cloud Foundry' : zoneName)}]] - + [[${isUaa ? 'Cloud Foundry' : zoneName}]] + @@ -17,7 +16,7 @@ - +
    diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index bbdfd81fd03..cfab5c27134 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -51,9 +51,6 @@ login: #selfServiceLinksEnabled: true # Enable sending invitations on the Login Server (disabled by default) #invitationsEnabled: true - # the brand to use for password reset emails and page titles - # (defaults to oss) - #brand: pivotal #base URL that the login server can be reached at url: http://localhost:8080/uaa @@ -202,6 +199,9 @@ login: authorize: url: http://localhost:8080/uaa/oauth/authorize +# branding: +# footerLegalText: This legal text should show up in the footer. + uaa: # The hostname of the UAA that this login server will connect to url: http://localhost:8080/uaa 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 03de6aacc27..e8f682c0a5e 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 @@ -77,6 +77,7 @@ import static java.util.Collections.EMPTY_LIST; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; @@ -168,20 +169,20 @@ public void testLogin_When_DisableInternalUserManagement_Is_True() throws Except webApplicationContext.getBean(LoginInfoEndpoint.class).setDisableInternalUserManagement(false); } + private static final String cfCopyrightText = "Copyright © CloudFoundry.org Foundation, Inc."; @Test - public void testCopyrightPivotal() throws Exception { - mockEnvironment.setProperty("login.brand", "pivotal"); - + public void testDefaultFooter() throws Exception { getMockMvc().perform(get("/login")) - .andExpect(content().string(containsString("Copyright © Pivotal Software, Inc."))); + .andExpect(content().string(containsString(cfCopyrightText))); } @Test - public void testCopyrightCloudFoundry() throws Exception { - mockEnvironment.setProperty("login.brand", "cloudfoundry"); + public void testCustomizedFooter() throws Exception { + String customFooterText = "This text should be in the footer."; + mockEnvironment.setProperty("login.branding.footerLegalText", customFooterText); getMockMvc().perform(get("/login")) - .andExpect(content().string(containsString("Copyright © CloudFoundry.org Foundation, Inc."))); + .andExpect(content().string(allOf(containsString(customFooterText), not(containsString(cfCopyrightText))))); } @Test From 63979f8715eb689c5704c7a9223468a1256fc8da Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 19 Jan 2016 16:33:17 -0800 Subject: [PATCH 10/25] Remove test cases for unsupported login.brand property [#109739844] https://www.pivotaltracker.com/story/show/109739844 --- .../login/AccountsControllerMockMvcTests.java | 51 ++----------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java index 0b553e6a1d3..d90c81b5283 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java @@ -109,61 +109,32 @@ public static void stopMailServer() throws Exception { @Test public void testCreateActivationEmailPage() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); - getMockMvc().perform(get("/create_account")) - .andExpect(content().string(containsString("Create your account"))) - .andExpect(content().string(not(containsString("Pivotal ID")))); - } - - @Test - public void testCreateActivationEmailPageWithPivotalBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - - getMockMvc().perform(get("/create_account")) - .andExpect(content().string(containsString("Create your Pivotal ID"))) - .andExpect(content().string(not(containsString("Create your account")))); + .andExpect(content().string(containsString("Create your account"))); } @Test public void testCreateActivationEmailPageWithinZone() throws Exception { String subdomain = generator.generate(); mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); getMockMvc().perform(get("/create_account") .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) - .andExpect(content().string(containsString("Create your account"))) - .andExpect(content().string(not(containsString("Pivotal ID")))); + .andExpect(content().string(containsString("Create your account"))); } @Test public void testActivationEmailSentPage() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); - getMockMvc().perform(get("/accounts/email_sent")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Create your account"))) - .andExpect(xpath("//input[@disabled='disabled']/@value").string("Email successfully sent")) - .andExpect(content().string(not(containsString("Pivotal ID")))); - } - - @Test - public void testActivationEmailSentPageWithPivotalBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - - getMockMvc().perform(get("/accounts/email_sent")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("Create your Pivotal ID"))) - .andExpect(xpath("//input[@disabled='disabled']/@value").string("Email successfully sent")) - .andExpect(content().string(not(containsString("Create your account")))); + .andExpect(xpath("//input[@disabled='disabled']/@value").string("Email successfully sent")); } @Test public void testActivationEmailSentPageWithinZone() throws Exception { String subdomain = generator.generate(); mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); getMockMvc().perform(get("/accounts/email_sent") .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) @@ -174,35 +145,23 @@ public void testActivationEmailSentPageWithinZone() throws Exception { } @Test - public void testPageTitleWithOssBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); - + public void testPageTitle() throws Exception { getMockMvc().perform(get("/create_account")) .andExpect(content().string(containsString("Cloud Foundry"))); } - @Test - public void testPageTitleWithPivotalBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - - getMockMvc().perform(get("/create_account")) - .andExpect(content().string(containsString("Pivotal"))); - } - @Test public void testPageTitleWithinZone() throws Exception { String subdomain = generator.generate(); IdentityZone zone = mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - getMockMvc().perform(get("/create_account") .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) .andExpect(content().string(containsString("" + zone.getName() + ""))); } @Test - public void testImageWithOssBrand() throws Exception { + public void testImage() throws Exception { ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("assetBaseUrl", "/resources/oss"); getMockMvc().perform(get("/create_account")) From cb5ff800ab5ed70c8131bba5c333ff32c182312c Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 19 Jan 2016 16:48:03 -0800 Subject: [PATCH 11/25] Add configurable links to footer [#109739844] https://www.pivotaltracker.com/story/show/109739844 --- .../main/resources/templates/web/layouts/main.html | 6 ++++-- uaa/src/main/resources/login.yml | 4 ++++ .../identity/uaa/login/LoginMockMvcTests.java | 13 +++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/src/main/resources/templates/web/layouts/main.html b/server/src/main/resources/templates/web/layouts/main.html index ee131b2da2e..7fe265d0ba6 100644 --- a/server/src/main/resources/templates/web/layouts/main.html +++ b/server/src/main/resources/templates/web/layouts/main.html @@ -3,13 +3,13 @@ th:with="assetBaseUrl=${@environment.getProperty('assetBaseUrl','/resources/oss')}, isUaa=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).isUaa()}, zoneName=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).get().getName()}, - companyName=(${pivotal} ? 'Pivotal Software' : 'CloudFoundry.org Foundation'), + companyName=('CloudFoundry.org Foundation'), copyright=(${@environment.getProperty('login.branding.footerLegalText')} ?: ('Copyright © ' + ${companyName} + ', Inc. ' + ${#dates.year(#dates.createNow())}) + '. All Rights Reserved.')"> [[${isUaa ? 'Cloud Foundry' : zoneName}]] - + @@ -26,9 +26,11 @@
    diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index cfab5c27134..71d8bcf1458 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -201,6 +201,10 @@ login: # branding: # footerLegalText: This legal text should show up in the footer. +# footerLegalLinks: +# Terms: /terms +# Privacy Agreement: /privacy.html +# Licensing: http://example.com/ uaa: # The hostname of the UAA that this login server will connect to 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 e8f682c0a5e..8083177cd8c 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 @@ -185,6 +185,19 @@ public void testCustomizedFooter() throws Exception { .andExpect(content().string(allOf(containsString(customFooterText), not(containsString(cfCopyrightText))))); } + @Test + public void testFooterLinks() throws Exception { + Map footerLinks = new HashMap<>(); + footerLinks.put("Terms of Use", "/terms.html"); + footerLinks.put("Privacy", "/privacy"); + // Insanity + propertySource.setProperty("login.branding.footerLegalLinks", footerLinks); + + getMockMvc().perform(get("/login")).andExpect(content().string(containsString("\n" + + " Privacy\n" + + " — Terms of Use"))); + } + @Test public void testForgotPasswordPageDoesNotHaveCsrf() throws Exception { getMockMvc().perform(get("/forgot_password")) From c3ffa8e58dc43f1c6fb23423c07446462568adf3 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 20 Jan 2016 09:56:21 -0700 Subject: [PATCH 12/25] [skip ci] Add helpful hint on how this magical property conversion happens --- uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml index b8f002b1ab0..cb0369e38ed 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml @@ -245,7 +245,7 @@ - +
    From dd39f28fccc64743cc804342ea8d14ebc799d3ac Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 19 Jan 2016 17:40:04 -0800 Subject: [PATCH 13/25] Enable customization of main logo via config. [#109739808] https://www.pivotaltracker.com/story/show/109739808 --- .../resources/templates/web/layouts/main.html | 2 +- uaa/src/main/resources/login.yml | 7 ++++--- .../identity/uaa/login/LoginMockMvcTests.java | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/server/src/main/resources/templates/web/layouts/main.html b/server/src/main/resources/templates/web/layouts/main.html index 7fe265d0ba6..d4ab50d3af1 100644 --- a/server/src/main/resources/templates/web/layouts/main.html +++ b/server/src/main/resources/templates/web/layouts/main.html @@ -20,7 +20,7 @@
    -
    +
    diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index 71d8bcf1458..78101a6a621 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -200,10 +200,11 @@ login: url: http://localhost:8080/uaa/oauth/authorize # branding: -# footerLegalText: This legal text should show up in the footer. +# productLogo: iVBORw0KGgoAAAANSUhEUgAAAJIAAACoCAYAAAALmrdFAAAgAElEQVR4Ae19CZwUxfX/t3quPdhlD/bgUhBMVOQS8EBUPEJQMZooaExMoiYiV9R4/I1X1kg0iQYiN+rP+PsZowJJjHgfAREMKCi3IiCIXMtes8dcfdT7f2pmeqZnpntm9gRN12d7u7rq1atXr99U1/HeK8AONgdsDtgcsDlgc8DmgM0BmwM2B2wO2BywOWBzwOaAzQGbAzYHbA7YHLA5YHPA5oDNAZsDNgdsDtgcsDlgc8DmgM0BmwM2B2wO2BywOWBzwOaAzQGbAzYHbA7YHLA5YHPA5oDNAZsDNgdsDtgcsDlgc8DmgM0BmwM2B2wO2BywOWBzwOaAzQGbAzYHbA7YHLA5YHPA5oDNAZsDNgdsDtgcsDlgc8DmgM0BmwM2B2wO2BywOWBzwOaAzQGbAzYHvn4cYJlInvFRw3Au4TwzOAlUM3dEyXNmecdcWtUKZ2VZ2XA4MIqAExnHADAMAFACIA9AoYHmZgDi2g/CfgbawSVscnC+8eDUoTsMcG2Ovrbf30fS+FVtRSBJ9Mm4vgXvZSrf0NAwHLB4f5JU07179w55f85MhAghYqDZZnAEbALQIYSY4W9vWvniLQMYlyYyRuNAOCMsMASEfz3pf0IFAMTVCwynExgYAZw5ULlwazUIKxmjV4NS6OWGySMb20InU7SBTGKmfM0GH+fscQAZBUkIEWPm9RBRh72/jIKUTaOOJZjjFmwulpnjBoB+BI7hAIX/OpDGCjBcTWBXe3iOXLlwyzsSOZ482GPrckyapHVgPV8rVN8YQeq1ePNJGme3ymDXASQ+VV0R3AC7hDN+SWXdKfuxaMvjmiYtqpk2qKUrKj+W6pCOJWLaQkufhRt791yw9SnOHVsZ2OToeKctqNpbpg+IPeqQaG/loi13DFqyzd1ehF+n8l9bQRIvqnLhlioVjl3E6EaAOyLfMMJRvpeC8GhdHd9WsXDLhK+TMLSH1q/lp63noi0j62q1ZwA2qA2NDxDwMSPsYMAXkGgvI/g4SY2QSEihuBURQwWR1AugbwMYDOAkkdWK+gYyYHnPBVv/5gKfvm/qkIZWlP3agWYUJDHFj87OUhrHgA6ZCqcgtkogYhULt/6aOB4CWGte6ocEvMQYe/ewFPwEk0cqVlVYpRcvXt/dzT3nMM6+C0aXA+hrBWtMJ9C1MjC254JN1xyaOvR9Pc/hkFoI4VmTnpR6JxwHoDg1I/sUSZJqorMzs0Id9v7ST4LNqj5KaSVzdhZ6XKH/Iwq/xCyooINg7AlJkv5ycPKgfVkUyB6EiPVcuHkMh3QTAyYC8GRRWGXE7jo07dSsp/xvftnyDICfmuEm4PHxx3e71SzvaKQ539zbstKqYnJoPx/ft/suq/yuSq9c+Ek/UOANIojPTKawi4DfVDvkpW3peaZ+eKRScjhfsKqEa+o1Cxg7fAgQvcv75U9uuUNS6DYA0wHkW5UD4CRGswa/uOvO0f1Lr1l8evGqNLBfuywnmPmqp2iJpDq6He0WVczdPJg43gTQMz0trBaM7q0+Uvc0qs5X08Na57pdrhyVk+lKvigl8o2lj/xicDWAu8v/vGU2c2MmSAz8I2ueRjg9fqTW3/ONZvmPg5ZsO3fbpEGynv51v7dmnNHlba1YuOlUSPRvgHqmn4nhrw5n6OTqqUOeaI8QtaeBR24dXF09dfAvwGg0gXakozcYUs6orVGXfZOWCI5ZQaqYu6k/NLwBQg+xOG1+sSYCXVk9bch1ByePrG2PIHRU2eqpQ9fm5RUO612W32xOc6wtl9VVq8+jio7Zd9AanhyTjRDbHMTodYB6W0gQQLSVa9rII9OG/aM1De4K2L3X9w+OHlBSf9qJJWDh6Yz5L4EY/aCybOMfuoKmzq7j2BMkIhbS+LNMDKzN+Q8ivC+71TE1twzf2dkMag/+AT3yMWZQOZwOyfr3wNkdFXM23tCeeo6FssecIFXM++T/AXSpJedBrzjd6vi27rp3NdMrCzwYO7gCLpfomix+GQzzxaSiq2nryPqcAD1ohVABO2yV1xnp5XM/ORtEMyMMT62BGFbldWucuPf684OpuR2W4k3HE4B5s6mJwP7MwIsEbHGuE6cNKK34aEfN9ZyTyZoT5RC0pf3+smJYQtsYewnE95rWJ7G1pulHKbFDFiRramp6OZ1O001Kznl9aWlpU6b2/WDV3p4fbGp4n4iEsplZ2Cx7+LnpeqKb1h/My3PklpsV1iSuzh1auv/VPS2VEhwJU3gdPgdB7/n9i7MSFL2M1V0orjGFJewcPPCfL8fsq/WLRUaHWTm3x7n4ytP7/p404o+PLOmQRdTXdtYVMmeeUN5LCQ4Hl8cdl3/wrX2+Xpommb4/Uv31l5yY+f0lNDSlpiwTXC7XawCGmoFLkiQW6/5slmdM230g9A/i3FSIJInJHI4rGiafllaJzI3ccSqnfxrxxuKcfQmgn5PhBTDNdJ1IhlP0zlWxMu2IODW+miQcb0Qx8+w+eOWLBizZfMSYHIvLIWVydXNocmm+W7Qz3JvFMtsYkdyeGxg009X06NbXMAK9Jkma6fsjtyer93dMjJHK56wfcPhI8ygigtl1Sr+S1dUzhu5pIy+PqWITTihGYZ7zP2btJE7YsPuYWMVoNc+OCUGCxu8nIlM1kL49C3By74KvWt2yY7jAnWf0nS8xNJgNvr2NAXxR0+L6uq0vdcinrT3vrHLO2lO4hh+LOX1ycLsdGNGvNDn5a/98fJHHV9aj26zqI00PmTVm/aeH88q7H9Yw+8MACHvA2OfE+FoH2L8P3zJqPZjQID+2wlEXJK6we8BEb5QaBp/QA26n6DSPOb6lEtvKlPO+VbH6HVlBfYM/XclcAKeA6BRGuIKDUDb7w6/Yn9Y9L0l84eHbzjKf0aXD2El5R/XTVvHomnKAJoZ7I9EjGa78fDdOKBOGHN/ccNqAHgltNrbfKs6I+gJ0F+dsd/mf1j7b87E1CQP6o8WtoypInDluICK32cDz1H6l0e2Fo8Wazq+3JN+DnhUFphMMM54kpUlE9GMV0o6yx9bejaoVR/Xr0iGVS5J0taZpohtOCU6n80BKop5A/Od61HiXJLavd0nBFVyKfNNcGuqN+VZxGbTCKTFhEJgSHMTDKhtCx8pKPaYjF2BV0CVOzkzXZhgP7eF5qgbZNbyom7vvQaKXWqnGa2yfB6BHyvI936PZq66sve3cQ8ZM7qbnnEFmqnOmEQ8IWK7yqx1MMn1/Wg5Zvz9DRRkFKRtL23t3qiM5WJkBryGqYtqGVHXlL4409l6/9YDpulFlecHHDmHdyyNoOGg7gH3T1tdPAWMmK8OA5OTL5g4t2Q9go6HylOirNeowTrxPSkb4TfK1M9bW+blLstz7khT+9Pcqck/RJH6mGQ4G2v/d4wuWvV4nn8lJMlrvxsAlkpodLkeIc4w9tW8P7K9u3tDUFBgVA2hb5CwnuXZMeHPvouN7dD8oUEjM+dzr+9U+6SylAey4+ITCdqvcZhSkbCxtCbiTgUwXtKx40uQLRcYHSQCSxHD6iZVXMNAVehZJ+F8AbzGGRwDqrqcb7yRLQoCEIKUNRGw6g7niGgEPOj3SYeLmlsUCscMjvcQlbTwD+41pRcSE9esyIvYAA5mOXwjYC1nyMilSz4XDjkNzMK7jRoSWdz7afdmgAZUPKho/t67Rj4YGf/gTaFpnNFFTtYIN2w7emT/chbLCXGhMXcm6yFI6oyClI7w9eQermwCeOhsrLsmHx2U6iWtPdcd0WTEzLe2WsGuj1d55zsrBH9ftAbFzBfGyqmH34Ubs/LIOAX9c6JIbpsoa3v/4S1wwqj8KCxJwJoN26PNRGWwrKkdLs/g8C0FKvPqUf7Nnam19e26nAyf3KcGE0QMx9JSekCSILj2FfyJNUVSs2vglahv86XTI20qKabmjIkg1TX4Q56ZXn1JbkEzfVDRRYgwn9S7B+WcM+KvEab0VH/0tQaz5ZM896XB1ZN7RESSv33T9xON2oluOqyPb943F1aObp2X0iN6XVpQXmvJSrEOFgsolW/YcPrkrmNAlYySNEw7Vt+BgbRPqvX40NwVMB9rFRV3l+6ErWNv5dRxfXir3LC3GvzfsQW2NuabO57uP/ODUfuUQPVlnhoyC1B5L25aAjG17j2D/gXrIcmYLoYI8t1gDMdOxCOvmMGALRfwWpfLEwbLzAEJsFxiZq2gwdlglVQYcwm+QaRD5xNyHmZWVLINuByiWLEx1mySgkQt6I/6JUuphESdfIl2025QWBnbA7VM1Ndex6dyh/aTX1mz/VsAvpyyNKDLvsf3Lmq9O7VeeshbXkZbSHSKm0zfUi6l3bPovZhgbdx7Cnj1HwE02Y1M4F01gjC6vvX/cy1b5dro1B0p/+84ogITWpNlw5dO6B75zinXp9uew6RvqTVc9BWoOx88XjMhsaWsUpEN1zfjPx7vF97nV1I0//9R1xd1yU9VoCW/MG1ny++kb6l8BYGq0ySV2KzTqJjHMbHXF0QIM7BlNU95IZ2mbGTdtnDei9NbpG+qFtW6lGTwn3AcHa5E4ZVT4Mysv0hiwTMtRn5GCTsGTcFi5Yfe3Dh2sNzUkPXvUiZuOqyxK7CGJ7Zo3svjn09c3PAVGA3U8xjtjNG/uaaXLjGlmcfFpM9UWDANL3PSlmSESadu+OIzN2/ZFZqVWQBbpTGJKcbdc4Z4vNUik73KPAWC6ICmpKAJjRbBYbExFmppC4CszWdqmlkpOiXXyYuXbdEFSAusBFU5Y+OZMxmj2TKCNbtnlVA3tHXVKXyw/WG+6cHmwtnHocZVJX3T9E89opPGLYqyPkyS2bzIGs24wYyEzgPXb92HTlr2RKT1xUJaXJEGo12wrK+u+zQyvnZY9B/Jz3aio7G7K+0OHUoZI2SPOAjLjYDsLHNjw6b7yz3dk3J2A0+lAeWUxKnoUorgwD4X5OcjLccMpsTEq50JXelg29dkw1hzo17sUh/anqusGWgLwBWXk55juI1sjzDKn3YJUfM8rl+749CvT77JOgyfHjVNO7ouBfcvgcv53bX/oPOiqe++y7qZLK6L+jZ/uw4n9KlFe3KoRS1akt0uQuv36tTJAeYaiu/RmNfY7oRKjBp8A93/Z/pkZL7oize1yIj8/By3NqZqXe3cfgrjyu+XiWyf2wrf6VXbY0KZdguQiZRaIepgxSKx/nTbiRJzUP21nZVbUTmsnBwoLc9HS5LPE4mv245OPd2H7tn0nldz9zystAVuRkdbSFopqaWlbdPey4SDtR1Z1Dfx235dO6l+5KZspnNfbGMwrLHxDAiVOT3XkPKwiIlD9HoxMt7SdTrZX1dKOsRqBDNNtLq2EJBYRra2PdZKs7gxSeIZptLRNheWfAQ7TpYFUWOsUr9cb7FZYmGIpLVRHQHysdclITigYFPtRy5a/vX7td84b9rsctyNl1ViKrE1lQgXnvBGlaQ0CrSw1b/yft37rD8mxua6xJo/H/eToYQMfFmlaiNefU1ASLMgL9jLCGOPrTiuRz9zf9CGp7s+M6Xo8x6O0zANweXHu31TJdMENOcg5/Pc6czmM4vGKtgqPbMnOsvR6hBAVeYtavEVeYQ1rGoq8Rfu9RV4xyEiaS0fAZUUJr4MJRTsHl0x7fL8WOOJGbrsFqd/YfrJ3YyqtLY0th4lrGQVJb2BjXeOZ/3p5jefmC4b8dPTAXuLojFgQlrZziNxer9fy/RUXF+81bWgMi1gmNbHUbPTLCIZkU32iboV5uOySM3+hcvqFwEMu6baiPN9KkvCJEa8xft5eb3FIcj0gSdotxnQ9LiuSUGz7GXdqmyWLdaQgbzlfh093F4uN1h7Z6EEhRConS2NMb5G3v8r5z2Ch2CY5nEKxbSypbLVqodjmRu73rbZP0tGenOfd6C00o3Xs2UOweu021B3xhojMfA0kYwIUWRn+0ie7No/o3wO5hvGssLT1er0rGWOW70+sj2YUpNQqgQ92HQTXzE9LGD5sIBxiccgOR40D3fJzMP7CEWgJhs4paWy4tiUo3/rRF0ew52D6taSvDjdg1psf454Jo6x9F1q0qk2CtH5PdXjhMRlnfkEejuttobqdDGw/dzoH8vJylHF9jvtSjD+uHDEQNc0BvLBuB9Zs/cpyD3TTzkN4fdMeXDK0f6voa1PXsWt/HcB5ynV8v0qwTlZXaFXrbOAEDpQV5GLGRcPwwMTRKMxzp7w//Z0+v2obvEKnvhWh1YIkdvP9/qCpdmOvXqYrAa0gxwbtCg6c0rsUf7j2PJQU5pq+x2BQxtJ1pvMeS/JaL0hCYV+sQJpcPYpNrW8sK7czjh4HSgtyce/3z0LEIj71fa7c9AX8oew1OFotSGG7D64BSZfLyeBytWnIdfS4+V9ec98ehbjq7JNT3qV4t3JIxurPsvf1lfHNczn0NHPmxVQJ9tU1eohTSr+nhNQap8ROT343Yh3J688X60iWo7f3+uU3nbm/6bdcdZvq54h1JIFXUh1D0q0jyTgk5Um5pvUIj20Ch/Dcb7mOBKavI5niEOWj60iCTtO1Jn0diTlpTLp1JLklxIuKiizrSeZj8jMLOJq6n1XY5N3otcRR5C46CPjSriddNvJbWL72M7T4U8dE6z4/gO8MHYCioqLtXq91PYK2jIIUdfsWVwieMcdTwFM0OsEkqVDhiBk16g2XHNJ7kwaF1yB0nSI9K/ku5qZp56fcpV7KyNzSNuAILPMgz6NwuiwZsXiWCKINTy84vdxytV7AiQVYye1JaYeOk3erf9rlzD3J2tLWuX8BsAwaximUcE6ujgIe5C1/4vxeuyEMJdOEGevrxnEmmWo2Sjn8kyoWNsZMi+ONL9NrIAszp7FD+uGVD4RmcGLYuU8caiDUfJgwpEtbT0ZBSkQNIOThwgQmOWice0LB0OycJDUFLjHhOi7dYlYyKutnYo8wiwVJpmgbCVRkef4uhV3/PW2NPJIj/C1aucoTEKJ35pLafktbQAhS2kASrmVE5ofaEGV7pm3aOkTmaSf0wvLVW1PgfL4gFJVnNfzJCiihhl6HNLOBtkirrU3boSSgsR+OHQ4MqCw2nTyJd6qqmuk2WDL1rRekqioOTTsEsbKddO3fk5XjimQa7OejzAGh7OYQVrtJ71M8E+OdJEhi/4z4CjNV2r07vgDXUj97R5lPdvVZcEBiwhFsqop0FkXDIK3vkUQxTXudhLQmXf6mFnz+WcZPf7a02XBdxAF/SEYoKKe8T/F+HZIUXvHJREqbBMkn5wiP9D6zsdLG9z+CLGe/kJWJQDu/8znwxYFayzGSk0lZfWJaP2sT7Vo6rQXX/OFZEN2c3EzRK61Y/q73u1eOFw7SISx1k2Ha+kxgWxgjUy8T4oxYYZDCLSxTAYSdT2WqW3jDtzrDV5QV+RxSuy1tM9ERzU9raZsNDkaogWRurauXf2/T572gaSm77QX5uYrT4TCzfNaLxu5ZDaRi0IZIzg9mHs8ckjidyNTrA2P0oH/JvWmV5gzo7OjR4sDEWbm5COyDsLVLDf8ILL0nK1XcjD3SjA31PxIe2VLrAFYuf2f37i07xDHmKUGcK5t75UN5Aenbv77lrvMHapLjxRSgaEJeQDvnwj65N0OCpequVVk9nYH9fNxx+ev1Z6v7m/tangIgDAJTAoEtGn9c/qKUjDYkeL1ecayGpVZhOpSMsebu3bufkw5G5N21uqbAn+uIndydDO/g2tWPjypL69Yvh/t+BcBMiMSqWUYLW73OjIIkfENaufU7+7vnbtm1efsuEEzNfQHcmcO3j9ywvmTWsNNPi/kG0CvX73K+00EMvRnF/QfoedneNZ6lVTDHQDDzehh4u9VfDfSKFWlTS1sDjGmUiNKeuaIXEnwDt3a5qDidpg5G9fJ5V/52OOfq/WSuxtYSVPEvHTbTPaMgpUPgcru5xLVrOLEPxLnBFrDnb3jz/bP9Dc0YNGooupeYqjpbFLWTO4sDnu/9ZiBX1eWRo+bNJmZsHpY/mGrTZEFQuwRJ4PS/9NCG3Evv/TGJU4fMPWGIpS739g/WQ1wlleWo7N8HxeWlyM3Lg8vjxgevvjvGNX50n4oSU7N+C9ITkz/Y+OmwnAn3JSaaPK3bsrMoLyd1r1CA7q+p75cz4b60m5wmKE2TVmzZkyP2sdoSNCJnNnQ8P+fpbmdPuMiyih0bNo/MmXBfyi+XMV5OXJ0LwPRIMsbQ6HG4/pDqzcOyKvM+zQg+bUPDrVb7V8J3z7wRJWEz65yL776BgCethMmI044f2xxgjP0k+Nojz7aGynb3SHplwdd//7Rn/F21RHjOyvWMDmvfj2EOEF4IvfXHVgmRaE2HCZJAFnrjjy97vnPnaQT6GxB2lXIMc8wmzYQD6+ScghtN0jMmZRQkCXytcGRuiomlnnkbevvRnaiqOsO12juZET0AsI6cCZmSYSe2nwMMWB1inglYXpX1ANtYa5sXJI1ILONn3Zbr8mg3MEY3EOE0Szg746hyoHt+znNLqm58aPjAihQ1yWzPJO5cQYqyp6GhYdjeww2frNy4Ex9+9hV27a/BobpG+AMhNJuoeB5Vrv5XVU67iXBz7SuPPGblsY2IbisuLjZVgTayKuOnzQjcnnj/niXo3/MMXH9xqnc/IiouLm7/6dYNDQ1XMMbMD0cGviwqKurXnjZ8c8s+0u6mtWn3v9212gi+cRywBekb90qPToNsQTo6fP/G1WoL0jfulR6dBtmCdHT4/o2rtUtmbZksNYuKiuIGmO1gcSAQeCs319zSlvOIpW070H9jiyqKconT6TTV3hDrSN/YhtsNszlgc8DmgM0BmwM2B2wO2BywOWBzwObAfw0HumT3P5mb5Yu3DGAc54GxwYxYLxCJs8s0EGoYYSeHtLa6x9aPMGlSqg/mKpLKy7eequPMAX21b+qQBv25zfcqknpVbB/KQWeD4SQQKkBh5/AKwL6CRFskjVYcnDb4q7R1tJG+fn/Zk+MPtHxLx13WQ/ps26RBwi9ROPSct/14TdIsldolSVI1jWSAtdQ4A3WYPLJLzZ27ZB1JZ0b5n9YNZYw9Tr7Aebrdgn7XYfR7+YF+u9mfPry3+vbTE+zhSkrWdWOqO3bOqwx2vZX3NB1XpnvFrA8v41j/BzVAaU+kFrbLZbM+XE4Su7P21lGm9mJtpc/n9Z4kOSjmR6ruABOe2GLOrYgHZzGOH1i1RfBRX12u0CQVcza8yZj02OEZwy1PCLXC1Zb0LhOk0pnvTudB3ywry1wT4gcAeKHH7/79vaKC4A27fnlJROmqHuA5Bi9kVpJogjAlqWqJu4ezxzwt0BI5pSAFwDRBeIT7TunDK26uu+d8cSJBYmgrfYofPBTvgB0Uj4sKNL84pCbrxor3eqm4yn6/alnI7Zzc9KvRnbqw2CWCVHLfK7dTMPCYCRuEbfsagB0BqAgMZ4CQbLl7bZ2fhKmTsMEKewfUDMqgjNr4dZ64xFGs5S3hqv/yCN74fwb6hMA2E6iFgYnjncRxraVxCIiDdZ4puf8VT/1DE54wpIfp4wEDTYZoIlzSk98LTnqfAihJ5TRfk/DBl1Qoq8ernH4MKal67cL6qksyn86YFcpUoE4XpO53vHgRD/iFBl4siGPCucR+2fjoxLdiidFIya9eOFNj0mwGiDNhwQhvNXwFYf4cCXX14DnGA5LaxFx0Pw4zye+/PEm4X9GI394865rP9erCdyF0femHxNgfARjPDVtYfOeL2xoevXpNDL6uHlquwcA1qYIYXFJE8YnhjcHxR9IxHDwQCJ+IHC7G8DmIX52EojsjqS8YziFA5BnHU2Ls9Vqf25acsX/2pEBSuQ557FxBqlriptrgYjJ2yQyrHaHghMYnJpuaJdfPumYtqqrOLqztX8WA7zM5OAlLJyf08xSK84K14jh4nWPFU/53iBYK3JXwjonNbJr/k/t1mIT70klaA/DX3Jv/d4XLiTcBDIrmiy7kSVStGIKq82NHVFHQSF+Wgq74QcKHeTQwKdG4ksShS/G2BhrnXLdRh026/7XHXf9zhxr0PEKgaYa8wc1EDwG4w5DWYdF4X9phKOOICg54r6VQ4AQKBRG+gsHDXPFf2WAhRLGSVVW8ad5PH2hs4aNSYevBg4HYpQVbYw8aqUFVQ/dTKCjF6JKD/7AUohhRQGDRTw8wOXg5hYLNsbKh4Mnd9u9I8NjBQwHol6ZYH8BnQA3F54uVEWURTDwyjAd8sTaL9qcLtX+8sdk758fTtaD/ASOveCg4I3/q/E6x6ulUQaJQ8DqSQ4hdqvxgy6IpWfnbCTPqmetTpaQecXxyCIgcj5aOr4l5E6vcJAeviNEkhxQ1GBSed7MKjU9M3k1KcJahPJgi/yxWuL4+8qOJ/niQraArStpyJMuJ7Y5VaB1peeKmmSSH1hpodUs+/hPrEm3P6URBIsaV0BguhxC9lBaf9Ne2kxovyeUgjFc8J3MsF4VlXA45DXS9Efi/W7J3cS/mTiFayOUQ6Tg0OSQG47HAlRBiV7be62RfvIwSSpE/Y3t5KPX3Fas8IcJICwUeM5bVlOB3EkA66KHzBGniUokU2U2KjPClyhvCnt7aSXg96hH5dYpfqAxNad26myb5CmM0hWlTVrWWJN/zt1aTomw14MlFVVWYl6n0ZfnSRY8U7nUi7QISyxnzSE0xP7NsQkDyv51QVg7FFnMtC7Uho/MG2zXbGRXFB4/E0DFTT/Fp88QWfMW8rnXNDqguwyxbzKjbRBep8n6ABscqX8mEIHFxdkEifTGI9BEhSEaIpN8HV+VYS1ksZixgEX+uqomu+I1QHNRPHKqwgGxXcucJkiBLjU1kxAeh4+oy4mUJ7M+CGQqgxoWPYBSrLIrrIGrSmy77PE6IMS9b+mQZkOJ0wZkwUQU0BWjrmpmqGqeASYj1BrXv3nEvN5mOsm1EzQnnrbTJe1kyWqAOpMbXaVji7zgVPDklSDJJcQEn0AnJINtc364AABDeSURBVNk8k6b0Tah60CDC0qWAy09kWE1kCPdUGVHKjDskLS6LzCDs4cKKGqsuDpURLfK+e09PTVXyDZBpz2IxwLUq2nljpKVLOWlayOCLeyjG3m7hq7BVNCf4g9Yszta1wuhkgUYDTWAab/XgM//CWypI1U414GmAOBFBBMrTSFURuxS5xIoWY7ojpBbHyqgqGJcTeg5DXeH2G8umi2ta6KqksrF9ynTlWpvXeYIkls80bZXhWALJzbTwnlZriUyB11QgdhlWg1MAUxP8hUV10DRZp4s07VzXRb8angppnaIQm6KXD99VLb5Cr9ZpxrPsiKx9PBprINIGGctJgkZjiLVXtD1BxoxQifGLZxQSqb+O8yos4PFdgkTodj11piCBkfoMcXEGTuy623PBDLEZm1VwXzjD9IgpcTqTfoWZnxW2KNDSKpk07SUDTYCqzMeIm0zdPCej9px/y7eJa7cby3OmxU9den2uwG/oidUJGHeH8dOSjDL8zDU1oefwOVyGnWlxzly8zaLujOHiGR53AC+SxnvqvCKNNynE/paxbBsAOlWQZOqxBMR3xE4IAC/kGl+eO3ZGn7S0Dqpyu86bPp9UvsU9dupVCbBiD9t4MHO2v04DEkY0Myrdusf7s1zdXM9hUJWpSY5eNGfMlBM4V18F8W6xNpH2vvrvufEeSWzRk/Z+PJ8XueTAH3QcZnf32GnXgPgYQ5kv8dZjiUviYntEb7c4BjZNcI2dOszl46uI8/GxMuLHR/xhrPxz4pJ5GjytyTJME1pTLHtY55ipYxjDewZ1GVFYnAZwr6J6/or/zI6v90+c6HAcKr9YAj0aVi6LVKOAS6OVNfMiPrTPmFHocqvGfbq5DPh3NhTJlbXLsXRp+OfsHjPlIWIs2XvpNk7sbq3nkdd1uDDei27q7gq6fgpGwgF9saEun0TstNDq+QmbvO5zp/6ACH83wIllir8obuev8e7jkdP0ROZZt+U6nfJ0BvqdUb2GiG5XVy8UKjex4Bwz5e+MxfSR9jIgcTWeWC6X+PGMs3FgEA5VE98tYYXSs/Y7Ce2KYW9/JLGy9uMzxeAaM+UmAItNMsWvbhMR1TCGbmBsGChBXSNchIiPU9csfjv8IATJpRgFyQSteZLiDOZi5TORlb6JEx2uQz2eBzDRBFr0e1sIaGKAWHcRDleTeyuFEy7X1ix83aQ8c42Z+iZAyQN5DsImxnCYGCsCkcAbn4JGEG1VCpwj8frchFXHsCCBLBXbTGgwJq1RcvileOeJNvHNiMgq3qmfNr1SZfXCJwD6CUBiCzuqoBW+5wM0mjEInaALQVSamA8fI7o6JkQ6wgQURnQZ4np5cV+6VFN61v4QECcxpiAsAeg8BnGsKZ0OkDsJppokfpGFEAnspICuBgmNxwTcEhgNJ9DFIH4WQLlJ+V84nHRZshCFyU5Ak6GdcVgNRH9WnMGLOlOIBH1dIkiiImX1omclTRpMRP8CkTgcLKIWYX4XAMsckjJYXrNoSZiRCf/inEp6ERk4nIAkIkyrF91KwPkgWpeBJkFvEJweVzTlFHXV4vRbK6sXNijB8LhnNojkDLg1cP60oimjgisXxdRrE6kV46Ks290AoieYREOUNYtvi/XCiQg79KlLPm0pFJ9zfZmDu0dJRAMZWBkYcjhRC2NSHYe2W3Oxj7DyidqUcpEEhtE3p5zkYwGbmPzBIjE2E2/DNOSMnnY8dyjDiUv9GVEJGDwc1MwYqyaOz9QQrceGJwz6maZoUhPHXZfv8uWdzMBOIEZF4PCAMZkxaiKS9ihB7VNsyPDZGXFTd3gkc0/zeo0SU6DlBBPGnXqefbc5YHPA5oDNAZsDNgdsDtgcsDlgc8DmgM2B/2IOdO70vypLpbHfgMAsNMAWr3cVA3m5KHBJDpUrbjVU/ZOhiftQxhdIxPBgdHugymgoZgQSiwBRuOS6dZrTlRWozOD0tKSqkFxHcr541ukxyzMrb4Q3yzfiMYGtnPNxmcPh6a05IBOptdVThh4R/gd8fr+zZtqghA1jIyqreOcJUtUKZ0VZaZIaoTkZDPTY4WlDE87NLZ/98VnMgYdBJBTrk+lsIOBxJjsfq74zUagq5296lMDuAOjL6mlDLT39V8zfLGzizwNhe/X0IWE7tfIFW4YworC+jsBxZNrgP5lRXDZ/2zAJWsROX6IK8RIEXMX8zZZrVADEZulbYDS7eurQtcl4K+dv+juBpdsCET+elzmkB2qmnbqrYsGmM0HsP1E866prBo+GmfAvWeKoqDlpHYARApZJWv/DU4bvFYIEp3MKJMcyaHwSk9h/iNNZkLDy8JTBrfYX0HkaksJevTFbc/NEOSmb+c493Ncw00SAdP6LjVOxgXpN0cx3x3vvuzB8NLzIVJu90SXHRJx6Qf2uNZk4MGlogBbvxP5Q9vDKD2vuGZt6+LD3CLSo+bTDFdc+ydBecZLjJHH1eHjlk0Xd/DNi/gzCvGqwXimNEC1UUX4I4LLSmasurJ4yZF3ZI++9TYDYzzuj1PPezXXAAr19+r3HrvJbNNSHhQiEp2rvHRteOT/8y9NqKudv3nf45kHbK+dv3suBb0sS7bQ431ZHZ3nvXEFqie0R/osBVpahgriYyXPJXf+8UW1uFrvhImgg9qQE+ovsoN0SUXeJO8YDdDcY+gI4iYFeLatacmZN1aRwd8ybG9OsXUexii9Jc2PKi1N8CcIl9JxfLLvz1dNqHr00QT1V8XnBortLQVdcN0jT28uwiRFeErURmBOMyolwHgN0tzW/qGthlaiqukLXrBRlw90ZwyZi/FadUokzDzHWHxQ+gXxM9FDFxWBsuHzfy5MlTlsACCH7fY+7/vVS7R8vP6iXLbr7n/20lkZhXSvCQeZ23R6Nh2/EHOFfurhXTxn0TI+5n/VyuZQ2nQzeqYLEW5rDBDNiLzXM/dEzxkaYxfNufbanFmieE81Tieh7TfN+atxdrwOwoPuU556HUxOm06OE+bTsJ6EOcrcop/mEILHUj2FShao/ApfQbwlr10Rdn54y6AVMXHIhlhp8Nfn94Lp5tRzXDdLbS8DGpjk/Fj1mQiia8dwPSKKnQWG7/Mu6Y8ANjYA4Ph6arzn6AyBv45zrUj8tE5c82b1Sfh8MZwlthILb/9ajceb39hTOeO4exsTGMwo4mOBdTH+L/L7FBOQJ/ER0U9Pc6xLcUFdPHfSyyNPvtTNOEkIYE8QE4jM8dOKm7Xvg/pbwpQUT6LckSWppma75fXnChYvm881MEqJYucaFP2qQW5qv1Py+oIBV/b7pmLgkbCnBhTCIen3W43GBiPuitPnj40rZ543RzH0t6yL0+84rKGp4OFa52IDW6wiXjdejt5f8kR+QsYyIe+f+6B+ar/kKHU7z+2K+BnR6RJ5pWDpJ4wHfy3pZ5m0IH3rcVLpzHve3rImkN19Z8IuFE0T5whsXXsf9LePC6QHfs01zr3vVFG8HJXaeIK0EtIAvfHFfdvucPOC/jAf94EG/JjUHxCnQliHwl2lf8YDveQFPwUB+Qe6RMGPDNvIBP8Q9XRDePXgYzkCbL0KvoFsONF2nBfz7w23wt9yVd+2suPsbAxx88Revt1fcrULLU1NWagHfWwKGB3zHYeKssD6SFvBHeBU00JOERAu09NPrUH0tEUBhdOBr+bkW9IXCeX7/gpxrZvdTg/5ZUdhqhJREJbgkvB3x2KmfNuFoQQQC7s696ndx+3gj5cQOB/5+zzVhuFBAH0Nsb1p2X8aROldCqxiR8NgGlQXCvm4iDhYSPljG2mJx3RFDAqSwdI0m5KpKTUhyToTEhLqIGFE/47n6oZGhF+/fDeE5RJ+fGXT09fZmMpEiObQCROMEMd3dUk4jENA9rOhoY4SGI8Ryr/rdVRQK6jw8EDhViY3bmp+/47Ocqx7+bVTTsi8D1pOqKwiyqc3LbhdDgk4NnStIcszs+NsAxJUaGGIzLpJDUTUJZv5tSCrNQoGm2CxD5eG2cEVB2NVNgoQkFRTCLRxQRIQ8lqkoMiRExzySA4F/3LfWc/n9dzCCGIMUScDfMfG2syCsYnUFfEe8U6doexPc+MSwxyM8FPDq5HFNDSMQ9vnRtJG537s/YWJCeKAnySiPYuAS0XRUzYwPzgAEa+U/egq1qxggLGLCTsEIWBZ6+aF/xGvuvFgnC1LMokboZccekpoTG0CRLItpnnAQlWBZmQQfe+SKfJzehTjIEcbP5IAWES79VcXAEyLCbj86uo2rtMo+ECJGqd6cSHLoXw/NyRn//84i4BoChroV93yZ+eZIPCpAukCFhTPSxJhwJ9RoeAjJ39Z7vmaWG1lrE71hBCRf1GOANkZ3AdIt/jceSTUpWlml8kvuvEHi0kfR09MbXUyaGm+cEU3HxztXkNQoYxmbqrw9K+OsjWvKh4zC6yI9nRf8arT671kfpGsyl5XLWHQ9xy9pYYHkitISnbL1xNgqJ1ZWxc1qDchIkXuLR4osFEZyRE/DotN5g61FMNj8C7fTI16ucFZ6vfAcS4iiNTjEomh79a7FUF08OnZqN9Lk8Kc87CtAzQ2PdWJlgQNEWBYuIIXX+8Xx6WJ6H3CAXxh453FLzynKa49udF902wEAx4Owt+Xd2UKRr0tCvF/uhOp0y1HxKcgqqMoLujGfxJXH0tmaeS6YcRnj2gVheK5+opvvkKp9FcXh9rD68DgkuW7P2NsGQlPFBUlTPzXmx2g2Jq5c0AJSryJV9Yl8qMoMHa4F8cG2nkZW7R1xk8vFnE+TqlYKWK6qr+qCzoVJdsRCd5fy7uxbw9fbs28hTftVND1XVbRnMXGi0Y7fSGU4HqNBGFR2YehUQYrZVGVpe6ZQ6f+Rpn0WNjHm/CxnvuvvOGdGilqt+5zpV3KVP6+bInOu3qPzjHH+cSxd0x5PsaEbO7Ub56GndRgyWskKRw667ZiOMHqX3527nXG6MZavw8XlKF5WX2PScQyqcjvOm36xK9/1PjRtYhSHLCn81zpIePQexpk43FZWzHmCOF8eLkN0rrO6LGErKVZej2g8Qkfiepie22n3Tv20hRsvSM/WG+vKKpWdM/lKgiT2kAoZcJkLfBfOuflVYmy7RCgk4AKCNkIfUDBGv1NWLnpD55C8at4W1zlThJ3bBQAGqhr/zHXOlGWMsb1EVM40+j4BldHx0SbZUZG4viJepggmPzF51bwXXWdPEQ7dZ+j1Jdz1ssBVrjFThG2Z+Mp5CNVl0KKDr0gBlYH9TF6zYFusvGGsFUuLRlSSbnSRuhVAOQMeco2Z8rayeuGGZLjwsxCgRFk0BevoRBN2dWAVuoWI+JVkGeT3F28H8XNB9GnU8qIQhB8yTg8R0Z0gGhFNDxJwl7xqUbKRIxRZuRZE66Nw+SD6KXH+GxBNIaLKaPqnDgddoX9aYuTpNMcSEiOKt/YOEOIWJ8ZsvSyRqPN4cUXrc0TrFJa9G4lwgbx6gbCpiwfx8kX55N5MQLw/t4aDbozicILwNwyamGxnF8EVpyGOuwtindcjrQTHaPqlaAPjDrH7nHVQVi/ehLFVQ1zy4R8zhkkEjARIfOICAO1kkF5zqOr8wLqnzJ1krXuqWhk08WxXUckNIHYNWNiRg1iwFAPyLWBYqgToSSXJIkQOSQdcTjVMM3K1uAWwkfJtS2Vl7M/Gu1T3j8CZBMqLL1VQpL1G8HCcUYARq2ES/zS0+smd+nTRCEdQn2IkrWQSM22TtnrRKxh90wQJ7ATR5eR0L+8ZRHzpJI5L/GBYIQPrsoG2qPv/AxKoa7GH1PhyAAAAAElFTkSuQmCC +# footerLegalText: This legal text will show up in the footer. # footerLegalLinks: -# Terms: /terms -# Privacy Agreement: /privacy.html +# Terms: /exampleTerms +# Privacy Agreement: privacy_example.html # Licensing: http://example.com/ uaa: 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 8083177cd8c..e75f9ebfa99 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 @@ -169,6 +169,22 @@ public void testLogin_When_DisableInternalUserManagement_Is_True() throws Except webApplicationContext.getBean(LoginInfoEndpoint.class).setDisableInternalUserManagement(false); } + @Test + public void testDefaultLogo() throws Exception { + mockEnvironment.setProperty("assetBaseUrl", "//cdn.example.com/resources"); + + getMockMvc().perform(get("/login")) + .andExpect(content().string(containsString("url(//cdn.example.com/resources/images/logo.png)"))); + } + + @Test + public void testCustomLogo() throws Exception { + mockEnvironment.setProperty("login.branding.productLogo","/bASe/64+"); + + getMockMvc().perform(get("/login")) + .andExpect(content().string(allOf(containsString("url(data:image/png;base64,/bASe/64+)"), not(containsString("url(/uaa/resources/oss/images/logo.png)"))))); + } + private static final String cfCopyrightText = "Copyright © CloudFoundry.org Foundation, Inc."; @Test public void testDefaultFooter() throws Exception { From 9df8730284683164e5b2405c60e0f1386d21900d Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 20 Jan 2016 16:18:49 -0700 Subject: [PATCH 14/25] Correct documentation to reflect behavior https://www.pivotaltracker.com/story/show/102530926 [#102530926] --- docs/UAA-APIs.rst | 2 +- .../client/ClientAdminEndpointsValidator.java | 18 ++--- .../identity/uaa/oauth/UaaTokenServices.java | 9 ++- .../ClientAdminEndpointsValidatorTests.java | 72 +++++++++++++++++++ 4 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 1c9319003a7..7c2f9fa39cc 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -32,7 +32,7 @@ Here is a summary of the different scopes that are known to the UAA. * **idps.read** - read only scopes to retrieve identity providers under /identity-providers * **idps.write** - read only scopes to retrieve identity providers under /identity-providers * **clients.admin** - super user scope to create, modify and delete clients -* **clients.write** - scope required to create and modify clients. The scopes/authorities are limited to be prefixed with the scope holder's client id. For example, id:testclient authorities:client.write may create a client that has scopes/authorities that have the 'testclient.' prefix. +* **clients.write** - scope required to create and modify clients. The scopes are limited to be prefixed with the scope holder's client id. For example, id:testclient authorities:client.write may create a client that has scopes that have the 'testclient.' prefix. Authorities are limited to uaa.resource * **clients.read** - scope to read information about clients * **clients.secret** - ``/oauth/clients/*/secret`` endpoint. Scope required to change the password of a client. Considered an admin scope. * **scim.write** - Admin write access to all SCIM endpoints, ``/Users``, ``/Groups/``. diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java index 09b0ff24de0..e9a23b7f2f0 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java @@ -12,12 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.client; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -30,17 +24,23 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.util.Assert; import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public class ClientAdminEndpointsValidator implements InitializingBean, ClientDetailsValidator { private final Log logger = LogFactory.getLog(getClass()); - private static final Set VALID_GRANTS = new HashSet(Arrays.asList("implicit", "password", + private static final Set VALID_GRANTS = new HashSet<>(Arrays.asList("implicit", "password", "client_credentials", "authorization_code", "refresh_token")); - private static final Collection NON_ADMIN_INVALID_GRANTS = new HashSet(Arrays.asList("password")); + private static final Collection NON_ADMIN_INVALID_GRANTS = new HashSet<>(Arrays.asList("password")); - private static final Collection NON_ADMIN_VALID_AUTHORITIES = new HashSet(Arrays.asList("uaa.none")); + private static final Collection NON_ADMIN_VALID_AUTHORITIES = new HashSet<>(Arrays.asList("uaa.none")); private QueryableResourceManager clientDetailsService; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java index 7d8da8bdd61..c8eccc4ace7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java @@ -55,6 +55,7 @@ import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.common.exceptions.UnauthorizedClientException; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.ClientDetails; @@ -1035,7 +1036,13 @@ private Map getClaimsForToken(String token) { String clientId = (String) claims.get(CID); String userId = (String) claims.get(USER_ID); UaaUser user = null; - ClientDetails client = clientDetailsService.loadClientByClientId(clientId); + ClientDetails client; + try { + client = clientDetailsService.loadClientByClientId(clientId); + } catch (NoSuchClientException x) { + //happens if the client is deleted and token exist + throw new UnauthorizedClientException("Invalid client ID "+clientId); + } try { user = userDatabase.retrieveUserById(userId); } catch (UsernameNotFoundException x) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java new file mode 100644 index 00000000000..afdbb133dcf --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java @@ -0,0 +1,72 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.client; + +import org.cloudfoundry.identity.uaa.resources.QueryableResourceManager; +import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; + +import java.util.Arrays; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ClientAdminEndpointsValidatorTests { + + BaseClientDetails client; + BaseClientDetails caller; + ClientAdminEndpointsValidator validator; + + @Before + public void createClient() throws Exception { + client = new BaseClientDetails("newclient","","","client_credentials",""); + client.setClientSecret("secret"); + caller = new BaseClientDetails("caller","","","client_credentials","clients.write"); + validator = new ClientAdminEndpointsValidator(); + } + + @Test + public void testValidate_Should_Allow_Prefix_Names() throws Exception { + QueryableResourceManager clientDetailsService = mock(QueryableResourceManager.class); + SecurityContextAccessor accessor = mock(SecurityContextAccessor.class); + when(accessor.isAdmin()).thenReturn(false); + when(accessor.getScopes()).thenReturn(Arrays.asList("clients.write")); + when(accessor.getClientId()).thenReturn(caller.getClientId()); + when(clientDetailsService.retrieve(eq(caller.getClientId()))).thenReturn(caller); + validator.setClientDetailsService(clientDetailsService); + validator.setSecurityContextAccessor(accessor); + + client.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("uaa.resource"))); + validator.validate(client, true, true); + client.setAuthorities(Arrays.asList(new SimpleGrantedAuthority(caller.getClientId()+".some.other.authority"))); + + try { + validator.validate(client, true, true); + fail(); + } catch (InvalidClientDetailsException x) { + assertTrue(x.getMessage().contains("not an allowed authority")); + } + + + } + +} \ No newline at end of file From 1d26ec3896a3bfc37be189ed43ebf80b09704ebc Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 20 Jan 2016 16:29:03 -0700 Subject: [PATCH 15/25] Add SAML SP metadata API to the docs --- docs/UAA-APIs.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 7c2f9fa39cc..eecbe69f694 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -2904,6 +2904,18 @@ Response Headers :: ================== =============================================== +SAML Service Provider (SP) Metadata: ``GET /saml/metadata`` +----------------------------------------------------------- + +================== =============================================== +Request ``GET /saml/metadata`` +Response Code ``200 - Found`` +Response Headers :: + + Content-Type: text/html;charset=utf-8 + +================== =============================================== + Management Endpoints ==================== From 3af95797d4a1dcf55928272a277e0f6267fdaf76 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 21 Jan 2016 09:22:09 -0700 Subject: [PATCH 16/25] Support nested groups in JdbcUaaUserDatabase https://www.pivotaltracker.com/story/show/104468594 [#104468594] --- .../uaa/user/JdbcUaaUserDatabase.java | 42 ++++++++------- .../uaa/user/JdbcUaaUserDatabaseTests.java | 51 +++++++++++++++++-- .../webapp/WEB-INF/spring/oauth-endpoints.xml | 4 -- .../LoginSamlAuthenticationProviderTests.java | 1 - 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java index e24b632ecb6..0ebe30a2378 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java @@ -12,7 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.user; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; + import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -27,10 +27,10 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; /** @@ -51,7 +51,7 @@ public class JdbcUaaUserDatabase implements UaaUserDatabase { public static final String DEFAULT_USER_BY_EMAIL_AND_ORIGIN_QUERY = "select " + USER_FIELDS + "from users " + "where lower(email)=? and active=? and origin=? and identity_zone_id=?"; - private String userAuthoritiesQuery = null; + private String AUTHORITIES_QUERY = "select g.id,g.displayName from groups g, group_membership m where g.id = m.group_id and m.member_id = ?"; private String userByUserNameQuery = DEFAULT_USER_BY_USERNAME_QUERY; @@ -65,10 +65,6 @@ public void setUserByUserNameQuery(String userByUserNameQuery) { this.userByUserNameQuery = userByUserNameQuery; } - public void setUserAuthoritiesQuery(String userAuthoritiesQuery) { - this.userAuthoritiesQuery = userAuthoritiesQuery; - } - public void setDefaultAuthorities(Set defaultAuthorities) { this.defaultAuthorities = defaultAuthorities; } @@ -133,13 +129,9 @@ public UaaUser mapRow(ResultSet rs, int rowNum) throws SQLException { .withLegacyVerificationBehavior(rs.getBoolean(17)) ; - if (userAuthoritiesQuery == null) { - return new UaaUser(prototype); - } else { - List authorities = AuthorityUtils - .commaSeparatedStringToAuthorityList(getAuthorities(id)); - return new UaaUser(prototype.withAuthorities(authorities)); - } + List authorities = + AuthorityUtils.commaSeparatedStringToAuthorityList(getAuthorities(id)); + return new UaaUser(prototype.withAuthorities(authorities)); } private List getDefaultAuthorities(String defaultAuth) { @@ -151,14 +143,26 @@ private List getDefaultAuthorities(String defaultAuth) { } private String getAuthorities(final String userId) { - List authorities; + Set authorities = new HashSet<>(); + getAuthorities(authorities, userId); + authorities.addAll(defaultAuthorities); + return StringUtils.collectionToCommaDelimitedString(new HashSet<>(authorities)); + } + + protected void getAuthorities(Set authorities, final String memberId) { + List> results; try { - authorities = jdbcTemplate.queryForList(userAuthoritiesQuery, String.class, userId); + results = jdbcTemplate.queryForList(AUTHORITIES_QUERY, memberId); + for (Map record : results) { + String displayName = (String)record.get("displayName"); + String groupId = (String)record.get("id"); + if (!authorities.contains(displayName)) { + authorities.add(displayName); + getAuthorities(authorities, groupId); + } + } } catch (EmptyResultDataAccessException ex) { - authorities = Collections. emptyList(); } - authorities.addAll(defaultAuthorities); - return StringUtils.collectionToCommaDelimitedString(new HashSet(authorities)); } } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java index 91e8a264e25..765c7b41008 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java @@ -23,16 +23,19 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; import java.util.UUID; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -58,6 +61,8 @@ public class JdbcUaaUserDatabaseTests extends JdbcTestBase { private JdbcTemplate template; + public static final String ADD_GROUP_SQL = "insert into groups (id, displayName, identity_zone_id) values (?,?,?)"; + public static final String ADD_MEMBER_SQL = "insert into group_membership (group_id, member_id, member_type, authorities) values (?,?,?,?)"; private void addUser(String id, String name, String password) { TestUtils.assertNoSuchUser(template, "id", id); @@ -66,9 +71,9 @@ private void addUser(String id, String name, String password) { } private void addAuthority(String authority, String userId) { - String authorities = template.queryForObject(getAuthoritiesSql, String.class, userId); - authorities = authorities == null ? authority : authorities + "," + authority; - template.update(addAuthoritySql, authorities, userId); + String id = new RandomValueStringGenerator().generate(); + jdbcTemplate.update(ADD_GROUP_SQL, id, authority, IdentityZoneHolder.get().getId()); + jdbcTemplate.update(ADD_MEMBER_SQL, id, userId, "USER", "MEMBER"); } @Before @@ -158,6 +163,46 @@ public void getUserWithExtraAuthorities() { joe.getAuthorities().contains(new SimpleGrantedAuthority("dash.admin"))); } + @Test + public void getUserWithNestedAuthoritiesWorks() { + UaaUser joe = db.retrieveUserByName("joe", OriginKeys.UAA); + assertThat(joe.getAuthorities(), + containsInAnyOrder( + new SimpleGrantedAuthority("uaa.user") + ) + ); + + String directId = new RandomValueStringGenerator().generate(); + String indirectId = new RandomValueStringGenerator().generate(); + + jdbcTemplate.update(ADD_GROUP_SQL, directId, "direct", IdentityZoneHolder.get().getId()); + jdbcTemplate.update(ADD_GROUP_SQL, indirectId, "indirect", IdentityZoneHolder.get().getId()); + jdbcTemplate.update(ADD_MEMBER_SQL, indirectId, directId, "GROUP", "MEMBER"); + jdbcTemplate.update(ADD_MEMBER_SQL, directId, joe.getId(), "USER", "MEMBER"); + + + evaluateNestedJoe(); + + //add a circular group + jdbcTemplate.update(ADD_MEMBER_SQL, directId, indirectId, "GROUP", "MEMBER"); + + evaluateNestedJoe(); + } + + protected void evaluateNestedJoe() { + UaaUser joe; + joe = db.retrieveUserByName("joe", OriginKeys.UAA); + + assertThat(joe.getAuthorities(), + containsInAnyOrder( + new SimpleGrantedAuthority("direct"), + new SimpleGrantedAuthority("uaa.user"), + new SimpleGrantedAuthority("indirect") + ) + ); + } + + @Test(expected = UsernameNotFoundException.class) public void getValidUserInDefaultZoneFromOtherZoneFails() { IdentityZoneHolder.set(otherIdentityZone); diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index d547bb484bb..cede0c790c1 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -376,10 +376,6 @@ - - select g.displayName from groups g, group_membership m where g.id = m.group_id and m.member_id = ? - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java index aac84c258e9..e0f10de0840 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java @@ -215,7 +215,6 @@ public void configureProvider() throws Exception { when(consumer.processAuthenticationResponse(anyObject())).thenReturn(credential); userDatabase = new JdbcUaaUserDatabase(jdbcTemplate); - userDatabase.setUserAuthoritiesQuery("select g.displayName from groups g, group_membership m where g.id = m.group_id and m.member_id = ?"); userDatabase.setDefaultAuthorities(new HashSet<>(Arrays.asList(UaaAuthority.UAA_USER.getAuthority()))); providerProvisioning = new JdbcIdentityProviderProvisioning(jdbcTemplate); publisher = new CreateUserPublisher(bootstrap); From ec2bd14448f64fdd8db3ecc81be4969968258599 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 19 Jan 2016 10:30:27 -0800 Subject: [PATCH 17/25] Database changes for new client metadata assets - Added new table definitions for all databases - Added database access API and implementation [#109263482] https://www.pivotaltracker.com/story/show/109263482 Signed-off-by: Jonathan Lo --- .../identity/uaa/client/ClientMetadata.java | 83 +++++++ .../client/ClientMetadataAdminEndpoints.java | 67 ++++++ .../uaa/client/ClientMetadataException.java | 51 +++++ .../ClientMetadataNotFoundException.java | 36 ++++ .../client/ClientMetadataProvisioning.java | 18 ++ .../uaa/client/ClientNotFoundException.java | 16 ++ .../JdbcClientMetadataProvisioning.java | 186 ++++++++++++++++ ..._New_Table_For_Storing_Client_Metadata.sql | 11 + ..._New_Table_For_Storing_Client_Metadata.sql | 11 + ..._New_Table_For_Storing_Client_Metadata.sql | 11 + .../JdbcClientMetadataProvisioningTest.java | 202 ++++++++++++++++++ ...ientMetadataAdminEndpointsMockMvcTest.java | 130 +++++++++++ 12 files changed, 822 insertions(+) create mode 100644 model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java new file mode 100644 index 00000000000..1342af2d8c5 --- /dev/null +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java @@ -0,0 +1,83 @@ +package org.cloudfoundry.identity.uaa.client; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.net.URL; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ClientMetadata { + + private String clientId; + private String identityZoneId; + private boolean showOnHomePage; + private URL appLaunchUrl; + private String appIcon; + private int version; + + @JsonIgnore + public String getClientId() { + return clientId; + } + + @JsonIgnore + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @JsonIgnore + public String getIdentityZoneId() { + return identityZoneId; + } + + @JsonIgnore + public void setIdentityZoneId(String identityZoneId) { + this.identityZoneId = identityZoneId; + } + + public boolean isShowOnHomePage() { + return showOnHomePage; + } + + public void setShowOnHomePage(boolean showOnHomePage) { + this.showOnHomePage = showOnHomePage; + } + + public URL getAppLaunchUrl() { + return appLaunchUrl; + } + + public void setAppLaunchUrl(URL appLaunchUrl) { + this.appLaunchUrl = appLaunchUrl; + } + + public String getAppIcon() { + return appIcon; + } + + public void setAppIcon(String appIcon) { + this.appIcon = appIcon; + } + + @JsonIgnore + public int getVersion() { + return version; + } + + @JsonIgnore + public void setVersion(int version) { + this.version = version; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java new file mode 100644 index 00000000000..047518fa56a --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java @@ -0,0 +1,67 @@ +package org.cloudfoundry.identity.uaa.client; + +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.NoSuchClientException; +import org.springframework.stereotype.Controller; +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.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +@Controller +public class ClientMetadataAdminEndpoints { + + private ClientMetaDetailsProvisioning clientMetaDetailsProvisioning; + private ClientDetailsService clients; + + @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public ClientMetaDetails createClientUIDetails(@RequestBody ClientMetaDetails clientMetaDetails, + @PathVariable("client") String clientId) + throws ClientNotFoundException { + + try { + clients.loadClientByClientId(clientId); + } catch (NoSuchClientException nsce) { + throw new ClientNotFoundException(clientId); + } + + clientMetaDetails.setClientId(clientId); + return clientMetaDetailsProvisioning.create(clientMetaDetails); + } + + // GET + @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ClientMetaDetails retrieveClientUIDetails(@PathVariable("client") String clientId) { + return null; + } + + // PUT (Update) + + // DELETE + + // GET (retrieveAll) + + public void setClientMetaDetailsProvisioning(ClientMetaDetailsProvisioning clientMetaDetailsProvisioning) { + this.clientMetaDetailsProvisioning = clientMetaDetailsProvisioning; + } + + public void setClients(ClientDetailsService clients) { + this.clients = clients; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java new file mode 100644 index 00000000000..72e7e643858 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.client; + +import org.springframework.http.HttpStatus; + +import java.util.Map; + +/** + * @author Luke Taylor + * @author Dave Syer + */ +public class ClientMetadataException extends RuntimeException { + + private final HttpStatus status; + protected Map extraInfo; + + public ClientMetadataException(String message, Throwable cause, HttpStatus status) { + super(message, cause); + this.status = status; + } + + public ClientMetadataException(String message, HttpStatus status) { + super(message); + this.status = status; + } + + public ClientMetadataException(String message, HttpStatus status, Map extraInformation) { + super(message); + this.status = status; + this.extraInfo = extraInformation; + } + + public HttpStatus getStatus() { + return status; + } + + public Map getExtraInfo() { + return extraInfo; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java new file mode 100644 index 00000000000..4f0f270b547 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java @@ -0,0 +1,36 @@ +package org.cloudfoundry.identity.uaa.client; + +import org.cloudfoundry.identity.uaa.error.UaaException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Client not found") +public class ClientMetadataNotFoundException extends UaaException { + +// private String clientId; + +// public ClientNotFoundException(String clientId) { +// this.clientId = clientId; +// } + + public ClientMetadataNotFoundException(String msg) { + super("client_not_found", msg, HttpStatus.NOT_FOUND.value()); + } + + +// public String getClientId() { +// return this.clientId; +// } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java new file mode 100644 index 00000000000..0da94461264 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java @@ -0,0 +1,18 @@ +package org.cloudfoundry.identity.uaa.client; + +import org.cloudfoundry.identity.uaa.resources.ResourceManager; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface ClientMetadataProvisioning extends ResourceManager { +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java new file mode 100644 index 00000000000..51ff475bdaa --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java @@ -0,0 +1,16 @@ +package org.cloudfoundry.identity.uaa.client; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public class ClientNotFoundException { +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java new file mode 100644 index 00000000000..d3e7d20f7fc --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java @@ -0,0 +1,186 @@ +package org.cloudfoundry.identity.uaa.client; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.provider.IdpAlreadyExistsException; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.util.Assert; + +import java.io.ByteArrayInputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public class JdbcClientMetadataProvisioning implements ClientMetadataProvisioning { + + private static final Log logger = LogFactory.getLog(JdbcClientMetadataProvisioning.class); + + private static final String CLIENT_UI_DETAILS_FIELDS = "id, client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; + private static final String CLIENT_UI_DETAILS_QUERY = "select " + CLIENT_UI_DETAILS_FIELDS + " from oauth_client_ui_details where client_id=? and identity_zone_id=?"; + private static final String CLIENT_UI_DETAILS_CREATE = "insert into oauth_client_ui_details(" + CLIENT_UI_DETAILS_FIELDS + ") values (?,?,?,?,?,?,?)"; + private static final String CLIENT_UI_DETAILS_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon, version"; + private static final String CLIENT_UI_DETAILS_UPDATE = "update oauth_client_ui_details set " + CLIENT_UI_DETAILS_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; + private static final String CLIENT_UI_DETAILS_DELETE_QUERY = "delete from oauth_client_ui_details where client_id=? and identity_zone_id=?"; + + private JdbcTemplate template; + private final RowMapper mapper = new ClientUIDetailsRowMapper(); + + JdbcClientMetadataProvisioning(JdbcTemplate template) { + Assert.notNull(template); + this.template = template; + } + + public void setTemplate(JdbcTemplate template) { + this.template = template; + } + + @Override + public List retrieveAll() { + logger.debug("Retrieving UI details for all client"); + return template.query(CLIENT_UI_DETAILS_QUERY, mapper, IdentityZoneHolder.get().getId()); + } + + @Override + public ClientMetaDetails retrieve(String clientId) { + logger.debug("Retrieving UI details for client: " + clientId); + return template.queryForObject(CLIENT_UI_DETAILS_QUERY, mapper, clientId, IdentityZoneHolder.get().getId()); + } + + @Override + public ClientMetaDetails create(ClientMetaDetails resource) { + logger.debug("Creating new UI details for client: " + resource.getClientId()); + final String id = UUID.randomUUID().toString(); + try { + template.update(CLIENT_UI_DETAILS_CREATE, new PreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps) throws SQLException { + int pos = 1; + ps.setString(pos++, id); + ps.setString(pos++, resource.getClientId()); + ps.setString(pos++, IdentityZoneHolder.get().getId()); + ps.setBoolean(pos++, resource.isShowOnHomePage()); + URL appLaunchUrl = resource.getAppLaunchUrl(); + ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); + String appIcon = resource.getAppIcon(); + if (appIcon != null) { + byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); + ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); + } else { + ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); + } +// pos++; + ps.setInt(pos++, 1); + } + }); + } catch (DuplicateKeyException e) { + throw new IdpAlreadyExistsException(e.getMostSpecificCause().getMessage()); + } + return retrieve(resource.getClientId()); + } + + @Override + public ClientMetaDetails update(String clientId, ClientMetaDetails resource) { + logger.debug("Updating UI details for client: " + clientId); + int updated = template.update(CLIENT_UI_DETAILS_UPDATE, new PreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps) throws SQLException { + int pos = 1; + ps.setBoolean(pos++, resource.isShowOnHomePage()); + URL appLaunchUrl = resource.getAppLaunchUrl(); + ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); + String appIcon = resource.getAppIcon(); + if (appIcon != null) { + byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); + ps.setBinaryStream(pos, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); + } + pos++; + ps.setInt(pos++, resource.getVersion() + 1); + ps.setString(pos++, clientId); + ps.setString(pos++, IdentityZoneHolder.get().getId()); + ps.setInt(pos++, resource.getVersion()); + } + }); + + ClientMetaDetails resultingClientMetaDetails = retrieve(clientId); + + if (updated == 0) { + throw new OptimisticLockingFailureException(String.format( + "Attempt to update the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", + clientId, + resultingClientMetaDetails.getVersion(), + resource.getVersion())); + } else if (updated > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return resultingClientMetaDetails; + } + + @Override + public ClientMetaDetails delete(String clientId, int version) { + logger.debug("Deleting UI details for client: " + clientId); + ClientMetaDetails clientMetaDetails = retrieve(clientId); + int updated; + + if (version < 0) { + updated = template.update(CLIENT_UI_DETAILS_DELETE_QUERY, clientId, IdentityZoneHolder.get().getId()); + } else { + updated = template.update(CLIENT_UI_DETAILS_DELETE_QUERY + " and version=?", clientId, IdentityZoneHolder.get().getId(), version); + } + + if (updated == 0) { + throw new OptimisticLockingFailureException(String.format( + "Attempt to delete the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", + clientId, + clientMetaDetails.getVersion(), + version)); + } + + return clientMetaDetails; + } + + + private class ClientUIDetailsRowMapper implements RowMapper { + + @Override + public ClientMetaDetails mapRow(ResultSet rs, int rowNum) throws SQLException { + ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); + int pos = 1; + pos++; // id + clientMetaDetails.setClientId(rs.getString(pos++)); + clientMetaDetails.setIdentityZoneId(rs.getString(pos++)); + clientMetaDetails.setShowOnHomePage(rs.getBoolean(pos++)); + try { + clientMetaDetails.setAppLaunchUrl(new URL(rs.getString(pos++))); + } catch (MalformedURLException mue) { + // it is safe to ignore this as client_meta_details rows are always created from a ClientMetaDetails instance whose launch url property is strongly typed to URL + } + clientMetaDetails.setAppIcon(new String(Base64.encode(rs.getBytes(pos++)))); + clientMetaDetails.setVersion(rs.getInt(pos++)); + return clientMetaDetails; + } + } +} diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql new file mode 100644 index 00000000000..31ec4fd60c4 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql @@ -0,0 +1,11 @@ +CREATE TABLE oauth_client_metadata ( + id VARCHAR(255) NOT NULL, + client_id VARCHAR(255) NOT NULL, + identity_zone_id VARCHAR(36) NOT NULL, + show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, + app_launch_url VARCHAR(1024), + app_icon BLOB, + version INT DEFAULT 0 NOT NULL, + PRIMARY KEY (id), + CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) +); \ No newline at end of file diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql new file mode 100644 index 00000000000..261d5bcc6a4 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql @@ -0,0 +1,11 @@ +CREATE TABLE oauth_client_metadata ( + id VARCHAR(255) NOT NULL, + client_id VARCHAR(255) NOT NULL, + identity_zone_id VARCHAR(36) NOT NULL, + show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, + app_launch_url VARCHAR(1024), + app_icon MEDIUMBLOB, + version INT DEFAULT 0 NOT NULL, + PRIMARY KEY (id), + CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) +); \ No newline at end of file diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql new file mode 100644 index 00000000000..fc9271862bf --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql @@ -0,0 +1,11 @@ +CREATE TABLE oauth_client_metadata ( + id VARCHAR(256) NOT NULL, + client_id VARCHAR(256) NOT NULL, + identity_zone_id VARCHAR(36) NOT NULL, + show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, + app_launch_url VARCHAR(1024), + app_icon BYTEA, + version INT DEFAULT 0 NOT NULL, + PRIMARY KEY (id), + CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) +); \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java new file mode 100644 index 00000000000..ed226ed59fb --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -0,0 +1,202 @@ +package org.cloudfoundry.identity.uaa.client; + +import org.apache.xml.security.utils.Base64; +import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public class JdbcClientMetadataProvisioningTest extends JdbcTestBase { + + JdbcClientMetadataProvisioning db; + + private RandomValueStringGenerator generator = new RandomValueStringGenerator(8); + + @Before + public void create_datasource() throws Exception { + db = new JdbcClientMetadataProvisioning(jdbcTemplate); + + // When running hsqldb uncomment these lines to invoke the in-built UI client +// org.hsqldb.util.DatabaseManagerSwing.main(new String[] { +// "--url", "jdbc:hsqldb:mem:uaadb", "--noexit" +// }); + } +// +// @Override +// public void setUp() throws Exception { +// System.setProperty("spring.profiles.active", "postgresql"); +// super.setUp(); +// } + + @Test + public void create_clientUIDetails() throws Exception { + //given + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + + //when a client ui details object is saved + ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + + //then + assertThat(createdClientMetaDetails.getClientId(), is(clientMetaDetails.getClientId())); + assertThat(createdClientMetaDetails.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); + assertThat(createdClientMetaDetails.isShowOnHomePage(), is(clientMetaDetails.isShowOnHomePage())); + assertThat(createdClientMetaDetails.getAppLaunchUrl(), is(clientMetaDetails.getAppLaunchUrl())); + assertThat(createdClientMetaDetails.getAppIcon(), is(clientMetaDetails.getAppIcon())); + assertThat(createdClientMetaDetails.getVersion(), is(1)); + + //and then app icon that is saved is really the base64 decoded bytes + byte[] blobbyblob = jdbcTemplate.queryForObject("select app_icon from oauth_client_ui_details where client_id='" + clientId + "'", byte[].class); + assertThat(blobbyblob, is(Base64.decode(base64EncodedImg))); + } + + @Test + public void when_multipleClients_with_theSameNameButDifferentZone_clientUIDetails_correctlyAssociated() throws Exception { + try { + //given + String clientId = generator.generate(); + String otherZoneId = generator.generate(); + IdentityZone otherZone = new IdentityZone(); + otherZone.setId(otherZoneId); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + otherZoneId + "')"); + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + IdentityZoneHolder.set(otherZone); + + //when a client is created in another zone + ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + + //then expect as such + assertThat(createdClientMetaDetails.getIdentityZoneId(), is(otherZoneId)); + } finally { + IdentityZoneHolder.set(IdentityZone.getUaa()); + } + } + + @Test(expected = DataIntegrityViolationException.class) + public void constraintViolation_when_noMatchingClientFound() throws Exception { + //given there is no oauth_client_details record + + //when we attempt to create an client ui details record + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(generator.generate(), true, new URL("http://app.launch/url"), base64EncodedImg); + db.create(clientMetaDetails); + + //then we expect a constraint violation + } + + @Test + public void retrieve_ClientUIDetails() throws Exception { + //given + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + + //when retrieving the client UI details + ClientMetaDetails retrievedClientMetaDetails = db.retrieve(createdClientMetaDetails.getClientId()); + + //then + assertThat(retrievedClientMetaDetails.getClientId(), is(clientMetaDetails.getClientId())); + assertThat(retrievedClientMetaDetails.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); + assertThat(retrievedClientMetaDetails.isShowOnHomePage(), is(clientMetaDetails.isShowOnHomePage())); + assertThat(retrievedClientMetaDetails.getAppLaunchUrl(), is(clientMetaDetails.getAppLaunchUrl())); + assertThat(retrievedClientMetaDetails.getAppIcon(), is(clientMetaDetails.getAppIcon())); + } + + @Test + public void update_ClientUIDetails() throws Exception { + //given + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + ClientMetaDetails newClientMetaDetails = createTestClientUIDetails(clientMetaDetails.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); + + //when + ClientMetaDetails updatedClientMetaDetails = db.update(createdClientMetaDetails.getClientId(), newClientMetaDetails); + try { + db.update(createdClientMetaDetails.getClientId(), newClientMetaDetails); + fail("another update should fail due to incorrect version"); + } catch (OptimisticLockingFailureException olfe) {} + + //then + assertThat(updatedClientMetaDetails.getClientId(), is(clientMetaDetails.getClientId())); + assertThat(updatedClientMetaDetails.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); + assertThat(updatedClientMetaDetails.isShowOnHomePage(), is(newClientMetaDetails.isShowOnHomePage())); + assertThat(updatedClientMetaDetails.getAppLaunchUrl(), is(newClientMetaDetails.getAppLaunchUrl())); + assertThat(updatedClientMetaDetails.getAppIcon(), is(newClientMetaDetails.getAppIcon())); + assertThat(updatedClientMetaDetails.getVersion(), is(clientMetaDetails.getVersion() + 1)); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void delete_ClientUIDetails() throws Exception { + //given + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + db.create(clientMetaDetails); + + //when you delete the client ui details + db.delete(clientMetaDetails.getClientId(), -1); + + //then subsequent retrieval should fail + db.retrieve(clientMetaDetails.getClientId()); + } + + @Test + public void deletion_after_version_update() throws Exception { + //given + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + ClientMetaDetails newClientMetaDetails = createTestClientUIDetails(clientMetaDetails.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); + db.update(createdClientMetaDetails.getClientId(), newClientMetaDetails); + + //when you delete + try { + db.delete(clientId, clientMetaDetails.getVersion()); + fail("should fail because wrong version"); + } catch (OptimisticLockingFailureException olfe) {} + + //then succeed with right version + db.delete(clientId, clientMetaDetails.getVersion() + 1); + } + + private ClientMetaDetails createTestClientUIDetails(String clientId, boolean showOnHomePage, URL appLaunchUrl, String appIcon) throws MalformedURLException { + ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); + clientMetaDetails.setClientId(clientId); + clientMetaDetails.setShowOnHomePage(showOnHomePage); + clientMetaDetails.setAppLaunchUrl(appLaunchUrl); + clientMetaDetails.setAppIcon(appIcon); + clientMetaDetails.setVersion(1); + return clientMetaDetails; + } + + private static final String base64EncodedImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAXRQTFRFAAAAOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ozk4Ojo6Ojk5NkZMFp/PFqDPNkVKOjo6Ojk5MFhnEq3nEqvjEqzjEbDpMFdlOjo5Ojo6Ojo6Ozg2GZ3TFqXeFKfgF6DVOjo6Ozg2G5jPGZ7ZGKHbGZvROjo6Ojo5M1FfG5vYGp3aM1BdOjo6Ojo6Ojk4KHWeH5PSHpTSKHSbOjk4Ojo6Ojs8IY/QIY/QOjs7Ojo6Ojo6Ozc0JYfJJYjKOzYyOjo5Ozc0KX7AKH/AOzUxOjo5Ojo6Ojo6Ojo6Ojs8LHi6LHi6Ojs7Ojo6Ojo6Ojo6Ojo6Ojo6L3K5L3S7LnW8LnS7Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6NlFvMmWeMmaeNVJwOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojk5Ojk4Ojk4Ojk5Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6FaXeFabfGZ/aGKDaHJnVG5rW////xZzURgAAAHV0Uk5TAAACPaXbAVzltTa4MykoM5HlPY/k5Iw85QnBs2D7+lzAtWD7+lyO6EKem0Ey47Mx2dYvtVZVop5Q2i4qlZAnBiGemh0EDXuddqypcHkShPJwYufmX2rvihSJ+qxlg4JiqP2HPtnW1NjZ2svRVAglGTi91RAXr3/WIQAAAAFiS0dEe0/StfwAAAAJcEhZcwAAAEgAAABIAEbJaz4AAADVSURBVBjTY2BgYGBkYmZhZWVhZmJkAANGNnYODk5ODg52NrAIIyMXBzcPLx8/NwcXIyNYQEBQSFhEVExcQgAiICklLSNbWiYnLy0lCRFQUFRSLq9QUVVUgAgwqqlraFZWaWmrqzFCTNXR1dM3MDQy1tWB2MvIaMJqamZuYWnCCHeIlbWNrZ0VG5QPFLF3cHRydoErcHVz9/D08nb3kYSY6evnHxAYFBwSGhYeAbbWNzIqOiY2Lj4hMckVoiQ5JTUtPSMzKzsH6pfcvPyCwqKc4pJcoAAA2pghnaBVZ0kAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTAtMDhUMTI6NDg6MDkrMDA6MDDsQS6eAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTEwLTA4VDEyOjQ4OjA5KzAwOjAwnRyWIgAAAEZ0RVh0c29mdHdhcmUASW1hZ2VNYWdpY2sgNi43LjgtOSAyMDE0LTA1LTEyIFExNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9yZ9yG7QAAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6aGVpZ2h0ADE5Mg8AcoUAAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMTky06whCAAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNDQ0MzA4NDg5qdC9PQAAAA90RVh0VGh1bWI6OlNpemUAMEJClKI+7AAAAFZ0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL21udGxvZy9mYXZpY29ucy8yMDE1LTEwLTA4LzJiMjljNmYwZWRhZWUzM2ViNmM1Mzg4ODMxMjg3OTg1Lmljby5wbmdoJKG+AAAAAElFTkSuQmCC"; + +} \ No newline at end of file diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java new file mode 100644 index 00000000000..7ff82293d83 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java @@ -0,0 +1,130 @@ +package org.cloudfoundry.identity.uaa.client; + +import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; +import org.cloudfoundry.identity.uaa.test.TestClient; +import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

    + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

    + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public class ClientMetadataAdminEndpointsMockMvcTest extends InjectedMockContextTest { + + private JdbcClientMetaDetailsProvisioning clientUIs; + private String adminClientTokenWithWrite; + private JdbcClientDetailsService clients; + private RandomValueStringGenerator generator = new RandomValueStringGenerator(8); + private TestClient testClient; + private UaaTestAccounts testAccounts; + private String adminClientTokenWithRead; + + @Before + public void setUp() throws Exception { + testClient = new TestClient(getMockMvc()); + testAccounts = UaaTestAccounts.standard(null); + adminClientTokenWithRead = testClient.getClientCredentialsOAuthAccessToken( + testAccounts.getAdminClientId(), + testAccounts.getAdminClientSecret(), + "clients.read"); + adminClientTokenWithWrite = testClient.getClientCredentialsOAuthAccessToken( + testAccounts.getAdminClientId(), + testAccounts.getAdminClientSecret(), + "clients.write"); + + clientUIs = getWebApplicationContext().getBean(JdbcClientMetaDetailsProvisioning.class); + clients = getWebApplicationContext().getBean(JdbcClientDetailsService.class); + } + + @Test + public void create_IsCreated() throws Exception { + String clientId = generator.generate(); + + ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); + clientMetaDetails.setClientId(clientId); + + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + + MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithWrite) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientMetaDetails)); + getMockMvc().perform(createClientPost).andExpect(status().isCreated()); + } + + @Test + public void create_noClient() throws Exception { + String clientId = generator.generate(); + ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); + clientMetaDetails.setClientId(clientId); + MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithWrite) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientMetaDetails)); + getMockMvc().perform(createClientPost).andExpect(status().isNotFound()); + } + + @Test + public void create_unauthorizedBecauseInsufficientScope() throws Exception { + // given a token with insufficient privileges + String userToken = testClient.getUserOAuthAccessToken( + "app", + "appclientsecret", + testAccounts.getUserName(), + testAccounts.getPassword(), + "openid"); + + // when a new client is created + String clientId = generator.generate(); + ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); + clientMetaDetails.setClientId(clientId); + MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + userToken) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientMetaDetails)); + MvcResult result = getMockMvc().perform(createClientPost).andReturn(); + + // then expect a 403 Forbidden + assertThat(result.getResponse().getStatus(), is(HttpStatus.FORBIDDEN.value())); + } + + @Test + public void get_client() throws Exception { + String clientId = generator.generate(); + + MockHttpServletRequestBuilder createClientPost = get("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithRead) + .accept(APPLICATION_JSON); + MvcResult result = getMockMvc().perform(createClientPost).andReturn(); + + // TEMP + assertThat(result.getResponse().getStatus(), is(HttpStatus.OK.value())); + assertThat(result.getResponse().getContentAsString(), is(1)); + } +} \ No newline at end of file From fa1984088eb5c6dad7863ea538cafe90bc9af7d2 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Tue, 19 Jan 2016 10:36:11 -0800 Subject: [PATCH 18/25] WIP - Added create, retrieve, and update endpoints TODO: version mismatch should return 412, delete, and retrieve all (update security) --- .../client/ClientMetadataAdminEndpoints.java | 34 ++-- .../uaa/client/ClientMetadataException.java | 3 +- .../ClientMetadataNotFoundException.java | 19 +-- .../client/ClientMetadataProvisioning.java | 2 +- .../uaa/client/ClientNotFoundException.java | 14 +- .../JdbcClientMetadataProvisioning.java | 83 +++++----- .../uaa/scim/bootstrap/ScimUserBootstrap.java | 1 - .../JdbcClientMetadataProvisioningTest.java | 120 +++++++------- .../WEB-INF/spring/client-admin-endpoints.xml | 5 + .../webapp/WEB-INF/spring/oauth-clients.xml | 4 + ...ientMetadataAdminEndpointsMockMvcTest.java | 147 ++++++++++++------ 11 files changed, 257 insertions(+), 175 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java index 047518fa56a..b236bffe562 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; @@ -25,40 +26,51 @@ @Controller public class ClientMetadataAdminEndpoints { - private ClientMetaDetailsProvisioning clientMetaDetailsProvisioning; + private ClientMetadataProvisioning clientMetadataProvisioning; private ClientDetailsService clients; @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) - public ClientMetaDetails createClientUIDetails(@RequestBody ClientMetaDetails clientMetaDetails, - @PathVariable("client") String clientId) + public ClientMetadata createClientMetadata(@RequestBody ClientMetadata clientMetadata, + @PathVariable("client") String clientId) throws ClientNotFoundException { - try { clients.loadClientByClientId(clientId); } catch (NoSuchClientException nsce) { - throw new ClientNotFoundException(clientId); + throw new ClientNotFoundException(clientId ); } - clientMetaDetails.setClientId(clientId); - return clientMetaDetailsProvisioning.create(clientMetaDetails); + clientMetadata.setClientId(clientId); + return clientMetadataProvisioning.create(clientMetadata); } // GET @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) - public ClientMetaDetails retrieveClientUIDetails(@PathVariable("client") String clientId) { - return null; + public ClientMetadata retrieveClientMetadata(@PathVariable("client") String clientId) { + return clientMetadataProvisioning.retrieve(clientId); } // PUT (Update) + @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + public ClientMetadata updateClientMetadata(@RequestBody ClientMetadata clientMetadata, + @RequestHeader(value = "If-Match", required = false) Integer etag, + @PathVariable("client") String clientId) { + if (etag == null) { + throw new ClientMetadataException("Missing If-Match header", HttpStatus.PRECONDITION_FAILED); + } + + clientMetadata.setVersion(etag); + return clientMetadataProvisioning.update(clientId, clientMetadata); + } // DELETE // GET (retrieveAll) - public void setClientMetaDetailsProvisioning(ClientMetaDetailsProvisioning clientMetaDetailsProvisioning) { - this.clientMetaDetailsProvisioning = clientMetaDetailsProvisioning; + public void setClientMetadataProvisioning(ClientMetadataProvisioning clientMetadataProvisioning) { + this.clientMetadataProvisioning = clientMetadataProvisioning; } public void setClients(ClientDetailsService clients) { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java index 72e7e643858..cf331b2cd97 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.client; +import org.cloudfoundry.identity.uaa.error.UaaException; import org.springframework.http.HttpStatus; import java.util.Map; @@ -20,7 +21,7 @@ * @author Luke Taylor * @author Dave Syer */ -public class ClientMetadataException extends RuntimeException { +public class ClientMetadataException extends UaaException { private final HttpStatus status; protected Map extraInfo; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java index 4f0f270b547..81135379abd 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java @@ -1,6 +1,5 @@ package org.cloudfoundry.identity.uaa.client; -import org.cloudfoundry.identity.uaa.error.UaaException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @@ -16,21 +15,11 @@ * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ -@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Client not found") -public class ClientMetadataNotFoundException extends UaaException { +@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Client metadata not found") +public class ClientMetadataNotFoundException extends ClientMetadataException { -// private String clientId; - -// public ClientNotFoundException(String clientId) { -// this.clientId = clientId; -// } - - public ClientMetadataNotFoundException(String msg) { - super("client_not_found", msg, HttpStatus.NOT_FOUND.value()); + public ClientMetadataNotFoundException(String clientId) { + super("No existing metadata found for client " + clientId, HttpStatus.NOT_FOUND); } - -// public String getClientId() { -// return this.clientId; -// } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java index 0da94461264..1cf006d4019 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java @@ -14,5 +14,5 @@ * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ -public interface ClientMetadataProvisioning extends ResourceManager { +public interface ClientMetadataProvisioning extends ResourceManager { } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java index 51ff475bdaa..4c7d971485f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java @@ -1,5 +1,9 @@ package org.cloudfoundry.identity.uaa.client; +import org.cloudfoundry.identity.uaa.error.UaaException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + /******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. @@ -12,5 +16,13 @@ * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ -public class ClientNotFoundException { +@ResponseStatus(value = HttpStatus.CONFLICT, reason = "Client not found.c Unable to create metadata resource") +public class ClientNotFoundException extends UaaException { + + public ClientNotFoundException(String clientId) { + super("client_not_found", + "Client" + clientId + "not found. Unable to create metadata resource", + HttpStatus.CONFLICT.value()); + } + } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java index d3e7d20f7fc..008099fae4d 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java @@ -5,6 +5,7 @@ import org.cloudfoundry.identity.uaa.provider.IdpAlreadyExistsException; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.jdbc.core.JdbcTemplate; @@ -38,15 +39,15 @@ public class JdbcClientMetadataProvisioning implements ClientMetadataProvisionin private static final Log logger = LogFactory.getLog(JdbcClientMetadataProvisioning.class); - private static final String CLIENT_UI_DETAILS_FIELDS = "id, client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; - private static final String CLIENT_UI_DETAILS_QUERY = "select " + CLIENT_UI_DETAILS_FIELDS + " from oauth_client_ui_details where client_id=? and identity_zone_id=?"; - private static final String CLIENT_UI_DETAILS_CREATE = "insert into oauth_client_ui_details(" + CLIENT_UI_DETAILS_FIELDS + ") values (?,?,?,?,?,?,?)"; - private static final String CLIENT_UI_DETAILS_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon, version"; - private static final String CLIENT_UI_DETAILS_UPDATE = "update oauth_client_ui_details set " + CLIENT_UI_DETAILS_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; - private static final String CLIENT_UI_DETAILS_DELETE_QUERY = "delete from oauth_client_ui_details where client_id=? and identity_zone_id=?"; + private static final String CLIENT_METADATA_FIELDS = "id, client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; + private static final String CLIENT_METADATA_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_metadata where client_id=? and identity_zone_id=?"; + private static final String CLIENT_METADATA_CREATE = "insert into oauth_client_metadata(" + CLIENT_METADATA_FIELDS + ") values (?,?,?,?,?,?,?)"; + private static final String CLIENT_METADATA_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon, version"; + private static final String CLIENT_METADATA_UPDATE = "update oauth_client_metadata set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; + private static final String CLIENT_METADATA_DELETE_QUERY = "delete from oauth_client_metadata where client_id=? and identity_zone_id=?"; private JdbcTemplate template; - private final RowMapper mapper = new ClientUIDetailsRowMapper(); + private final RowMapper mapper = new ClientMetadataRowMapper(); JdbcClientMetadataProvisioning(JdbcTemplate template) { Assert.notNull(template); @@ -58,23 +59,27 @@ public void setTemplate(JdbcTemplate template) { } @Override - public List retrieveAll() { + public List retrieveAll() { logger.debug("Retrieving UI details for all client"); - return template.query(CLIENT_UI_DETAILS_QUERY, mapper, IdentityZoneHolder.get().getId()); + return template.query(CLIENT_METADATA_QUERY, mapper, IdentityZoneHolder.get().getId()); } @Override - public ClientMetaDetails retrieve(String clientId) { + public ClientMetadata retrieve(String clientId) { logger.debug("Retrieving UI details for client: " + clientId); - return template.queryForObject(CLIENT_UI_DETAILS_QUERY, mapper, clientId, IdentityZoneHolder.get().getId()); + try { + return template.queryForObject(CLIENT_METADATA_QUERY, mapper, clientId, IdentityZoneHolder.get().getId()); + } catch (EmptyResultDataAccessException erdae) { + throw new ClientMetadataNotFoundException("No existing metadata found for client " + clientId); + } } @Override - public ClientMetaDetails create(ClientMetaDetails resource) { + public ClientMetadata create(ClientMetadata resource) { logger.debug("Creating new UI details for client: " + resource.getClientId()); final String id = UUID.randomUUID().toString(); try { - template.update(CLIENT_UI_DETAILS_CREATE, new PreparedStatementSetter() { + template.update(CLIENT_METADATA_CREATE, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { int pos = 1; @@ -91,7 +96,6 @@ public void setValues(PreparedStatement ps) throws SQLException { } else { ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); } -// pos++; ps.setInt(pos++, 1); } }); @@ -102,9 +106,9 @@ public void setValues(PreparedStatement ps) throws SQLException { } @Override - public ClientMetaDetails update(String clientId, ClientMetaDetails resource) { - logger.debug("Updating UI details for client: " + clientId); - int updated = template.update(CLIENT_UI_DETAILS_UPDATE, new PreparedStatementSetter() { + public ClientMetadata update(String clientId, ClientMetadata resource) { + logger.debug("Updating metadata for client: " + clientId); + int updated = template.update(CLIENT_METADATA_UPDATE, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { int pos = 1; @@ -114,9 +118,10 @@ public void setValues(PreparedStatement ps) throws SQLException { String appIcon = resource.getAppIcon(); if (appIcon != null) { byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); - ps.setBinaryStream(pos, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); + ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); + } else { + ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); } - pos++; ps.setInt(pos++, resource.getVersion() + 1); ps.setString(pos++, clientId); ps.setString(pos++, IdentityZoneHolder.get().getId()); @@ -124,63 +129,63 @@ public void setValues(PreparedStatement ps) throws SQLException { } }); - ClientMetaDetails resultingClientMetaDetails = retrieve(clientId); + ClientMetadata resultingClientMetadata = retrieve(clientId); if (updated == 0) { throw new OptimisticLockingFailureException(String.format( "Attempt to update the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", clientId, - resultingClientMetaDetails.getVersion(), + resultingClientMetadata.getVersion(), resource.getVersion())); } else if (updated > 1) { throw new IncorrectResultSizeDataAccessException(1); } - return resultingClientMetaDetails; + return resultingClientMetadata; } @Override - public ClientMetaDetails delete(String clientId, int version) { + public ClientMetadata delete(String clientId, int version) { logger.debug("Deleting UI details for client: " + clientId); - ClientMetaDetails clientMetaDetails = retrieve(clientId); + ClientMetadata clientMetadata = retrieve(clientId); int updated; if (version < 0) { - updated = template.update(CLIENT_UI_DETAILS_DELETE_QUERY, clientId, IdentityZoneHolder.get().getId()); + updated = template.update(CLIENT_METADATA_DELETE_QUERY, clientId, IdentityZoneHolder.get().getId()); } else { - updated = template.update(CLIENT_UI_DETAILS_DELETE_QUERY + " and version=?", clientId, IdentityZoneHolder.get().getId(), version); + updated = template.update(CLIENT_METADATA_DELETE_QUERY + " and version=?", clientId, IdentityZoneHolder.get().getId(), version); } if (updated == 0) { throw new OptimisticLockingFailureException(String.format( "Attempt to delete the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", clientId, - clientMetaDetails.getVersion(), + clientMetadata.getVersion(), version)); } - return clientMetaDetails; + return clientMetadata; } - private class ClientUIDetailsRowMapper implements RowMapper { + private class ClientMetadataRowMapper implements RowMapper { @Override - public ClientMetaDetails mapRow(ResultSet rs, int rowNum) throws SQLException { - ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); + public ClientMetadata mapRow(ResultSet rs, int rowNum) throws SQLException { + ClientMetadata clientMetadata = new ClientMetadata(); int pos = 1; pos++; // id - clientMetaDetails.setClientId(rs.getString(pos++)); - clientMetaDetails.setIdentityZoneId(rs.getString(pos++)); - clientMetaDetails.setShowOnHomePage(rs.getBoolean(pos++)); + clientMetadata.setClientId(rs.getString(pos++)); + clientMetadata.setIdentityZoneId(rs.getString(pos++)); + clientMetadata.setShowOnHomePage(rs.getBoolean(pos++)); try { - clientMetaDetails.setAppLaunchUrl(new URL(rs.getString(pos++))); + clientMetadata.setAppLaunchUrl(new URL(rs.getString(pos++))); } catch (MalformedURLException mue) { - // it is safe to ignore this as client_meta_details rows are always created from a ClientMetaDetails instance whose launch url property is strongly typed to URL + // it is safe to ignore this as client_metadata rows are always created from a ClientMetadata instance whose launch url property is strongly typed to URL } - clientMetaDetails.setAppIcon(new String(Base64.encode(rs.getBytes(pos++)))); - clientMetaDetails.setVersion(rs.getInt(pos++)); - return clientMetaDetails; + clientMetadata.setAppIcon(new String(Base64.encode(rs.getBytes(pos++)))); + clientMetadata.setVersion(rs.getInt(pos++)); + return clientMetadata; } } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java index a4e2d13f09e..769a271e428 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java @@ -14,7 +14,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.authentication.event.UnverifiedUserAuthenticationEvent; import org.cloudfoundry.identity.uaa.authentication.manager.AuthEvent; import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent; import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index ed226ed59fb..8ade1c43ea3 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -36,7 +36,7 @@ public class JdbcClientMetadataProvisioningTest extends JdbcTestBase { private RandomValueStringGenerator generator = new RandomValueStringGenerator(8); @Before - public void create_datasource() throws Exception { + public void createDatasource() throws Exception { db = new JdbcClientMetadataProvisioning(jdbcTemplate); // When running hsqldb uncomment these lines to invoke the in-built UI client @@ -52,30 +52,30 @@ public void create_datasource() throws Exception { // } @Test - public void create_clientUIDetails() throws Exception { + public void createClientMetadata() throws Exception { //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); //when a client ui details object is saved - ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + ClientMetadata createdClientMetadata = db.create(clientMetadata); //then - assertThat(createdClientMetaDetails.getClientId(), is(clientMetaDetails.getClientId())); - assertThat(createdClientMetaDetails.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); - assertThat(createdClientMetaDetails.isShowOnHomePage(), is(clientMetaDetails.isShowOnHomePage())); - assertThat(createdClientMetaDetails.getAppLaunchUrl(), is(clientMetaDetails.getAppLaunchUrl())); - assertThat(createdClientMetaDetails.getAppIcon(), is(clientMetaDetails.getAppIcon())); - assertThat(createdClientMetaDetails.getVersion(), is(1)); + assertThat(createdClientMetadata.getClientId(), is(clientMetadata.getClientId())); + assertThat(createdClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); + assertThat(createdClientMetadata.isShowOnHomePage(), is(clientMetadata.isShowOnHomePage())); + assertThat(createdClientMetadata.getAppLaunchUrl(), is(clientMetadata.getAppLaunchUrl())); + assertThat(createdClientMetadata.getAppIcon(), is(clientMetadata.getAppIcon())); + assertThat(createdClientMetadata.getVersion(), is(1)); //and then app icon that is saved is really the base64 decoded bytes - byte[] blobbyblob = jdbcTemplate.queryForObject("select app_icon from oauth_client_ui_details where client_id='" + clientId + "'", byte[].class); + byte[] blobbyblob = jdbcTemplate.queryForObject("select app_icon from oauth_client_metadata where client_id='" + clientId + "'", byte[].class); assertThat(blobbyblob, is(Base64.decode(base64EncodedImg))); } @Test - public void when_multipleClients_with_theSameNameButDifferentZone_clientUIDetails_correctlyAssociated() throws Exception { + public void whenMultipleClients_WithTheSameNameButDifferentZone_ClientMetadataCorrectlyAssociated() throws Exception { try { //given String clientId = generator.generate(); @@ -84,117 +84,117 @@ public void when_multipleClients_with_theSameNameButDifferentZone_clientUIDetail otherZone.setId(otherZoneId); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + otherZoneId + "')"); - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); IdentityZoneHolder.set(otherZone); //when a client is created in another zone - ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + ClientMetadata createdClientMetadata = db.create(clientMetadata); //then expect as such - assertThat(createdClientMetaDetails.getIdentityZoneId(), is(otherZoneId)); + assertThat(createdClientMetadata.getIdentityZoneId(), is(otherZoneId)); } finally { IdentityZoneHolder.set(IdentityZone.getUaa()); } } @Test(expected = DataIntegrityViolationException.class) - public void constraintViolation_when_noMatchingClientFound() throws Exception { + public void constraintViolation_WhenNoMatchingClientFound() throws Exception { //given there is no oauth_client_details record //when we attempt to create an client ui details record - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(generator.generate(), true, new URL("http://app.launch/url"), base64EncodedImg); - db.create(clientMetaDetails); + ClientMetadata clientMetadata = createTestClientMetadata(generator.generate(), true, new URL("http://app.launch/url"), base64EncodedImg); + db.create(clientMetadata); //then we expect a constraint violation } @Test - public void retrieve_ClientUIDetails() throws Exception { + public void retrieveClientMetadata() throws Exception { //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetadata createdClientMetadata = db.create(clientMetadata); //when retrieving the client UI details - ClientMetaDetails retrievedClientMetaDetails = db.retrieve(createdClientMetaDetails.getClientId()); + ClientMetadata retrievedClientMetadata = db.retrieve(createdClientMetadata.getClientId()); //then - assertThat(retrievedClientMetaDetails.getClientId(), is(clientMetaDetails.getClientId())); - assertThat(retrievedClientMetaDetails.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); - assertThat(retrievedClientMetaDetails.isShowOnHomePage(), is(clientMetaDetails.isShowOnHomePage())); - assertThat(retrievedClientMetaDetails.getAppLaunchUrl(), is(clientMetaDetails.getAppLaunchUrl())); - assertThat(retrievedClientMetaDetails.getAppIcon(), is(clientMetaDetails.getAppIcon())); + assertThat(retrievedClientMetadata.getClientId(), is(clientMetadata.getClientId())); + assertThat(retrievedClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); + assertThat(retrievedClientMetadata.isShowOnHomePage(), is(clientMetadata.isShowOnHomePage())); + assertThat(retrievedClientMetadata.getAppLaunchUrl(), is(clientMetadata.getAppLaunchUrl())); + assertThat(retrievedClientMetadata.getAppIcon(), is(clientMetadata.getAppIcon())); } @Test - public void update_ClientUIDetails() throws Exception { + public void updateClientMetadata() throws Exception { //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); - ClientMetaDetails newClientMetaDetails = createTestClientUIDetails(clientMetaDetails.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetadata createdClientMetadata = db.create(clientMetadata); + ClientMetadata newClientMetadata = createTestClientMetadata(clientMetadata.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); //when - ClientMetaDetails updatedClientMetaDetails = db.update(createdClientMetaDetails.getClientId(), newClientMetaDetails); + ClientMetadata updatedClientMetadata = db.update(createdClientMetadata.getClientId(), newClientMetadata); try { - db.update(createdClientMetaDetails.getClientId(), newClientMetaDetails); + db.update(createdClientMetadata.getClientId(), newClientMetadata); fail("another update should fail due to incorrect version"); } catch (OptimisticLockingFailureException olfe) {} //then - assertThat(updatedClientMetaDetails.getClientId(), is(clientMetaDetails.getClientId())); - assertThat(updatedClientMetaDetails.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); - assertThat(updatedClientMetaDetails.isShowOnHomePage(), is(newClientMetaDetails.isShowOnHomePage())); - assertThat(updatedClientMetaDetails.getAppLaunchUrl(), is(newClientMetaDetails.getAppLaunchUrl())); - assertThat(updatedClientMetaDetails.getAppIcon(), is(newClientMetaDetails.getAppIcon())); - assertThat(updatedClientMetaDetails.getVersion(), is(clientMetaDetails.getVersion() + 1)); + assertThat(updatedClientMetadata.getClientId(), is(clientMetadata.getClientId())); + assertThat(updatedClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); + assertThat(updatedClientMetadata.isShowOnHomePage(), is(newClientMetadata.isShowOnHomePage())); + assertThat(updatedClientMetadata.getAppLaunchUrl(), is(newClientMetadata.getAppLaunchUrl())); + assertThat(updatedClientMetadata.getAppIcon(), is(newClientMetadata.getAppIcon())); + assertThat(updatedClientMetadata.getVersion(), is(clientMetadata.getVersion() + 1)); } - @Test(expected = EmptyResultDataAccessException.class) - public void delete_ClientUIDetails() throws Exception { + @Test(expected = ClientMetadataNotFoundException.class) + public void deleteClientMetadata() throws Exception { //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - db.create(clientMetaDetails); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + db.create(clientMetadata); //when you delete the client ui details - db.delete(clientMetaDetails.getClientId(), -1); + db.delete(clientMetadata.getClientId(), -1); //then subsequent retrieval should fail - db.retrieve(clientMetaDetails.getClientId()); + db.retrieve(clientMetadata.getClientId()); } @Test - public void deletion_after_version_update() throws Exception { + public void deleteClientMetadata_AfterVersionUpdate() throws Exception { //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetaDetails clientMetaDetails = createTestClientUIDetails(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - ClientMetaDetails createdClientMetaDetails = db.create(clientMetaDetails); - ClientMetaDetails newClientMetaDetails = createTestClientUIDetails(clientMetaDetails.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); - db.update(createdClientMetaDetails.getClientId(), newClientMetaDetails); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + ClientMetadata createdClientMetadata = db.create(clientMetadata); + ClientMetadata newClientMetadata = createTestClientMetadata(clientMetadata.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); + db.update(createdClientMetadata.getClientId(), newClientMetadata); //when you delete try { - db.delete(clientId, clientMetaDetails.getVersion()); + db.delete(clientId, clientMetadata.getVersion()); fail("should fail because wrong version"); } catch (OptimisticLockingFailureException olfe) {} //then succeed with right version - db.delete(clientId, clientMetaDetails.getVersion() + 1); + db.delete(clientId, clientMetadata.getVersion() + 1); } - private ClientMetaDetails createTestClientUIDetails(String clientId, boolean showOnHomePage, URL appLaunchUrl, String appIcon) throws MalformedURLException { - ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); - clientMetaDetails.setClientId(clientId); - clientMetaDetails.setShowOnHomePage(showOnHomePage); - clientMetaDetails.setAppLaunchUrl(appLaunchUrl); - clientMetaDetails.setAppIcon(appIcon); - clientMetaDetails.setVersion(1); - return clientMetaDetails; + private ClientMetadata createTestClientMetadata(String clientId, boolean showOnHomePage, URL appLaunchUrl, String appIcon) throws MalformedURLException { + ClientMetadata clientMetadata = new ClientMetadata(); + clientMetadata.setClientId(clientId); + clientMetadata.setShowOnHomePage(showOnHomePage); + clientMetadata.setAppLaunchUrl(appLaunchUrl); + clientMetadata.setAppIcon(appIcon); + clientMetadata.setVersion(1); + return clientMetadata; } private static final String base64EncodedImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAXRQTFRFAAAAOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ozk4Ojo6Ojk5NkZMFp/PFqDPNkVKOjo6Ojk5MFhnEq3nEqvjEqzjEbDpMFdlOjo5Ojo6Ojo6Ozg2GZ3TFqXeFKfgF6DVOjo6Ozg2G5jPGZ7ZGKHbGZvROjo6Ojo5M1FfG5vYGp3aM1BdOjo6Ojo6Ojk4KHWeH5PSHpTSKHSbOjk4Ojo6Ojs8IY/QIY/QOjs7Ojo6Ojo6Ozc0JYfJJYjKOzYyOjo5Ozc0KX7AKH/AOzUxOjo5Ojo6Ojo6Ojo6Ojs8LHi6LHi6Ojs7Ojo6Ojo6Ojo6Ojo6Ojo6L3K5L3S7LnW8LnS7Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6NlFvMmWeMmaeNVJwOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojk5Ojk4Ojk4Ojk5Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6FaXeFabfGZ/aGKDaHJnVG5rW////xZzURgAAAHV0Uk5TAAACPaXbAVzltTa4MykoM5HlPY/k5Iw85QnBs2D7+lzAtWD7+lyO6EKem0Ey47Mx2dYvtVZVop5Q2i4qlZAnBiGemh0EDXuddqypcHkShPJwYufmX2rvihSJ+qxlg4JiqP2HPtnW1NjZ2svRVAglGTi91RAXr3/WIQAAAAFiS0dEe0/StfwAAAAJcEhZcwAAAEgAAABIAEbJaz4AAADVSURBVBjTY2BgYGBkYmZhZWVhZmJkAANGNnYODk5ODg52NrAIIyMXBzcPLx8/NwcXIyNYQEBQSFhEVExcQgAiICklLSNbWiYnLy0lCRFQUFRSLq9QUVVUgAgwqqlraFZWaWmrqzFCTNXR1dM3MDQy1tWB2MvIaMJqamZuYWnCCHeIlbWNrZ0VG5QPFLF3cHRydoErcHVz9/D08nb3kYSY6evnHxAYFBwSGhYeAbbWNzIqOiY2Lj4hMckVoiQ5JTUtPSMzKzsH6pfcvPyCwqKc4pJcoAAA2pghnaBVZ0kAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTAtMDhUMTI6NDg6MDkrMDA6MDDsQS6eAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTEwLTA4VDEyOjQ4OjA5KzAwOjAwnRyWIgAAAEZ0RVh0c29mdHdhcmUASW1hZ2VNYWdpY2sgNi43LjgtOSAyMDE0LTA1LTEyIFExNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9yZ9yG7QAAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6aGVpZ2h0ADE5Mg8AcoUAAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMTky06whCAAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNDQ0MzA4NDg5qdC9PQAAAA90RVh0VGh1bWI6OlNpemUAMEJClKI+7AAAAFZ0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL21udGxvZy9mYXZpY29ucy8yMDE1LTEwLTA4LzJiMjljNmYwZWRhZWUzM2ViNmM1Mzg4ODMxMjg3OTg1Lmljby5wbmdoJKG+AAAAAElFTkSuQmCC"; diff --git a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml index a5c1af77376..29836aefc6c 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml @@ -88,6 +88,11 @@ + + + + + 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 0ff86a58f22..8569cef8c5c 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml @@ -26,6 +26,10 @@ + + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java index 7ff82293d83..41dce4967c4 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java @@ -7,18 +7,22 @@ import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; -import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import java.net.URL; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.core.Is.is; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; /******************************************************************************* * Cloud Foundry @@ -34,97 +38,148 @@ *******************************************************************************/ public class ClientMetadataAdminEndpointsMockMvcTest extends InjectedMockContextTest { - private JdbcClientMetaDetailsProvisioning clientUIs; - private String adminClientTokenWithWrite; + private JdbcClientMetadataProvisioning clientMetadata; + private String adminClientTokenWithClientsWrite; private JdbcClientDetailsService clients; private RandomValueStringGenerator generator = new RandomValueStringGenerator(8); private TestClient testClient; private UaaTestAccounts testAccounts; - private String adminClientTokenWithRead; + private String adminClientTokenWithClientsRead; @Before public void setUp() throws Exception { testClient = new TestClient(getMockMvc()); testAccounts = UaaTestAccounts.standard(null); - adminClientTokenWithRead = testClient.getClientCredentialsOAuthAccessToken( + adminClientTokenWithClientsRead = testClient.getClientCredentialsOAuthAccessToken( testAccounts.getAdminClientId(), testAccounts.getAdminClientSecret(), "clients.read"); - adminClientTokenWithWrite = testClient.getClientCredentialsOAuthAccessToken( + adminClientTokenWithClientsWrite = testClient.getClientCredentialsOAuthAccessToken( testAccounts.getAdminClientId(), testAccounts.getAdminClientSecret(), "clients.write"); - clientUIs = getWebApplicationContext().getBean(JdbcClientMetaDetailsProvisioning.class); + clientMetadata = getWebApplicationContext().getBean(JdbcClientMetadataProvisioning.class); clients = getWebApplicationContext().getBean(JdbcClientDetailsService.class); } @Test - public void create_IsCreated() throws Exception { + public void createClientMetadata() throws Exception { String clientId = generator.generate(); - - ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); - clientMetaDetails.setClientId(clientId); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithWrite) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientMetaDetails)); - getMockMvc().perform(createClientPost).andExpect(status().isCreated()); + MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + assertThat(response.getStatus(), is(HttpStatus.CREATED.value())); } @Test - public void create_noClient() throws Exception { + public void createClientMetadata_WithNoClient() throws Exception { String clientId = generator.generate(); - ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); - clientMetaDetails.setClientId(clientId); - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithWrite) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientMetaDetails)); - getMockMvc().perform(createClientPost).andExpect(status().isNotFound()); + + MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + assertThat(response.getStatus(), is(HttpStatus.CONFLICT.value())); } @Test - public void create_unauthorizedBecauseInsufficientScope() throws Exception { + public void createUnauthorized_BecauseInsufficientScope() throws Exception { // given a token with insufficient privileges - String userToken = testClient.getUserOAuthAccessToken( + String userTokenWithInsufficientScope = testClient.getUserOAuthAccessToken( "app", "appclientsecret", testAccounts.getUserName(), testAccounts.getPassword(), "openid"); - // when a new client is created + // when a new client metadata is created + String clientId = generator.generate(); + MockHttpServletResponse response = createTestClientMetadata(clientId, userTokenWithInsufficientScope); + + // then expect a 403 Forbidden + assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN.value())); + } + + @Test + public void getClientMetadata() throws Exception { String clientId = generator.generate(); - ClientMetaDetails clientMetaDetails = new ClientMetaDetails(); - clientMetaDetails.setClientId(clientId); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + + MockHttpServletResponse response = getTestClientMetadata(clientId); + + assertThat(response.getStatus(), is(HttpStatus.OK.value())); + } + + private MockHttpServletResponse createTestClientMetadata(String clientId, String token) throws Exception { + ClientMetadata clientMetadata = new ClientMetadata(); + clientMetadata.setClientId(clientId); + MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + userToken) + .header("Authorization", "Bearer " + token) .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientMetaDetails)); - MvcResult result = getMockMvc().perform(createClientPost).andReturn(); + .content(JsonUtils.writeValueAsString(clientMetadata)); + return getMockMvc().perform(createClientPost).andReturn().getResponse(); + } + + @Test + public void getClientMetadata_WhichDoesNotExist() throws Exception { + String clientId = generator.generate(); - // then expect a 403 Forbidden - assertThat(result.getResponse().getStatus(), is(HttpStatus.FORBIDDEN.value())); + MockHttpServletResponse response = getTestClientMetadata(clientId); + + assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND.value())); } @Test - public void get_client() throws Exception { + public void updateClientMetadata_WithCorrectVersion() throws Exception { String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - MockHttpServletRequestBuilder createClientPost = get("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithRead) - .accept(APPLICATION_JSON); - MvcResult result = getMockMvc().perform(createClientPost).andReturn(); + ClientMetadata updatedClientMetadata = new ClientMetadata(); + updatedClientMetadata.setClientId(clientId); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); + + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", "1") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + ResultActions perform = getMockMvc().perform(updateClientPut); + assertThat(perform.andReturn().getResponse().getContentAsString(), containsString(appLaunchUrl.toString())); + + MockHttpServletResponse response = getTestClientMetadata(clientId); + assertThat(response.getStatus(), is(HttpStatus.OK.value())); + assertThat(response.getContentAsString(), containsString(appLaunchUrl.toString())); + } + + @Test + public void updateClentMetadata_WithIncorrectVersion() throws Exception { + String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + + ClientMetadata updatedClientMetadata = new ClientMetadata(); + updatedClientMetadata.setClientId(clientId); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); - // TEMP - assertThat(result.getResponse().getStatus(), is(HttpStatus.OK.value())); - assertThat(result.getResponse().getContentAsString(), is(1)); + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + ResultActions perform = getMockMvc().perform(updateClientPut); + assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.PRECONDITION_FAILED.value())); + } + + private MockHttpServletResponse getTestClientMetadata(String clientId) throws Exception { + MockHttpServletRequestBuilder createClientGet = get("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) + .accept(APPLICATION_JSON); + return getMockMvc().perform(createClientGet).andReturn().getResponse(); } + } \ No newline at end of file From e54f06ca33bd6d9ff5dbe5fd5543ecc66778572e Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 19 Jan 2016 17:42:04 -0800 Subject: [PATCH 19/25] Add delete and retrieveAll endpoints for client metadata [#109263482] https://www.pivotaltracker.com/story/show/109263482 Signed-off-by: Jonathan Lo Signed-off-by: Madhura Bhave --- .../identity/uaa/client/ClientMetadata.java | 2 - .../client/ClientMetadataAdminEndpoints.java | 78 +++++++++- .../uaa/client/ClientMetadataException.java | 4 - .../ClientMetadataNotFoundException.java | 25 --- .../uaa/client/ClientNotFoundException.java | 28 ---- .../JdbcClientMetadataProvisioning.java | 51 +++--- ..._New_Table_For_Storing_Client_Metadata.sql | 4 +- ..._New_Table_For_Storing_Client_Metadata.sql | 4 +- ..._New_Table_For_Storing_Client_Metadata.sql | 4 +- .../JdbcClientMetadataProvisioningTest.java | 50 +++++- .../WEB-INF/spring/client-admin-endpoints.xml | 3 + ...ientMetadataAdminEndpointsMockMvcTest.java | 145 ++++++++++++++++-- 12 files changed, 278 insertions(+), 120 deletions(-) delete mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java delete mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java index 1342af2d8c5..84037e13e32 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java @@ -27,12 +27,10 @@ public class ClientMetadata { private String appIcon; private int version; - @JsonIgnore public String getClientId() { return clientId; } - @JsonIgnore public void setClientId(String clientId) { this.clientId = clientId; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java index b236bffe562..5a0dd2f92e7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java @@ -1,15 +1,30 @@ package org.cloudfoundry.identity.uaa.client; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.web.ConvertingExceptionView; +import org.cloudfoundry.identity.uaa.web.ExceptionReport; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.View; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; /******************************************************************************* * Cloud Foundry @@ -28,27 +43,57 @@ public class ClientMetadataAdminEndpoints { private ClientMetadataProvisioning clientMetadataProvisioning; private ClientDetailsService clients; + private HttpMessageConverter[] messageConverters; + + private static Log logger = LogFactory.getLog(ClientMetadataAdminEndpoints.class); @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) public ClientMetadata createClientMetadata(@RequestBody ClientMetadata clientMetadata, - @PathVariable("client") String clientId) - throws ClientNotFoundException { + @PathVariable("client") String clientId) { try { clients.loadClientByClientId(clientId); + clientMetadata.setClientId(clientId); + return clientMetadataProvisioning.create(clientMetadata); } catch (NoSuchClientException nsce) { - throw new ClientNotFoundException(clientId ); + throw new ClientMetadataException("No client found with id: " + clientId, HttpStatus.NOT_FOUND); + } catch (DuplicateKeyException e) { + throw new ClientMetadataException("Client metadata already exists for this clientId: " + clientId, HttpStatus.CONFLICT); } - clientMetadata.setClientId(clientId); - return clientMetadataProvisioning.create(clientMetadata); } // GET @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public ClientMetadata retrieveClientMetadata(@PathVariable("client") String clientId) { - return clientMetadataProvisioning.retrieve(clientId); + try { + return clientMetadataProvisioning.retrieve(clientId); + } catch (EmptyResultDataAccessException erdae) { + throw new ClientMetadataException("No client metadata found for " + clientId, HttpStatus.NOT_FOUND); + } + } + + // GET + @RequestMapping(value = "/oauth/clients/meta", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public List retrieveAllClientMetadata() { + return clientMetadataProvisioning.retrieveAll(); + } + + @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public ClientMetadata deleteClientMetadata(@PathVariable("client") String clientId, @RequestHeader(value = "If-Match", required = false) Integer etag) { + if (etag == null) { + throw new ClientMetadataException("Missing If-Match header", HttpStatus.BAD_REQUEST); + } + try { + return clientMetadataProvisioning.delete(clientId, etag); + } catch (EmptyResultDataAccessException erdae) { + throw new ClientMetadataException("No client metadata found for " + clientId, HttpStatus.NOT_FOUND); + } catch (OptimisticLockingFailureException olfe) { + throw new ClientMetadataException(olfe.getMessage(), HttpStatus.PRECONDITION_FAILED); + } } // PUT (Update) @@ -58,11 +103,24 @@ public ClientMetadata updateClientMetadata(@RequestBody ClientMetadata clientMet @RequestHeader(value = "If-Match", required = false) Integer etag, @PathVariable("client") String clientId) { if (etag == null) { - throw new ClientMetadataException("Missing If-Match header", HttpStatus.PRECONDITION_FAILED); + throw new ClientMetadataException("Missing If-Match header", HttpStatus.BAD_REQUEST); } clientMetadata.setVersion(etag); - return clientMetadataProvisioning.update(clientId, clientMetadata); + try { + return clientMetadataProvisioning.update(clientId, clientMetadata); + } catch (OptimisticLockingFailureException olfe) { + throw new ClientMetadataException(olfe.getMessage(), HttpStatus.PRECONDITION_FAILED); + } + } + + @ExceptionHandler + public View handleException(ClientMetadataException cme, HttpServletRequest request) { + logger.error("Unhandled exception in client metadata admin endpoints.", cme); + + boolean trace = request.getParameter("trace") != null && !request.getParameter("trace").equals("false"); + return new ConvertingExceptionView(new ResponseEntity<>(new ExceptionReport(cme, trace, cme.getExtraInfo()), + cme.getStatus()), messageConverters); } // DELETE @@ -76,4 +134,8 @@ public void setClientMetadataProvisioning(ClientMetadataProvisioning clientMetad public void setClients(ClientDetailsService clients) { this.clients = clients; } + + public void setMessageConverters(HttpMessageConverter[] messageConverters) { + this.messageConverters = messageConverters; + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java index cf331b2cd97..a4db9af491f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataException.java @@ -17,10 +17,6 @@ import java.util.Map; -/** - * @author Luke Taylor - * @author Dave Syer - */ public class ClientMetadataException extends UaaException { private final HttpStatus status; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java deleted file mode 100644 index 81135379abd..00000000000 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataNotFoundException.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.cloudfoundry.identity.uaa.client; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. - *

    - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - *

    - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. - *******************************************************************************/ -@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Client metadata not found") -public class ClientMetadataNotFoundException extends ClientMetadataException { - - public ClientMetadataNotFoundException(String clientId) { - super("No existing metadata found for client " + clientId, HttpStatus.NOT_FOUND); - } - -} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java deleted file mode 100644 index 4c7d971485f..00000000000 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientNotFoundException.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.cloudfoundry.identity.uaa.client; - -import org.cloudfoundry.identity.uaa.error.UaaException; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. - *

    - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - *

    - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. - *******************************************************************************/ -@ResponseStatus(value = HttpStatus.CONFLICT, reason = "Client not found.c Unable to create metadata resource") -public class ClientNotFoundException extends UaaException { - - public ClientNotFoundException(String clientId) { - super("client_not_found", - "Client" + clientId + "not found. Unable to create metadata resource", - HttpStatus.CONFLICT.value()); - } - -} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java index 008099fae4d..c5ade11a631 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java @@ -41,6 +41,7 @@ public class JdbcClientMetadataProvisioning implements ClientMetadataProvisionin private static final String CLIENT_METADATA_FIELDS = "id, client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; private static final String CLIENT_METADATA_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_metadata where client_id=? and identity_zone_id=?"; + private static final String CLIENT_METADATAS_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_metadata where identity_zone_id=?"; private static final String CLIENT_METADATA_CREATE = "insert into oauth_client_metadata(" + CLIENT_METADATA_FIELDS + ") values (?,?,?,?,?,?,?)"; private static final String CLIENT_METADATA_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon, version"; private static final String CLIENT_METADATA_UPDATE = "update oauth_client_metadata set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; @@ -61,47 +62,39 @@ public void setTemplate(JdbcTemplate template) { @Override public List retrieveAll() { logger.debug("Retrieving UI details for all client"); - return template.query(CLIENT_METADATA_QUERY, mapper, IdentityZoneHolder.get().getId()); + return template.query(CLIENT_METADATAS_QUERY, mapper, IdentityZoneHolder.get().getId()); } @Override public ClientMetadata retrieve(String clientId) { logger.debug("Retrieving UI details for client: " + clientId); - try { - return template.queryForObject(CLIENT_METADATA_QUERY, mapper, clientId, IdentityZoneHolder.get().getId()); - } catch (EmptyResultDataAccessException erdae) { - throw new ClientMetadataNotFoundException("No existing metadata found for client " + clientId); - } + return template.queryForObject(CLIENT_METADATA_QUERY, mapper, clientId, IdentityZoneHolder.get().getId()); } @Override public ClientMetadata create(ClientMetadata resource) { logger.debug("Creating new UI details for client: " + resource.getClientId()); final String id = UUID.randomUUID().toString(); - try { - template.update(CLIENT_METADATA_CREATE, new PreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps) throws SQLException { - int pos = 1; - ps.setString(pos++, id); - ps.setString(pos++, resource.getClientId()); - ps.setString(pos++, IdentityZoneHolder.get().getId()); - ps.setBoolean(pos++, resource.isShowOnHomePage()); - URL appLaunchUrl = resource.getAppLaunchUrl(); - ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); - String appIcon = resource.getAppIcon(); - if (appIcon != null) { - byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); - ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); - } else { - ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); - } - ps.setInt(pos++, 1); + template.update(CLIENT_METADATA_CREATE, new PreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps) throws SQLException { + int pos = 1; + ps.setString(pos++, id); + ps.setString(pos++, resource.getClientId()); + ps.setString(pos++, IdentityZoneHolder.get().getId()); + ps.setBoolean(pos++, resource.isShowOnHomePage()); + URL appLaunchUrl = resource.getAppLaunchUrl(); + ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); + String appIcon = resource.getAppIcon(); + if (appIcon != null) { + byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); + ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); + } else { + ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); } - }); - } catch (DuplicateKeyException e) { - throw new IdpAlreadyExistsException(e.getMostSpecificCause().getMessage()); - } + ps.setInt(pos++, 1); + } + }); return retrieve(resource.getClientId()); } diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql index 31ec4fd60c4..9746b2c381a 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql @@ -1,6 +1,6 @@ CREATE TABLE oauth_client_metadata ( id VARCHAR(255) NOT NULL, - client_id VARCHAR(255) NOT NULL, + client_id VARCHAR(255) NOT NULL UNIQUE, identity_zone_id VARCHAR(36) NOT NULL, show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, app_launch_url VARCHAR(1024), @@ -8,4 +8,4 @@ CREATE TABLE oauth_client_metadata ( version INT DEFAULT 0 NOT NULL, PRIMARY KEY (id), CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) -); \ No newline at end of file +); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql index 261d5bcc6a4..850c0fae51f 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql @@ -1,6 +1,6 @@ CREATE TABLE oauth_client_metadata ( id VARCHAR(255) NOT NULL, - client_id VARCHAR(255) NOT NULL, + client_id VARCHAR(255) NOT NULL UNIQUE, identity_zone_id VARCHAR(36) NOT NULL, show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, app_launch_url VARCHAR(1024), @@ -8,4 +8,4 @@ CREATE TABLE oauth_client_metadata ( version INT DEFAULT 0 NOT NULL, PRIMARY KEY (id), CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) -); \ No newline at end of file +); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql index fc9271862bf..1ba4210b88b 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql @@ -1,6 +1,6 @@ CREATE TABLE oauth_client_metadata ( id VARCHAR(256) NOT NULL, - client_id VARCHAR(256) NOT NULL, + client_id VARCHAR(256) NOT NULL UNIQUE, identity_zone_id VARCHAR(36) NOT NULL, show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, app_launch_url VARCHAR(1024), @@ -8,4 +8,4 @@ CREATE TABLE oauth_client_metadata ( version INT DEFAULT 0 NOT NULL, PRIMARY KEY (id), CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) -); \ No newline at end of file +); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index 8ade1c43ea3..1573259b6d7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -2,18 +2,24 @@ import org.apache.xml.security.utils.Base64; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.cloudfoundry.identity.uaa.util.PredicateMatcher; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.Before; import org.junit.Test; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; @@ -74,6 +80,18 @@ public void createClientMetadata() throws Exception { assertThat(blobbyblob, is(Base64.decode(base64EncodedImg))); } + @Test(expected = DuplicateKeyException.class) + public void createClientMetadata_withAlreadyExistingDuplicate() throws Exception { + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + + db.create(clientMetadata); + + //duplicate client metadata + db.create(clientMetadata); + } + @Test public void whenMultipleClients_WithTheSameNameButDifferentZone_ClientMetadataCorrectlyAssociated() throws Exception { try { @@ -127,6 +145,30 @@ public void retrieveClientMetadata() throws Exception { assertThat(retrievedClientMetadata.getAppIcon(), is(clientMetadata.getAppIcon())); } + @Test(expected = EmptyResultDataAccessException.class) + public void retrieveClientMetadata_ThatDoesNotExist() throws Exception { + String clientId = generator.generate(); + db.retrieve(clientId); + } + + @Test + public void retrieveAllClientMetadata() throws Exception { + String clientId = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetadata clientMetadata1 = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); + db.create(clientMetadata1); + String clientId2 = generator.generate(); + jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId2 + "', '" + IdentityZone.getUaa().getId() + "')"); + ClientMetadata clientMetadata2 = createTestClientMetadata(clientId2, true, new URL("http://app.launch/url"), base64EncodedImg); + db.create(clientMetadata2); + + List clientMetadatas = db.retrieveAll(); + + + assertThat(clientMetadatas, PredicateMatcher.has(m -> m.getClientId().equals(clientId))); + assertThat(clientMetadatas, PredicateMatcher.has(m -> m.getClientId().equals(clientId2))); + } + @Test public void updateClientMetadata() throws Exception { //given @@ -152,7 +194,7 @@ public void updateClientMetadata() throws Exception { assertThat(updatedClientMetadata.getVersion(), is(clientMetadata.getVersion() + 1)); } - @Test(expected = ClientMetadataNotFoundException.class) + @Test public void deleteClientMetadata() throws Exception { //given String clientId = generator.generate(); @@ -164,7 +206,11 @@ public void deleteClientMetadata() throws Exception { db.delete(clientMetadata.getClientId(), -1); //then subsequent retrieval should fail - db.retrieve(clientMetadata.getClientId()); + try { + db.retrieve(clientMetadata.getClientId()); + fail("Metadata should have been deleted"); + } catch(EmptyResultDataAccessException e) { + } } @Test diff --git a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml index 29836aefc6c..8ac7420e670 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml @@ -91,6 +91,9 @@ + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java index 41dce4967c4..aa64b65492f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java @@ -1,9 +1,11 @@ package org.cloudfoundry.identity.uaa.client; +import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.PredicateMatcher; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; @@ -15,14 +17,18 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import java.net.URL; +import java.util.ArrayList; -import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /******************************************************************************* * Cloud Foundry @@ -77,7 +83,16 @@ public void createClientMetadata_WithNoClient() throws Exception { String clientId = generator.generate(); MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - assertThat(response.getStatus(), is(HttpStatus.CONFLICT.value())); + assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND.value())); + } + + @Test + public void createDuplicateClientMetadata_isConflict() throws Exception { + String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + assertThat(response.getStatus(), is(CONFLICT.value())); } @Test @@ -108,18 +123,6 @@ public void getClientMetadata() throws Exception { assertThat(response.getStatus(), is(HttpStatus.OK.value())); } - - private MockHttpServletResponse createTestClientMetadata(String clientId, String token) throws Exception { - ClientMetadata clientMetadata = new ClientMetadata(); - clientMetadata.setClientId(clientId); - - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + token) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientMetadata)); - return getMockMvc().perform(createClientPost).andReturn().getResponse(); - } @Test public void getClientMetadata_WhichDoesNotExist() throws Exception { @@ -130,6 +133,30 @@ public void getClientMetadata_WhichDoesNotExist() throws Exception { assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND.value())); } + @Test + public void retrieveAllClientMetadata() throws Exception { + String clientId1 = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId1, null, null, null, null)); + createTestClientMetadata(clientId1, adminClientTokenWithClientsWrite); + + String clientId2 = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId2, null, null, null, null)); + createTestClientMetadata(clientId2, adminClientTokenWithClientsWrite); + + String clientId3 = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId3, null, null, null, null)); + createTestClientMetadata(clientId3, adminClientTokenWithClientsWrite); + + MockHttpServletResponse response = getMockMvc().perform(get("/oauth/clients/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) + .accept(APPLICATION_JSON)).andReturn().getResponse(); + ArrayList clientMetadataList = JsonUtils.readValue(response.getContentAsString(), new TypeReference>() {}); + + assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId1))); + assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId2))); + assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId3))); + } + @Test public void updateClientMetadata_WithCorrectVersion() throws Exception { String clientId = generator.generate(); @@ -156,7 +183,28 @@ public void updateClientMetadata_WithCorrectVersion() throws Exception { } @Test - public void updateClentMetadata_WithIncorrectVersion() throws Exception { + public void updateClientMetadata_WithIncorrectVersion() throws Exception { + String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + + ClientMetadata updatedClientMetadata = new ClientMetadata(); + updatedClientMetadata.setClientId(clientId); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); + + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", "100") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + ResultActions perform = getMockMvc().perform(updateClientPut); + assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.PRECONDITION_FAILED.value())); + } + + @Test + public void updateClientMetadata_WithNoVersion() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); @@ -172,14 +220,79 @@ public void updateClentMetadata_WithIncorrectVersion() throws Exception { .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(updatedClientMetadata)); ResultActions perform = getMockMvc().perform(updateClientPut); + assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.BAD_REQUEST.value())); + } + + @Test + public void deleteClientMetadata() throws Exception { + String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + + getMockMvc().perform(delete("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", 1) + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + public void deleteClientMetadata_withInvalidId() throws Exception { + String clientId = generator.generate(); + getMockMvc().perform(delete("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", 1) + .accept(APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteClientMetadata_WithIncorrectVersion() throws Exception { + String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + + MockHttpServletRequestBuilder deleteRequest = delete("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", 100) + .accept(APPLICATION_JSON); + + + ResultActions perform = getMockMvc().perform(deleteRequest); assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.PRECONDITION_FAILED.value())); } + @Test + public void deleteClientMetadata_WithNoVersion() throws Exception { + String clientId = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); + createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); + + MockHttpServletRequestBuilder deleteRequest = delete("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .accept(APPLICATION_JSON); + + + ResultActions perform = getMockMvc().perform(deleteRequest); + assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.BAD_REQUEST.value())); + } + private MockHttpServletResponse getTestClientMetadata(String clientId) throws Exception { MockHttpServletRequestBuilder createClientGet = get("/oauth/clients/" + clientId + "/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) .accept(APPLICATION_JSON); return getMockMvc().perform(createClientGet).andReturn().getResponse(); } + + private MockHttpServletResponse createTestClientMetadata(String clientId, String token) throws Exception { + ClientMetadata clientMetadata = new ClientMetadata(); + + MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + token) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientMetadata)); + return getMockMvc().perform(createClientPost).andReturn().getResponse(); + } -} \ No newline at end of file +} From f4b1289179c1ba3505145f82402ad8ac078849cf Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Wed, 20 Jan 2016 12:01:50 -0800 Subject: [PATCH 20/25] Client metadata is now part of the oauth_client_details table itself - create and delete endpoints no longer exist. [#109263482] https://www.pivotaltracker.com/story/show/109263482 Signed-off-by: Madhura Bhave Signed-off-by: Jeremy Coffield --- .../client/ClientMetadataAdminEndpoints.java | 60 ++----- .../client/ClientMetadataProvisioning.java | 11 +- .../JdbcClientMetadataProvisioning.java | 77 ++------ .../V3_0_2__Add_Client_Metadata_Columns.sql | 9 + ..._New_Table_For_Storing_Client_Metadata.sql | 11 -- .../V3_0_2__Add_Client_Metadata_Columns.sql | 4 + ..._New_Table_For_Storing_Client_Metadata.sql | 11 -- .../V3_0_2__Add_Client_Metadata_Columns.sql | 4 + ..._New_Table_For_Storing_Client_Metadata.sql | 11 -- .../JdbcClientMetadataProvisioningTest.java | 155 ++-------------- .../WEB-INF/spring/client-admin-endpoints.xml | 1 - ...ientMetadataAdminEndpointsMockMvcTest.java | 166 ++++++------------ 12 files changed, 120 insertions(+), 400 deletions(-) create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql delete mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql delete mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql delete mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java index 5a0dd2f92e7..ee8ca673888 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java @@ -4,15 +4,13 @@ import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.web.ConvertingExceptionView; import org.cloudfoundry.identity.uaa.web.ExceptionReport; -import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -24,7 +22,6 @@ import javax.servlet.http.HttpServletRequest; import java.util.List; -import java.util.Map; /******************************************************************************* * Cloud Foundry @@ -42,28 +39,10 @@ public class ClientMetadataAdminEndpoints { private ClientMetadataProvisioning clientMetadataProvisioning; - private ClientDetailsService clients; private HttpMessageConverter[] messageConverters; private static Log logger = LogFactory.getLog(ClientMetadataAdminEndpoints.class); - @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public ClientMetadata createClientMetadata(@RequestBody ClientMetadata clientMetadata, - @PathVariable("client") String clientId) { - try { - clients.loadClientByClientId(clientId); - clientMetadata.setClientId(clientId); - return clientMetadataProvisioning.create(clientMetadata); - } catch (NoSuchClientException nsce) { - throw new ClientMetadataException("No client found with id: " + clientId, HttpStatus.NOT_FOUND); - } catch (DuplicateKeyException e) { - throw new ClientMetadataException("Client metadata already exists for this clientId: " + clientId, HttpStatus.CONFLICT); - } - - } - - // GET @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public ClientMetadata retrieveClientMetadata(@PathVariable("client") String clientId) { @@ -74,29 +53,12 @@ public ClientMetadata retrieveClientMetadata(@PathVariable("client") String clie } } - // GET @RequestMapping(value = "/oauth/clients/meta", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public List retrieveAllClientMetadata() { return clientMetadataProvisioning.retrieveAll(); } - @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.DELETE) - @ResponseStatus(HttpStatus.OK) - public ClientMetadata deleteClientMetadata(@PathVariable("client") String clientId, @RequestHeader(value = "If-Match", required = false) Integer etag) { - if (etag == null) { - throw new ClientMetadataException("Missing If-Match header", HttpStatus.BAD_REQUEST); - } - try { - return clientMetadataProvisioning.delete(clientId, etag); - } catch (EmptyResultDataAccessException erdae) { - throw new ClientMetadataException("No client metadata found for " + clientId, HttpStatus.NOT_FOUND); - } catch (OptimisticLockingFailureException olfe) { - throw new ClientMetadataException(olfe.getMessage(), HttpStatus.PRECONDITION_FAILED); - } - } - - // PUT (Update) @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) public ClientMetadata updateClientMetadata(@RequestBody ClientMetadata clientMetadata, @@ -106,11 +68,21 @@ public ClientMetadata updateClientMetadata(@RequestBody ClientMetadata clientMet throw new ClientMetadataException("Missing If-Match header", HttpStatus.BAD_REQUEST); } + if (StringUtils.hasText(clientMetadata.getClientId())) { + if (!clientId.equals(clientMetadata.getClientId())) { + throw new ClientMetadataException("Client ID in body {" + clientMetadata.getClientId() + "} does not match URL path {" + clientId + "}", HttpStatus.BAD_REQUEST); + } + } else { + clientMetadata.setClientId(clientId); + } + clientMetadata.setVersion(etag); try { - return clientMetadataProvisioning.update(clientId, clientMetadata); + return clientMetadataProvisioning.update(clientMetadata); } catch (OptimisticLockingFailureException olfe) { throw new ClientMetadataException(olfe.getMessage(), HttpStatus.PRECONDITION_FAILED); + } catch (EmptyResultDataAccessException e) { + throw new ClientMetadataException("No client with ID " + clientMetadata.getClientId(), HttpStatus.NOT_FOUND); } } @@ -123,18 +95,10 @@ public View handleException(ClientMetadataException cme, HttpServletRequest requ cme.getStatus()), messageConverters); } - // DELETE - - // GET (retrieveAll) - public void setClientMetadataProvisioning(ClientMetadataProvisioning clientMetadataProvisioning) { this.clientMetadataProvisioning = clientMetadataProvisioning; } - public void setClients(ClientDetailsService clients) { - this.clients = clients; - } - public void setMessageConverters(HttpMessageConverter[] messageConverters) { this.messageConverters = messageConverters; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java index 1cf006d4019..741bc4014f7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java @@ -2,6 +2,8 @@ import org.cloudfoundry.identity.uaa.resources.ResourceManager; +import java.util.List; + /******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. @@ -14,5 +16,12 @@ * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ -public interface ClientMetadataProvisioning extends ResourceManager { +public interface ClientMetadataProvisioning { + + List retrieveAll(); + + ClientMetadata retrieve(String id); + + ClientMetadata update(ClientMetadata resource); + } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java index c5ade11a631..375c76f68d1 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java @@ -2,10 +2,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.provider.IdpAlreadyExistsException; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.springframework.dao.DuplicateKeyException; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.jdbc.core.JdbcTemplate; @@ -39,13 +36,11 @@ public class JdbcClientMetadataProvisioning implements ClientMetadataProvisionin private static final Log logger = LogFactory.getLog(JdbcClientMetadataProvisioning.class); - private static final String CLIENT_METADATA_FIELDS = "id, client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; - private static final String CLIENT_METADATA_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_metadata where client_id=? and identity_zone_id=?"; - private static final String CLIENT_METADATAS_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_metadata where identity_zone_id=?"; - private static final String CLIENT_METADATA_CREATE = "insert into oauth_client_metadata(" + CLIENT_METADATA_FIELDS + ") values (?,?,?,?,?,?,?)"; + private static final String CLIENT_METADATA_FIELDS = "client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; + private static final String CLIENT_METADATA_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where client_id=? and identity_zone_id=?"; + private static final String CLIENT_METADATAS_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where identity_zone_id=?"; private static final String CLIENT_METADATA_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon, version"; - private static final String CLIENT_METADATA_UPDATE = "update oauth_client_metadata set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; - private static final String CLIENT_METADATA_DELETE_QUERY = "delete from oauth_client_metadata where client_id=? and identity_zone_id=?"; + private static final String CLIENT_METADATA_UPDATE = "update oauth_client_details set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; private JdbcTemplate template; private final RowMapper mapper = new ClientMetadataRowMapper(); @@ -72,35 +67,8 @@ public ClientMetadata retrieve(String clientId) { } @Override - public ClientMetadata create(ClientMetadata resource) { - logger.debug("Creating new UI details for client: " + resource.getClientId()); - final String id = UUID.randomUUID().toString(); - template.update(CLIENT_METADATA_CREATE, new PreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps) throws SQLException { - int pos = 1; - ps.setString(pos++, id); - ps.setString(pos++, resource.getClientId()); - ps.setString(pos++, IdentityZoneHolder.get().getId()); - ps.setBoolean(pos++, resource.isShowOnHomePage()); - URL appLaunchUrl = resource.getAppLaunchUrl(); - ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); - String appIcon = resource.getAppIcon(); - if (appIcon != null) { - byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); - ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); - } else { - ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); - } - ps.setInt(pos++, 1); - } - }); - return retrieve(resource.getClientId()); - } - - @Override - public ClientMetadata update(String clientId, ClientMetadata resource) { - logger.debug("Updating metadata for client: " + clientId); + public ClientMetadata update(ClientMetadata resource) { + logger.debug("Updating metadata for client: " + resource.getClientId()); int updated = template.update(CLIENT_METADATA_UPDATE, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { @@ -116,18 +84,18 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); } ps.setInt(pos++, resource.getVersion() + 1); - ps.setString(pos++, clientId); + ps.setString(pos++, resource.getClientId()); ps.setString(pos++, IdentityZoneHolder.get().getId()); ps.setInt(pos++, resource.getVersion()); } }); - ClientMetadata resultingClientMetadata = retrieve(clientId); + ClientMetadata resultingClientMetadata = retrieve(resource.getClientId()); if (updated == 0) { throw new OptimisticLockingFailureException(String.format( "Attempt to update the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", - clientId, + resource.getClientId(), resultingClientMetadata.getVersion(), resource.getVersion())); } else if (updated > 1) { @@ -137,29 +105,6 @@ public void setValues(PreparedStatement ps) throws SQLException { return resultingClientMetadata; } - @Override - public ClientMetadata delete(String clientId, int version) { - logger.debug("Deleting UI details for client: " + clientId); - ClientMetadata clientMetadata = retrieve(clientId); - int updated; - - if (version < 0) { - updated = template.update(CLIENT_METADATA_DELETE_QUERY, clientId, IdentityZoneHolder.get().getId()); - } else { - updated = template.update(CLIENT_METADATA_DELETE_QUERY + " and version=?", clientId, IdentityZoneHolder.get().getId(), version); - } - - if (updated == 0) { - throw new OptimisticLockingFailureException(String.format( - "Attempt to delete the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", - clientId, - clientMetadata.getVersion(), - version)); - } - - return clientMetadata; - } - private class ClientMetadataRowMapper implements RowMapper { @@ -167,7 +112,6 @@ private class ClientMetadataRowMapper implements RowMapper { public ClientMetadata mapRow(ResultSet rs, int rowNum) throws SQLException { ClientMetadata clientMetadata = new ClientMetadata(); int pos = 1; - pos++; // id clientMetadata.setClientId(rs.getString(pos++)); clientMetadata.setIdentityZoneId(rs.getString(pos++)); clientMetadata.setShowOnHomePage(rs.getBoolean(pos++)); @@ -176,7 +120,8 @@ public ClientMetadata mapRow(ResultSet rs, int rowNum) throws SQLException { } catch (MalformedURLException mue) { // it is safe to ignore this as client_metadata rows are always created from a ClientMetadata instance whose launch url property is strongly typed to URL } - clientMetadata.setAppIcon(new String(Base64.encode(rs.getBytes(pos++)))); + byte[] iconBytes = rs.getBytes(pos++); + if(iconBytes != null) { clientMetadata.setAppIcon(new String(Base64.encode(iconBytes))); } clientMetadata.setVersion(rs.getInt(pos++)); return clientMetadata; } diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql new file mode 100644 index 00000000000..9b6b3ab58b2 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql @@ -0,0 +1,9 @@ +ALTER TABLE oauth_client_details ADD COLUMN show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE oauth_client_details ADD COLUMN app_launch_url VARCHAR(1024); +ALTER TABLE oauth_client_details ADD COLUMN app_icon BLOB; +ALTER TABLE oauth_client_details ADD COLUMN version INT DEFAULT 0 NOT NULL; + +ALTER TABLE oauth_client_details ALTER COLUMN identity_zone_id SET DATA TYPE VARCHAR(36); -- to avoid whitespace padding + + + diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql deleted file mode 100644 index 9746b2c381a..00000000000 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__New_Table_For_Storing_Client_Metadata.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE oauth_client_metadata ( - id VARCHAR(255) NOT NULL, - client_id VARCHAR(255) NOT NULL UNIQUE, - identity_zone_id VARCHAR(36) NOT NULL, - show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, - app_launch_url VARCHAR(1024), - app_icon BLOB, - version INT DEFAULT 0 NOT NULL, - PRIMARY KEY (id), - CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) -); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql new file mode 100644 index 00000000000..4dd97cc6e34 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql @@ -0,0 +1,4 @@ +ALTER TABLE oauth_client_details ADD COLUMN show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE oauth_client_details ADD COLUMN app_launch_url VARCHAR(1024); +ALTER TABLE oauth_client_details ADD COLUMN app_icon MEDIUMBLOB; +ALTER TABLE oauth_client_details ADD COLUMN version INT DEFAULT 0 NOT NULL; diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql deleted file mode 100644 index 850c0fae51f..00000000000 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE oauth_client_metadata ( - id VARCHAR(255) NOT NULL, - client_id VARCHAR(255) NOT NULL UNIQUE, - identity_zone_id VARCHAR(36) NOT NULL, - show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, - app_launch_url VARCHAR(1024), - app_icon MEDIUMBLOB, - version INT DEFAULT 0 NOT NULL, - PRIMARY KEY (id), - CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) -); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql new file mode 100644 index 00000000000..9af47ecb036 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql @@ -0,0 +1,4 @@ +ALTER TABLE oauth_client_details ADD COLUMN show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE oauth_client_details ADD COLUMN app_launch_url VARCHAR(1024); +ALTER TABLE oauth_client_details ADD COLUMN app_icon BYTEA; +ALTER TABLE oauth_client_details ADD COLUMN version INT DEFAULT 0 NOT NULL; diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql deleted file mode 100644 index 1ba4210b88b..00000000000 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__New_Table_For_Storing_Client_Metadata.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE oauth_client_metadata ( - id VARCHAR(256) NOT NULL, - client_id VARCHAR(256) NOT NULL UNIQUE, - identity_zone_id VARCHAR(36) NOT NULL, - show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL, - app_launch_url VARCHAR(1024), - app_icon BYTEA, - version INT DEFAULT 0 NOT NULL, - PRIMARY KEY (id), - CONSTRAINT FK_client_details FOREIGN KEY (client_id,identity_zone_id) REFERENCES oauth_client_details(client_id,identity_zone_id) -); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index 1573259b6d7..d1b243eed40 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -1,14 +1,10 @@ package org.cloudfoundry.identity.uaa.client; -import org.apache.xml.security.utils.Base64; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; import org.cloudfoundry.identity.uaa.util.PredicateMatcher; import org.cloudfoundry.identity.uaa.zone.IdentityZone; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.Before; import org.junit.Test; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; @@ -17,11 +13,9 @@ import java.net.URL; import java.util.List; -import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; /******************************************************************************* * Cloud Foundry @@ -44,100 +38,23 @@ public class JdbcClientMetadataProvisioningTest extends JdbcTestBase { @Before public void createDatasource() throws Exception { db = new JdbcClientMetadataProvisioning(jdbcTemplate); - - // When running hsqldb uncomment these lines to invoke the in-built UI client -// org.hsqldb.util.DatabaseManagerSwing.main(new String[] { -// "--url", "jdbc:hsqldb:mem:uaadb", "--noexit" -// }); - } -// -// @Override -// public void setUp() throws Exception { -// System.setProperty("spring.profiles.active", "postgresql"); -// super.setUp(); -// } - - @Test - public void createClientMetadata() throws Exception { - //given - String clientId = generator.generate(); - jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - - //when a client ui details object is saved - ClientMetadata createdClientMetadata = db.create(clientMetadata); - - //then - assertThat(createdClientMetadata.getClientId(), is(clientMetadata.getClientId())); - assertThat(createdClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); - assertThat(createdClientMetadata.isShowOnHomePage(), is(clientMetadata.isShowOnHomePage())); - assertThat(createdClientMetadata.getAppLaunchUrl(), is(clientMetadata.getAppLaunchUrl())); - assertThat(createdClientMetadata.getAppIcon(), is(clientMetadata.getAppIcon())); - assertThat(createdClientMetadata.getVersion(), is(1)); - - //and then app icon that is saved is really the base64 decoded bytes - byte[] blobbyblob = jdbcTemplate.queryForObject("select app_icon from oauth_client_metadata where client_id='" + clientId + "'", byte[].class); - assertThat(blobbyblob, is(Base64.decode(base64EncodedImg))); - } - - @Test(expected = DuplicateKeyException.class) - public void createClientMetadata_withAlreadyExistingDuplicate() throws Exception { - String clientId = generator.generate(); - jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - - db.create(clientMetadata); - - //duplicate client metadata - db.create(clientMetadata); } - @Test - public void whenMultipleClients_WithTheSameNameButDifferentZone_ClientMetadataCorrectlyAssociated() throws Exception { - try { - //given - String clientId = generator.generate(); - String otherZoneId = generator.generate(); - IdentityZone otherZone = new IdentityZone(); - otherZone.setId(otherZoneId); - jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + otherZoneId + "')"); - ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - IdentityZoneHolder.set(otherZone); - - //when a client is created in another zone - ClientMetadata createdClientMetadata = db.create(clientMetadata); - - //then expect as such - assertThat(createdClientMetadata.getIdentityZoneId(), is(otherZoneId)); - } finally { - IdentityZoneHolder.set(IdentityZone.getUaa()); - } - } - - @Test(expected = DataIntegrityViolationException.class) + @Test(expected = EmptyResultDataAccessException.class) public void constraintViolation_WhenNoMatchingClientFound() throws Exception { - //given there is no oauth_client_details record - - //when we attempt to create an client ui details record ClientMetadata clientMetadata = createTestClientMetadata(generator.generate(), true, new URL("http://app.launch/url"), base64EncodedImg); - db.create(clientMetadata); - - //then we expect a constraint violation + db.update(clientMetadata); } @Test public void retrieveClientMetadata() throws Exception { - //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - ClientMetadata createdClientMetadata = db.create(clientMetadata); + ClientMetadata createdClientMetadata = db.update(clientMetadata); - //when retrieving the client UI details ClientMetadata retrievedClientMetadata = db.retrieve(createdClientMetadata.getClientId()); - //then assertThat(retrievedClientMetadata.getClientId(), is(clientMetadata.getClientId())); assertThat(retrievedClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); assertThat(retrievedClientMetadata.isShowOnHomePage(), is(clientMetadata.isShowOnHomePage())); @@ -156,11 +73,11 @@ public void retrieveAllClientMetadata() throws Exception { String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); ClientMetadata clientMetadata1 = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - db.create(clientMetadata1); + db.update(clientMetadata1); String clientId2 = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId2 + "', '" + IdentityZone.getUaa().getId() + "')"); ClientMetadata clientMetadata2 = createTestClientMetadata(clientId2, true, new URL("http://app.launch/url"), base64EncodedImg); - db.create(clientMetadata2); + db.update(clientMetadata2); List clientMetadatas = db.retrieveAll(); @@ -171,66 +88,22 @@ public void retrieveAllClientMetadata() throws Exception { @Test public void updateClientMetadata() throws Exception { - //given String clientId = generator.generate(); jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - ClientMetadata createdClientMetadata = db.create(clientMetadata); - ClientMetadata newClientMetadata = createTestClientMetadata(clientMetadata.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); + ClientMetadata newClientMetadata = createTestClientMetadata(clientId, false, new URL("http://updated.app/launch/url"), base64EncodedImg); - //when - ClientMetadata updatedClientMetadata = db.update(createdClientMetadata.getClientId(), newClientMetadata); + ClientMetadata updatedClientMetadata = db.update(newClientMetadata); try { - db.update(createdClientMetadata.getClientId(), newClientMetadata); + db.update(createTestClientMetadata(clientId, true, new URL("http://redundant-set.com/"), "dogsDOGSdogs")); fail("another update should fail due to incorrect version"); } catch (OptimisticLockingFailureException olfe) {} - //then - assertThat(updatedClientMetadata.getClientId(), is(clientMetadata.getClientId())); + assertThat(updatedClientMetadata.getClientId(), is(clientId)); assertThat(updatedClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); assertThat(updatedClientMetadata.isShowOnHomePage(), is(newClientMetadata.isShowOnHomePage())); assertThat(updatedClientMetadata.getAppLaunchUrl(), is(newClientMetadata.getAppLaunchUrl())); assertThat(updatedClientMetadata.getAppIcon(), is(newClientMetadata.getAppIcon())); - assertThat(updatedClientMetadata.getVersion(), is(clientMetadata.getVersion() + 1)); - } - - @Test - public void deleteClientMetadata() throws Exception { - //given - String clientId = generator.generate(); - jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - db.create(clientMetadata); - - //when you delete the client ui details - db.delete(clientMetadata.getClientId(), -1); - - //then subsequent retrieval should fail - try { - db.retrieve(clientMetadata.getClientId()); - fail("Metadata should have been deleted"); - } catch(EmptyResultDataAccessException e) { - } - } - - @Test - public void deleteClientMetadata_AfterVersionUpdate() throws Exception { - //given - String clientId = generator.generate(); - jdbcTemplate.execute("insert into oauth_client_details(client_id, identity_zone_id) values ('" + clientId + "', '" + IdentityZone.getUaa().getId() + "')"); - ClientMetadata clientMetadata = createTestClientMetadata(clientId, true, new URL("http://app.launch/url"), base64EncodedImg); - ClientMetadata createdClientMetadata = db.create(clientMetadata); - ClientMetadata newClientMetadata = createTestClientMetadata(clientMetadata.getClientId(), false, new URL("http://updated.app/launch/url"), base64EncodedImg); - db.update(createdClientMetadata.getClientId(), newClientMetadata); - - //when you delete - try { - db.delete(clientId, clientMetadata.getVersion()); - fail("should fail because wrong version"); - } catch (OptimisticLockingFailureException olfe) {} - - //then succeed with right version - db.delete(clientId, clientMetadata.getVersion() + 1); + assertThat(updatedClientMetadata.getVersion(), is(newClientMetadata.getVersion() + 1)); } private ClientMetadata createTestClientMetadata(String clientId, boolean showOnHomePage, URL appLaunchUrl, String appIcon) throws MalformedURLException { @@ -239,10 +112,10 @@ private ClientMetadata createTestClientMetadata(String clientId, boolean showOnH clientMetadata.setShowOnHomePage(showOnHomePage); clientMetadata.setAppLaunchUrl(appLaunchUrl); clientMetadata.setAppIcon(appIcon); - clientMetadata.setVersion(1); + clientMetadata.setVersion(0); return clientMetadata; } private static final String base64EncodedImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAXRQTFRFAAAAOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ozk4Ojo6Ojk5NkZMFp/PFqDPNkVKOjo6Ojk5MFhnEq3nEqvjEqzjEbDpMFdlOjo5Ojo6Ojo6Ozg2GZ3TFqXeFKfgF6DVOjo6Ozg2G5jPGZ7ZGKHbGZvROjo6Ojo5M1FfG5vYGp3aM1BdOjo6Ojo6Ojk4KHWeH5PSHpTSKHSbOjk4Ojo6Ojs8IY/QIY/QOjs7Ojo6Ojo6Ozc0JYfJJYjKOzYyOjo5Ozc0KX7AKH/AOzUxOjo5Ojo6Ojo6Ojo6Ojs8LHi6LHi6Ojs7Ojo6Ojo6Ojo6Ojo6Ojo6L3K5L3S7LnW8LnS7Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6NlFvMmWeMmaeNVJwOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojk5Ojk4Ojk4Ojk5Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6FaXeFabfGZ/aGKDaHJnVG5rW////xZzURgAAAHV0Uk5TAAACPaXbAVzltTa4MykoM5HlPY/k5Iw85QnBs2D7+lzAtWD7+lyO6EKem0Ey47Mx2dYvtVZVop5Q2i4qlZAnBiGemh0EDXuddqypcHkShPJwYufmX2rvihSJ+qxlg4JiqP2HPtnW1NjZ2svRVAglGTi91RAXr3/WIQAAAAFiS0dEe0/StfwAAAAJcEhZcwAAAEgAAABIAEbJaz4AAADVSURBVBjTY2BgYGBkYmZhZWVhZmJkAANGNnYODk5ODg52NrAIIyMXBzcPLx8/NwcXIyNYQEBQSFhEVExcQgAiICklLSNbWiYnLy0lCRFQUFRSLq9QUVVUgAgwqqlraFZWaWmrqzFCTNXR1dM3MDQy1tWB2MvIaMJqamZuYWnCCHeIlbWNrZ0VG5QPFLF3cHRydoErcHVz9/D08nb3kYSY6evnHxAYFBwSGhYeAbbWNzIqOiY2Lj4hMckVoiQ5JTUtPSMzKzsH6pfcvPyCwqKc4pJcoAAA2pghnaBVZ0kAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTAtMDhUMTI6NDg6MDkrMDA6MDDsQS6eAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTEwLTA4VDEyOjQ4OjA5KzAwOjAwnRyWIgAAAEZ0RVh0c29mdHdhcmUASW1hZ2VNYWdpY2sgNi43LjgtOSAyMDE0LTA1LTEyIFExNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9yZ9yG7QAAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6aGVpZ2h0ADE5Mg8AcoUAAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMTky06whCAAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNDQ0MzA4NDg5qdC9PQAAAA90RVh0VGh1bWI6OlNpemUAMEJClKI+7AAAAFZ0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL21udGxvZy9mYXZpY29ucy8yMDE1LTEwLTA4LzJiMjljNmYwZWRhZWUzM2ViNmM1Mzg4ODMxMjg3OTg1Lmljby5wbmdoJKG+AAAAAElFTkSuQmCC"; -} \ No newline at end of file +} diff --git a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml index 8ac7420e670..54db65aaf27 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml @@ -90,7 +90,6 @@ - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java index aa64b65492f..c2519cfe13a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java @@ -21,8 +21,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -44,7 +46,6 @@ *******************************************************************************/ public class ClientMetadataAdminEndpointsMockMvcTest extends InjectedMockContextTest { - private JdbcClientMetadataProvisioning clientMetadata; private String adminClientTokenWithClientsWrite; private JdbcClientDetailsService clients; private RandomValueStringGenerator generator = new RandomValueStringGenerator(8); @@ -65,59 +66,13 @@ public void setUp() throws Exception { testAccounts.getAdminClientSecret(), "clients.write"); - clientMetadata = getWebApplicationContext().getBean(JdbcClientMetadataProvisioning.class); clients = getWebApplicationContext().getBean(JdbcClientDetailsService.class); } - @Test - public void createClientMetadata() throws Exception { - String clientId = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - - MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - assertThat(response.getStatus(), is(HttpStatus.CREATED.value())); - } - - @Test - public void createClientMetadata_WithNoClient() throws Exception { - String clientId = generator.generate(); - - MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND.value())); - } - - @Test - public void createDuplicateClientMetadata_isConflict() throws Exception { - String clientId = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - MockHttpServletResponse response = createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - assertThat(response.getStatus(), is(CONFLICT.value())); - } - - @Test - public void createUnauthorized_BecauseInsufficientScope() throws Exception { - // given a token with insufficient privileges - String userTokenWithInsufficientScope = testClient.getUserOAuthAccessToken( - "app", - "appclientsecret", - testAccounts.getUserName(), - testAccounts.getPassword(), - "openid"); - - // when a new client metadata is created - String clientId = generator.generate(); - MockHttpServletResponse response = createTestClientMetadata(clientId, userTokenWithInsufficientScope); - - // then expect a 403 Forbidden - assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN.value())); - } - @Test public void getClientMetadata() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); MockHttpServletResponse response = getTestClientMetadata(clientId); @@ -137,15 +92,12 @@ public void getClientMetadata_WhichDoesNotExist() throws Exception { public void retrieveAllClientMetadata() throws Exception { String clientId1 = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId1, null, null, null, null)); - createTestClientMetadata(clientId1, adminClientTokenWithClientsWrite); String clientId2 = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId2, null, null, null, null)); - createTestClientMetadata(clientId2, adminClientTokenWithClientsWrite); String clientId3 = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId3, null, null, null, null)); - createTestClientMetadata(clientId3, adminClientTokenWithClientsWrite); MockHttpServletResponse response = getMockMvc().perform(get("/oauth/clients/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) @@ -161,7 +113,6 @@ public void retrieveAllClientMetadata() throws Exception { public void updateClientMetadata_WithCorrectVersion() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); ClientMetadata updatedClientMetadata = new ClientMetadata(); updatedClientMetadata.setClientId(clientId); @@ -170,7 +121,7 @@ public void updateClientMetadata_WithCorrectVersion() throws Exception { MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .header("If-Match", "1") + .header("If-Match", "0") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(updatedClientMetadata)); @@ -183,97 +134,104 @@ public void updateClientMetadata_WithCorrectVersion() throws Exception { } @Test - public void updateClientMetadata_WithIncorrectVersion() throws Exception { + public void updateClientMetadata_WithNoClientIdInBody() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); ClientMetadata updatedClientMetadata = new ClientMetadata(); - updatedClientMetadata.setClientId(clientId); + updatedClientMetadata.setClientId(null); URL appLaunchUrl = new URL("http://changed.app.launch/url"); updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .header("If-Match", "100") + .header("If-Match", "0") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(updatedClientMetadata)); ResultActions perform = getMockMvc().perform(updateClientPut); - assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.PRECONDITION_FAILED.value())); + assertThat(perform.andReturn().getResponse().getContentAsString(), containsString(appLaunchUrl.toString())); + + MockHttpServletResponse response = getTestClientMetadata(clientId); + assertThat(response.getStatus(), is(HttpStatus.OK.value())); + assertThat(response.getContentAsString(), containsString(appLaunchUrl.toString())); } @Test - public void updateClientMetadata_WithNoVersion() throws Exception { + public void updateClientMetadata_ForNonExistentClient() throws Exception { String clientId = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - ClientMetadata updatedClientMetadata = new ClientMetadata(); - updatedClientMetadata.setClientId(clientId); + ClientMetadata clientMetadata = new ClientMetadata(); + clientMetadata.setClientId(clientId); URL appLaunchUrl = new URL("http://changed.app.launch/url"); - updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); + clientMetadata.setAppLaunchUrl(appLaunchUrl); MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", "0") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientMetadata)); ResultActions perform = getMockMvc().perform(updateClientPut); - assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.BAD_REQUEST.value())); + assertEquals(perform.andReturn().getResponse().getStatus(), NOT_FOUND.value()); } @Test - public void deleteClientMetadata() throws Exception { + public void updateClientMetadata_ClientIdMismatch() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - getMockMvc().perform(delete("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .header("If-Match", 1) - .accept(APPLICATION_JSON)) - .andExpect(status().isOk()); - } + ClientMetadata clientMetadata = new ClientMetadata(); + clientMetadata.setClientId("other-client-id"); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + clientMetadata.setAppLaunchUrl(appLaunchUrl); - @Test - public void deleteClientMetadata_withInvalidId() throws Exception { - String clientId = generator.generate(); - getMockMvc().perform(delete("/oauth/clients/" + clientId + "/meta") + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .header("If-Match", 1) - .accept(APPLICATION_JSON)) - .andExpect(status().isNotFound()); + .header("If-Match", "0") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientMetadata)); + ResultActions perform = getMockMvc().perform(updateClientPut); + assertEquals(perform.andReturn().getResponse().getStatus(), HttpStatus.BAD_REQUEST.value()); } @Test - public void deleteClientMetadata_WithIncorrectVersion() throws Exception { + public void updateClientMetadata_WithIncorrectVersion() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - - MockHttpServletRequestBuilder deleteRequest = delete("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .header("If-Match", 100) - .accept(APPLICATION_JSON); + ClientMetadata updatedClientMetadata = new ClientMetadata(); + updatedClientMetadata.setClientId(clientId); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); - ResultActions perform = getMockMvc().perform(deleteRequest); + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .header("If-Match", "100") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + ResultActions perform = getMockMvc().perform(updateClientPut); assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.PRECONDITION_FAILED.value())); } @Test - public void deleteClientMetadata_WithNoVersion() throws Exception { + public void updateClientMetadata_WithNoVersion() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - createTestClientMetadata(clientId, adminClientTokenWithClientsWrite); - - MockHttpServletRequestBuilder deleteRequest = delete("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .accept(APPLICATION_JSON); + ClientMetadata updatedClientMetadata = new ClientMetadata(); + updatedClientMetadata.setClientId(clientId); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); - ResultActions perform = getMockMvc().perform(deleteRequest); + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + ResultActions perform = getMockMvc().perform(updateClientPut); assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.BAD_REQUEST.value())); } @@ -283,16 +241,4 @@ private MockHttpServletResponse getTestClientMetadata(String clientId) throws Ex .accept(APPLICATION_JSON); return getMockMvc().perform(createClientGet).andReturn().getResponse(); } - - private MockHttpServletResponse createTestClientMetadata(String clientId, String token) throws Exception { - ClientMetadata clientMetadata = new ClientMetadata(); - - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + token) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientMetadata)); - return getMockMvc().perform(createClientPost).andReturn().getResponse(); - } - } From 8abbf0d6da1305d58c1efbb040b70577572b9133 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 20 Jan 2016 17:37:57 -0800 Subject: [PATCH 21/25] Bootstrap client metadata [#109263482] https://www.pivotaltracker.com/story/show/109263482 Signed-off-by: Madhura Bhave --- .../identity/uaa/client/ClientMetadata.java | 10 ---- .../uaa/client/ClientAdminBootstrap.java | 43 +++++++++++++- .../client/ClientMetadataAdminEndpoints.java | 8 --- .../client/ClientMetadataProvisioning.java | 2 - .../JdbcClientMetadataProvisioning.java | 56 +++++++------------ .../identity/uaa/util/LineAwareLayout.java | 5 +- .../V3_0_2__Add_Client_Metadata_Columns.sql | 1 - .../V3_0_2__Add_Client_Metadata_Columns.sql | 1 - .../V3_0_2__Add_Client_Metadata_Columns.sql | 1 - .../uaa/client/ClientAdminBootstrapTests.java | 38 +++++++++++++ .../JdbcClientMetadataProvisioningTest.java | 6 -- .../webapp/WEB-INF/spring/oauth-clients.xml | 2 + ...ientMetadataAdminEndpointsMockMvcTest.java | 41 +------------- 13 files changed, 105 insertions(+), 109 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java index 84037e13e32..41c1ba2d486 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java @@ -68,14 +68,4 @@ public String getAppIcon() { public void setAppIcon(String appIcon) { this.appIcon = appIcon; } - - @JsonIgnore - public int getVersion() { - return version; - } - - @JsonIgnore - public void setVersion(int version) { - this.version = version; - } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java index db322c1e861..b4e5d86cd33 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java @@ -12,6 +12,8 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.client; +import java.net.MalformedURLException; +import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -26,6 +28,7 @@ import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpStatus; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -48,6 +51,8 @@ public class ClientAdminBootstrap implements InitializingBean { private ClientRegistrationService clientRegistrationService; + private ClientMetadataProvisioning clientMetadataProvisioning; + private String domain = "cloudfoundry\\.com"; private boolean defaultOverride = true; @@ -194,8 +199,9 @@ private String getRedirectUris(Map map) { } private void addNewClients() throws Exception { - for (String clientId : clients.keySet()) { - Map map = clients.get(clientId); + for (Map.Entry> entry : clients.entrySet()) { + String clientId = entry.getKey(); + Map map = entry.getValue(); BaseClientDetails client = new BaseClientDetails(clientId, (String) map.get("resource-ids"), (String) map.get("scope"), (String) map.get("authorized-grant-types"), (String) map.get("authorities"), getRedirectUris(map)); @@ -226,9 +232,10 @@ private void addNewClients() throws Exception { } for (String key : Arrays.asList("resource-ids", "scope", "authorized-grant-types", "authorities", "redirect-uri", "secret", "id", "override", "access-token-validity", - "refresh-token-validity")) { + "refresh-token-validity","show-on-homepage","app-launch-url","app-icon")) { info.remove(key); } + client.setAdditionalInformation(info); try { clientRegistrationService.addClientDetails(client); @@ -244,9 +251,31 @@ private void addNewClients() throws Exception { logger.debug(e.getMessage()); } } + ClientMetadata clientMetadata = buildClientMetadata(map, clientId); + clientMetadataProvisioning.update(clientMetadata); } } + private ClientMetadata buildClientMetadata(Map map, String clientId) { + Boolean showOnHomepage = (Boolean) map.get("show-on-homepage"); + String appLaunchUrl = (String) map.get("app-launch-url"); + String appIcon = (String) map.get("app-icon"); + ClientMetadata clientMetadata = new ClientMetadata(); + clientMetadata.setClientId(clientId); + + clientMetadata.setAppIcon(appIcon); + clientMetadata.setShowOnHomePage(showOnHomepage != null && showOnHomepage); + if(StringUtils.hasText(appLaunchUrl)) { + try { + clientMetadata.setAppLaunchUrl(new URL(appLaunchUrl)); + } catch (MalformedURLException e) { + logger.info(new ClientMetadataException("Invalid app-launch-url for client " + clientId, e, HttpStatus.INTERNAL_SERVER_ERROR)); + } + } + + return clientMetadata; + } + protected boolean didPasswordChange(String clientId, String rawPassword) { if (getPasswordEncoder()!=null && clientRegistrationService instanceof ClientDetailsService) { ClientDetails existing = ((ClientDetailsService)clientRegistrationService).loadClientByClientId(clientId); @@ -256,4 +285,12 @@ protected boolean didPasswordChange(String clientId, String rawPassword) { return true; } } + + public ClientMetadataProvisioning getClientMetadataProvisioning() { + return clientMetadataProvisioning; + } + + public void setClientMetadataProvisioning(ClientMetadataProvisioning clientMetadataProvisioning) { + this.clientMetadataProvisioning = clientMetadataProvisioning; + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java index ee8ca673888..21d8d305e06 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpoints.java @@ -62,11 +62,7 @@ public List retrieveAllClientMetadata() { @RequestMapping(value = "/oauth/clients/{client}/meta", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) public ClientMetadata updateClientMetadata(@RequestBody ClientMetadata clientMetadata, - @RequestHeader(value = "If-Match", required = false) Integer etag, @PathVariable("client") String clientId) { - if (etag == null) { - throw new ClientMetadataException("Missing If-Match header", HttpStatus.BAD_REQUEST); - } if (StringUtils.hasText(clientMetadata.getClientId())) { if (!clientId.equals(clientMetadata.getClientId())) { @@ -75,12 +71,8 @@ public ClientMetadata updateClientMetadata(@RequestBody ClientMetadata clientMet } else { clientMetadata.setClientId(clientId); } - - clientMetadata.setVersion(etag); try { return clientMetadataProvisioning.update(clientMetadata); - } catch (OptimisticLockingFailureException olfe) { - throw new ClientMetadataException(olfe.getMessage(), HttpStatus.PRECONDITION_FAILED); } catch (EmptyResultDataAccessException e) { throw new ClientMetadataException("No client with ID " + clientMetadata.getClientId(), HttpStatus.NOT_FOUND); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java index 741bc4014f7..834e5901f9a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadataProvisioning.java @@ -1,7 +1,5 @@ package org.cloudfoundry.identity.uaa.client; -import org.cloudfoundry.identity.uaa.resources.ResourceManager; - import java.util.List; /******************************************************************************* diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java index 375c76f68d1..afc5af2b082 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java @@ -6,19 +6,16 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; -import org.springframework.security.crypto.codec.Base64; import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; import java.io.ByteArrayInputStream; import java.net.MalformedURLException; import java.net.URL; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -import java.util.UUID; /******************************************************************************* * Cloud Foundry @@ -36,11 +33,11 @@ public class JdbcClientMetadataProvisioning implements ClientMetadataProvisionin private static final Log logger = LogFactory.getLog(JdbcClientMetadataProvisioning.class); - private static final String CLIENT_METADATA_FIELDS = "client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon, version"; + private static final String CLIENT_METADATA_FIELDS = "client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon"; private static final String CLIENT_METADATA_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where client_id=? and identity_zone_id=?"; private static final String CLIENT_METADATAS_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where identity_zone_id=?"; - private static final String CLIENT_METADATA_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon, version"; - private static final String CLIENT_METADATA_UPDATE = "update oauth_client_details set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=? and version=?"; + private static final String CLIENT_METADATA_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon"; + private static final String CLIENT_METADATA_UPDATE = "update oauth_client_details set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=?"; private JdbcTemplate template; private final RowMapper mapper = new ClientMetadataRowMapper(); @@ -69,38 +66,26 @@ public ClientMetadata retrieve(String clientId) { @Override public ClientMetadata update(ClientMetadata resource) { logger.debug("Updating metadata for client: " + resource.getClientId()); - int updated = template.update(CLIENT_METADATA_UPDATE, new PreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps) throws SQLException { - int pos = 1; - ps.setBoolean(pos++, resource.isShowOnHomePage()); - URL appLaunchUrl = resource.getAppLaunchUrl(); - ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); - String appIcon = resource.getAppIcon(); - if (appIcon != null) { - byte[] decodedAppIcon = Base64.decode(appIcon.getBytes()); - ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), (int) decodedAppIcon.length); - } else { - ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[] {}), (int) 0); - } - ps.setInt(pos++, resource.getVersion() + 1); - ps.setString(pos++, resource.getClientId()); - ps.setString(pos++, IdentityZoneHolder.get().getId()); - ps.setInt(pos++, resource.getVersion()); + int updated = template.update(CLIENT_METADATA_UPDATE, ps -> { + int pos = 1; + ps.setBoolean(pos++, resource.isShowOnHomePage()); + URL appLaunchUrl = resource.getAppLaunchUrl(); + ps.setString(pos++, appLaunchUrl == null ? null : appLaunchUrl.toString()); + String appIcon = resource.getAppIcon(); + if (appIcon != null) { + byte[] decodedAppIcon = Base64Utils.decode(appIcon.getBytes()); + ps.setBinaryStream(pos++, new ByteArrayInputStream(decodedAppIcon), decodedAppIcon.length); + } else { + ps.setBinaryStream(pos++, new ByteArrayInputStream(new byte[]{}), 0); } + ps.setString(pos++, resource.getClientId()); + String zone = IdentityZoneHolder.get().getId(); + ps.setString(pos++, zone); }); ClientMetadata resultingClientMetadata = retrieve(resource.getClientId()); - if (updated == 0) { - throw new OptimisticLockingFailureException(String.format( - "Attempt to update the UI details of client (%s) failed with incorrect version: expected=%d but found=%d", - resource.getClientId(), - resultingClientMetadata.getVersion(), - resource.getVersion())); - } else if (updated > 1) { - throw new IncorrectResultSizeDataAccessException(1); - } + if (updated > 1) { throw new IncorrectResultSizeDataAccessException(1); } return resultingClientMetadata; } @@ -121,8 +106,7 @@ public ClientMetadata mapRow(ResultSet rs, int rowNum) throws SQLException { // it is safe to ignore this as client_metadata rows are always created from a ClientMetadata instance whose launch url property is strongly typed to URL } byte[] iconBytes = rs.getBytes(pos++); - if(iconBytes != null) { clientMetadata.setAppIcon(new String(Base64.encode(iconBytes))); } - clientMetadata.setVersion(rs.getInt(pos++)); + if(iconBytes != null) { clientMetadata.setAppIcon(new String(Base64Utils.encode(iconBytes))); } return clientMetadata; } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/LineAwareLayout.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/LineAwareLayout.java index cd0d83df9a9..fc9fa23c4c4 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/LineAwareLayout.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/LineAwareLayout.java @@ -16,6 +16,7 @@ import org.apache.log4j.Layout; import org.apache.log4j.spi.LoggingEvent; +import org.springframework.util.StringUtils; /** * Created by pivotal on 10/28/15. @@ -46,8 +47,10 @@ public String format(LoggingEvent event) { String[] throwable; if(messageLayout == null && (throwable = event.getThrowableStrRep()) != null) { lines = throwable; - } else { + } else if(message != null) { lines = message.split("\r?\n"); + } else { + lines = new String[0]; } StringBuffer strBuf = new StringBuffer(); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql index 9b6b3ab58b2..454d1d40420 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V3_0_2__Add_Client_Metadata_Columns.sql @@ -1,7 +1,6 @@ ALTER TABLE oauth_client_details ADD COLUMN show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL; ALTER TABLE oauth_client_details ADD COLUMN app_launch_url VARCHAR(1024); ALTER TABLE oauth_client_details ADD COLUMN app_icon BLOB; -ALTER TABLE oauth_client_details ADD COLUMN version INT DEFAULT 0 NOT NULL; ALTER TABLE oauth_client_details ALTER COLUMN identity_zone_id SET DATA TYPE VARCHAR(36); -- to avoid whitespace padding diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql index 4dd97cc6e34..3fdc30b2ddf 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V3_0_2__Add_Client_Metadata_Columns.sql @@ -1,4 +1,3 @@ ALTER TABLE oauth_client_details ADD COLUMN show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL; ALTER TABLE oauth_client_details ADD COLUMN app_launch_url VARCHAR(1024); ALTER TABLE oauth_client_details ADD COLUMN app_icon MEDIUMBLOB; -ALTER TABLE oauth_client_details ADD COLUMN version INT DEFAULT 0 NOT NULL; diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql index 9af47ecb036..fe54d23c00e 100644 --- a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V3_0_2__Add_Client_Metadata_Columns.sql @@ -1,4 +1,3 @@ ALTER TABLE oauth_client_details ADD COLUMN show_on_home_page BOOLEAN DEFAULT TRUE NOT NULL; ALTER TABLE oauth_client_details ADD COLUMN app_launch_url VARCHAR(1024); ALTER TABLE oauth_client_details ADD COLUMN app_icon BYTEA; -ALTER TABLE oauth_client_details ADD COLUMN version INT DEFAULT 0 NOT NULL; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrapTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrapTests.java index 45fef8abb35..a0101bb744c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrapTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrapTests.java @@ -55,13 +55,16 @@ public class ClientAdminBootstrapTests extends JdbcTestBase { private ClientAdminBootstrap bootstrap; private JdbcClientDetailsService clientRegistrationService; + private ClientMetadataProvisioning clientMetadataProvisioning; @Before public void setUpClientAdminTests() throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); bootstrap = new ClientAdminBootstrap(encoder); clientRegistrationService = new MultitenantJdbcClientDetailsService(dataSource); + clientMetadataProvisioning = new JdbcClientMetadataProvisioning(jdbcTemplate); bootstrap.setClientRegistrationService(clientRegistrationService); + bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning); clientRegistrationService.setPasswordEncoder(encoder); } @@ -95,6 +98,23 @@ public void testSimpleAddClientWithSignupSuccessRedirectUrl() throws Exception { assertTrue(clientDetails.getRegisteredRedirectUri().contains("callback_url")); } + @Test + public void clientMetadata_getsBootstrapped() throws Exception { + Map map = new HashMap<>(); + map.put("id", "foo"); + map.put("secret", "bar"); + map.put("show-on-homepage", true); + map.put("app-launch-url", "http://takemetothispage.com"); + map.put("app-icon", "bAsE64encODEd/iMAgE="); + bootstrap.setClients(Collections.singletonMap((String) map.get("id"), map)); + bootstrap.afterPropertiesSet(); + + ClientMetadata clientMetadata = clientMetadataProvisioning.retrieve("foo"); + assertTrue(clientMetadata.isShowOnHomePage()); + assertEquals("http://takemetothispage.com", clientMetadata.getAppLaunchUrl().toString()); + assertEquals("bAsE64encODEd/iMAgE=", clientMetadata.getAppIcon()); + } + @Test public void testAdditionalInformation() throws Exception { List idps = Arrays.asList("idp1", "idp1"); @@ -129,7 +149,10 @@ public void testSimpleAddClientWithChangeEmailRedirectUrl() throws Exception { @Test public void testSimpleAddClientWithAutoApprove() throws Exception { ClientRegistrationService clientRegistrationService = mock(ClientRegistrationService.class); + ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class); bootstrap.setClientRegistrationService(clientRegistrationService); + bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning); + Map map = new HashMap<>(); map.put("id", "foo"); map.put("secret", "bar"); @@ -140,8 +163,11 @@ public void testSimpleAddClientWithAutoApprove() throws Exception { "uaa.none"); output.setClientSecret("bar"); bootstrap.setAutoApproveClients(Arrays.asList("foo")); + + when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata()); when(clientRegistrationService.listClientDetails()).thenReturn(Collections. emptyList()) .thenReturn(Collections. singletonList(output)); + bootstrap.setClients(Collections.singletonMap((String) map.get("id"), map)); bootstrap.afterPropertiesSet(); verify(clientRegistrationService).addClientDetails(output); @@ -153,11 +179,16 @@ public void testSimpleAddClientWithAutoApprove() throws Exception { @Test public void testOverrideClient() throws Exception { ClientRegistrationService clientRegistrationService = mock(ClientRegistrationService.class); + ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class); bootstrap.setClientRegistrationService(clientRegistrationService); + bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning); + Map map = new HashMap<>(); map.put("secret", "bar"); map.put("override", true); bootstrap.setClients(Collections.singletonMap("foo", map)); + when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata()); + doThrow(new ClientAlreadyExistsException("Planned")).when(clientRegistrationService).addClientDetails( any(ClientDetails.class)); bootstrap.afterPropertiesSet(); @@ -169,10 +200,14 @@ public void testOverrideClient() throws Exception { @Test public void testOverrideClientByDefault() throws Exception { ClientRegistrationService clientRegistrationService = mock(ClientRegistrationService.class); + ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class); bootstrap.setClientRegistrationService(clientRegistrationService); + bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning); + Map map = new HashMap<>(); map.put("secret", "bar"); bootstrap.setClients(Collections.singletonMap("foo", map)); + when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata()); doThrow(new ClientAlreadyExistsException("Planned")).when(clientRegistrationService).addClientDetails( any(ClientDetails.class)); bootstrap.afterPropertiesSet(); @@ -185,7 +220,9 @@ public void testOverrideClientByDefault() throws Exception { @SuppressWarnings("unchecked") public void testOverrideClientWithYaml() throws Exception { ClientRegistrationService clientRegistrationService = mock(ClientRegistrationService.class); + ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class); bootstrap.setClientRegistrationService(clientRegistrationService); + bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning); @SuppressWarnings("rawtypes") Map fooClient = new Yaml().loadAs("id: foo\noverride: true\nsecret: bar\n" @@ -198,6 +235,7 @@ public void testOverrideClientWithYaml() throws Exception { clients.put("foo", fooClient); clients.put("bar", barClient); bootstrap.setClients(clients); + when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata()); doThrow(new ClientAlreadyExistsException("Planned")).when(clientRegistrationService).addClientDetails( any(ClientDetails.class)); bootstrap.afterPropertiesSet(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java index d1b243eed40..dafaa0975de 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioningTest.java @@ -93,17 +93,12 @@ public void updateClientMetadata() throws Exception { ClientMetadata newClientMetadata = createTestClientMetadata(clientId, false, new URL("http://updated.app/launch/url"), base64EncodedImg); ClientMetadata updatedClientMetadata = db.update(newClientMetadata); - try { - db.update(createTestClientMetadata(clientId, true, new URL("http://redundant-set.com/"), "dogsDOGSdogs")); - fail("another update should fail due to incorrect version"); - } catch (OptimisticLockingFailureException olfe) {} assertThat(updatedClientMetadata.getClientId(), is(clientId)); assertThat(updatedClientMetadata.getIdentityZoneId(), is(IdentityZone.getUaa().getId())); assertThat(updatedClientMetadata.isShowOnHomePage(), is(newClientMetadata.isShowOnHomePage())); assertThat(updatedClientMetadata.getAppLaunchUrl(), is(newClientMetadata.getAppLaunchUrl())); assertThat(updatedClientMetadata.getAppIcon(), is(newClientMetadata.getAppIcon())); - assertThat(updatedClientMetadata.getVersion(), is(newClientMetadata.getVersion() + 1)); } private ClientMetadata createTestClientMetadata(String clientId, boolean showOnHomePage, URL appLaunchUrl, String appIcon) throws MalformedURLException { @@ -112,7 +107,6 @@ private ClientMetadata createTestClientMetadata(String clientId, boolean showOnH clientMetadata.setShowOnHomePage(showOnHomePage); clientMetadata.setAppLaunchUrl(appLaunchUrl); clientMetadata.setAppIcon(appIcon); - clientMetadata.setVersion(0); return clientMetadata; } 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 8569cef8c5c..aa94d08d189 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml @@ -33,6 +33,7 @@ + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java index c2519cfe13a..77daf2e2359 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java @@ -110,7 +110,7 @@ public void retrieveAllClientMetadata() throws Exception { } @Test - public void updateClientMetadata_WithCorrectVersion() throws Exception { + public void updateClientMetadata() throws Exception { String clientId = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); @@ -196,45 +196,6 @@ public void updateClientMetadata_ClientIdMismatch() throws Exception { assertEquals(perform.andReturn().getResponse().getStatus(), HttpStatus.BAD_REQUEST.value()); } - @Test - public void updateClientMetadata_WithIncorrectVersion() throws Exception { - String clientId = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - - ClientMetadata updatedClientMetadata = new ClientMetadata(); - updatedClientMetadata.setClientId(clientId); - URL appLaunchUrl = new URL("http://changed.app.launch/url"); - updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); - - MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .header("If-Match", "100") - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(updatedClientMetadata)); - ResultActions perform = getMockMvc().perform(updateClientPut); - assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.PRECONDITION_FAILED.value())); - } - - @Test - public void updateClientMetadata_WithNoVersion() throws Exception { - String clientId = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - - ClientMetadata updatedClientMetadata = new ClientMetadata(); - updatedClientMetadata.setClientId(clientId); - URL appLaunchUrl = new URL("http://changed.app.launch/url"); - updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); - - MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(updatedClientMetadata)); - ResultActions perform = getMockMvc().perform(updateClientPut); - assertThat(perform.andReturn().getResponse().getStatus(), is(HttpStatus.BAD_REQUEST.value())); - } - private MockHttpServletResponse getTestClientMetadata(String clientId) throws Exception { MockHttpServletRequestBuilder createClientGet = get("/oauth/clients/" + clientId + "/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) From 942c6ac73b17163a994077f13ad527d9042f1888 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Thu, 21 Jan 2016 14:29:40 -0800 Subject: [PATCH 22/25] When retrieving all client metadata, exclude empty metadata. [#109263820] https://www.pivotaltracker.com/story/show/109263820 Signed-off-by: Jonathan Lo --- .../identity/uaa/client/ClientMetadata.java | 1 - .../JdbcClientMetadataProvisioning.java | 2 +- .../WEB-INF/spring/client-admin-endpoints.xml | 1 + ...ientMetadataAdminEndpointsMockMvcTest.java | 92 ++++++++++++++----- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java index 41c1ba2d486..360c99458ba 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/client/ClientMetadata.java @@ -25,7 +25,6 @@ public class ClientMetadata { private boolean showOnHomePage; private URL appLaunchUrl; private String appIcon; - private int version; public String getClientId() { return clientId; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java index afc5af2b082..2ff08a7a5da 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcClientMetadataProvisioning.java @@ -35,7 +35,7 @@ public class JdbcClientMetadataProvisioning implements ClientMetadataProvisionin private static final String CLIENT_METADATA_FIELDS = "client_id, identity_zone_id, show_on_home_page, app_launch_url, app_icon"; private static final String CLIENT_METADATA_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where client_id=? and identity_zone_id=?"; - private static final String CLIENT_METADATAS_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where identity_zone_id=?"; + private static final String CLIENT_METADATAS_QUERY = "select " + CLIENT_METADATA_FIELDS + " from oauth_client_details where identity_zone_id=? and ((app_launch_url is not null and char_length(app_launch_url)>0) or (app_icon is not null and octet_length(app_icon)>0))"; private static final String CLIENT_METADATA_UPDATE_FIELDS = "show_on_home_page, app_launch_url, app_icon"; private static final String CLIENT_METADATA_UPDATE = "update oauth_client_details set " + CLIENT_METADATA_UPDATE_FIELDS.replace(",", "=?,") + "=?" + " where client_id=? and identity_zone_id=?"; diff --git a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml index 54db65aaf27..0937b58b9aa 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/client-admin-endpoints.xml @@ -42,6 +42,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java index 77daf2e2359..02b14312aa7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/client/ClientMetadataAdminEndpointsMockMvcTest.java @@ -21,14 +21,12 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -72,41 +70,65 @@ public void setUp() throws Exception { @Test public void getClientMetadata() throws Exception { String clientId = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId, null, null, null, null)); - MockHttpServletResponse response = getTestClientMetadata(clientId); + String marissaToken = getUserAccessToken(clientId); + MockHttpServletResponse response = getTestClientMetadata(clientId, marissaToken); assertThat(response.getStatus(), is(HttpStatus.OK.value())); } - + + private String getUserAccessToken(String clientId) throws Exception { + BaseClientDetails newClient = new BaseClientDetails(clientId, "oauth", "oauth.approvals", "password", "oauth.login"); + newClient.setClientSecret("secret"); + clients.addClientDetails(newClient); + return testClient.getUserOAuthAccessToken(clientId, "secret", "marissa", "koala", "oauth.approvals"); + } + @Test public void getClientMetadata_WhichDoesNotExist() throws Exception { String clientId = generator.generate(); - MockHttpServletResponse response = getTestClientMetadata(clientId); + MockHttpServletResponse response = getTestClientMetadata(clientId, adminClientTokenWithClientsRead); assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND.value())); } @Test - public void retrieveAllClientMetadata() throws Exception { + public void getAllClientMetadata() throws Exception { String clientId1 = generator.generate(); - clients.addClientDetails(new BaseClientDetails(clientId1, null, null, null, null)); + String marissaToken = getUserAccessToken(clientId1); String clientId2 = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId2, null, null, null, null)); String clientId3 = generator.generate(); clients.addClientDetails(new BaseClientDetails(clientId3, null, null, null, null)); + ClientMetadata client3Metadata = new ClientMetadata(); + client3Metadata.setClientId(clientId3); + client3Metadata.setIdentityZoneId("uaa"); + client3Metadata.setAppLaunchUrl(new URL("http://client3.com/app")); + client3Metadata.setShowOnHomePage(true); + client3Metadata.setAppIcon("Y2xpZW50IDMgaWNvbg=="); + performUpdate(client3Metadata); + + String clientId4 = generator.generate(); + clients.addClientDetails(new BaseClientDetails(clientId4, null, null, null, null)); + ClientMetadata client4Metadata = new ClientMetadata(); + client4Metadata.setClientId(clientId4); + client4Metadata.setIdentityZoneId("uaa"); + client4Metadata.setAppLaunchUrl(new URL("http://client4.com/app")); + client4Metadata.setAppIcon("aWNvbiBmb3IgY2xpZW50IDQ="); + performUpdate(client4Metadata); MockHttpServletResponse response = getMockMvc().perform(get("/oauth/clients/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) - .accept(APPLICATION_JSON)).andReturn().getResponse(); + .header("Authorization", "Bearer " + marissaToken) + .accept(APPLICATION_JSON)).andExpect(status().isOk()).andReturn().getResponse(); ArrayList clientMetadataList = JsonUtils.readValue(response.getContentAsString(), new TypeReference>() {}); - assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId1))); - assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId2))); - assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId3))); + assertThat(clientMetadataList, not(PredicateMatcher.has(m -> m.getClientId().equals(clientId1)))); + assertThat(clientMetadataList, not(PredicateMatcher.has(m -> m.getClientId().equals(clientId2)))); + assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId3) && m.getAppIcon().equals(client3Metadata.getAppIcon()) && m.getAppLaunchUrl().equals(client3Metadata.getAppLaunchUrl()) && m.isShowOnHomePage() == client3Metadata.isShowOnHomePage())); + assertThat(clientMetadataList, PredicateMatcher.has(m -> m.getClientId().equals(clientId4) && m.getAppIcon().equals(client4Metadata.getAppIcon()) && m.getAppLaunchUrl().equals(client4Metadata.getAppLaunchUrl()) && m.isShowOnHomePage() == client4Metadata.isShowOnHomePage())); } @Test @@ -119,18 +141,42 @@ public void updateClientMetadata() throws Exception { URL appLaunchUrl = new URL("http://changed.app.launch/url"); updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); - MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + ResultActions perform = performUpdate(updatedClientMetadata); + assertThat(perform.andReturn().getResponse().getContentAsString(), containsString(appLaunchUrl.toString())); + + MockHttpServletResponse response = getTestClientMetadata(clientId, adminClientTokenWithClientsRead); + assertThat(response.getStatus(), is(HttpStatus.OK.value())); + assertThat(response.getContentAsString(), containsString(appLaunchUrl.toString())); + } + + private ResultActions performUpdate(ClientMetadata updatedClientMetadata) throws Exception { + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + updatedClientMetadata.getClientId() + "/meta") .header("Authorization", "Bearer " + adminClientTokenWithClientsWrite) .header("If-Match", "0") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(updatedClientMetadata)); - ResultActions perform = getMockMvc().perform(updateClientPut); - assertThat(perform.andReturn().getResponse().getContentAsString(), containsString(appLaunchUrl.toString())); + return getMockMvc().perform(updateClientPut); + } - MockHttpServletResponse response = getTestClientMetadata(clientId); - assertThat(response.getStatus(), is(HttpStatus.OK.value())); - assertThat(response.getContentAsString(), containsString(appLaunchUrl.toString())); + @Test + public void updateClientMetadata_InsufficientScope() throws Exception { + String clientId = generator.generate(); + String marissaToken = getUserAccessToken(clientId); + + ClientMetadata updatedClientMetadata = new ClientMetadata(); + updatedClientMetadata.setClientId(clientId); + URL appLaunchUrl = new URL("http://changed.app.launch/url"); + updatedClientMetadata.setAppLaunchUrl(appLaunchUrl); + + MockHttpServletRequestBuilder updateClientPut = put("/oauth/clients/" + clientId + "/meta") + .header("Authorization", "Bearer " + marissaToken) + .header("If-Match", "0") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatedClientMetadata)); + MockHttpServletResponse response = getMockMvc().perform(updateClientPut).andReturn().getResponse(); + assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN.value())); } @Test @@ -152,7 +198,7 @@ public void updateClientMetadata_WithNoClientIdInBody() throws Exception { ResultActions perform = getMockMvc().perform(updateClientPut); assertThat(perform.andReturn().getResponse().getContentAsString(), containsString(appLaunchUrl.toString())); - MockHttpServletResponse response = getTestClientMetadata(clientId); + MockHttpServletResponse response = getTestClientMetadata(clientId, adminClientTokenWithClientsRead); assertThat(response.getStatus(), is(HttpStatus.OK.value())); assertThat(response.getContentAsString(), containsString(appLaunchUrl.toString())); } @@ -196,9 +242,9 @@ public void updateClientMetadata_ClientIdMismatch() throws Exception { assertEquals(perform.andReturn().getResponse().getStatus(), HttpStatus.BAD_REQUEST.value()); } - private MockHttpServletResponse getTestClientMetadata(String clientId) throws Exception { + private MockHttpServletResponse getTestClientMetadata(String clientId, String token) throws Exception { MockHttpServletRequestBuilder createClientGet = get("/oauth/clients/" + clientId + "/meta") - .header("Authorization", "Bearer " + adminClientTokenWithClientsRead) + .header("Authorization", "Bearer " + token) .accept(APPLICATION_JSON); return getMockMvc().perform(createClientGet).andReturn().getResponse(); } From 3574c396d717f18e430100642e89cf22cec40824 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 21 Jan 2016 16:48:12 -0700 Subject: [PATCH 23/25] Document and implement user token revocation https://www.pivotaltracker.com/story/show/109380786 [#109380786] --- docs/UAA-APIs.rst | 43 ++++++ .../uaa/oauth/TokenRevocationEndpoint.java | 13 +- .../webapp/WEB-INF/spring/oauth-endpoints.xml | 19 +++ .../uaa/mock/token/TokenMvcMockTests.java | 132 ++++++++++++++++-- 4 files changed, 194 insertions(+), 13 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index eecbe69f694..b0a07b74225 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -656,6 +656,49 @@ Notes: .. _oauth2 token endpoint: + +OAuth2 Token Revocation Service/Client: ``GET /oauth/token/revoke/client/{client-id}`` +-------------------------------------------------------------------------------------- + +An endpoint that allows all tokens for a specific client to be revoked +* Request: uses token authorization and requires `uaa.admin` scope:: + + GET /oauth/token/revoke/client/{client-id} HTTP/1.1 + Host: server.example.com + Authorization: Bearer token + +* Successful Response:: + + HTTP/1.1 200 OK + +* Error Response:: + + HTTP/1.1 401 Unauthorized - Authentication is not sufficient + HTTP/1.1 403 Forbidden - Authenticated, but uaa.admin scope is not present + HTTP/1.1 404 Not Found - Client ID is invalid + +OAuth2 Token Revocal Service/User: ``GET /oauth/token/revoke/user/{user-id}`` +----------------------------------------------------------------------------- + +An endpoint that allows all tokens for a specific user to be revoked +* Request: uses token authorization and requires `uaa.admin` scope:: + + GET /oauth/token/revoke/client/{client-id} HTTP/1.1 + Host: server.example.com + Authorization: Bearer token + +* Successful Response:: + + HTTP/1.1 200 OK + +* Error Response:: + + HTTP/1.1 401 Unauthorized - Authentication is not sufficient + HTTP/1.1 403 Forbidden - Authenticated, but uaa.admin scope is not present + HTTP/1.1 404 Not Found - User ID is invalid + + + OpenID User Info Endpoint: ``GET /userinfo`` -------------------------------------------- diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java index 65723550c59..bc6e79f6246 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java @@ -25,6 +25,7 @@ import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; @@ -33,6 +34,8 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import static org.springframework.http.HttpStatus.OK; + @Controller public class TokenRevocationEndpoint { @@ -48,24 +51,26 @@ public TokenRevocationEndpoint(MultitenantJdbcClientDetailsService clientDetails } @RequestMapping("/oauth/token/revoke/user/{userId}") - public void revokeTokensForUser(@PathVariable String userId) { + public ResponseEntity revokeTokensForUser(@PathVariable String userId) { logger.debug("Revoking tokens for user: "+userId); ScimUser user = userProvisioning.retrieve(userId); user.setSalt(generator.generate()); userProvisioning.update(userId, user); logger.debug("Tokens revoked for user: "+userId); + return new ResponseEntity<>(OK); } - @RequestMapping("/oauth/token/revoke/user/{clientId}") - public void revokeTokensForClient(@PathVariable String clientId) { + @RequestMapping("/oauth/token/revoke/client/{clientId}") + public ResponseEntity revokeTokensForClient(@PathVariable String clientId) { logger.debug("Revoking tokens for client: " + clientId); BaseClientDetails client = (BaseClientDetails)clientDetailsService.loadClientByClientId(clientId); client.addAdditionalInformation(ClientConstants.TOKEN_SALT,generator.generate()); clientDetailsService.updateClientDetails(client); logger.debug("Tokens revoked for client: " + clientId); + return new ResponseEntity<>(OK); } - @ExceptionHandler(ScimResourceNotFoundException.class) + @ExceptionHandler({ScimResourceNotFoundException.class, NoSuchClientException.class}) public ResponseEntity handleException(Exception e) throws Exception { logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage()); InvalidTokenException e404 = new InvalidTokenException("Resource not found") { diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index cede0c790c1..6e40db804cc 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -41,6 +41,25 @@ + + + + + + + + + + + + + + originalExclude = getWebApplicationContext().getBean(UaaTokenServices.class).getExcludedClaims(); From e3191448d67079fe2ada0802b37b381b9cbbe98b Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 22 Jan 2016 07:44:43 -0700 Subject: [PATCH 24/25] Refactor zone id handling at dao layer --- .../uaa/account/ResetPasswordController.java | 40 ++++- .../AutologinAuthenticationManager.java | 51 ++++-- .../JdbcQueryableClientDetailsService.java | 20 ++- .../JdbcIdentityProviderProvisioning.java | 9 +- .../uaa/resources/jdbc/AbstractQueryable.java | 19 ++- .../resources/jdbc/SearchQueryConverter.java | 18 +- .../jdbc/SimpleSearchQueryConverter.java | 30 ++-- .../scim/jdbc/JdbcScimGroupProvisioning.java | 12 +- .../scim/jdbc/JdbcScimUserProvisioning.java | 65 +++++--- .../uaa/user/JdbcUaaUserDatabase.java | 16 +- server/src/main/resources/login-ui.xml | 5 +- .../AutologinAuthenticationManagerTest.java | 44 ++++- .../login/ResetPasswordControllerTest.java | 11 +- .../uaa/user/JdbcUaaUserDatabaseTests.java | 8 +- ...JdbcIdentityProviderProvisioningTests.java | 12 +- .../main/webapp/WEB-INF/spring-servlet.xml | 7 + .../InvitationsEndpointMockMvcTests.java | 41 +++-- .../login/InvitationsServiceMockMvcTests.java | 2 +- .../identity/uaa/mock/util/MockMvcUtils.java | 154 +++++++++++++----- .../IdentityZoneEndpointsMockMvcTests.java | 3 +- .../ScimUserEndpointsMockMvcTests.java | 40 +++-- 21 files changed, 413 insertions(+), 194 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/account/ResetPasswordController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/account/ResetPasswordController.java index 4d510bcb0cf..f14a0d61511 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/account/ResetPasswordController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/account/ResetPasswordController.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"). @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.account; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -24,12 +25,15 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -44,8 +48,11 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.sql.Timestamp; +import java.util.Map; import java.util.regex.Pattern; +import static org.springframework.util.StringUtils.hasText; + @Controller public class ResetPasswordController { protected final Log logger = LogFactory.getLog(getClass()); @@ -57,13 +64,15 @@ public class ResetPasswordController { private final String brand; private final Pattern emailPattern; private final ExpiringCodeStore codeStore; + private final UaaUserDatabase userDatabase; public ResetPasswordController(ResetPasswordService resetPasswordService, MessageService messageService, TemplateEngine templateEngine, UaaUrlUtils uaaUrlUtils, String brand, - ExpiringCodeStore codeStore) { + ExpiringCodeStore codeStore, + UaaUserDatabase userDatabase) { this.resetPasswordService = resetPasswordService; this.messageService = messageService; this.templateEngine = templateEngine; @@ -71,6 +80,7 @@ public ResetPasswordController(ResetPasswordService resetPasswordService, this.brand = brand; emailPattern = Pattern.compile("^\\S+@\\S+\\.\\S+$"); this.codeStore = codeStore; + this.userDatabase = userDatabase; } @RequestMapping(value = "/forgot_password", method = RequestMethod.GET) @@ -162,7 +172,7 @@ public String resetPasswordPage(Model model, @RequestParam("code") String code, @RequestParam("email") String email) { - ExpiringCode expiringCode = codeStore.retrieveCode(code); + ExpiringCode expiringCode = validateUserAndClient(codeStore.retrieveCode(code)); if (expiringCode==null) { return handleUnprocessableEntity(model, response, "message_code", "bad_code"); } else { @@ -173,6 +183,30 @@ public String resetPasswordPage(Model model, } } + public ExpiringCode validateUserAndClient(ExpiringCode code) { + if (code==null) { + logger.debug("reset_password ExpiringCode object is null. Aborting."); + return null; + } + if (!hasText(code.getData())) { + logger.debug("reset_password ExpiringCode["+code.getCode()+"] data string is null or empty. Aborting."); + return null; + } + Map data = JsonUtils.readValue(code.getData(), new TypeReference>() {}); + if (!hasText(data.get("user_id"))) { + logger.debug("reset_password ExpiringCode["+code.getCode()+"] user_id string is null or empty. Aborting."); + return null; + } + String userId = data.get("user_id"); + try { + userDatabase.retrieveUserById(userId); + } catch (UsernameNotFoundException e) { + logger.debug("reset_password ExpiringCode["+code.getCode()+"] user_id is invalid. Aborting."); + return null; + } + return code; + } + @RequestMapping(value = "/reset_password.do", method = RequestMethod.POST) public String resetPassword(Model model, @RequestParam("code") String code, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AutologinAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AutologinAuthenticationManager.java index 99ec3b5ed56..8b01ecb01c9 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AutologinAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AutologinAuthenticationManager.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"). @@ -17,43 +17,58 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.authentication.AuthzAuthenticationRequest; +import org.cloudfoundry.identity.uaa.authentication.InvalidCodeException; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.authentication.InvalidCodeException; import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.NoSuchClientException; import java.util.Map; /** * @author Dave Syer - * + * */ public class AutologinAuthenticationManager implements AuthenticationManager { private Log logger = LogFactory.getLog(getClass()); private ExpiringCodeStore codeStore; + private ClientDetailsService clientDetailsService; + private UaaUserDatabase userDatabase; public void setExpiringCodeStore(ExpiringCodeStore expiringCodeStore) { this.codeStore= expiringCodeStore; } + public void setClientDetailsService(ClientDetailsService clientDetailsService) { + this.clientDetailsService = clientDetailsService; + } + + public void setUserDatabase(UaaUserDatabase userDatabase) { + this.userDatabase = userDatabase; + } + public ExpiringCode doRetrieveCode(String code) { return codeStore.retrieveCode(code); } + + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { @@ -81,25 +96,33 @@ public Authentication authenticate(Authentication authentication) throws Authent throw new BadCredentialsException("JsonConversion error", x); } - String origin; - String userId; - String username; - String clientId; - username = codeData.get("username"); - origin = codeData.get(OriginKeys.ORIGIN); - userId = codeData.get("user_id"); - clientId = codeData.get(OAuth2Utils.CLIENT_ID); + String userId = codeData.get("user_id"); + String clientId = codeData.get(OAuth2Utils.CLIENT_ID); if (clientId == null) { throw new BadCredentialsException("Cannot redeem provided code for user, client id missing"); } + try { + clientDetailsService.loadClientByClientId(clientId); + } catch (NoSuchClientException x) { + throw new BadCredentialsException("Cannot redeem provided code for user, client is missing"); + } + + UaaUser user = null; + + try { + user = userDatabase.retrieveUserById(userId); + } catch (UsernameNotFoundException e) { + throw new BadCredentialsException("Cannot redeem provided code for user, user is missing"); + } + UaaAuthenticationDetails details = (UaaAuthenticationDetails) authentication.getDetails(); if (!clientId.equals(details.getClientId())) { throw new BadCredentialsException("Cannot redeem provided code for user, client mismatch"); } - UaaPrincipal principal = new UaaPrincipal(userId,username,null,origin,null, IdentityZoneHolder.get().getId()); + UaaPrincipal principal = new UaaPrincipal(user); return new UaaAuthentication( principal, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcQueryableClientDetailsService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcQueryableClientDetailsService.java index e0d6ff4c7e5..602eccac795 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcQueryableClientDetailsService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/client/JdbcQueryableClientDetailsService.java @@ -12,11 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.client; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.List; -import java.util.Map; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.resources.QueryableResourceManager; @@ -26,11 +21,16 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.util.StringUtils; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + public class JdbcQueryableClientDetailsService extends AbstractQueryable implements QueryableResourceManager { @@ -63,11 +63,13 @@ protected String getTableName() { @Override public List query(String filter, String sortBy, boolean ascending) { - if (StringUtils.hasText(filter)) { - filter += " and"; + //validate syntax + getQueryConverter().convert(filter, sortBy, ascending); + if (StringUtils.hasText(filter)) { + filter = "(" + filter + ") and "; } filter += " identity_zone_id eq \""+IdentityZoneHolder.get().getId()+"\""; - return super.query(filter, sortBy, ascending); + return super.query(filter, sortBy, ascending); } @Override diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java index 60d4144ba23..e8914912bf6 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java @@ -18,6 +18,7 @@ import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.ObjectUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.JdbcTemplate; @@ -48,13 +49,13 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisi public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active".replace(",","=?,")+"=?"; - public static final String UPDATE_IDENTITY_PROVIDER_SQL = "update identity_provider set " + ID_PROVIDER_UPDATE_FIELDS + " where id=?"; + public static final String UPDATE_IDENTITY_PROVIDER_SQL = "update identity_provider set " + ID_PROVIDER_UPDATE_FIELDS + " where id=? and identity_zone_id=?"; public static final String DELETE_IDENTITY_PROVIDER_BY_ORIGIN_SQL = "delete from identity_provider where identity_zone_id=? and origin_key = ?"; public static final String DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL = "delete from identity_provider where identity_zone_id=?"; - public static final String IDENTITY_PROVIDER_BY_ID_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider " + "where id=?"; + public static final String IDENTITY_PROVIDER_BY_ID_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider " + "where id=? and identity_zone_id=?"; public static final String IDENTITY_PROVIDER_BY_ORIGIN_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider " + "where origin_key=? and identity_zone_id=? "; @@ -69,7 +70,7 @@ public JdbcIdentityProviderProvisioning(JdbcTemplate jdbcTemplate) { @Override public IdentityProvider retrieve(String id) { - IdentityProvider identityProvider = jdbcTemplate.queryForObject(IDENTITY_PROVIDER_BY_ID_QUERY, mapper, id); + IdentityProvider identityProvider = jdbcTemplate.queryForObject(IDENTITY_PROVIDER_BY_ID_QUERY, mapper, id, IdentityZoneHolder.get().getId()); return identityProvider; } @@ -123,6 +124,7 @@ public void setValues(PreparedStatement ps) throws SQLException { @Override public IdentityProvider update(final IdentityProvider identityProvider) { validate(identityProvider); + final String zoneId = IdentityZoneHolder.get().getId(); jdbcTemplate.update(UPDATE_IDENTITY_PROVIDER_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { @@ -134,6 +136,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); ps.setBoolean(pos++, identityProvider.isActive()); ps.setString(pos++, identityProvider.getId().trim()); + ps.setString(pos++, zoneId); } }); return retrieve(identityProvider.getId()); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/AbstractQueryable.java b/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/AbstractQueryable.java index bcfde4fe257..4e21447db1d 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/AbstractQueryable.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/AbstractQueryable.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"). @@ -12,8 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.resources.jdbc; -import java.util.List; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.resources.Queryable; @@ -22,6 +20,8 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import java.util.List; + public abstract class AbstractQueryable implements Queryable { private NamedParameterJdbcTemplate jdbcTemplate; @@ -51,7 +51,7 @@ public void setQueryConverter(SearchQueryConverter queryConverter) { * The maximum number of items fetched from the database in one hit. If less * than or equal to zero, then there is no * limit. - * + * * @param pageSize the page size to use for backing queries (default 200) */ public void setPageSize(int pageSize) { @@ -102,9 +102,14 @@ public List query(String filter, String sortBy, boolean ascending) { } protected String getQuerySQL(String filter, SearchQueryConverter.ProcessedFilter where) { - return filter == null || filter.trim().length()==0 ? - getBaseSqlQuery() : - getBaseSqlQuery() + " where " + where.getSql(); + if (filter == null || filter.trim().length()==0) { + return getBaseSqlQuery(); + } + if (where.hasOrderBy()) { + return getBaseSqlQuery() + " where (" + where.getSql().replace(where.ORDER_BY, ")"+where.ORDER_BY); + } else { + return getBaseSqlQuery() + " where (" + where.getSql() + ")"; + } } protected abstract String getBaseSqlQuery(); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SearchQueryConverter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SearchQueryConverter.java index 98a70ab4d9f..26b56c6a58b 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SearchQueryConverter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SearchQueryConverter.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"). @@ -12,15 +12,18 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.resources.jdbc; -import java.util.Map; - import org.cloudfoundry.identity.uaa.resources.AttributeNameMapper; +import java.util.Map; + public interface SearchQueryConverter { - public static final class ProcessedFilter { + final class ProcessedFilter { + public static final String ORDER_BY_NO_SPACE = "ORDER BY"; + public static final String ORDER_BY = " "+ORDER_BY_NO_SPACE+" "; private final String sql; private final Map params; + private final boolean hasOrderBy; public String getParamPrefix() { return paramPrefix; @@ -36,13 +39,18 @@ public String getSql() { return sql; } + public boolean hasOrderBy() { + return hasOrderBy; + } + public Map getParams() { return params; } - public ProcessedFilter(String sql, Map params) { + public ProcessedFilter(String sql, Map params, boolean hasOrderBy) { this.sql = sql; this.params = params; + this.hasOrderBy = hasOrderBy; } @Override diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SimpleSearchQueryConverter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SimpleSearchQueryConverter.java index 7d98b10b89e..8d6ad751105 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SimpleSearchQueryConverter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/resources/jdbc/SimpleSearchQueryConverter.java @@ -13,13 +13,6 @@ package org.cloudfoundry.identity.uaa.resources.jdbc; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import com.unboundid.scim.sdk.SCIMException; import com.unboundid.scim.sdk.SCIMFilter; import org.apache.commons.logging.Log; @@ -29,6 +22,15 @@ import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.util.StringUtils; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.cloudfoundry.identity.uaa.resources.jdbc.SearchQueryConverter.ProcessedFilter.ORDER_BY; + public class SimpleSearchQueryConverter implements SearchQueryConverter { private static Log logger = LogFactory.getLog(SimpleSearchQueryConverter.class); @@ -56,9 +58,9 @@ public ProcessedFilter convert(String filter, String sortBy, boolean ascending) @Override public ProcessedFilter convert(String filter, String sortBy, boolean ascending, AttributeNameMapper mapper) { String paramPrefix = generateParameterPrefix(filter); - Map values = new HashMap(); + Map values = new HashMap<>(); String where = StringUtils.hasText(filter) ? getWhereClause(filter, sortBy, ascending, values, mapper, paramPrefix) : null; - ProcessedFilter pf = new ProcessedFilter(where, values); + ProcessedFilter pf = new ProcessedFilter(where, values, StringUtils.hasText(sortBy)); pf.setParamPrefix(paramPrefix); return pf; } @@ -81,7 +83,7 @@ private String getWhereClause(String filter, String sortBy, boolean ascending, M sortBy = mapper.mapToInternal(sortBy); // Need to add "asc" or "desc" explicitly to ensure that the pattern // splitting below works - whereClause += " ORDER BY " + sortBy + (ascending ? " ASC" : " DESC"); + whereClause += ORDER_BY + sortBy + (ascending ? " ASC" : " DESC"); } return whereClause; } catch (SCIMException e) { @@ -137,6 +139,14 @@ protected String comparisonClause(SCIMFilter filter, String comparator, Map query(String filter, String sortBy, boolean ascending) { + //validate syntax + getQueryConverter().convert(filter, sortBy, ascending); + if (StringUtils.hasText(filter)) { - filter += " and"; + filter = "("+ filter+ ") and"; } filter += " identity_zone_id eq \""+IdentityZoneHolder.get().getId()+"\""; return super.query(filter, sortBy, ascending); @@ -131,6 +134,7 @@ public ScimGroup create(final ScimGroup group) throws InvalidScimResourceExcepti logger.debug("creating new group with id: " + id); try { validateGroup(group); + final String zoneId = IdentityZoneHolder.get().getId(); jdbcTemplate.update(ADD_GROUP_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { @@ -141,7 +145,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); ps.setInt(pos++, group.getVersion()); - ps.setString(pos++, group.getZoneId()); + ps.setString(pos++, zoneId); } }); } catch (DuplicateKeyException ex) { @@ -156,6 +160,7 @@ public ScimGroup update(final String id, final ScimGroup group) throws InvalidSc ScimResourceNotFoundException { try { validateGroup(group); + final String zoneId = IdentityZoneHolder.get().getId(); int updated = jdbcTemplate.update(UPDATE_GROUP_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { @@ -166,6 +171,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); ps.setString(pos++, id); ps.setInt(pos++, group.getVersion()); + ps.setString(pos++, zoneId); } }); if (updated != 1) { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java index c5d74ff0bc2..7be2540e300 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java @@ -14,8 +14,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.resources.ResourceMonitor; import org.cloudfoundry.identity.uaa.resources.jdbc.AbstractQueryable; import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory; @@ -46,7 +46,6 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; -import java.sql.Types; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -56,6 +55,8 @@ import java.util.UUID; import java.util.regex.Pattern; +import static java.sql.Types.VARCHAR; + /** * @author Luke Taylor * @author Dave Syer @@ -75,19 +76,19 @@ public Log getLogger() { public static final String CREATE_USER_SQL = "insert into users (" + USER_FIELDS + ",password) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; - public static final String UPDATE_USER_SQL = "update users set version=?, lastModified=?, userName=?, email=?, givenName=?, familyName=?, active=?, phoneNumber=?, verified=?, origin=?, external_id=?, salt=? where id=? and version=?"; + public static final String UPDATE_USER_SQL = "update users set version=?, lastModified=?, userName=?, email=?, givenName=?, familyName=?, active=?, phoneNumber=?, verified=?, origin=?, external_id=?, salt=? where id=? and version=? and identity_zone_id=?"; - public static final String DEACTIVATE_USER_SQL = "update users set active=? where id=?"; + public static final String DEACTIVATE_USER_SQL = "update users set active=? where id=? and identity_zone_id=?"; - public static final String VERIFY_USER_SQL = "update users set verified=? where id=?"; + public static final String VERIFY_USER_SQL = "update users set verified=? where id=? and identity_zone_id=?"; - public static final String DELETE_USER_SQL = "delete from users where id=?"; + public static final String DELETE_USER_SQL = "delete from users where id=? and identity_zone_id=?"; - public static final String CHANGE_PASSWORD_SQL = "update users set lastModified=?, password=?, passwd_lastmodified=? where id=?"; + public static final String CHANGE_PASSWORD_SQL = "update users set lastModified=?, password=?, passwd_lastmodified=? where id=? and identity_zone_id=?"; - public static final String READ_PASSWORD_SQL = "select password from users where id=?"; + public static final String READ_PASSWORD_SQL = "select password from users where id=? and identity_zone_id=?"; - public static final String USER_BY_ID_QUERY = "select " + USER_FIELDS + " from users " + "where id=?"; + public static final String USER_BY_ID_QUERY = "select " + USER_FIELDS + " from users " + "where id=? and identity_zone_id=?"; public static final String ALL_USERS = "select " + USER_FIELDS + " from users"; @@ -123,7 +124,7 @@ public JdbcScimUserProvisioning(JdbcTemplate jdbcTemplate, JdbcPagingListFactory @Override public ScimUser retrieve(String id) { try { - ScimUser u = jdbcTemplate.queryForObject(USER_BY_ID_QUERY, mapper, id); + ScimUser u = jdbcTemplate.queryForObject(USER_BY_ID_QUERY, mapper, id, IdentityZoneHolder.get().getId()); return u; } catch (EmptyResultDataAccessException e) { throw new ScimResourceNotFoundException("User " + id + " does not exist"); @@ -147,8 +148,11 @@ public List retrieveAll() { @Override public List query(String filter, String sortBy, boolean ascending) { + //validate syntax + getQueryConverter().convert(filter, sortBy, ascending); + if (StringUtils.hasText(filter)) { - filter += " and"; + filter = "("+ filter+ ") and"; } filter += " identity_zone_id eq \""+IdentityZoneHolder.get().getId()+"\""; return super.query(filter, sortBy, ascending); @@ -241,7 +245,7 @@ public ScimUser update(final String id, final ScimUser user) throws InvalidScimR validate(user); logger.debug("Updating user " + user.getUserName()); final String origin = StringUtils.hasText(user.getOrigin()) ? user.getOrigin() : OriginKeys.UAA; - + final String zoneId = IdentityZoneHolder.get().getId(); int updated = jdbcTemplate.update(UPDATE_USER_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { @@ -261,6 +265,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setString(pos++, user.getSalt()); ps.setString(pos++, id); ps.setInt(pos++, user.getVersion()); + ps.setString(pos++, zoneId); } }); ScimUser result = retrieve(id); @@ -285,6 +290,7 @@ public void changePassword(final String id, String oldPassword, final String new return; //we don't want to update the same password } final String encNewPassword = passwordEncoder.encode(newPassword); + final String zoneId = IdentityZoneHolder.get().getId(); int updated = jdbcTemplate.update(CHANGE_PASSWORD_SQL, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { @@ -293,6 +299,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setString(2, encNewPassword); ps.setTimestamp(3, getPasswordLastModifiedTimestamp(t)); ps.setString(4, id); + ps.setString(5, zoneId); } }); if (updated == 0) { @@ -307,8 +314,13 @@ public void setValues(PreparedStatement ps) throws SQLException { public boolean checkPasswordMatches(String id, String password) { String currentPassword; try { - currentPassword = jdbcTemplate.queryForObject(READ_PASSWORD_SQL, new Object[] { id }, - new int[] { Types.VARCHAR }, String.class); + currentPassword = + jdbcTemplate.queryForObject( + READ_PASSWORD_SQL, + new Object[] { id, IdentityZoneHolder.get().getId() }, + new int[] { VARCHAR, VARCHAR }, + String.class + ); } catch (IncorrectResultSizeDataAccessException e) { throw new ScimResourceNotFoundException("User " + id + " does not exist"); } @@ -327,10 +339,9 @@ private ScimUser deactivateUser(ScimUser user, int version) { int updated; if (version < 0) { // Ignore - updated = jdbcTemplate.update(DEACTIVATE_USER_SQL, false, user.getId()); - } - else { - updated = jdbcTemplate.update(DEACTIVATE_USER_SQL + " and version=?", false, user.getId(), version); + updated = jdbcTemplate.update(DEACTIVATE_USER_SQL, false, user.getId(), IdentityZoneHolder.get().getId()); + } else { + updated = jdbcTemplate.update(DEACTIVATE_USER_SQL + " and version=?", false, user.getId(), IdentityZoneHolder.get().getId(), version); } if (updated == 0) { throw new OptimisticLockingFailureException(String.format( @@ -351,10 +362,10 @@ public ScimUser verifyUser(String id, int version) throws ScimResourceNotFoundEx int updated; if (version < 0) { // Ignore - updated = jdbcTemplate.update(VERIFY_USER_SQL, true, id); + updated = jdbcTemplate.update(VERIFY_USER_SQL, true, id, IdentityZoneHolder.get().getId()); } else { - updated = jdbcTemplate.update(VERIFY_USER_SQL + " and version=?", true, id, version); + updated = jdbcTemplate.update(VERIFY_USER_SQL + " and version=?", true, id, IdentityZoneHolder.get().getId(), version); } ScimUser user = retrieve(id); if (updated == 0) { @@ -373,10 +384,10 @@ private ScimUser deleteUser(ScimUser user, int version) { int updated; if (version < 0) { - updated = jdbcTemplate.update(DELETE_USER_SQL, user.getId()); + updated = jdbcTemplate.update(DELETE_USER_SQL, user.getId(), IdentityZoneHolder.get().getId()); } else { - updated = jdbcTemplate.update(DELETE_USER_SQL + " and version=?", user.getId(), version); + updated = jdbcTemplate.update(DELETE_USER_SQL + " and version=?", user.getId(), IdentityZoneHolder.get().getId(), version); } if (updated == 0) { throw new OptimisticLockingFailureException(String.format( @@ -468,11 +479,11 @@ public ScimUser mapRow(ResultSet rs, int rowNum) throws SQLException { @Override public int getTotalCount() { - Integer count = jdbcTemplate.queryForObject("select count(*) from users",Integer.class); - if (count == null) { - return 0; - } - return count; + Integer count = jdbcTemplate.queryForObject("select count(*) from users",Integer.class); + if (count == null) { + return 0; + } + return count; } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java index 0ebe30a2378..ae7a4214863 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java @@ -46,14 +46,12 @@ public class JdbcUaaUserDatabase implements UaaUserDatabase { + "where lower(username) = ? and active=? and origin=? and identity_zone_id=?"; public static final String DEFAULT_USER_BY_ID_QUERY = "select " + USER_FIELDS + "from users " - + "where id = ? and active=?"; + + "where id = ? and active=? and identity_zone_id=?"; public static final String DEFAULT_USER_BY_EMAIL_AND_ORIGIN_QUERY = "select " + USER_FIELDS + "from users " + "where lower(email)=? and active=? and origin=? and identity_zone_id=?"; - private String AUTHORITIES_QUERY = "select g.id,g.displayName from groups g, group_membership m where g.id = m.group_id and m.member_id = ?"; - - private String userByUserNameQuery = DEFAULT_USER_BY_USERNAME_QUERY; + private String AUTHORITIES_QUERY = "select g.id,g.displayName from groups g, group_membership m where g.id = m.group_id and m.member_id = ? and g.identity_zone_id=?"; private JdbcTemplate jdbcTemplate; @@ -61,10 +59,6 @@ public class JdbcUaaUserDatabase implements UaaUserDatabase { private Set defaultAuthorities = new HashSet(); - public void setUserByUserNameQuery(String userByUserNameQuery) { - this.userByUserNameQuery = userByUserNameQuery; - } - public void setDefaultAuthorities(Set defaultAuthorities) { this.defaultAuthorities = defaultAuthorities; } @@ -77,7 +71,7 @@ public JdbcUaaUserDatabase(JdbcTemplate jdbcTemplate) { @Override public UaaUser retrieveUserByName(String username, String origin) throws UsernameNotFoundException { try { - return jdbcTemplate.queryForObject(userByUserNameQuery, mapper, username.toLowerCase(Locale.US), true, origin, IdentityZoneHolder.get().getId()); + return jdbcTemplate.queryForObject(DEFAULT_USER_BY_USERNAME_QUERY, mapper, username.toLowerCase(Locale.US), true, origin, IdentityZoneHolder.get().getId()); } catch (EmptyResultDataAccessException e) { throw new UsernameNotFoundException(username); } @@ -86,7 +80,7 @@ public UaaUser retrieveUserByName(String username, String origin) throws Usernam @Override public UaaUser retrieveUserById(String id) throws UsernameNotFoundException { try { - return jdbcTemplate.queryForObject(DEFAULT_USER_BY_ID_QUERY, mapper, id, true); + return jdbcTemplate.queryForObject(DEFAULT_USER_BY_ID_QUERY, mapper, id, true, IdentityZoneHolder.get().getId()); } catch (EmptyResultDataAccessException e) { throw new UsernameNotFoundException(id); } @@ -152,7 +146,7 @@ private String getAuthorities(final String userId) { protected void getAuthorities(Set authorities, final String memberId) { List> results; try { - results = jdbcTemplate.queryForList(AUTHORITIES_QUERY, memberId); + results = jdbcTemplate.queryForList(AUTHORITIES_QUERY, memberId, IdentityZoneHolder.get().getId()); for (Map record : results) { String displayName = (String)record.get("displayName"); String groupId = (String)record.get("id"); diff --git a/server/src/main/resources/login-ui.xml b/server/src/main/resources/login-ui.xml index 4ab185b99c0..59b23840095 100644 --- a/server/src/main/resources/login-ui.xml +++ b/server/src/main/resources/login-ui.xml @@ -400,10 +400,6 @@ - - - - @@ -592,6 +588,7 @@ + diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/AutologinAuthenticationManagerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/AutologinAuthenticationManagerTest.java index 38a0037a536..c9c9a2797b7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/AutologinAuthenticationManagerTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/AutologinAuthenticationManagerTest.java @@ -1,6 +1,7 @@ package org.cloudfoundry.identity.uaa.login; import org.cloudfoundry.identity.uaa.authentication.AuthzAuthenticationRequest; +import org.cloudfoundry.identity.uaa.authentication.InvalidCodeException; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.manager.AutologinAuthenticationManager; @@ -8,22 +9,29 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType; import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.authentication.InvalidCodeException; +import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import java.sql.Timestamp; +import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.Map; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /******************************************************************************* @@ -43,13 +51,18 @@ public class AutologinAuthenticationManagerTest { private AutologinAuthenticationManager manager; private ExpiringCodeStore codeStore; private Authentication authenticationToken; + private UaaUserDatabase userDatabase; + private ClientDetailsService clientDetailsService; @Before public void setUp() { manager = new AutologinAuthenticationManager(); - codeStore = Mockito.mock(ExpiringCodeStore.class); - + codeStore = mock(ExpiringCodeStore.class); + userDatabase = mock(UaaUserDatabase.class); + clientDetailsService = mock(ClientDetailsService.class); manager.setExpiringCodeStore(codeStore); + manager.setClientDetailsService(clientDetailsService); + manager.setUserDatabase(userDatabase); Map info = new HashMap<>(); info.put("code", "the_secret_code"); UaaAuthenticationDetails details = new UaaAuthenticationDetails(new MockHttpServletRequest(), "test-client-id"); @@ -66,6 +79,27 @@ public void authentication_successful() throws Exception { codeData.put("action", ExpiringCodeType.AUTOLOGIN.name()); when(codeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("the_secret_code", new Timestamp(123), JsonUtils.writeValueAsString(codeData), null)); + when(clientDetailsService.loadClientByClientId(eq("test-client-id"))).thenReturn(new BaseClientDetails("test-client-details","","","","")); + when(userDatabase.retrieveUserById(eq("test-user-id"))) + .thenReturn( + new UaaUser("test-user-id", + "test-username", + "password", + "email@email.com", + Collections.EMPTY_LIST, + "given name", + "family name", + new Date(System.currentTimeMillis()), + new Date(System.currentTimeMillis()), + OriginKeys.UAA, + "test-external-id", + true, + IdentityZoneHolder.get().getId(), + "test-salt", + new Date(System.currentTimeMillis()) + ) + ); + Authentication authenticate = manager.authenticate(authenticationToken); assertThat(authenticate, is(instanceOf(UaaAuthentication.class))); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java index a0654a1c58c..0bf4ce9afd6 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java @@ -28,6 +28,8 @@ import org.cloudfoundry.identity.uaa.scim.ScimMeta; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; +import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -81,6 +83,7 @@ public class ResetPasswordControllerTest extends TestClassNullifier { private ResetPasswordService resetPasswordService; private MessageService messageService; private ExpiringCodeStore codeStore; + private UaaUserDatabase userDatabase; @Autowired @Qualifier("mailTemplateEngine") @@ -93,7 +96,9 @@ public void setUp() throws Exception { resetPasswordService = mock(ResetPasswordService.class); messageService = mock(MessageService.class); codeStore = mock(ExpiringCodeStore.class); - ResetPasswordController controller = new ResetPasswordController(resetPasswordService, messageService, templateEngine, new UaaUrlUtils(), "pivotal", codeStore); + userDatabase = mock(UaaUserDatabase.class); + when(userDatabase.retrieveUserById(anyString())).thenReturn(new UaaUser("username","password","email","givenname","familyname")); + ResetPasswordController controller = new ResetPasswordController(resetPasswordService, messageService, templateEngine, new UaaUrlUtils(), "pivotal", codeStore, userDatabase); InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/jsp"); @@ -242,10 +247,10 @@ public void testInstructions() throws Exception { @Test public void testResetPasswordPage() throws Exception { - ExpiringCode code = new ExpiringCode("code1", new Timestamp(System.currentTimeMillis()), "someData", null); + ExpiringCode code = new ExpiringCode("code1", new Timestamp(System.currentTimeMillis()), "{\"user_id\" : \"some-user-id\"}", null); when(codeStore.generateCode(anyString(), any(Timestamp.class), eq(null))).thenReturn(code); when(codeStore.retrieveCode(anyString())).thenReturn(code); - mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", "secret_code")) + mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", "code1")) .andExpect(status().isOk()) .andExpect(view().name("reset_password")); } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java index 765c7b41008..91b84839a88 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabaseTests.java @@ -26,7 +26,6 @@ import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import java.sql.Timestamp; -import java.util.Arrays; import java.util.Collections; import java.util.UUID; @@ -78,7 +77,7 @@ private void addAuthority(String authority, String userId) { @Before public void initializeDb() throws Exception { - + IdentityZoneHolder.clear(); otherIdentityZone = new IdentityZone(); otherIdentityZone.setId("some-other-zone-id"); @@ -107,7 +106,10 @@ public void clearDb() throws Exception { @Test public void addedUserHasNoLegacyVerificationBehavior() { - Arrays.asList(JOE_ID, MABEL_ID, ALICE_ID).stream().map(id -> db.retrieveUserById(id)).forEach(user -> assertFalse(user.isLegacyVerificationBehavior())); + assertFalse(db.retrieveUserById(JOE_ID).isLegacyVerificationBehavior()); + assertFalse(db.retrieveUserById(MABEL_ID).isLegacyVerificationBehavior()); + IdentityZoneHolder.set(otherIdentityZone); + assertFalse(db.retrieveUserById(ALICE_ID).isLegacyVerificationBehavior()); } @Test diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java index 5a18469fd87..66d708c1311 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java @@ -128,7 +128,7 @@ public void testCreateAndUpdateIdentityProviderInDefaultZone() throws Exception @Test public void testCreateIdentityProviderInOtherZone() throws Exception { IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); - + IdentityZoneHolder.set(zone); String originKey = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zone.getId()); @@ -159,6 +159,7 @@ public void testCreateIdentityProviderWithNonUniqueOriginKeyInDefaultZone() thro @Test(expected=IdpAlreadyExistsException.class) public void testCreateIdentityProviderWithNonUniqueOriginKeyInOtherZone() throws Exception { IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); String originKey = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zone.getId()); db.create(idp); @@ -171,7 +172,9 @@ public void testCreateIdentityProvidersWithSameOriginKeyInBothZones() throws Exc String originKey = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zoneId); db.create(idp); - idp.setIdentityZoneId(MultitenancyFixture.identityZone(UUID.randomUUID().toString(),"myzone").getId()); + IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(zone); + idp.setIdentityZoneId(zone.getId()); db.create(idp); } @@ -199,6 +202,7 @@ public void testUpdateIdentityProviderInDefaultZone() throws Exception { @Test public void testUpdateIdentityProviderInOtherZone() throws Exception { IdentityZone zone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(),"myzone"); + IdentityZoneHolder.set(zone); String originKey = RandomStringUtils.randomAlphabetic(6); String idpId = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zone.getId()); @@ -225,6 +229,7 @@ public void testRetrieveIdentityProviderById() { IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, uaaZoneId); idp.setId(idpId); IdentityZone zone = MultitenancyFixture.identityZone(identityZoneId, identityZoneId); + IdentityZoneHolder.set(zone); idp.setIdentityZoneId(zone.getId()); idp = db.create(idp); IdentityProvider retrievedIdp = db.retrieve(idp.getId()); @@ -247,6 +252,7 @@ public void testRetrieveAll() throws Exception { assertEquals(numberOfIdps + 1, identityProviders.size()); IdentityZone otherZone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(), "myzone"); + IdentityZoneHolder.set(otherZone); String originKey = RandomStringUtils.randomAlphabetic(6); IdentityProvider otherZoneIdp = MultitenancyFixture.identityProvider(originKey, otherZone.getId()); db.create(otherZoneIdp); @@ -261,7 +267,7 @@ public void testRetrieveIdentityProviderByOriginInSameZone() { String identityZoneId = RandomStringUtils.randomAlphabetic(6); String idpId = RandomStringUtils.randomAlphabetic(6); IdentityZone identityZone = MultitenancyFixture.identityZone(identityZoneId, "myzone"); - + IdentityZoneHolder.set(identityZone); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, identityZone.getId()); idp.setId(idpId); idp = db.create(idp); diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 25c762db674..21ffaeefa25 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -413,4 +413,11 @@ + + + + + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java index 874f56acd2a..afeaff8a381 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java @@ -6,13 +6,14 @@ import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.cloudfoundry.identity.uaa.scim.ScimUser; -import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService; import org.flywaydb.core.internal.util.StringUtils; import org.junit.After; import org.junit.Before; @@ -105,7 +106,7 @@ public void invite_Multiple_Users_With_Client_Credentials() throws Exception { public void invite_User_With_User_Credentials() throws Exception { String email = "user1@example.com"; String redirectUri = "example.com"; - String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, null); InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, redirectUri, email); assertResponseAndCodeCorrect(new String[] {email}, redirectUri, null, response, clientDetails); } @@ -143,7 +144,7 @@ public void multiple_Users_Email_Exists_With_One_Origin() throws Exception { user2.setOrigin(UAA); utils().createUser(getMockMvc(), clientAdminToken, user2); - String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, null); InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, "example.com", email); assertEquals(0, response.getNewInvites().size()); assertEquals(1, response.getFailedInvites().size()); @@ -154,7 +155,7 @@ public void multiple_Users_Email_Exists_With_One_Origin() throws Exception { public void accept_Invitation_Email_With_Oss_Brand() throws Exception { ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); - getMockMvc().perform(get(getAcceptInvitationLink())) + getMockMvc().perform(get(getAcceptInvitationLink(null))) .andExpect(content().string(containsString("Create your account"))) .andExpect(content().string(not(containsString("Pivotal ID")))) .andExpect(content().string(not(containsString("Create Pivotal ID")))) @@ -165,7 +166,7 @@ public void accept_Invitation_Email_With_Oss_Brand() throws Exception { public void accept_Invitation_Email_With_Pivotal_Brand() throws Exception { ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - getMockMvc().perform(get(getAcceptInvitationLink())) + getMockMvc().perform(get(getAcceptInvitationLink(null))) .andExpect(content().string(containsString("Create your Pivotal ID"))) .andExpect(content().string(containsString("Pivotal products"))) .andExpect(content().string(not(containsString("Create your account")))) @@ -176,11 +177,21 @@ public void accept_Invitation_Email_With_Pivotal_Brand() throws Exception { @Test public void accept_Invitation_Email_Within_Zone() throws Exception { String subdomain = generator.generate(); - utils().createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); + IdentityZone zone = utils().createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - getMockMvc().perform(get(getAcceptInvitationLink()) - .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) + BaseClientDetails client = utils().getClientDetailsModification(clientId, clientSecret, Collections.singleton("oauth"), Arrays.asList("scim.read","scim.invite"), Arrays.asList(new String[]{"client_credentials", "password"}), authorities, Collections.EMPTY_SET); + IdentityZone original = IdentityZoneHolder.get(); + try { + IdentityZoneHolder.set(zone); + getWebApplicationContext().getBean(MultitenantJdbcClientDetailsService.class).addClientDetails(client); + } finally { + IdentityZoneHolder.set(original); + } + String acceptInvitationLink = getAcceptInvitationLink(zone); + + getMockMvc().perform(get(acceptInvitationLink) + .header("Host",(subdomain + ".localhost"))) .andExpect(content().string(containsString("Create your account"))) .andExpect(content().string(not(containsString("Pivotal ID")))) .andExpect(content().string(not(containsString("Create Pivotal ID")))) @@ -191,7 +202,7 @@ public void accept_Invitation_Email_Within_Zone() throws Exception { public void invitations_Accept_Get_Security() throws Exception { getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store"); - String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, null); sendRequestWithToken(userToken, null, clientId, "example.com", "user1@"+domain); String code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT code FROM expiring_code_store", String.class); @@ -248,10 +259,10 @@ private void assertResponseAndCodeCorrect(String[] emails, String redirectUrl, S } } - private String getAcceptInvitationLink() throws Exception { - String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + private String getAcceptInvitationLink(IdentityZone zone) throws Exception { + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, zone); String email = generator.generate().toLowerCase() + "@"+domain; - InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, "example.com", email); + InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, zone==null?null:zone.getSubdomain(), clientId, "example.com", email); return response.getNewInvites().get(0).getInviteLink().toString(); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java index 1007fe05516..32d1ab3b9cc 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java @@ -85,7 +85,7 @@ public void setUp() throws Exception { clientSecret = generator.generate().toLowerCase(); authorities = "scim.read,scim.invite"; MockMvcUtils.utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, Collections.singleton("oauth"), Arrays.asList("scim.read","scim.invite"), Arrays.asList(new String[]{"client_credentials", "password"}), authorities, Collections.singleton(REDIRECT_URI), IdentityZone.getUaa()); - userInviteToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + userInviteToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, null); getWebApplicationContext().getBean(JdbcTemplate.class).update("delete from expiring_code_store"); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index ad781cb6a67..6408c277088 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -17,22 +17,26 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.RandomStringUtils; -import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.invitations.InvitationsRequest; import org.cloudfoundry.identity.uaa.invitations.InvitationsResponse; +import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; +import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; +import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.resources.SearchResults; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; +import org.cloudfoundry.identity.uaa.security.web.CookieBasedCsrfTokenRepository; import org.cloudfoundry.identity.uaa.test.TestApplicationEventListener; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.test.TestClient.OAuthToken; @@ -40,14 +44,10 @@ import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; -import org.cloudfoundry.identity.uaa.security.web.CookieBasedCsrfTokenRepository; -import org.cloudfoundry.identity.uaa.provider.IdentityProvider; -import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; -import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; import org.junit.Assert; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; @@ -229,14 +229,14 @@ public static InvitationsResponse sendRequestWithTokenAndReturnResponse(Applicat .header("Authorization", "Bearer " + token) .contentType(APPLICATION_JSON) .content(requestBody); - if (org.flywaydb.core.internal.util.StringUtils.hasText(subdomain)) { - post.with(new SetServerNameRequestPostProcessor(subdomain+".localhost")); + if (hasText(subdomain)) { + post.header("Host",(subdomain+".localhost")); } MvcResult result = mockMvc.perform( post ) - .andExpect(status().isOk()) - .andReturn(); + .andExpect(status().isOk()) + .andReturn(); return JsonUtils.readValue(result.getResponse().getContentAsString(), InvitationsResponse.class); } @@ -404,7 +404,7 @@ public IdentityZone createOtherIdentityZone(String subdomain, MockMvc mockMvc, ApplicationContext webApplicationContext) throws Exception { BaseClientDetails client = new BaseClientDetails("admin", null, null, "client_credentials", - "clients.admin,scim.read,scim.write,idps.write"); + "clients.admin,scim.read,scim.write,idps.write,uaa.admin"); client.setClientSecret("admin-secret"); return createOtherIdentityZone(subdomain, mockMvc, webApplicationContext, client); @@ -509,9 +509,16 @@ public ScimUser createAdminForZone(MockMvc mockMvc, String accessToken, String s } public ScimGroup getGroup(MockMvc mockMvc, String accessToken, String displayName) throws Exception { + return getGroup(mockMvc, accessToken, displayName, null); + } + public ScimGroup getGroup(MockMvc mockMvc, String accessToken, String displayName, String subdomain) throws Exception { String filter = "displayName eq \""+displayName+"\""; + MockHttpServletRequestBuilder builder = get("/Groups"); + if (hasText(subdomain)) { + builder.header("Host", subdomain+".localhost"); + } SearchResults results = JsonUtils.readValue( - mockMvc.perform(get("/Groups") + mockMvc.perform(builder .header("Authorization", "Bearer " + accessToken) .contentType(APPLICATION_JSON) .param("filter", filter)) @@ -527,6 +534,23 @@ public ScimGroup getGroup(MockMvc mockMvc, String accessToken, String displayNam public ScimGroup createGroup(MockMvc mockMvc, String accessToken, ScimGroup group) throws Exception { return createGroup(mockMvc, accessToken, group, null); } + + public ScimGroup createGroup(MockMvc mockMvc, String accessToken, String subdomain, ScimGroup group) throws Exception { + MockHttpServletRequestBuilder post = post("/Groups") + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(group)); + if (hasText(subdomain)) { + post.header("Host", subdomain+".localhost"); + } + return JsonUtils.readValue( + mockMvc.perform(post) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(), + ScimGroup.class); + } + + public ScimGroup createGroup(MockMvc mockMvc, String accessToken, ScimGroup group, String zoneId) throws Exception { MockHttpServletRequestBuilder post = post("/Groups") .header("Authorization", "Bearer " + accessToken) @@ -543,12 +567,18 @@ public ScimGroup createGroup(MockMvc mockMvc, String accessToken, ScimGroup grou } public ScimGroup updateGroup(MockMvc mockMvc, String accessToken, ScimGroup group) throws Exception { + return updateGroup(mockMvc, accessToken, group, null); + } + public ScimGroup updateGroup(MockMvc mockMvc, String accessToken, ScimGroup group, IdentityZone zone) throws Exception { + MockHttpServletRequestBuilder put = put("/Groups/" + group.getId()); + if (zone!=null) { + put.header("Host", zone.getSubdomain()+".localhost"); + } return JsonUtils.readValue( - mockMvc.perform(put("/Groups/" + group.getId()) - .header("If-Match", group.getVersion()) - .header("Authorization", "Bearer " + accessToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(group))) + mockMvc.perform(put.header("If-Match", group.getVersion()) + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(group))) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), ScimGroup.class); @@ -579,6 +609,11 @@ public ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, Stri } public ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, Collection resourceIds, Collection scopes, Collection grantTypes, String authorities, Set redirectUris, IdentityZone zone) throws Exception { + ClientDetailsModification client = getClientDetailsModification(id, secret, resourceIds, scopes, grantTypes, authorities, redirectUris); + return createClient(mockMvc,adminAccessToken, client, zone); + } + + public ClientDetailsModification getClientDetailsModification(String id, String secret, Collection resourceIds, Collection scopes, Collection grantTypes, String authorities, Set redirectUris) { ClientDetailsModification detailsModification = new ClientDetailsModification(); detailsModification.setClientId(id); detailsModification.setResourceIds(resourceIds); @@ -588,7 +623,7 @@ public ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, Stri detailsModification.setRegisteredRedirectUri(redirectUris); ClientDetailsModification client = detailsModification; client.setClientSecret(secret); - return createClient(mockMvc,adminAccessToken, client, zone); + return client; } public BaseClientDetails updateClient(MockMvc mockMvc, String accessToken, BaseClientDetails clientDetails, IdentityZone zone) @@ -651,18 +686,34 @@ public String getZoneAdminToken(MockMvc mockMvc, String adminToken, String zoneI } - public String getUserOAuthAccessToken(MockMvc mockMvc, String clientId, String clientSecret, String username, - String password, String scope) - throws Exception { + public String getUserOAuthAccessToken(MockMvc mockMvc, + String clientId, + String clientSecret, + String username, + String password, + String scope) throws Exception { + return getUserOAuthAccessToken(mockMvc, clientId, clientSecret, username, password, scope, null); + } + public String getUserOAuthAccessToken(MockMvc mockMvc, + String clientId, + String clientSecret, + String username, + String password, + String scope, + IdentityZone zone) throws Exception { String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); - MockHttpServletRequestBuilder oauthTokenPost = post("/oauth/token") + MockHttpServletRequestBuilder oauthTokenPost = + post("/oauth/token") .header("Authorization", basicDigestHeaderValue) .param("grant_type", "password") .param("client_id", clientId) .param("username", username) .param("password", password) .param("scope", scope); + if (zone!=null) { + oauthTokenPost.header("Host", zone.getSubdomain()+".localhost"); + } MvcResult result = mockMvc.perform(oauthTokenPost).andExpect(status().isOk()).andReturn(); TestClient.OAuthToken oauthToken = JsonUtils.readValue(result.getResponse().getContentAsString(), TestClient.OAuthToken.class); @@ -671,16 +722,7 @@ public String getUserOAuthAccessToken(MockMvc mockMvc, String clientId, String c public String getClientOAuthAccessToken(MockMvc mockMvc, String clientId, String clientSecret, String scope) throws Exception { - String basicDigestHeaderValue = "Basic " - + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); - MockHttpServletRequestBuilder oauthTokenPost = post("/oauth/token") - .header("Authorization", basicDigestHeaderValue) - .param("grant_type", "client_credentials") - .param("client_id", clientId) - .param("scope", scope); - MvcResult result = mockMvc.perform(oauthTokenPost).andExpect(status().isOk()).andReturn(); - TestClient.OAuthToken oauthToken = JsonUtils.readValue(result.getResponse().getContentAsString(), TestClient.OAuthToken.class); - return oauthToken.accessToken; + return getClientCredentialsOAuthAccessToken(mockMvc, clientId, clientSecret, scope, null); } public String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String clientId, String clientSecret, String userId, String username, String password, String scope) throws Exception { @@ -731,43 +773,65 @@ public String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String clientId, } - public String getScimInviteUserToken(MockMvc mockMvc, String clientId, String clientSecret) throws Exception { - String adminToken = getClientCredentialsOAuthAccessToken(mockMvc, "admin", "adminsecret", "", null); + public String getScimInviteUserToken(MockMvc mockMvc, String clientId, String clientSecret, IdentityZone zone) throws Exception { + String adminToken = getClientCredentialsOAuthAccessToken(mockMvc, + "admin", + zone==null?"adminsecret":"admin-secret", + "", + zone==null?null:zone.getSubdomain() + ); // create a user (with the required permissions) to perform the actual /invite_users action String username = new RandomValueStringGenerator().generate().toLowerCase()+"@example.com"; ScimUser user = new ScimUser(clientId, username, "given-name", "family-name"); user.setPrimaryEmail(username); user.setPassword("password"); - user = createUser(mockMvc, adminToken, user); + user = (zone == null) ? createUser(mockMvc, adminToken, user) : createUserInZone(mockMvc,adminToken,user,zone.getSubdomain(), null); String scope = "scim.invite"; ScimGroupMember member = new ScimGroupMember(user.getId(), ScimGroupMember.Type.USER, Arrays.asList(ScimGroupMember.Role.READER)); + ScimGroup inviteGroup = new ScimGroup(scope); - ScimGroup group = getGroup(mockMvc, adminToken, scope); + if (zone!=null) { + createGroup(mockMvc, adminToken, zone.getSubdomain(), inviteGroup); + } + ScimGroup group = getGroup(mockMvc, + adminToken, + scope, + zone==null?null:zone.getSubdomain() + ); group.getMembers().add(member); - updateGroup(mockMvc, adminToken, group); + updateGroup(mockMvc, adminToken, group, zone); user.getGroups().add(new ScimUser.Group(group.getId(), scope)); // get a bearer token for the user - return getUserOAuthAccessToken(mockMvc, clientId, clientSecret, user.getUserName(), "password", "scim.invite"); + return getUserOAuthAccessToken(mockMvc, + clientId, + clientSecret, + user.getUserName(), + "password", + "scim.invite", + zone + ); } - public String getClientCredentialsOAuthAccessToken(MockMvc mockMvc, String username, String password, String scope, - String subdomain) - throws Exception { + public String getClientCredentialsOAuthAccessToken(MockMvc mockMvc, + String clientId, + String clientSecret, + String scope, + String subdomain) throws Exception { String basicDigestHeaderValue = "Basic " - + new String(Base64.encodeBase64((username + ":" + password).getBytes())); + + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); MockHttpServletRequestBuilder oauthTokenPost = post("/oauth/token") .header("Authorization", basicDigestHeaderValue) .param("grant_type", "client_credentials") - .param("client_id", username) + .param("client_id", clientId) .param("scope", scope); if (subdomain != null && !subdomain.equals("")) oauthTokenPost.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); MvcResult result = mockMvc.perform(oauthTokenPost) - .andExpect(status().isOk()) - .andReturn(); + .andExpect(status().isOk()) + .andReturn(); OAuthToken oauthToken = JsonUtils.readValue(result.getResponse().getContentAsString(), OAuthToken.class); return oauthToken.accessToken; } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java index c0ee78a1697..4484bc762a8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java @@ -520,11 +520,11 @@ public void test_delete_zone_cleans_db() throws Exception { .setIdentityZoneId(zone.getId()) .setName("Delete Test") .setType(LOGIN_SERVER); + IdentityZoneHolder.set(zone); provider = idpp.create(provider); assertNotNull(idpp.retrieveByOrigin(LOGIN_SERVER, zone.getId())); assertEquals(provider.getId(), idpp.retrieveByOrigin(LOGIN_SERVER, zone.getId()).getId()); - IdentityZoneHolder.set(zone); //create user and add user to group ScimUser user = getScimUser(); user.setOrigin(LOGIN_SERVER); @@ -551,7 +551,6 @@ public void test_delete_zone_cleans_db() throws Exception { .param("username", user.getUserName()) .param("password", "adasda") ) - .andDo(print()) .andExpect(status().isFound()); //ensure we have some audit records diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java index 5f395839f44..d75c1520d25 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java @@ -29,13 +29,13 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.hamcrest.MatcherAssert; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -43,7 +43,6 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.request.RequestPostProcessor; import java.nio.charset.Charset; import java.util.Arrays; @@ -53,16 +52,13 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertThat; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -161,7 +157,6 @@ public void test_Create_User_Too_Long_Password() throws Exception { user.setPassword(new RandomValueStringGenerator(300).generate()); ResultActions result = createUserAndReturnResult(user, scimReadWriteToken, null, null); result.andExpect(status().isBadRequest()) - .andDo(print()) .andExpect(jsonPath("$.error").value("invalid_password")) .andExpect(jsonPath("$.message").value("Password must be no more than 255 characters in length.")) .andExpect(jsonPath("$.error_description").value("Password must be no more than 255 characters in length.")); @@ -213,17 +208,10 @@ public void verification_link_in_non_default_zone() throws Exception { zonedClientDetails.setClientSecret(zonedClientSecret); String zonedScimCreateToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonedClientDetails.getClientId(), zonedClientDetails.getClientSecret(), "scim.create", subdomain); - ScimUser joel = setUpScimUser(); + ScimUser joel = setUpScimUser(zoneResult.getIdentityZone()); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + joel.getId() + "/verify-link") - .with(new RequestPostProcessor() { - - @Override - public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - request.setServerName(subdomain + ".localhost"); - return request; - } - }) + .header("Host", subdomain + ".localhost") .header("Authorization", "Bearer " + zonedScimCreateToken) .param("redirect_uri", HTTP_REDIRECT_EXAMPLE_COM) .accept(APPLICATION_JSON); @@ -557,12 +545,22 @@ private MockHttpServletRequestBuilder setUpVerificationLinkRequest(ScimUser user } private ScimUser setUpScimUser() { - String email = "joe@"+generator.generate().toLowerCase()+".com"; - ScimUser joel = new ScimUser(null, email, "Joel", "D'sa"); - joel.setVerified(false); - joel.addEmail(email); - joel = usersRepository.createUser(joel, "pas5Word"); - return joel; + return setUpScimUser(IdentityZoneHolder.get()); + } + + private ScimUser setUpScimUser(IdentityZone zone) { + IdentityZone original = IdentityZoneHolder.get(); + try { + IdentityZoneHolder.set(zone); + String email = "joe@" + generator.generate().toLowerCase() + ".com"; + ScimUser joel = new ScimUser(null, email, "Joel", "D'sa"); + joel.setVerified(false); + joel.addEmail(email); + joel = usersRepository.createUser(joel, "pas5Word"); + return joel; + } finally { + IdentityZoneHolder.set(original); + } } private String getQueryStringParam(String query, String key) { From d7c32e9c86e53259187271cd870f3143f54559ff Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 25 Jan 2016 11:43:17 -0800 Subject: [PATCH 25/25] Bump release version to 3.0.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a6b377530ff..bad42edb837 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=3.0.1-SNAPSHOT +version=3.0.1